Merge remote-tracking branch 'playground/master'
This commit is contained in:
commit
7301e0e6dc
16233
package-lock.json
generated
16233
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -101,10 +101,10 @@
|
||||
"@koa/router": "^12.0.1",
|
||||
"@sealcode/add-to-head": "^1.0.0",
|
||||
"@sealcode/file-manager": "^1.0.2",
|
||||
"@sealcode/jdd": "^0.8.6",
|
||||
"@sealcode/jdd-editor": "^0.2.23",
|
||||
"@sealcode/monaco-wrapper": "^0.0.4",
|
||||
"@sealcode/sealgen": "^0.19.21",
|
||||
"@sealcode/jdd": "^0.8.13",
|
||||
"@sealcode/jdd-editor": "^0.2.37",
|
||||
"@sealcode/monaco-wrapper": "^0.0.12",
|
||||
"@sealcode/sealgen": "^0.19.23",
|
||||
"@sealcode/show-first-row": "^0.1.0",
|
||||
"@sealcode/simplemde": "^1.12.1",
|
||||
"@sealcode/sortable": "^0.0.1",
|
||||
@ -112,7 +112,9 @@
|
||||
"@types/kill-port": "^2.0.0",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/qrcode-svg": "^1.1.5",
|
||||
"@types/simplemde": "^1.11.11",
|
||||
"@types/wcag-contrast": "^3.0.3",
|
||||
"a11y.css": "^5.3.0",
|
||||
"c8": "^10.1.3",
|
||||
"colord": "^2.9.3",
|
||||
@ -125,9 +127,10 @@
|
||||
"js-convert-case": "^4.2.0",
|
||||
"koa-mount": "^4.2.0",
|
||||
"koa-qs": "^3.0.0",
|
||||
"koa-responsive-image-router": "^0.2.38",
|
||||
"koa-responsive-image-router": "^0.2.41",
|
||||
"koa-static": "^5.0.0",
|
||||
"locreq": "^3.0.0",
|
||||
"mailparser": "^3.9.15",
|
||||
"meilisearch": "^0.41.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"mongodb": "^6.17.0",
|
||||
@ -136,13 +139,15 @@
|
||||
"normalize-diacritics": "^5.0.0",
|
||||
"nyc": "^17.1.0",
|
||||
"object-path": "^0.11.8",
|
||||
"qrcode-svg": "^1.1.0",
|
||||
"qs": "^6.12.0",
|
||||
"sealious": "^0.21.30",
|
||||
"slug": "^9.1.0",
|
||||
"stimulus": "^3.2.2",
|
||||
"tempstream": "^0.4.6",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"turndown": "^7.2.0"
|
||||
"turndown": "^7.2.0",
|
||||
"wcag-contrast": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.54.1",
|
||||
@ -152,6 +157,7 @@
|
||||
"@types/koa-mount": "^4.0.5",
|
||||
"@types/koa-qs": "^2.0.3",
|
||||
"@types/koa-static": "^4.0.4",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/object-path": "^0.11.4",
|
||||
"@types/slug": "^5.0.9",
|
||||
|
||||
@ -16,6 +16,10 @@ import {
|
||||
MONGO_PORT,
|
||||
MONGO_USERNAME,
|
||||
PORT,
|
||||
SMTP_HOST,
|
||||
SMTP_LOGIN,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_PORT,
|
||||
} from "./config.js";
|
||||
import ADMIN_CREDENTIALS from "./default-admin-credentials.js";
|
||||
import { TheFileManager } from "./file-manager.js";
|
||||
@ -94,7 +98,17 @@ export default class TheApp extends App {
|
||||
user: "any",
|
||||
password: "any",
|
||||
})
|
||||
: new LoggerMailer();
|
||||
: MAILER === "SMTP"
|
||||
? new SMTPMailer(
|
||||
{
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT,
|
||||
user: SMTP_LOGIN,
|
||||
password: SMTP_PASSWORD,
|
||||
},
|
||||
{ secure: false, requireTLS: true }
|
||||
)
|
||||
: new LoggerMailer();
|
||||
|
||||
async start() {
|
||||
await super.start();
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { Collection, FieldTypes, Policies } from "sealious";
|
||||
import { Roles } from "../policy-types/roles.js";
|
||||
|
||||
export default class NavbarLinks extends Collection {
|
||||
fields = {
|
||||
label: new FieldTypes.Text(),
|
||||
href: new FieldTypes.Text(),
|
||||
};
|
||||
defaultPolicy = new Policies.Public();
|
||||
defaultPolicy = new Roles(["admin"]);
|
||||
policies = {
|
||||
show: new Policies.Public(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { colord } from "colord";
|
||||
import { hex as measureHexContrast } from "wcag-contrast";
|
||||
|
||||
export const colors: Record<string, Record<string, string>> = {
|
||||
basic: {
|
||||
@ -68,3 +69,108 @@ export function shade(color: string, shade_step: number /* 0-9 */) {
|
||||
shifted = shifted.rotate(hue_step * steps);
|
||||
return shifted.toHex();
|
||||
}
|
||||
|
||||
export function getColorVariables(): string[] {
|
||||
return Object.entries(colors).flatMap(([group, values]) =>
|
||||
Object.keys(values).map((name) => `--color-${group}-${name}`)
|
||||
);
|
||||
}
|
||||
|
||||
function filterCandidates(
|
||||
candidates: { color: string; contrast: number; shadeStep: number }[],
|
||||
minimumContrast: number,
|
||||
min_brightness: number,
|
||||
max_brightness: number
|
||||
) {
|
||||
return candidates
|
||||
.filter(({ contrast }) => contrast >= minimumContrast)
|
||||
.filter(({ color }) => {
|
||||
const brightness = colord(color).brightness();
|
||||
return brightness > min_brightness && brightness < max_brightness;
|
||||
})
|
||||
.sort((a, b) => b.contrast - a.contrast);
|
||||
}
|
||||
|
||||
export function getFgColorVariable(
|
||||
bgColorName: string,
|
||||
minimumContrast: number = 4.5
|
||||
): string {
|
||||
if (bgColorName.startsWith("--color-")) {
|
||||
bgColorName = bgColorName.slice("--color-".length);
|
||||
}
|
||||
if (bgColorName.startsWith("--")) {
|
||||
bgColorName = bgColorName.slice(2);
|
||||
}
|
||||
const [group, ...nameParts] = bgColorName.split("-");
|
||||
const name = nameParts.join("-");
|
||||
|
||||
if (!group || !name || !colors[group]?.[name]) {
|
||||
throw new Error(`Unknown color: ${bgColorName}`);
|
||||
}
|
||||
|
||||
// Prefer an explicitly defined text-on token.
|
||||
const explicitTextName = `text-on-${name}`;
|
||||
|
||||
if (colors[group]?.[explicitTextName]) {
|
||||
return `--color-${group}-${explicitTextName}`;
|
||||
}
|
||||
|
||||
const background = colors[group][name];
|
||||
|
||||
// Check both ends of the generated scale. This gives us the closest
|
||||
// suitable shade rather than blindly choosing black or white.
|
||||
const candidates = Array.from({ length: 10 }, (_, shadeStep) => {
|
||||
shadeStep += 0;
|
||||
const color = shade(background, shadeStep);
|
||||
const contrast = measureHexContrast(
|
||||
colord(color).toHex(),
|
||||
colord(background).toHex()
|
||||
);
|
||||
|
||||
const candidate = {
|
||||
shadeStep,
|
||||
contrast,
|
||||
color,
|
||||
};
|
||||
return candidate;
|
||||
});
|
||||
|
||||
let suitable = filterCandidates(candidates, minimumContrast, 0.2, 0.8);
|
||||
|
||||
if (suitable.length === 0) {
|
||||
suitable = filterCandidates(candidates, minimumContrast, 0.1, 0.9);
|
||||
}
|
||||
|
||||
let shadeStep;
|
||||
if (suitable.length > 0) {
|
||||
shadeStep = suitable[0]!.shadeStep;
|
||||
} else {
|
||||
const darkest = shade(background, 0);
|
||||
const lightest = shade(background, 9);
|
||||
const contrast_d = measureHexContrast(colord(darkest).toHex(), background);
|
||||
const contrast_l = measureHexContrast(colord(lightest).toHex(), background);
|
||||
if (contrast_l < minimumContrast && contrast_d < minimumContrast) {
|
||||
if (colord(background).brightness() > 0.5) {
|
||||
return `--color-basic-black`;
|
||||
} else {
|
||||
return `--color-basic-white`;
|
||||
}
|
||||
} else if (contrast_l >= minimumContrast && contrast_d >= minimumContrast) {
|
||||
// pick the smaller contrast so it's prettier than just near black/white
|
||||
if (contrast_l > contrast_d) {
|
||||
shadeStep = 0;
|
||||
} else {
|
||||
shadeStep = 9;
|
||||
}
|
||||
} else if (contrast_l > minimumContrast) {
|
||||
shadeStep = 9;
|
||||
} else if (contrast_d > minimumContrast) {
|
||||
shadeStep = 0;
|
||||
} else {
|
||||
shadeStep = 9;
|
||||
// fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return `--color-${group}-${name}-${String(shadeStep).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
@ -49,3 +49,10 @@ export const UPLOADS_FS_DIR =
|
||||
export const MEILISEARCH_MASTER_KEY = process.env.MEILISEARCH_MASTER_KEY || "qwerty";
|
||||
export const MEILISEARCH_HOST = process.env.MEILISEARCH_HOST || "http://localhost:1098";
|
||||
export const SCAN_A11Y = false;
|
||||
|
||||
export const SMTP_HOST = process.env.SEALIOUS_SMTP_HOST || MAILCATCHER_HOST;
|
||||
export const SMTP_PORT = parseInt(
|
||||
process.env.SEALIOUS_SMTP_PORT || MAILCATCHER_SMTP_PORT.toString()
|
||||
);
|
||||
export const SMTP_LOGIN = process.env.SEALIOUS_SMTP_LOGIN || "";
|
||||
export const SMTP_PASSWORD = process.env.SEALIOUS_SMTP_PASSWORD || "";
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
.button {
|
||||
--icon-width: 32px;
|
||||
--gap: 8px;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
margin: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--button-fg-color);
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.1);
|
||||
@ -13,14 +21,25 @@
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
svg * {
|
||||
fill: var(--button-fg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.button--centered {
|
||||
span {
|
||||
text-align: center;
|
||||
margin-right: calc(var(--gap) + var(--icon-width));
|
||||
}
|
||||
}
|
||||
|
||||
.button--color-accent1 {
|
||||
background-color: var(--color-brand-accent);
|
||||
color: var(--color-brand-text-on-accent);
|
||||
--button-fg-color: var(--color-brand-text-on-accent);
|
||||
}
|
||||
|
||||
.button--color-accent2 {
|
||||
background-color: var(--color-brand-accent2);
|
||||
color: var(--color-brand-text-on-accent2);
|
||||
--button-fg-color: var(--color-brand-text-on-accent2);
|
||||
}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
export const button_variants = ["accent1", "accent2"] as const;
|
||||
type variant = (typeof button_variants)[number];
|
||||
|
||||
export function button({
|
||||
text,
|
||||
href,
|
||||
variant,
|
||||
disabled = false,
|
||||
submit = false,
|
||||
}: {
|
||||
text: string;
|
||||
href: string;
|
||||
variant: variant;
|
||||
disabled?: boolean;
|
||||
submit?: boolean;
|
||||
}): JSX.Element {
|
||||
const classstr = `class="button button--color-${variant}"`;
|
||||
if (submit) {
|
||||
return /* HTML */ `<button type="submit" formaction="${href}" ${classstr}>
|
||||
${text}
|
||||
</button>`;
|
||||
}
|
||||
return /* HTML */ `<a
|
||||
${classstr}
|
||||
${disabled ? "" : `href="${href}"`}
|
||||
${disabled ? "disabled" : ""}
|
||||
>
|
||||
${text}
|
||||
</a>`;
|
||||
}
|
||||
43
src/back/elements/button.tsx
Normal file
43
src/back/elements/button.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import { icon as render_icon } from "../icons.js";
|
||||
|
||||
export const button_variants = ["accent1", "accent2"] as const;
|
||||
type variant = (typeof button_variants)[number];
|
||||
|
||||
export function button({
|
||||
text,
|
||||
href,
|
||||
variant,
|
||||
disabled = false,
|
||||
submit = false,
|
||||
icon,
|
||||
centered = false,
|
||||
}: {
|
||||
text: string;
|
||||
href: string;
|
||||
variant: variant;
|
||||
disabled?: boolean;
|
||||
submit?: boolean;
|
||||
icon?: string;
|
||||
centered?: boolean;
|
||||
}): JSX.Element {
|
||||
const classstr = `button button--color-${variant}`;
|
||||
if (submit) {
|
||||
return /* HTML */ `<button type="submit" formaction="${href}" ${classstr}>
|
||||
${text}
|
||||
</button>`;
|
||||
}
|
||||
return (
|
||||
<a
|
||||
class={[
|
||||
"button",
|
||||
`button--color-${variant}`,
|
||||
{ "button--centered": centered },
|
||||
]}
|
||||
href={disabled ? false : href}
|
||||
disabled={disabled}
|
||||
>
|
||||
{icon ? render_icon(icon) : ""} <span>{text}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
45
src/back/email/send-eml.ts
Normal file
45
src/back/email/send-eml.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { Context } from "koa";
|
||||
import { simpleParser } from "mailparser";
|
||||
import { htmlEscape } from "escape-goat";
|
||||
import { promises as fs } from "fs";
|
||||
import { SMTPMailer } from "sealious";
|
||||
|
||||
export async function sendEml(
|
||||
context: Context,
|
||||
eml_path: string,
|
||||
template_data: Record<string, string>,
|
||||
recipient: string
|
||||
): Promise<void> {
|
||||
let eml_content = await fs.readFile(eml_path, "utf-8");
|
||||
for (const [key, value] of Object.entries(template_data)) {
|
||||
eml_content = eml_content.replaceAll(`{{${key}}}`, htmlEscape(value));
|
||||
}
|
||||
const parsed = await simpleParser(eml_content);
|
||||
|
||||
const mailer = context.$app.mailer;
|
||||
|
||||
if (!(mailer instanceof SMTPMailer)) {
|
||||
throw new Error("Setup SMTP in order to use this feature");
|
||||
}
|
||||
|
||||
await mailer.transport.sendMail({
|
||||
from: context.$app.config.email.from_address,
|
||||
to: recipient,
|
||||
subject: parsed.subject,
|
||||
|
||||
text: parsed.text,
|
||||
html: parsed.html || undefined,
|
||||
|
||||
attachments: parsed.attachments.map((attachment) => ({
|
||||
filename: attachment.filename,
|
||||
content: attachment.content,
|
||||
contentType: attachment.contentType,
|
||||
cid: attachment.cid,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment
|
||||
contentDisposition: attachment.contentDisposition as
|
||||
| "attachment"
|
||||
| "inline"
|
||||
| undefined,
|
||||
})),
|
||||
});
|
||||
}
|
||||
@ -4,6 +4,8 @@ import _locreq from "locreq";
|
||||
|
||||
const locreq = _locreq(import.meta.dirname);
|
||||
|
||||
let all_icon_names: Promise<string[]> | null = null;
|
||||
|
||||
const cache = {} as Record<string, Promise<string> | undefined>;
|
||||
|
||||
export const svg_icon_path = "src/icons";
|
||||
@ -26,3 +28,12 @@ export async function icon(icon_name: string) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllIconNames() {
|
||||
if (!all_icon_names) {
|
||||
all_icon_names = fs
|
||||
.readdir(locreq.resolve(svg_icon_path))
|
||||
.then((files) => files.map((file) => path.parse(file).name));
|
||||
}
|
||||
return all_icon_names;
|
||||
}
|
||||
|
||||
@ -24,14 +24,14 @@ images.getExampleCount = () => {
|
||||
|
||||
const component_arguments = {
|
||||
title: new ComponentArguments.ShortText(),
|
||||
imagesPerPage: new ComponentArguments.ShortText().setExampleValues(["6"]),
|
||||
imagesPerPage: new ComponentArguments.ShortText().setDefaultValue("6"),
|
||||
background_mode: new ComponentArguments.Enum([
|
||||
"transparent",
|
||||
"white",
|
||||
"white-square",
|
||||
]),
|
||||
speed: new ComponentArguments.ShortText().setExampleValues(["5"]),
|
||||
desktop_interval: new ComponentArguments.ShortText().setExampleValues(["5"]),
|
||||
speed: new ComponentArguments.ShortText().setDefaultValue("5"),
|
||||
desktop_interval: new ComponentArguments.ShortText().setDefaultValue("5"),
|
||||
endless: new ComponentArguments.Enum(["true", "false"]),
|
||||
images,
|
||||
} as const;
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
.background-wrapper {
|
||||
width: 100%;
|
||||
grid-column: screen;
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
--vertical-padding: 16px;
|
||||
|
||||
padding: var(--vertical-padding) 0;
|
||||
|
||||
&.background-wrapper--padding-no {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.background-wrapper--big-padding-yes {
|
||||
padding: 64px;
|
||||
}
|
||||
|
||||
& > * {
|
||||
grid-column: content;
|
||||
}
|
||||
|
||||
&.background-wrapper--grow-height {
|
||||
min-height: 80vh;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
&.background-wrapper--variant1 {
|
||||
background-color: var(--color-brand-accent);
|
||||
--color-brand-text-bg: var(--color-brand-accent);
|
||||
color: var(--color-brand-text-on-accent);
|
||||
--color-brand-text-fg: var(--color-brand-text-on-accent);
|
||||
}
|
||||
|
||||
&.background-wrapper--variant2 {
|
||||
background-color: var(--color-brand-accent2);
|
||||
--color-brand-text-bg: var(--color-brand-accent2);
|
||||
color: var(--color-brand-text-on-accent2);
|
||||
--color-brand-text-fg: var(--color-brand-text-on-accent2);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type {
|
||||
ComponentToHTMLArgs,
|
||||
ExtractStructuredComponentArgumentsParsed,
|
||||
JDDContext,
|
||||
} from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
|
||||
export const background_variants = ["variant1", "variant2"];
|
||||
|
||||
const component_arguments = {
|
||||
variant: new ComponentArguments.Enum(background_variants),
|
||||
grow_to_screen_height: new ComponentArguments.Boolean(),
|
||||
padding: new ComponentArguments.Boolean(),
|
||||
big_padding: new ComponentArguments.Boolean(),
|
||||
margin: new ComponentArguments.Boolean(),
|
||||
content: new ComponentArguments.NestedComponent(),
|
||||
} as const;
|
||||
|
||||
export class BackgroundWrapper extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
getCSSClumps(
|
||||
jdd_context: JDDContext,
|
||||
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||
) {
|
||||
const result = [...(args?.content?.getCSSClumps(jdd_context) || [])];
|
||||
return result;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { variant, content, grow_to_screen_height, padding, margin, big_padding },
|
||||
classes,
|
||||
jdd_context,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
"background-wrapper",
|
||||
`background-wrapper--${variant}`,
|
||||
`background-wrapper--padding-${padding ? "yes" : "no"}`,
|
||||
`background-wrapper--big-padding-${big_padding ? "yes" : "no"}`,
|
||||
{
|
||||
"background-wrapper--grow-height": grow_to_screen_height,
|
||||
"jdd-no-gap": !margin,
|
||||
},
|
||||
...classes,
|
||||
]}
|
||||
>
|
||||
{content.render(jdd_context)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
31
src/back/jdd-components/buttons/buttons.css
Normal file
31
src/back/jdd-components/buttons/buttons.css
Normal file
@ -0,0 +1,31 @@
|
||||
.buttons {
|
||||
display: flex;
|
||||
|
||||
&.buttons--mode-row {
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
&.buttons--mode-column {
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
&.buttons--gap-none {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
&.buttons--gap-narrow {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&.buttons--gap-medium {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&.buttons--gap-wide {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
40
src/back/jdd-components/buttons/buttons.jdd.tsx
Normal file
40
src/back/jdd-components/buttons/buttons.jdd.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments, insert_nbsp } from "@sealcode/jdd";
|
||||
import { button, button_variants } from "src/back/elements/button.js";
|
||||
|
||||
const component_arguments = {
|
||||
mode: new ComponentArguments.Enum(["column", "row"]),
|
||||
gap: new ComponentArguments.Enum(["none", "narrow", "medium", "wide"]),
|
||||
buttons: new ComponentArguments.List(
|
||||
new ComponentArguments.Structured({
|
||||
variant: new ComponentArguments.Enum(button_variants),
|
||||
text: new ComponentArguments.ShortText(),
|
||||
href: new ComponentArguments.ShortText(),
|
||||
})
|
||||
),
|
||||
} as const;
|
||||
|
||||
export class Buttons extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { buttons, mode, gap },
|
||||
classes,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
"buttons",
|
||||
`buttons--mode-${mode}`,
|
||||
`buttons--gap-${gap}`,
|
||||
...classes,
|
||||
]}
|
||||
>
|
||||
{buttons.map((b) => button({ ...b, text: insert_nbsp(b.text) }))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
import { calculateDifference } from "./difference.js";
|
||||
|
||||
const component_arguments = {
|
||||
date: new ComponentArguments.ShortText().setExampleValues(["2025-08-05"]),
|
||||
date: new ComponentArguments.ShortText().setDefaultValue("2028-08-05"),
|
||||
} as const;
|
||||
|
||||
function renderBlock({
|
||||
|
||||
5
src/back/jdd-components/crossover/crossover.css
Normal file
5
src/back/jdd-components/crossover/crossover.css
Normal file
@ -0,0 +1,5 @@
|
||||
.crossover {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: screen !important;
|
||||
}
|
||||
65
src/back/jdd-components/crossover/crossover.jdd.tsx
Normal file
65
src/back/jdd-components/crossover/crossover.jdd.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type {
|
||||
ComponentToHTMLArgs,
|
||||
ExtractStructuredComponentArgumentsParsed,
|
||||
JDDContext,
|
||||
} from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
|
||||
const component_arguments = {
|
||||
breakpoints_ascending: new ComponentArguments.List(
|
||||
new ComponentArguments.Structured({
|
||||
display_up_to_px: new ComponentArguments.Number(),
|
||||
content: new ComponentArguments.NestedComponent(),
|
||||
})
|
||||
),
|
||||
} as const;
|
||||
|
||||
export class Crossover extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
getCSSClumps(
|
||||
jdd_context: JDDContext,
|
||||
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||
) {
|
||||
const result = (args.breakpoints_ascending || [])
|
||||
.map((breakpoint) => breakpoint.content.getCSSClumps(jdd_context))
|
||||
.flat();
|
||||
return result;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { breakpoints_ascending },
|
||||
classes,
|
||||
jdd_context,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
const uid = crypto.randomUUID();
|
||||
const id = "id-" + uid;
|
||||
return (
|
||||
<div class={["crossover", `crossover--id-${uid}`, ...classes]} id={id}>
|
||||
{breakpoints_ascending.map((breakpoint) =>
|
||||
breakpoint.content.render(jdd_context)
|
||||
)}
|
||||
{
|
||||
/* HTML */ `<style>
|
||||
${breakpoints_ascending
|
||||
.map(
|
||||
(breakpoint, index) =>
|
||||
`@container (min-width: ${breakpoints_ascending[index - 1]?.display_up_to_px || 0}px) and (max-width: ${breakpoint.display_up_to_px}px){#${id} > *:not(:nth-child(${index + 1})){display: none;}}`
|
||||
)
|
||||
.join("")}
|
||||
@container
|
||||
(min-width: ${breakpoints_ascending.at(-1)
|
||||
?.display_up_to_px}px) {
|
||||
#${id} > * {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>`
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import type {
|
||||
ComponentToHTMLArgs,
|
||||
ExtractStructuredComponentArgumentsParsed,
|
||||
JDDContext,
|
||||
} from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
@ -13,6 +17,14 @@ export class CustomCssWrapper extends Component<typeof component_arguments> {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
getCSSClumps(
|
||||
jdd_context: JDDContext,
|
||||
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||
) {
|
||||
const result = args?.content?.getCSSClumps(jdd_context) || [];
|
||||
return result;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { custom_css, content },
|
||||
classes,
|
||||
@ -26,10 +38,7 @@ export class CustomCssWrapper extends Component<typeof component_arguments> {
|
||||
.${unique_class}{ ${custom_css} }
|
||||
</style>`
|
||||
}
|
||||
{ComponentArguments.NestedComponent.render({
|
||||
jdd_context,
|
||||
data: content as Record<string, unknown>,
|
||||
})}
|
||||
{content.render(jdd_context)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
22
src/back/jdd-components/empty-space/empty-space.jdd.tsx
Normal file
22
src/back/jdd-components/empty-space/empty-space.jdd.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
|
||||
const component_arguments = {
|
||||
size: new ComponentArguments.Number(),
|
||||
} as const;
|
||||
|
||||
export class EmptySpace extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { size },
|
||||
classes,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div class={["empty-space", ...classes]} style={`height: ${size}px`}></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,9 @@ const component_arguments = {
|
||||
title: new ComponentArguments.ShortText(),
|
||||
content: new ComponentArguments.Structured({
|
||||
text: new ComponentArguments.Markdown(),
|
||||
color: new ComponentArguments.Enum(["normal", "accent"]),
|
||||
color: new ComponentArguments.Enum(["normal", "accent"]).setDefaultValue(
|
||||
"normal"
|
||||
),
|
||||
}),
|
||||
accordion_color: new ComponentArguments.Enum(["accent", "accent2"]),
|
||||
faq: new ComponentArguments.List(
|
||||
|
||||
@ -8,9 +8,9 @@ import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
|
||||
const component_arguments = {
|
||||
title: new ComponentArguments.ShortText(),
|
||||
level: new ComponentArguments.Enum(["1", "2", "3", "4", "5", "6"]).setExampleValues([
|
||||
"2",
|
||||
]),
|
||||
level: new ComponentArguments.Enum(["1", "2", "3", "4", "5", "6"]).setDefaultValue(
|
||||
"2"
|
||||
),
|
||||
blur_backdrop: new ComponentArguments.Enum(["true", "false"]),
|
||||
font_family: new ComponentArguments.Enum([
|
||||
"industrial",
|
||||
|
||||
38
src/back/jdd-components/heading/heading.css
Normal file
38
src/back/jdd-components/heading/heading.css
Normal file
@ -0,0 +1,38 @@
|
||||
.heading {
|
||||
& > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.heading--font-size-tiny > * {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&.heading--font-size-small > * {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&.heading--font-size-medium > * {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
&.heading--font-size-large > * {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
&.heading--font-size-extra_large > * {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
&.heading--color-normal {
|
||||
color: var(--color-brand-text-fg);
|
||||
}
|
||||
|
||||
&.heading--color-accent1 {
|
||||
color: var(--color-brand-accent);
|
||||
}
|
||||
|
||||
&.heading--color-accent2 {
|
||||
color: var(--color-brand-accent2);
|
||||
}
|
||||
}
|
||||
49
src/back/jdd-components/heading/heading.jdd.tsx
Normal file
49
src/back/jdd-components/heading/heading.jdd.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments, insert_nbsp } from "@sealcode/jdd";
|
||||
|
||||
const component_arguments = {
|
||||
level: new ComponentArguments.Enum([
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
]).setDefaultValue("h2"),
|
||||
font_size: new ComponentArguments.Enum([
|
||||
"tiny",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"extra_large",
|
||||
]).setDefaultValue("medium"),
|
||||
color: new ComponentArguments.Enum(["normal", "accent1", "accent2"]).setDefaultValue(
|
||||
"normal"
|
||||
),
|
||||
title: new ComponentArguments.ShortText(),
|
||||
} as const;
|
||||
|
||||
export class Heading extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { title, level, font_size, color },
|
||||
classes,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
"heading",
|
||||
`heading--font-size-${font_size}`,
|
||||
`heading--color-${color}`,
|
||||
...classes,
|
||||
]}
|
||||
>
|
||||
{`<${level}>${insert_nbsp(title)}</${level}>`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
91
src/back/jdd-components/icon-tiles-grid/icon-tiles-grid.css
Normal file
91
src/back/jdd-components/icon-tiles-grid/icon-tiles-grid.css
Normal file
@ -0,0 +1,91 @@
|
||||
.icon-tiles-grid {
|
||||
container-type: inline-size;
|
||||
|
||||
&.icon-tiles-grid--bleed {
|
||||
grid-column: bleed;
|
||||
}
|
||||
|
||||
.icon-tiles-grid__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 16px;
|
||||
width: max-content;
|
||||
max-width: 100cqw;
|
||||
margin: 0 auto;
|
||||
|
||||
@container (max-width: 900px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@container (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.icon-tiles-grid__tile {
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
background-color: var(--tile-bg-color);
|
||||
display: grid;
|
||||
grid-template-rows: subgrid;
|
||||
grid-row: span 2;
|
||||
flex-flow: column;
|
||||
gap: 32px;
|
||||
min-height: 320px;
|
||||
max-width: 400px;
|
||||
text-decoration: none; /* in case it's an a href */
|
||||
|
||||
&,
|
||||
* {
|
||||
color: var(--tile-fg-color);
|
||||
}
|
||||
|
||||
&:has(.icon),
|
||||
&:has(.icon-tiles__tile__cta) {
|
||||
grid-row: span 3;
|
||||
}
|
||||
|
||||
&:has(.icon):has(.icon-tiles__tile__cta) {
|
||||
grid-row: span 4;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
* {
|
||||
fill: var(--tile-fg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.icon-tiles__tile__cta {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
a[href].icon-tiles-grid__tile {
|
||||
.icon-tiles__tile__cta {
|
||||
text-align: right;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
|
||||
.icon-tiles__tile__cta {
|
||||
text-align: right;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments as Args, insert_nbsp } from "@sealcode/jdd";
|
||||
import { getAllIconNames, icon } from "src/back/icons.js";
|
||||
import { getColorVariables, getFgColorVariable } from "src/back/colors.js";
|
||||
|
||||
const component_arguments = {
|
||||
title: new Args.ShortText(),
|
||||
description: new Args.Markdown(),
|
||||
bleed: new Args.Boolean(),
|
||||
tiles: new Args.List(
|
||||
new Args.Structured({
|
||||
icon: new Args.Enum(async () => [
|
||||
"none",
|
||||
...(await getAllIconNames()),
|
||||
]).setDefaultValue("none"),
|
||||
color: new Args.Enum(async () =>
|
||||
getColorVariables().filter(
|
||||
(e) => !e.includes("text") && !e.includes("link")
|
||||
)
|
||||
),
|
||||
name: new Args.ShortText(),
|
||||
description: new Args.ShortText(),
|
||||
url: new Args.ShortText().setExampleValues([""]),
|
||||
cta: new Args.ShortText().setExampleValues([""]),
|
||||
})
|
||||
),
|
||||
} as const;
|
||||
|
||||
export class IconTilesGrid extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { title, description, tiles, bleed },
|
||||
classes,
|
||||
jdd_context: { render_markdown, language },
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
"icon-tiles-grid",
|
||||
{ "icon-tiles-grid--bleed": bleed },
|
||||
...classes,
|
||||
]}
|
||||
>
|
||||
<h2>{title}</h2>
|
||||
<div>{render_markdown(language, description)}</div>
|
||||
<section class="icon-tiles-grid__grid">
|
||||
{tiles.map((tile) => (
|
||||
<a
|
||||
class="icon-tiles-grid__tile"
|
||||
style={`--tile-bg-color: var(${tile.color}); --tile-fg-color: var(${getFgColorVariable(tile.color, 4.2)})`}
|
||||
href={tile.url ? tile.url : false}
|
||||
>
|
||||
{tile.icon != "none" ? (
|
||||
<div class="icon">{icon(tile.icon)}</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<div class="name">{insert_nbsp(tile.name)}</div>
|
||||
<div class="description">{insert_nbsp(tile.description)}</div>
|
||||
{tile.cta ? (
|
||||
<div class="icon-tiles__tile__cta">
|
||||
{insert_nbsp(tile.cta)}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
60
src/back/jdd-components/linktree/linktree.css
Normal file
60
src/back/jdd-components/linktree/linktree.css
Normal file
@ -0,0 +1,60 @@
|
||||
.linktree {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
--image-width: 200px;
|
||||
|
||||
.linktree__photo {
|
||||
picture {
|
||||
border-radius: calc(var(--image-width) / 2);
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: var(--image-width) !important;
|
||||
height: var(--image-width) !important;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.linktree__sections {
|
||||
width: max-content;
|
||||
max-width: 100cqw;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 8px;
|
||||
justify-content: stretch;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
a.button {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/back/jdd-components/linktree/linktree.jdd.tsx
Normal file
69
src/back/jdd-components/linktree/linktree.jdd.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments as Args, insert_nbsp } from "@sealcode/jdd";
|
||||
import { getAllIconNames } from "src/back/icons.js";
|
||||
import { button, button_variants } from "src/back/elements/button.js";
|
||||
|
||||
const component_arguments = {
|
||||
photo: new Args.Image(),
|
||||
title: new Args.ShortText(),
|
||||
description: new Args.Markdown(),
|
||||
sections: new Args.List(
|
||||
new Args.Structured({
|
||||
name: new Args.ShortText(),
|
||||
links: new Args.List(
|
||||
new Args.Structured({
|
||||
href: new Args.URL("absolute"),
|
||||
text: new Args.ShortText(),
|
||||
icon: new Args.Enum(getAllIconNames),
|
||||
variant: new Args.Enum(button_variants),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
} as const;
|
||||
|
||||
export class Linktree extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { photo, title, description, sections },
|
||||
classes,
|
||||
jdd_context: { render_markdown, render_image, language },
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div class={["linktree", ...classes]}>
|
||||
<div class="linktree__photo">
|
||||
{photo
|
||||
? render_image(photo, {
|
||||
sizesAttr: "200px",
|
||||
|
||||
container: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
objectFit: "cover",
|
||||
},
|
||||
alt: title || "",
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
{title ? <h2>{insert_nbsp(title)}</h2> : ""}
|
||||
{description ? <div>{render_markdown(language, description)}</div> : ""}
|
||||
<div class="linktree__sections">
|
||||
{sections.map((section) => (
|
||||
/* HTML */ <section>
|
||||
<h3>{section.name}</h3>
|
||||
<ul>
|
||||
{(section.links || []).map((link) => (
|
||||
<li>{button({ ...link, centered: true })}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,7 @@ const component_arguments = {
|
||||
href: new ComponentArguments.ShortText(),
|
||||
}),
|
||||
header_mode: new ComponentArguments.Enum(["below", "above"]),
|
||||
starting_index: new ComponentArguments.ShortText().setExampleValues(["1"]),
|
||||
starting_index: new ComponentArguments.ShortText().setDefaultValue("1"),
|
||||
} as const;
|
||||
|
||||
export class OrderedBlocks extends Component<typeof component_arguments> {
|
||||
|
||||
64
src/back/jdd-components/payment-details/payment-details.css
Normal file
64
src/back/jdd-components/payment-details/payment-details.css
Normal file
@ -0,0 +1,64 @@
|
||||
.payment-details {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
|
||||
@container (max-width: 680px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
background-color: var(--color-brand-canvas);
|
||||
box-shadow: 4px 4px #00000026;
|
||||
padding: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 24px;
|
||||
|
||||
&:first-child() {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.copy-section {
|
||||
label {
|
||||
color: var(--color-brand-accent);
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copy-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments, insert_nbsp } from "@sealcode/jdd";
|
||||
import { icon } from "src/back/icons.js";
|
||||
import generateQRCached from "src/back/qr.js";
|
||||
|
||||
const component_arguments = {
|
||||
title: new ComponentArguments.ShortText(),
|
||||
recipient: new ComponentArguments.ShortText(),
|
||||
account_no: new ComponentArguments.ShortText(),
|
||||
payment_title: new ComponentArguments.ShortText(),
|
||||
qr_title: new ComponentArguments.ShortText(),
|
||||
qr_subtitle: new ComponentArguments.ShortText(),
|
||||
} as const;
|
||||
|
||||
export class PaymentDetails extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { title, recipient, account_no, payment_title, qr_title, qr_subtitle },
|
||||
classes,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div class={["payment-details", ...classes]}>
|
||||
<div class="left">
|
||||
<h2>{insert_nbsp(title)}</h2>
|
||||
<div class="copy-section" data-controller="copy">
|
||||
<label>
|
||||
Odbiorca{" "}
|
||||
<button
|
||||
data-action="click->copy#copy"
|
||||
data-copy-target="button"
|
||||
>
|
||||
{icon("copy")}
|
||||
</button>
|
||||
</label>
|
||||
<div class="copy-value" data-copy-target="content">
|
||||
{recipient}
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy-section" data-controller="copy">
|
||||
<label>
|
||||
Numer rachunku{" "}
|
||||
<button
|
||||
data-action="click->copy#copy"
|
||||
data-copy-target="button"
|
||||
>
|
||||
{icon("copy")}
|
||||
</button>
|
||||
</label>
|
||||
<div class="copy-value" data-copy-target="content">
|
||||
{account_no}
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy-section" data-controller="copy">
|
||||
<label>
|
||||
Tytuł przelewu{" "}
|
||||
<button
|
||||
data-action="click->copy#copy"
|
||||
data-copy-target="button"
|
||||
>
|
||||
{icon("copy")}
|
||||
</button>
|
||||
</label>
|
||||
<div class="copy-value" data-copy-target="content">
|
||||
{payment_title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h2>{qr_title}</h2>
|
||||
<div class="qr">
|
||||
{generateQRCached(
|
||||
`|PL|${account_no.replaceAll(/[^0-9]/g, "")}|010000|${recipient.trim()}|${payment_title.trim()}|||`
|
||||
)}
|
||||
</div>
|
||||
<p>{qr_subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
.image-jdd {
|
||||
display: flex; /* to get rid of added whitespace */
|
||||
|
||||
picture {
|
||||
border-radius: 10px;
|
||||
.image-jdd--rounded & {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
|
||||
@ -12,6 +12,7 @@ const component_arguments = {
|
||||
image: new ComponentArguments.Image(),
|
||||
alt: new ComponentArguments.ShortText(),
|
||||
}),
|
||||
rounded_corners: new ComponentArguments.Boolean(),
|
||||
} as const;
|
||||
|
||||
export class Photo extends Component<typeof component_arguments> {
|
||||
@ -31,13 +32,20 @@ export class Photo extends Component<typeof component_arguments> {
|
||||
}
|
||||
|
||||
toHTML({
|
||||
args: { image },
|
||||
args: { image, rounded_corners },
|
||||
classes,
|
||||
jdd_context: { render_image },
|
||||
index,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
||||
return (
|
||||
<div class={["image-jdd", ...classes]} style={`--jdd-index: ${index}`}>
|
||||
<div
|
||||
class={[
|
||||
"image-jdd",
|
||||
{ "image-jdd--rounded": rounded_corners },
|
||||
...classes,
|
||||
]}
|
||||
style={`--jdd-index: ${index}`}
|
||||
>
|
||||
{render_image(image.image, {
|
||||
sizesAttr: "100vw",
|
||||
alt: image.alt,
|
||||
|
||||
10
src/back/jdd-components/quote/quote.css
Normal file
10
src/back/jdd-components/quote/quote.css
Normal file
@ -0,0 +1,10 @@
|
||||
.quote {
|
||||
background-color: var(--color-brand-accent);
|
||||
color: var(--color-brand-text-on-accent);
|
||||
padding: 16px;
|
||||
|
||||
blockquote {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
27
src/back/jdd-components/quote/quote.jdd.tsx
Normal file
27
src/back/jdd-components/quote/quote.jdd.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
|
||||
const component_arguments = {
|
||||
content: new ComponentArguments.Markdown(),
|
||||
signature: new ComponentArguments.ShortText(),
|
||||
} as const;
|
||||
|
||||
export class Quote extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { content, signature },
|
||||
classes,
|
||||
jdd_context: { render_markdown, language },
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div class={["quote", ...classes]}>
|
||||
<blockquote>{render_markdown(language, content)}</blockquote>
|
||||
<p>{signature}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,9 @@
|
||||
.split-view {
|
||||
width: 100%;
|
||||
border: 1px solid red;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
|
||||
@container (max-width: 640px) {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--row-gap);
|
||||
}
|
||||
grid-column: screen !important;
|
||||
|
||||
.split-view__half {
|
||||
flex-basis: 50%;
|
||||
@ -20,8 +14,138 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:nth-child(1) {
|
||||
grid-column: left-half;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
grid-column: right-half;
|
||||
}
|
||||
}
|
||||
|
||||
&.split-view--without-column-gap {
|
||||
.split-view__half {
|
||||
&:nth-child(1) {
|
||||
grid-column-end: center;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
grid-column-start: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.split-view--bleed--bleed {
|
||||
grid-column: bleed;
|
||||
|
||||
.split-view__half {
|
||||
&:nth-child(1) {
|
||||
grid-column-start: bleed-start;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
grid-column-end: bleed-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.split-view--bleed-left--screen {
|
||||
.split-view__half {
|
||||
&:nth-child(1) {
|
||||
grid-column-start: screen-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.split-view--bleed-right--screen {
|
||||
.split-view__half {
|
||||
&:nth-child(2) {
|
||||
grid-column-end: screen-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.split-view--bleed-left--bleed {
|
||||
.split-view__half {
|
||||
&:nth-child(1) {
|
||||
grid-column-start: bleed-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.split-view--bleed-right--bleed {
|
||||
.split-view__half {
|
||||
&:nth-child(2) {
|
||||
grid-column-end: bleed-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 640px) {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
row-gap: var(--row-gap);
|
||||
|
||||
&.split-view--bleed-top-mobile--none .split-view__half:nth-child(1) {
|
||||
grid-column: content !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-top-mobile--bleed .split-view__half:nth-child(1) {
|
||||
grid-column: bleed !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-top-mobile--screen .split-view__half:nth-child(1) {
|
||||
grid-column: screen !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-bottom-mobile--none .split-view__half:nth-child(2) {
|
||||
grid-column: content !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-bottom-mobile--bleed .split-view__half:nth-child(2) {
|
||||
grid-column: bleed !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-bottom-mobile--screen .split-view__half:nth-child(2) {
|
||||
grid-column: screen !important;
|
||||
}
|
||||
|
||||
&.split-view--swap-on-mobile {
|
||||
.split-view__half:nth-child(1) {
|
||||
grid-row: 2/3;
|
||||
}
|
||||
|
||||
.split-view__half:nth-child(2) {
|
||||
grid-row: 1/2;
|
||||
}
|
||||
|
||||
&.split-view--bleed-top-mobile--none .split-view__half:nth-child(2) {
|
||||
grid-column: content !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-top-mobile--bleed .split-view__half:nth-child(2) {
|
||||
grid-column: bleed !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-top-mobile--screen .split-view__half:nth-child(2) {
|
||||
grid-column: screen !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-bottom-mobile--none .split-view__half:nth-child(1) {
|
||||
grid-column: content !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-bottom-mobile--bleed .split-view__half:nth-child(1) {
|
||||
grid-column: bleed !important;
|
||||
}
|
||||
|
||||
&.split-view--bleed-bottom-mobile--screen .split-view__half:nth-child(1) {
|
||||
grid-column: screen !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.split-view--without-row-gap {
|
||||
row-gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,25 @@ import type {
|
||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
|
||||
const component_arguments = {
|
||||
bleed_left: new ComponentArguments.Enum(["none", "bleed", "screen"]).setDefaultValue(
|
||||
"none"
|
||||
),
|
||||
bleed_right: new ComponentArguments.Enum(["none", "bleed", "screen"]).setDefaultValue(
|
||||
"none"
|
||||
),
|
||||
bleed_top_mobile: new ComponentArguments.Enum([
|
||||
"none",
|
||||
"bleed",
|
||||
"screen",
|
||||
]).setDefaultValue("none"),
|
||||
bleed_bottom_mobile: new ComponentArguments.Enum([
|
||||
"none",
|
||||
"bleed",
|
||||
"screen",
|
||||
]).setDefaultValue("none"),
|
||||
column_gap: new ComponentArguments.Boolean(),
|
||||
row_gap: new ComponentArguments.Boolean(),
|
||||
swap_on_mobile: new ComponentArguments.Boolean(),
|
||||
component1: new ComponentArguments.NestedComponent(),
|
||||
component2: new ComponentArguments.NestedComponent(),
|
||||
} as const;
|
||||
@ -21,37 +40,43 @@ export class SplitView extends Component<typeof component_arguments> {
|
||||
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||
) {
|
||||
const result = [
|
||||
...ComponentArguments.NestedComponent.getCSSClumpsForNested(
|
||||
jdd_context,
|
||||
(args?.component1 as Record<string, unknown>) || {}
|
||||
),
|
||||
...ComponentArguments.NestedComponent.getCSSClumpsForNested(
|
||||
jdd_context,
|
||||
(args?.component2 as Record<string, unknown>) || {}
|
||||
),
|
||||
...(args?.component1?.getCSSClumps(jdd_context) || []),
|
||||
...(args?.component2?.getCSSClumps(jdd_context) || []),
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { component1, component2 },
|
||||
args: {
|
||||
component1,
|
||||
component2,
|
||||
bleed_left,
|
||||
bleed_right,
|
||||
bleed_top_mobile,
|
||||
bleed_bottom_mobile,
|
||||
column_gap,
|
||||
row_gap,
|
||||
swap_on_mobile,
|
||||
},
|
||||
classes,
|
||||
jdd_context,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
return (
|
||||
<div class={["split-view", ...classes]}>
|
||||
<div class="split-view__half">
|
||||
{ComponentArguments.NestedComponent.render({
|
||||
jdd_context,
|
||||
data: component1 as Record<string, unknown>,
|
||||
})}
|
||||
</div>
|
||||
<div class="split-view__half">
|
||||
{ComponentArguments.NestedComponent.render({
|
||||
jdd_context,
|
||||
data: component2 as Record<string, unknown>,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
class={[
|
||||
"split-view",
|
||||
`split-view--bleed-left--${bleed_left}`,
|
||||
`split-view--bleed-right--${bleed_right}`,
|
||||
`split-view--bleed-top-mobile--${bleed_top_mobile}`,
|
||||
`split-view--bleed-bottom-mobile--${bleed_bottom_mobile}`,
|
||||
`split-view--with${column_gap ? "" : "out"}-column-gap`,
|
||||
`split-view--with${row_gap ? "" : "out"}-row-gap`,
|
||||
{ "split-view--swap-on-mobile": swap_on_mobile },
|
||||
...classes,
|
||||
]}
|
||||
>
|
||||
<div class="split-view__half">{component1?.render(jdd_context)}</div>
|
||||
<div class="split-view__half">{component2?.render(jdd_context)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
117
src/back/jdd-components/subtree/subtree.css
Normal file
117
src/back/jdd-components/subtree/subtree.css
Normal file
@ -0,0 +1,117 @@
|
||||
.subtree {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
--vertical-padding: 0px;
|
||||
--horizontal-padding: 0px;
|
||||
padding: var(--vertical-padding) var(--horizontal-padding);
|
||||
|
||||
&.subtree--bleed-bleed {
|
||||
grid-column: bleed;
|
||||
}
|
||||
|
||||
&.subtree--bleed-screen {
|
||||
grid-column: screen;
|
||||
}
|
||||
|
||||
&.subtree--gap-none {
|
||||
gap: 0px;
|
||||
}
|
||||
|
||||
&.subtree--gap-narrow {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&.subtree--gap-medium {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&.subtree--gap-wide {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&.subtree--padding-vertical-none {
|
||||
--vertical-padding: 0px;
|
||||
}
|
||||
|
||||
&.subtree--padding-vertical-narrow {
|
||||
--vertical-padding: 8px;
|
||||
}
|
||||
|
||||
&.subtree--padding-vertical-medium {
|
||||
--vertical-padding: 16px;
|
||||
}
|
||||
|
||||
&.subtree--padding-vertical-wide {
|
||||
--vertical-padding: 24px;
|
||||
}
|
||||
|
||||
&.subtree--padding-vertical-extra-wide {
|
||||
--vertical-padding: 48px;
|
||||
}
|
||||
|
||||
&.subtree--padding-vertical-super-wide {
|
||||
--vertical-padding: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtree--mode-horizontal-scroller {
|
||||
.horizontal-scroller {
|
||||
.next-button,
|
||||
.prev-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-scroller.has-next .next-button,
|
||||
.horizontal-scroller.has-prev .prev-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scroller-wrapper {
|
||||
display: grid;
|
||||
--button-width: 60px;
|
||||
grid-template-columns:
|
||||
calc(var(--button-width) / 2) calc(var(--button-width) / 2)
|
||||
1fr calc(var(--button-width) / 2) calc(var(--button-width) / 2);
|
||||
|
||||
& > * {
|
||||
grid-column: 2/5;
|
||||
grid-row: 1/2;
|
||||
}
|
||||
|
||||
.button-panel {
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
|
||||
&.button-panel--left {
|
||||
grid-column: 1/3 !important;
|
||||
grid-row: 1/2;
|
||||
}
|
||||
|
||||
&.button-panel--right {
|
||||
grid-column: 4/6 !important;
|
||||
grid-row: 1/2;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-weight: bold;
|
||||
width: var(--button-width);
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-scroller__element {
|
||||
width: var(--horizontal-scroller-item-width) !important;
|
||||
max-width: calc(100cqw - 80px);
|
||||
flex-shrink: 0;
|
||||
container-type: inline-size;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.horizontal-scroller__markers {
|
||||
grid-row: 2/3;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/back/jdd-components/subtree/subtree.jdd.tsx
Normal file
125
src/back/jdd-components/subtree/subtree.jdd.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { TempstreamJSX } from "tempstream";
|
||||
import type {
|
||||
ComponentToHTMLArgs,
|
||||
ExtractStructuredComponentArgumentsParsed,
|
||||
JDDContext,
|
||||
} from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
import { horizontalScroller } from "src/back/routes/common/horizontal-scroller/horizontal-scroller.js";
|
||||
import { getColorVariables, getFgColorVariable } from "src/back/colors.js";
|
||||
|
||||
const component_arguments = {
|
||||
mode: new ComponentArguments.Enum(["column", "horizontal-scroller"]).setDefaultValue(
|
||||
"column"
|
||||
),
|
||||
scroller_mode_item_width: new ComponentArguments.Number().setDefaultValue(320),
|
||||
scroller_mode_arrows_color: new ComponentArguments.Enum(
|
||||
getColorVariables()
|
||||
).setDefaultValue("--color-brand-accent"),
|
||||
bleed: new ComponentArguments.Enum(["none", "bleed", "screen"]).setDefaultValue(
|
||||
"none"
|
||||
),
|
||||
gap: new ComponentArguments.Enum([
|
||||
"none",
|
||||
"narrow",
|
||||
"medium",
|
||||
"wide",
|
||||
]).setDefaultValue("medium"),
|
||||
padding_vertical: new ComponentArguments.Enum([
|
||||
"none",
|
||||
"narrow",
|
||||
"medium",
|
||||
"wide",
|
||||
"extra-wide",
|
||||
"super-wide",
|
||||
]).setDefaultValue("medium"),
|
||||
components: new ComponentArguments.List(new ComponentArguments.NestedComponent()),
|
||||
} as const;
|
||||
|
||||
export class Subtree extends Component<typeof component_arguments> {
|
||||
getArguments() {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
getCSSClumps(
|
||||
jdd_context: JDDContext,
|
||||
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||
) {
|
||||
const result = (args.components || [])
|
||||
.map((component) => component.getCSSClumps(jdd_context))
|
||||
.flat();
|
||||
return result;
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: {
|
||||
mode,
|
||||
components,
|
||||
gap,
|
||||
padding_vertical,
|
||||
bleed,
|
||||
scroller_mode_item_width,
|
||||
scroller_mode_arrows_color,
|
||||
},
|
||||
classes,
|
||||
jdd_context,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
const cls = [
|
||||
"subtree",
|
||||
`subtree--gap-${gap}`,
|
||||
`subtree--padding-vertical-${padding_vertical}`,
|
||||
`subtree--mode-${mode}`,
|
||||
`subtree--bleed-${bleed}`,
|
||||
...classes,
|
||||
];
|
||||
if (mode == "horizontal-scroller") {
|
||||
const button_styles = [
|
||||
`background-color: var(${scroller_mode_arrows_color})`,
|
||||
`color: var(${getFgColorVariable(scroller_mode_arrows_color)})`,
|
||||
].join(";");
|
||||
return (
|
||||
<div
|
||||
class={cls}
|
||||
style={`--horizontal-scroller-item-width: ${scroller_mode_item_width}px;`}
|
||||
>
|
||||
{horizontalScroller({
|
||||
render: async ({ scroller, markers }) => (
|
||||
<div class="scroller-wrapper">
|
||||
<div class="button-panel button-panel--left">
|
||||
<button
|
||||
class="button prev-button"
|
||||
type="button"
|
||||
data-action="horizontal-scroller#scrollLeft"
|
||||
style={button_styles}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
</div>
|
||||
<div class="button-panel button-panel--right">
|
||||
<button
|
||||
class="button next-button"
|
||||
type="button"
|
||||
data-action="horizontal-scroller#scrollRight"
|
||||
style={button_styles}
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
{scroller}
|
||||
{markers}
|
||||
</div>
|
||||
),
|
||||
elements: components?.map((component) =>
|
||||
component.render(jdd_context)
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div class={cls}>
|
||||
{components?.map((component) => component.render(jdd_context))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,14 @@
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
|
||||
&.tekst--size-small {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.tekst--size-large {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
text-align: justify;
|
||||
@ -40,4 +48,20 @@
|
||||
hyphens: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.side-content {
|
||||
float: right;
|
||||
max-width: 250px;
|
||||
margin: 0 0 16px 16px;
|
||||
container-type: inline-size;
|
||||
width: 100%;
|
||||
margin-right: calc(-1 * var(--bleed-outer-width));
|
||||
|
||||
@container (max-width: 570px) {
|
||||
float: none;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
margin-bottom: var(--row-gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,12 @@ const component_arguments = {
|
||||
"normal",
|
||||
"accent",
|
||||
"accent2",
|
||||
]).setExampleValues(["normal"]),
|
||||
]).setDefaultValue("normal"),
|
||||
size: new ComponentArguments.Enum(["small", "normal", "large"]).setDefaultValue(
|
||||
"normal"
|
||||
),
|
||||
content: new ComponentArguments.Markdown(),
|
||||
side_content: new ComponentArguments.NestedComponent(),
|
||||
} as const;
|
||||
|
||||
export class Tekst extends Component<typeof component_arguments> {
|
||||
@ -29,17 +33,36 @@ export class Tekst extends Component<typeof component_arguments> {
|
||||
return args.content.slice(0, 20) + "..." || null;
|
||||
}
|
||||
|
||||
getCSSClumps(
|
||||
jdd_context: JDDContext,
|
||||
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||
) {
|
||||
const result = args?.side_content?.getCSSClumps(jdd_context) || [];
|
||||
return result;
|
||||
}
|
||||
|
||||
toHTML({
|
||||
args: { color, content },
|
||||
args: { color, content, side_content, size },
|
||||
classes,
|
||||
jdd_context: { render_markdown, language },
|
||||
jdd_context,
|
||||
index,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
||||
const { render_markdown, language } = jdd_context;
|
||||
return (
|
||||
<div
|
||||
class={["tekst markdown", ...classes, `tekst--color-${color}`]}
|
||||
class={[
|
||||
"tekst markdown",
|
||||
...classes,
|
||||
`tekst--color-${color}`,
|
||||
`tekst--size-${size}`,
|
||||
]}
|
||||
style={`--jdd-index: ${index}`}
|
||||
>
|
||||
{side_content.isEmpty() ? (
|
||||
""
|
||||
) : (
|
||||
<div class="side-content">{side_content.render(jdd_context)}</div>
|
||||
)}
|
||||
{render_markdown(language, content)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,11 +2,13 @@ import { TempstreamJSX } from "tempstream";
|
||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||
import { PathFilePointer } from "@sealcode/file-manager";
|
||||
import { imageRouter } from "src/back/image-router.js";
|
||||
|
||||
const component_arguments = {
|
||||
file: new ComponentArguments.File(),
|
||||
max_height: new ComponentArguments.Number(),
|
||||
max_width: new ComponentArguments.Number(),
|
||||
cover_image: new ComponentArguments.Image().setExampleValues([null]),
|
||||
} as const;
|
||||
|
||||
export class Video extends Component<typeof component_arguments> {
|
||||
@ -15,14 +17,25 @@ export class Video extends Component<typeof component_arguments> {
|
||||
}
|
||||
|
||||
async toHTML({
|
||||
args: { file, max_width, max_height },
|
||||
args: { file, max_width, max_height, cover_image },
|
||||
classes,
|
||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||
const poster = cover_image
|
||||
? await imageRouter.singleImage(
|
||||
await cover_image.getPath(),
|
||||
max_width || 320,
|
||||
"jpg",
|
||||
false
|
||||
)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div class={["video", ...classes]}>
|
||||
{file instanceof PathFilePointer ? (
|
||||
<video
|
||||
controls
|
||||
loading="lazy"
|
||||
poster={poster}
|
||||
style={[
|
||||
`${max_width ? `max-width: min(${max_width}px, 100cqw);` : ""}`,
|
||||
`${max_height ? `max-height: min(${max_height}px, 80cqh);` : ""}`,
|
||||
|
||||
@ -9,7 +9,7 @@ import { registry } from "./jdd-components/registry.js";
|
||||
export function makeJDDContext(ctx: Context): JDDContext {
|
||||
return {
|
||||
language: "pl",
|
||||
...makeSimpleJDDContext(TheFileManager, registry),
|
||||
...makeSimpleJDDContext(TheFileManager, registry, "empty"),
|
||||
render_image: async (image: string | FilePointer | null, args) => {
|
||||
if (!image) {
|
||||
return "";
|
||||
|
||||
10
src/back/qr.ts
Normal file
10
src/back/qr.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { default as qrcode } from "qrcode-svg";
|
||||
|
||||
const qr_cache: Record<string, string> = {};
|
||||
|
||||
export default function generateQRCached(text: string) {
|
||||
if (!qr_cache[text]) {
|
||||
qr_cache[text] = new qrcode(text).svg();
|
||||
}
|
||||
return qr_cache[text];
|
||||
}
|
||||
134
src/back/routes/common/navbar.css
Normal file
134
src/back/routes/common/navbar.css
Normal file
@ -0,0 +1,134 @@
|
||||
.nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
ul.desktop-menu {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger-menu__icon {
|
||||
cursor: pointer;
|
||||
|
||||
svg * {
|
||||
fill: var(--color-brand-accent) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger-menu-toggle {
|
||||
display: none;
|
||||
anchor-name: --hamburger-toggle;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hamburger-menu__open-icon {
|
||||
display: block;
|
||||
anchor-name: --my-anchor;
|
||||
}
|
||||
|
||||
.hamburger-menu__close-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hamburger-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.hamburger-menu-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul.desktop-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hamburger-menu {
|
||||
position: fixed;
|
||||
position-anchor: --my-anchor;
|
||||
position-try-fallbacks: flip-inline;
|
||||
top: calc(anchor(top) - 2px);
|
||||
right: calc(anchor(right) - 14px);
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: opacity 50ms;
|
||||
pointer-events: none;
|
||||
box-shadow: -3px 3px var(--color-brand-canvas-07);
|
||||
background-color: var(--color-brand-canvas);
|
||||
flex-flow: column;
|
||||
padding: 12px;
|
||||
padding-top: 2px;
|
||||
align-items: flex-end;
|
||||
z-index: 10;
|
||||
|
||||
ul {
|
||||
display: flex !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
flex-flow: column;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
width: 50%;
|
||||
width: fit-content;
|
||||
row-gap: 12px;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
|
||||
a {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:has(#hamburger-menu-toggle-checkbox:checked) {
|
||||
.hamburger-menu__close-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hamburger-menu {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.hamburger-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: #0000002b;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Context } from "koa";
|
||||
import { button } from "src/back/elements/button.js";
|
||||
import { icon } from "src/back/icons.js";
|
||||
import { tempstream } from "tempstream";
|
||||
import type { FlatTemplatable } from "tempstream";
|
||||
import { breadcrumbs } from "./breadcrumbs.js";
|
||||
@ -27,6 +28,8 @@ export async function default_navbar(ctx: Context): Promise<FlatTemplatable> {
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const checkbox_id = `hamburger-menu-toggle-checkbox`;
|
||||
|
||||
return tempstream /* HTML */ `<nav>
|
||||
<a href="/" class="nav-logo">
|
||||
<img
|
||||
@ -37,10 +40,30 @@ export async function default_navbar(ctx: Context): Promise<FlatTemplatable> {
|
||||
style="width:220px; height: auto"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<ul>
|
||||
<ul class="desktop-menu">
|
||||
${linksHTML}
|
||||
</ul>
|
||||
<div class="hamburger-menu-toggle">
|
||||
<input type="checkbox" id="${checkbox_id}" />
|
||||
<label
|
||||
for="${checkbox_id}"
|
||||
class="hamburger-menu__icon hamburger-menu__open-icon"
|
||||
>
|
||||
${icon("hamburger")}
|
||||
</label>
|
||||
</div>
|
||||
<label for="${checkbox_id}" class="hamburger-backdrop"></label>
|
||||
<div class="hamburger-menu">
|
||||
<label
|
||||
for="${checkbox_id}"
|
||||
class="hamburger-menu__icon hamburger-menu__close-icon"
|
||||
>
|
||||
${icon("hamburger-close")}
|
||||
</label>
|
||||
<ul>
|
||||
${linksHTML}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
${breadcrumbs(ctx)} `;
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ export const customUrlView =
|
||||
showFooter: !hideNavigation,
|
||||
},
|
||||
body: (
|
||||
<main class="custom-page">
|
||||
<main class="custom-page subgrid-layout">
|
||||
{heading?.trim() ? <h1>{heading}</h1> : ""}
|
||||
{jdd.render()}
|
||||
</main>
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
/* see https://gwfh.mranftl.com/fonts/ for available weights */
|
||||
{
|
||||
"googleFonts": {
|
||||
"families": {
|
||||
"Atkinson Hyperlegible": true,
|
||||
"Libre Baskerville": true,
|
||||
"Ubuntu Mono": true,
|
||||
"Atkinson Hyperlegible": {
|
||||
"wght": [400, 700],
|
||||
"ital": [400, 700]
|
||||
},
|
||||
"Libre Baskerville": {
|
||||
"wght": [400, 500, 600, 700],
|
||||
"ital": [400, 500, 600, 700]
|
||||
},
|
||||
"Ubuntu Mono": {
|
||||
"wght": [400, 700],
|
||||
"ital": [400, 700]
|
||||
},
|
||||
"Anton": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,12 +16,18 @@ application.register("map-with-pins", MapWithPins);
|
||||
import { default as HorizontalScroller } from "./../back/routes/common/horizontal-scroller/horizontal-scroller.stimulus.js";
|
||||
application.register("horizontal-scroller", HorizontalScroller);
|
||||
|
||||
import { default as Copy } from "./controllers/copy.stimulus.js";
|
||||
application.register("copy", Copy);
|
||||
|
||||
import { default as Glightbox } from "./controllers/glightbox.stimulus.js";
|
||||
application.register("glightbox", Glightbox);
|
||||
|
||||
import { default as AutogrowTextarea } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/autogrow-textarea.stimulus.js";
|
||||
application.register("autogrow-textarea", AutogrowTextarea);
|
||||
|
||||
import { default as Autosubmit } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/autosubmit.stimulus.js";
|
||||
application.register("autosubmit", Autosubmit);
|
||||
|
||||
import { default as CodeSnippet } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/code-snippet.stimulus.js";
|
||||
application.register("code-snippet", CodeSnippet);
|
||||
|
||||
@ -37,6 +43,9 @@ application.register("input-image-preview", InputImagePreview);
|
||||
import { default as JddTablePaste } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/jdd-table-paste.stimulus.js";
|
||||
application.register("jdd-table-paste", JddTablePaste);
|
||||
|
||||
import { default as JsonDialog } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/json-dialog.stimulus.js";
|
||||
application.register("json-dialog", JsonDialog);
|
||||
|
||||
import { default as JsonEditor } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/json-editor.stimulus.js";
|
||||
application.register("json-editor", JsonEditor);
|
||||
|
||||
|
||||
85
src/front/controllers/copy.stimulus.ts
Normal file
85
src/front/controllers/copy.stimulus.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
export default class CopyController extends Controller {
|
||||
static targets = ["content", "button"];
|
||||
declare contentTarget: HTMLElement;
|
||||
declare buttonTarget: HTMLButtonElement;
|
||||
|
||||
private toast: HTMLDivElement | null = null;
|
||||
|
||||
private anchorName = `--copy-button-${crypto.randomUUID()}`;
|
||||
|
||||
connect() {
|
||||
// Anchor the toast to the copy button.
|
||||
(this.buttonTarget.style as any).anchorName = this.anchorName;
|
||||
}
|
||||
|
||||
async copy() {
|
||||
const value = this.contentTarget.textContent.trim();
|
||||
|
||||
if (!value) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
this.showToast();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.showToast("Nie udało się skopiować");
|
||||
}
|
||||
}
|
||||
|
||||
private showToast(message = "Skopiowano!") {
|
||||
this.removeToast();
|
||||
|
||||
const toast = document.createElement("div");
|
||||
|
||||
Object.assign(toast.style, {
|
||||
position: "fixed",
|
||||
positionAnchor: this.anchorName,
|
||||
positionArea: "right",
|
||||
marginLeft: "8px",
|
||||
|
||||
padding: "5px 8px",
|
||||
borderRadius: "6px",
|
||||
background: "#222",
|
||||
color: "#fff",
|
||||
fontSize: "12px",
|
||||
lineHeight: "1",
|
||||
whiteSpace: "nowrap",
|
||||
|
||||
opacity: "0",
|
||||
transform: "translateX(-4px)",
|
||||
transition: "opacity 120ms ease, transform 120ms ease",
|
||||
|
||||
pointerEvents: "none",
|
||||
zIndex: "1000",
|
||||
});
|
||||
|
||||
toast.textContent = message;
|
||||
|
||||
this.element.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateX(0)";
|
||||
});
|
||||
|
||||
this.toast = toast;
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateX(-4px)";
|
||||
|
||||
toast.addEventListener("transitionend", () => toast.remove(), {
|
||||
once: true,
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
private removeToast() {
|
||||
this.toast?.remove();
|
||||
this.toast = null;
|
||||
}
|
||||
}
|
||||
7
src/front/css.d.ts
vendored
Normal file
7
src/front/css.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
declare global {
|
||||
interface CSSStyleDeclaration {
|
||||
fieldSizing: string;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@ -31,3 +31,33 @@ export * from "./controllers.js";
|
||||
shouldPreserveScroll = false;
|
||||
});
|
||||
})();
|
||||
|
||||
/* fix for https://github.com/hotwired/turbo/issues/1255 */
|
||||
(() => {
|
||||
const markHashLinks = (root: ParentNode): void => {
|
||||
root.querySelectorAll<HTMLAnchorElement>('a[href^="#"]').forEach((link) => {
|
||||
link.dataset.turbo = "false";
|
||||
});
|
||||
};
|
||||
|
||||
markHashLinks(document);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (!(node instanceof Element)) return;
|
||||
|
||||
if (node instanceof HTMLAnchorElement && node.matches('a[href^="#"]')) {
|
||||
node.dataset.turbo = "false";
|
||||
}
|
||||
|
||||
markHashLinks(node);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
})();
|
||||
|
||||
41
src/icons/copy.svg
Normal file
41
src/icons/copy.svg
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
fill="currentColor"
|
||||
class="bi bi-copy"
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="copy.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="17.699693"
|
||||
inkscape:cx="13.361814"
|
||||
inkscape:cy="13.983294"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1048"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="m 8,4 a 4,4 0 0 1 4,-4 h 16 a 4,4 0 0 1 4,4 v 16 a 4,4 0 0 1 -4,4 H 12 A 4,4 0 0 1 8,20 Z m 4,-2 a 2,2 0 0 0 -2,2 v 16 a 2,2 0 0 0 2,2 h 16 a 2,2 0 0 0 2,-2 V 4 A 2,2 0 0 0 28,2 Z m -8,8 a 2,2 0 0 0 -2,2 v 16 a 2,2 0 0 0 2,2 h 16 a 2,2 0 0 0 2,-2 v -2 h 2 v 2 a 4,4 0 0 1 -4,4 H 4 A 4,4 0 0 1 0,28 V 12 A 4,4 0 0 1 4,8 h 2 v 2 z"
|
||||
id="path1"
|
||||
style="stroke-width:1.99999" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
67
src/icons/hamburger-close.svg
Normal file
67
src/icons/hamburger-close.svg
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="icon"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
sodipodi:docname="hamburger-close.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="7.6390567"
|
||||
inkscape:cx="9.6870599"
|
||||
inkscape:cy="26.377602"
|
||||
inkscape:window-width="1536"
|
||||
inkscape:window-height="832"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g2" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<style
|
||||
id="style1">
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g
|
||||
id="g1"
|
||||
transform="translate(-0.33652353,-1.2228587)">
|
||||
<g
|
||||
id="g2"
|
||||
transform="translate(0,1.89814)">
|
||||
<rect
|
||||
id="rect1"
|
||||
width="23.110205"
|
||||
height="3.0114162"
|
||||
x="10.832777"
|
||||
y="-2.2211621"
|
||||
inkscape:transform-center-x="-8.1456912"
|
||||
transform="rotate(45)"
|
||||
inkscape:transform-center-y="8.1456912" />
|
||||
<rect
|
||||
id="use2"
|
||||
width="23.110205"
|
||||
height="3.0114162"
|
||||
x="-12.270555"
|
||||
y="-23.893587"
|
||||
inkscape:transform-center-x="-7.9605633"
|
||||
transform="rotate(135)"
|
||||
inkscape:transform-center-y="-7.960561" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
69
src/icons/hamburger.svg
Normal file
69
src/icons/hamburger.svg
Normal file
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="icon"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
sodipodi:docname="hamburger.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="7.6390567"
|
||||
inkscape:cx="9.6870599"
|
||||
inkscape:cy="26.377602"
|
||||
inkscape:window-width="1536"
|
||||
inkscape:window-height="832"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g1" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<style
|
||||
id="style1">
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g
|
||||
id="g1"
|
||||
transform="translate(-0.33652353,-1.2228587)">
|
||||
<g
|
||||
id="g2"
|
||||
transform="translate(0,1.89814)">
|
||||
<rect
|
||||
style=""
|
||||
id="rect1"
|
||||
width="23.110205"
|
||||
height="3.0114162"
|
||||
x="4.7814212"
|
||||
y="8.0485344" />
|
||||
<use
|
||||
x="0"
|
||||
y="0"
|
||||
xlink:href="#rect1"
|
||||
id="use1"
|
||||
transform="translate(0,5.7704762)" />
|
||||
<use
|
||||
x="0"
|
||||
y="0"
|
||||
xlink:href="#use1"
|
||||
id="use2"
|
||||
transform="translate(0,5.7704762)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
66
src/main.css
66
src/main.css
@ -1,3 +1,4 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -27,7 +28,10 @@ a {
|
||||
[totalbleed-start] 1fr [bleed-start] var(--bleed-outer-width)
|
||||
[main-column-start] var(--column-padding)
|
||||
[content-start left-half-start] var(--half-width)
|
||||
[left-half-end] var(--margin-between-halves) [right-half-start] var(--half-width)
|
||||
[left-half-end] calc(var(--margin-between-halves) / 2) [center] calc(
|
||||
var(--margin-between-halves) / 2
|
||||
)
|
||||
[right-half-start] var(--half-width)
|
||||
[content-end right-half-end] var(--column-padding)
|
||||
[main-column-end] var(--bleed-outer-width)
|
||||
[bleed-end] 1fr [totalbleed-end] var(--screen-padding)
|
||||
@ -39,7 +43,7 @@ a {
|
||||
--column-padding: 16px;
|
||||
--target-column-width: 800px;
|
||||
--screen-padding: 16px;
|
||||
--margin-between-halves: 8px;
|
||||
--margin-between-halves: 32px;
|
||||
--total-horizontal-spacing: 2 * var(--screen-padding) - 2 * var(--column-padding) -
|
||||
var(--margin-between-halves);
|
||||
--half-width: min(
|
||||
@ -52,8 +56,9 @@ a {
|
||||
2
|
||||
)
|
||||
);
|
||||
--bleed-outer-target-width: 150px;
|
||||
--bleed-outer-width: min(
|
||||
150px,
|
||||
var(--bleed-outer-target-width),
|
||||
max(
|
||||
0px,
|
||||
calc(
|
||||
@ -73,7 +78,7 @@ a {
|
||||
}
|
||||
|
||||
@container (max-width: 600px) {
|
||||
--screen-padding: 0px;
|
||||
--screen-padding: 8px;
|
||||
}
|
||||
|
||||
@container (max-width: 400px) {
|
||||
@ -83,6 +88,16 @@ a {
|
||||
& > * {
|
||||
grid-column: content;
|
||||
}
|
||||
|
||||
& > .jdd-no-gap {
|
||||
margin-bottom: calc(-1 * var(--row-gap));
|
||||
margin-top: calc(-1 * var(--row-gap));
|
||||
|
||||
&:first-child,
|
||||
& + & {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subgrid-layout {
|
||||
@ -92,7 +107,17 @@ a {
|
||||
row-gap: var(--row-gap);
|
||||
|
||||
& > * {
|
||||
grid-column: main-column-content-start / main-column-content-end;
|
||||
grid-column: content;
|
||||
}
|
||||
|
||||
& > .jdd-no-gap {
|
||||
margin-bottom: calc(-1 * var(--row-gap));
|
||||
margin-top: calc(-1 * var(--row-gap));
|
||||
|
||||
&:first-child,
|
||||
& + & {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,37 +127,6 @@ a {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
margin-top: 24px;
|
||||
column-gap: 16px;
|
||||
row-gap: 16px;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
|
||||
a {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.jdd-editor {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user