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",
|
"@koa/router": "^12.0.1",
|
||||||
"@sealcode/add-to-head": "^1.0.0",
|
"@sealcode/add-to-head": "^1.0.0",
|
||||||
"@sealcode/file-manager": "^1.0.2",
|
"@sealcode/file-manager": "^1.0.2",
|
||||||
"@sealcode/jdd": "^0.8.6",
|
"@sealcode/jdd": "^0.8.13",
|
||||||
"@sealcode/jdd-editor": "^0.2.23",
|
"@sealcode/jdd-editor": "^0.2.37",
|
||||||
"@sealcode/monaco-wrapper": "^0.0.4",
|
"@sealcode/monaco-wrapper": "^0.0.12",
|
||||||
"@sealcode/sealgen": "^0.19.21",
|
"@sealcode/sealgen": "^0.19.23",
|
||||||
"@sealcode/show-first-row": "^0.1.0",
|
"@sealcode/show-first-row": "^0.1.0",
|
||||||
"@sealcode/simplemde": "^1.12.1",
|
"@sealcode/simplemde": "^1.12.1",
|
||||||
"@sealcode/sortable": "^0.0.1",
|
"@sealcode/sortable": "^0.0.1",
|
||||||
@ -112,7 +112,9 @@
|
|||||||
"@types/kill-port": "^2.0.0",
|
"@types/kill-port": "^2.0.0",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/object-hash": "^3.0.6",
|
"@types/object-hash": "^3.0.6",
|
||||||
|
"@types/qrcode-svg": "^1.1.5",
|
||||||
"@types/simplemde": "^1.11.11",
|
"@types/simplemde": "^1.11.11",
|
||||||
|
"@types/wcag-contrast": "^3.0.3",
|
||||||
"a11y.css": "^5.3.0",
|
"a11y.css": "^5.3.0",
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
@ -125,9 +127,10 @@
|
|||||||
"js-convert-case": "^4.2.0",
|
"js-convert-case": "^4.2.0",
|
||||||
"koa-mount": "^4.2.0",
|
"koa-mount": "^4.2.0",
|
||||||
"koa-qs": "^3.0.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",
|
"koa-static": "^5.0.0",
|
||||||
"locreq": "^3.0.0",
|
"locreq": "^3.0.0",
|
||||||
|
"mailparser": "^3.9.15",
|
||||||
"meilisearch": "^0.41.0",
|
"meilisearch": "^0.41.0",
|
||||||
"monaco-editor": "^0.55.1",
|
"monaco-editor": "^0.55.1",
|
||||||
"mongodb": "^6.17.0",
|
"mongodb": "^6.17.0",
|
||||||
@ -136,13 +139,15 @@
|
|||||||
"normalize-diacritics": "^5.0.0",
|
"normalize-diacritics": "^5.0.0",
|
||||||
"nyc": "^17.1.0",
|
"nyc": "^17.1.0",
|
||||||
"object-path": "^0.11.8",
|
"object-path": "^0.11.8",
|
||||||
|
"qrcode-svg": "^1.1.0",
|
||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"sealious": "^0.21.30",
|
"sealious": "^0.21.30",
|
||||||
"slug": "^9.1.0",
|
"slug": "^9.1.0",
|
||||||
"stimulus": "^3.2.2",
|
"stimulus": "^3.2.2",
|
||||||
"tempstream": "^0.4.6",
|
"tempstream": "^0.4.6",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"turndown": "^7.2.0"
|
"turndown": "^7.2.0",
|
||||||
|
"wcag-contrast": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.54.1",
|
"@playwright/test": "1.54.1",
|
||||||
@ -152,6 +157,7 @@
|
|||||||
"@types/koa-mount": "^4.0.5",
|
"@types/koa-mount": "^4.0.5",
|
||||||
"@types/koa-qs": "^2.0.3",
|
"@types/koa-qs": "^2.0.3",
|
||||||
"@types/koa-static": "^4.0.4",
|
"@types/koa-static": "^4.0.4",
|
||||||
|
"@types/mailparser": "^3.4.6",
|
||||||
"@types/node": "^20.8.4",
|
"@types/node": "^20.8.4",
|
||||||
"@types/object-path": "^0.11.4",
|
"@types/object-path": "^0.11.4",
|
||||||
"@types/slug": "^5.0.9",
|
"@types/slug": "^5.0.9",
|
||||||
|
|||||||
@ -16,6 +16,10 @@ import {
|
|||||||
MONGO_PORT,
|
MONGO_PORT,
|
||||||
MONGO_USERNAME,
|
MONGO_USERNAME,
|
||||||
PORT,
|
PORT,
|
||||||
|
SMTP_HOST,
|
||||||
|
SMTP_LOGIN,
|
||||||
|
SMTP_PASSWORD,
|
||||||
|
SMTP_PORT,
|
||||||
} from "./config.js";
|
} from "./config.js";
|
||||||
import ADMIN_CREDENTIALS from "./default-admin-credentials.js";
|
import ADMIN_CREDENTIALS from "./default-admin-credentials.js";
|
||||||
import { TheFileManager } from "./file-manager.js";
|
import { TheFileManager } from "./file-manager.js";
|
||||||
@ -94,7 +98,17 @@ export default class TheApp extends App {
|
|||||||
user: "any",
|
user: "any",
|
||||||
password: "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() {
|
async start() {
|
||||||
await super.start();
|
await super.start();
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { Collection, FieldTypes, Policies } from "sealious";
|
import { Collection, FieldTypes, Policies } from "sealious";
|
||||||
|
import { Roles } from "../policy-types/roles.js";
|
||||||
|
|
||||||
export default class NavbarLinks extends Collection {
|
export default class NavbarLinks extends Collection {
|
||||||
fields = {
|
fields = {
|
||||||
label: new FieldTypes.Text(),
|
label: new FieldTypes.Text(),
|
||||||
href: 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 { colord } from "colord";
|
||||||
|
import { hex as measureHexContrast } from "wcag-contrast";
|
||||||
|
|
||||||
export const colors: Record<string, Record<string, string>> = {
|
export const colors: Record<string, Record<string, string>> = {
|
||||||
basic: {
|
basic: {
|
||||||
@ -68,3 +69,108 @@ export function shade(color: string, shade_step: number /* 0-9 */) {
|
|||||||
shifted = shifted.rotate(hue_step * steps);
|
shifted = shifted.rotate(hue_step * steps);
|
||||||
return shifted.toHex();
|
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_MASTER_KEY = process.env.MEILISEARCH_MASTER_KEY || "qwerty";
|
||||||
export const MEILISEARCH_HOST = process.env.MEILISEARCH_HOST || "http://localhost:1098";
|
export const MEILISEARCH_HOST = process.env.MEILISEARCH_HOST || "http://localhost:1098";
|
||||||
export const SCAN_A11Y = false;
|
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 {
|
.button {
|
||||||
|
--icon-width: 32px;
|
||||||
|
--gap: 8px;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
color: var(--button-fg-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
@ -13,14 +21,25 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
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 {
|
.button--color-accent1 {
|
||||||
background-color: var(--color-brand-accent);
|
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 {
|
.button--color-accent2 {
|
||||||
background-color: var(--color-brand-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);
|
const locreq = _locreq(import.meta.dirname);
|
||||||
|
|
||||||
|
let all_icon_names: Promise<string[]> | null = null;
|
||||||
|
|
||||||
const cache = {} as Record<string, Promise<string> | undefined>;
|
const cache = {} as Record<string, Promise<string> | undefined>;
|
||||||
|
|
||||||
export const svg_icon_path = "src/icons";
|
export const svg_icon_path = "src/icons";
|
||||||
@ -26,3 +28,12 @@ export async function icon(icon_name: string) {
|
|||||||
return "";
|
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 = {
|
const component_arguments = {
|
||||||
title: new ComponentArguments.ShortText(),
|
title: new ComponentArguments.ShortText(),
|
||||||
imagesPerPage: new ComponentArguments.ShortText().setExampleValues(["6"]),
|
imagesPerPage: new ComponentArguments.ShortText().setDefaultValue("6"),
|
||||||
background_mode: new ComponentArguments.Enum([
|
background_mode: new ComponentArguments.Enum([
|
||||||
"transparent",
|
"transparent",
|
||||||
"white",
|
"white",
|
||||||
"white-square",
|
"white-square",
|
||||||
]),
|
]),
|
||||||
speed: new ComponentArguments.ShortText().setExampleValues(["5"]),
|
speed: new ComponentArguments.ShortText().setDefaultValue("5"),
|
||||||
desktop_interval: new ComponentArguments.ShortText().setExampleValues(["5"]),
|
desktop_interval: new ComponentArguments.ShortText().setDefaultValue("5"),
|
||||||
endless: new ComponentArguments.Enum(["true", "false"]),
|
endless: new ComponentArguments.Enum(["true", "false"]),
|
||||||
images,
|
images,
|
||||||
} as const;
|
} 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";
|
import { calculateDifference } from "./difference.js";
|
||||||
|
|
||||||
const component_arguments = {
|
const component_arguments = {
|
||||||
date: new ComponentArguments.ShortText().setExampleValues(["2025-08-05"]),
|
date: new ComponentArguments.ShortText().setDefaultValue("2028-08-05"),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function renderBlock({
|
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 { TempstreamJSX } from "tempstream";
|
||||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
import type {
|
||||||
|
ComponentToHTMLArgs,
|
||||||
|
ExtractStructuredComponentArgumentsParsed,
|
||||||
|
JDDContext,
|
||||||
|
} from "@sealcode/jdd";
|
||||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
@ -13,6 +17,14 @@ export class CustomCssWrapper extends Component<typeof component_arguments> {
|
|||||||
return 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({
|
async toHTML({
|
||||||
args: { custom_css, content },
|
args: { custom_css, content },
|
||||||
classes,
|
classes,
|
||||||
@ -26,10 +38,7 @@ export class CustomCssWrapper extends Component<typeof component_arguments> {
|
|||||||
.${unique_class}{ ${custom_css} }
|
.${unique_class}{ ${custom_css} }
|
||||||
</style>`
|
</style>`
|
||||||
}
|
}
|
||||||
{ComponentArguments.NestedComponent.render({
|
{content.render(jdd_context)}
|
||||||
jdd_context,
|
|
||||||
data: content as Record<string, unknown>,
|
|
||||||
})}
|
|
||||||
</div>
|
</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(),
|
title: new ComponentArguments.ShortText(),
|
||||||
content: new ComponentArguments.Structured({
|
content: new ComponentArguments.Structured({
|
||||||
text: new ComponentArguments.Markdown(),
|
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"]),
|
accordion_color: new ComponentArguments.Enum(["accent", "accent2"]),
|
||||||
faq: new ComponentArguments.List(
|
faq: new ComponentArguments.List(
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import { Component, ComponentArguments } from "@sealcode/jdd";
|
|||||||
|
|
||||||
const component_arguments = {
|
const component_arguments = {
|
||||||
title: new ComponentArguments.ShortText(),
|
title: new ComponentArguments.ShortText(),
|
||||||
level: new ComponentArguments.Enum(["1", "2", "3", "4", "5", "6"]).setExampleValues([
|
level: new ComponentArguments.Enum(["1", "2", "3", "4", "5", "6"]).setDefaultValue(
|
||||||
"2",
|
"2"
|
||||||
]),
|
),
|
||||||
blur_backdrop: new ComponentArguments.Enum(["true", "false"]),
|
blur_backdrop: new ComponentArguments.Enum(["true", "false"]),
|
||||||
font_family: new ComponentArguments.Enum([
|
font_family: new ComponentArguments.Enum([
|
||||||
"industrial",
|
"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(),
|
href: new ComponentArguments.ShortText(),
|
||||||
}),
|
}),
|
||||||
header_mode: new ComponentArguments.Enum(["below", "above"]),
|
header_mode: new ComponentArguments.Enum(["below", "above"]),
|
||||||
starting_index: new ComponentArguments.ShortText().setExampleValues(["1"]),
|
starting_index: new ComponentArguments.ShortText().setDefaultValue("1"),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class OrderedBlocks extends Component<typeof component_arguments> {
|
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 {
|
.image-jdd {
|
||||||
|
display: flex; /* to get rid of added whitespace */
|
||||||
|
|
||||||
picture {
|
picture {
|
||||||
border-radius: 10px;
|
.image-jdd--rounded & {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const component_arguments = {
|
|||||||
image: new ComponentArguments.Image(),
|
image: new ComponentArguments.Image(),
|
||||||
alt: new ComponentArguments.ShortText(),
|
alt: new ComponentArguments.ShortText(),
|
||||||
}),
|
}),
|
||||||
|
rounded_corners: new ComponentArguments.Boolean(),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class Photo extends Component<typeof component_arguments> {
|
export class Photo extends Component<typeof component_arguments> {
|
||||||
@ -31,13 +32,20 @@ export class Photo extends Component<typeof component_arguments> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toHTML({
|
toHTML({
|
||||||
args: { image },
|
args: { image, rounded_corners },
|
||||||
classes,
|
classes,
|
||||||
jdd_context: { render_image },
|
jdd_context: { render_image },
|
||||||
index,
|
index,
|
||||||
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
||||||
return (
|
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, {
|
{render_image(image.image, {
|
||||||
sizesAttr: "100vw",
|
sizesAttr: "100vw",
|
||||||
alt: image.alt,
|
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 {
|
.split-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid red;
|
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: subgrid;
|
grid-template-columns: subgrid;
|
||||||
|
grid-column: screen !important;
|
||||||
@container (max-width: 640px) {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
gap: var(--row-gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-view__half {
|
.split-view__half {
|
||||||
flex-basis: 50%;
|
flex-basis: 50%;
|
||||||
@ -20,8 +14,138 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
grid-column: left-half;
|
||||||
|
}
|
||||||
|
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
grid-column: right-half;
|
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";
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
|
|
||||||
const component_arguments = {
|
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(),
|
component1: new ComponentArguments.NestedComponent(),
|
||||||
component2: new ComponentArguments.NestedComponent(),
|
component2: new ComponentArguments.NestedComponent(),
|
||||||
} as const;
|
} as const;
|
||||||
@ -21,37 +40,43 @@ export class SplitView extends Component<typeof component_arguments> {
|
|||||||
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
|
||||||
) {
|
) {
|
||||||
const result = [
|
const result = [
|
||||||
...ComponentArguments.NestedComponent.getCSSClumpsForNested(
|
...(args?.component1?.getCSSClumps(jdd_context) || []),
|
||||||
jdd_context,
|
...(args?.component2?.getCSSClumps(jdd_context) || []),
|
||||||
(args?.component1 as Record<string, unknown>) || {}
|
|
||||||
),
|
|
||||||
...ComponentArguments.NestedComponent.getCSSClumpsForNested(
|
|
||||||
jdd_context,
|
|
||||||
(args?.component2 as Record<string, unknown>) || {}
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async toHTML({
|
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,
|
classes,
|
||||||
jdd_context,
|
jdd_context,
|
||||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||||
return (
|
return (
|
||||||
<div class={["split-view", ...classes]}>
|
<div
|
||||||
<div class="split-view__half">
|
class={[
|
||||||
{ComponentArguments.NestedComponent.render({
|
"split-view",
|
||||||
jdd_context,
|
`split-view--bleed-left--${bleed_left}`,
|
||||||
data: component1 as Record<string, unknown>,
|
`split-view--bleed-right--${bleed_right}`,
|
||||||
})}
|
`split-view--bleed-top-mobile--${bleed_top_mobile}`,
|
||||||
</div>
|
`split-view--bleed-bottom-mobile--${bleed_bottom_mobile}`,
|
||||||
<div class="split-view__half">
|
`split-view--with${column_gap ? "" : "out"}-column-gap`,
|
||||||
{ComponentArguments.NestedComponent.render({
|
`split-view--with${row_gap ? "" : "out"}-row-gap`,
|
||||||
jdd_context,
|
{ "split-view--swap-on-mobile": swap_on_mobile },
|
||||||
data: component2 as Record<string, unknown>,
|
...classes,
|
||||||
})}
|
]}
|
||||||
</div>
|
>
|
||||||
|
<div class="split-view__half">{component1?.render(jdd_context)}</div>
|
||||||
|
<div class="split-view__half">{component2?.render(jdd_context)}</div>
|
||||||
</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;
|
font-size: 16px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
|
||||||
|
&.tekst--size-small {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tekst--size-large {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
@ -40,4 +48,20 @@
|
|||||||
hyphens: auto;
|
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",
|
"normal",
|
||||||
"accent",
|
"accent",
|
||||||
"accent2",
|
"accent2",
|
||||||
]).setExampleValues(["normal"]),
|
]).setDefaultValue("normal"),
|
||||||
|
size: new ComponentArguments.Enum(["small", "normal", "large"]).setDefaultValue(
|
||||||
|
"normal"
|
||||||
|
),
|
||||||
content: new ComponentArguments.Markdown(),
|
content: new ComponentArguments.Markdown(),
|
||||||
|
side_content: new ComponentArguments.NestedComponent(),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class Tekst extends Component<typeof component_arguments> {
|
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;
|
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({
|
toHTML({
|
||||||
args: { color, content },
|
args: { color, content, side_content, size },
|
||||||
classes,
|
classes,
|
||||||
jdd_context: { render_markdown, language },
|
jdd_context,
|
||||||
index,
|
index,
|
||||||
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
|
||||||
|
const { render_markdown, language } = jdd_context;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={["tekst markdown", ...classes, `tekst--color-${color}`]}
|
class={[
|
||||||
|
"tekst markdown",
|
||||||
|
...classes,
|
||||||
|
`tekst--color-${color}`,
|
||||||
|
`tekst--size-${size}`,
|
||||||
|
]}
|
||||||
style={`--jdd-index: ${index}`}
|
style={`--jdd-index: ${index}`}
|
||||||
>
|
>
|
||||||
|
{side_content.isEmpty() ? (
|
||||||
|
""
|
||||||
|
) : (
|
||||||
|
<div class="side-content">{side_content.render(jdd_context)}</div>
|
||||||
|
)}
|
||||||
{render_markdown(language, content)}
|
{render_markdown(language, content)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,11 +2,13 @@ import { TempstreamJSX } from "tempstream";
|
|||||||
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||||
import { Component, ComponentArguments } from "@sealcode/jdd";
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
||||||
import { PathFilePointer } from "@sealcode/file-manager";
|
import { PathFilePointer } from "@sealcode/file-manager";
|
||||||
|
import { imageRouter } from "src/back/image-router.js";
|
||||||
|
|
||||||
const component_arguments = {
|
const component_arguments = {
|
||||||
file: new ComponentArguments.File(),
|
file: new ComponentArguments.File(),
|
||||||
max_height: new ComponentArguments.Number(),
|
max_height: new ComponentArguments.Number(),
|
||||||
max_width: new ComponentArguments.Number(),
|
max_width: new ComponentArguments.Number(),
|
||||||
|
cover_image: new ComponentArguments.Image().setExampleValues([null]),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class Video extends Component<typeof component_arguments> {
|
export class Video extends Component<typeof component_arguments> {
|
||||||
@ -15,14 +17,25 @@ export class Video extends Component<typeof component_arguments> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async toHTML({
|
async toHTML({
|
||||||
args: { file, max_width, max_height },
|
args: { file, max_width, max_height, cover_image },
|
||||||
classes,
|
classes,
|
||||||
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||||
|
const poster = cover_image
|
||||||
|
? await imageRouter.singleImage(
|
||||||
|
await cover_image.getPath(),
|
||||||
|
max_width || 320,
|
||||||
|
"jpg",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={["video", ...classes]}>
|
<div class={["video", ...classes]}>
|
||||||
{file instanceof PathFilePointer ? (
|
{file instanceof PathFilePointer ? (
|
||||||
<video
|
<video
|
||||||
controls
|
controls
|
||||||
|
loading="lazy"
|
||||||
|
poster={poster}
|
||||||
style={[
|
style={[
|
||||||
`${max_width ? `max-width: min(${max_width}px, 100cqw);` : ""}`,
|
`${max_width ? `max-width: min(${max_width}px, 100cqw);` : ""}`,
|
||||||
`${max_height ? `max-height: min(${max_height}px, 80cqh);` : ""}`,
|
`${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 {
|
export function makeJDDContext(ctx: Context): JDDContext {
|
||||||
return {
|
return {
|
||||||
language: "pl",
|
language: "pl",
|
||||||
...makeSimpleJDDContext(TheFileManager, registry),
|
...makeSimpleJDDContext(TheFileManager, registry, "empty"),
|
||||||
render_image: async (image: string | FilePointer | null, args) => {
|
render_image: async (image: string | FilePointer | null, args) => {
|
||||||
if (!image) {
|
if (!image) {
|
||||||
return "";
|
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 type { Context } from "koa";
|
||||||
import { button } from "src/back/elements/button.js";
|
import { button } from "src/back/elements/button.js";
|
||||||
|
import { icon } from "src/back/icons.js";
|
||||||
import { tempstream } from "tempstream";
|
import { tempstream } from "tempstream";
|
||||||
import type { FlatTemplatable } from "tempstream";
|
import type { FlatTemplatable } from "tempstream";
|
||||||
import { breadcrumbs } from "./breadcrumbs.js";
|
import { breadcrumbs } from "./breadcrumbs.js";
|
||||||
@ -27,6 +28,8 @@ export async function default_navbar(ctx: Context): Promise<FlatTemplatable> {
|
|||||||
)
|
)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
|
const checkbox_id = `hamburger-menu-toggle-checkbox`;
|
||||||
|
|
||||||
return tempstream /* HTML */ `<nav>
|
return tempstream /* HTML */ `<nav>
|
||||||
<a href="/" class="nav-logo">
|
<a href="/" class="nav-logo">
|
||||||
<img
|
<img
|
||||||
@ -37,10 +40,30 @@ export async function default_navbar(ctx: Context): Promise<FlatTemplatable> {
|
|||||||
style="width:220px; height: auto"
|
style="width:220px; height: auto"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<ul class="desktop-menu">
|
||||||
<ul>
|
|
||||||
${linksHTML}
|
${linksHTML}
|
||||||
</ul>
|
</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>
|
</nav>
|
||||||
${breadcrumbs(ctx)} `;
|
${breadcrumbs(ctx)} `;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export const customUrlView =
|
|||||||
showFooter: !hideNavigation,
|
showFooter: !hideNavigation,
|
||||||
},
|
},
|
||||||
body: (
|
body: (
|
||||||
<main class="custom-page">
|
<main class="custom-page subgrid-layout">
|
||||||
{heading?.trim() ? <h1>{heading}</h1> : ""}
|
{heading?.trim() ? <h1>{heading}</h1> : ""}
|
||||||
{jdd.render()}
|
{jdd.render()}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -1,9 +1,19 @@
|
|||||||
|
/* see https://gwfh.mranftl.com/fonts/ for available weights */
|
||||||
{
|
{
|
||||||
"googleFonts": {
|
"googleFonts": {
|
||||||
"families": {
|
"families": {
|
||||||
"Atkinson Hyperlegible": true,
|
"Atkinson Hyperlegible": {
|
||||||
"Libre Baskerville": true,
|
"wght": [400, 700],
|
||||||
"Ubuntu Mono": true,
|
"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
|
"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";
|
import { default as HorizontalScroller } from "./../back/routes/common/horizontal-scroller/horizontal-scroller.stimulus.js";
|
||||||
application.register("horizontal-scroller", HorizontalScroller);
|
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";
|
import { default as Glightbox } from "./controllers/glightbox.stimulus.js";
|
||||||
application.register("glightbox", Glightbox);
|
application.register("glightbox", Glightbox);
|
||||||
|
|
||||||
import { default as AutogrowTextarea } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/autogrow-textarea.stimulus.js";
|
import { default as AutogrowTextarea } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/autogrow-textarea.stimulus.js";
|
||||||
application.register("autogrow-textarea", AutogrowTextarea);
|
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";
|
import { default as CodeSnippet } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/code-snippet.stimulus.js";
|
||||||
application.register("code-snippet", CodeSnippet);
|
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";
|
import { default as JddTablePaste } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/jdd-table-paste.stimulus.js";
|
||||||
application.register("jdd-table-paste", JddTablePaste);
|
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";
|
import { default as JsonEditor } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/json-editor.stimulus.js";
|
||||||
application.register("json-editor", JsonEditor);
|
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;
|
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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@ -27,7 +28,10 @@ a {
|
|||||||
[totalbleed-start] 1fr [bleed-start] var(--bleed-outer-width)
|
[totalbleed-start] 1fr [bleed-start] var(--bleed-outer-width)
|
||||||
[main-column-start] var(--column-padding)
|
[main-column-start] var(--column-padding)
|
||||||
[content-start left-half-start] var(--half-width)
|
[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)
|
[content-end right-half-end] var(--column-padding)
|
||||||
[main-column-end] var(--bleed-outer-width)
|
[main-column-end] var(--bleed-outer-width)
|
||||||
[bleed-end] 1fr [totalbleed-end] var(--screen-padding)
|
[bleed-end] 1fr [totalbleed-end] var(--screen-padding)
|
||||||
@ -39,7 +43,7 @@ a {
|
|||||||
--column-padding: 16px;
|
--column-padding: 16px;
|
||||||
--target-column-width: 800px;
|
--target-column-width: 800px;
|
||||||
--screen-padding: 16px;
|
--screen-padding: 16px;
|
||||||
--margin-between-halves: 8px;
|
--margin-between-halves: 32px;
|
||||||
--total-horizontal-spacing: 2 * var(--screen-padding) - 2 * var(--column-padding) -
|
--total-horizontal-spacing: 2 * var(--screen-padding) - 2 * var(--column-padding) -
|
||||||
var(--margin-between-halves);
|
var(--margin-between-halves);
|
||||||
--half-width: min(
|
--half-width: min(
|
||||||
@ -52,8 +56,9 @@ a {
|
|||||||
2
|
2
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
--bleed-outer-target-width: 150px;
|
||||||
--bleed-outer-width: min(
|
--bleed-outer-width: min(
|
||||||
150px,
|
var(--bleed-outer-target-width),
|
||||||
max(
|
max(
|
||||||
0px,
|
0px,
|
||||||
calc(
|
calc(
|
||||||
@ -73,7 +78,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 600px) {
|
@container (max-width: 600px) {
|
||||||
--screen-padding: 0px;
|
--screen-padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 400px) {
|
@container (max-width: 400px) {
|
||||||
@ -83,6 +88,16 @@ a {
|
|||||||
& > * {
|
& > * {
|
||||||
grid-column: content;
|
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 {
|
.subgrid-layout {
|
||||||
@ -92,7 +107,17 @@ a {
|
|||||||
row-gap: var(--row-gap);
|
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;
|
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 {
|
body.jdd-editor {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user