Merge remote-tracking branch 'playground/master'

This commit is contained in:
Kuba Orlik 2026-08-17 19:55:03 +02:00
commit 7301e0e6dc
57 changed files with 16972 additions and 1537 deletions

16233
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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();

View File

@ -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(),
};
} }

View File

@ -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")}`;
}

View File

@ -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 || "";

View File

@ -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);
} }

View File

@ -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>`;
}

View 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>
);
}

View 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,
})),
});
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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>
);
}
}

View 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;
}
}

View 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>
);
}
}

View File

@ -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({

View File

@ -0,0 +1,5 @@
.crossover {
display: grid;
grid-template-columns: subgrid;
grid-column: screen !important;
}

View 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>
);
}
}

View File

@ -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>
); );
} }

View 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>
);
}
}

View File

@ -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(

View File

@ -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",

View 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);
}
}

View 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>
);
}
}

View 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;
}
}
}
}
}

View File

@ -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>
);
}
}

View 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;
}
}
}
}
}
}

View 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>
);
}
}

View File

@ -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> {

View 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;
}
}

View File

@ -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>
);
}
}

View File

@ -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;

View File

@ -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,

View 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;
}
}

View 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>
);
}
}

View File

@ -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;
}
} }

View File

@ -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>
); );
} }

View 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;
}
}
}

View 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>
);
}
}

View File

@ -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);
}
}
} }

View File

@ -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>
); );

View File

@ -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);` : ""}`,

View File

@ -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
View 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];
}

View 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);
}
}
}
}

View File

@ -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)} `;
} }

View File

@ -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>

View File

@ -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
} }
} }

View File

@ -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);

View 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
View File

@ -0,0 +1,7 @@
declare global {
interface CSSStyleDeclaration {
fieldSizing: string;
}
}
export {};

View File

@ -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
View 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

View 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
View 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

View File

@ -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;