Update jdd. Add a linktree-component

This commit is contained in:
Kuba Orlik 2026-08-06 17:20:13 +02:00
parent ea72b8d016
commit 4569bd5a1a
7 changed files with 200 additions and 41 deletions

14
package-lock.json generated
View File

@ -15,7 +15,7 @@
"@sealcode/add-to-head": "^1.0.0",
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.8.10",
"@sealcode/jdd-editor": "^0.2.30",
"@sealcode/jdd-editor": "^0.2.31",
"@sealcode/monaco-wrapper": "^0.0.11",
"@sealcode/sealgen": "^0.19.21",
"@sealcode/show-first-row": "^0.1.0",
@ -1380,9 +1380,9 @@
}
},
"node_modules/@sealcode/jdd-editor": {
"version": "0.2.30",
"resolved": "https://registry.npmjs.org/@sealcode/jdd-editor/-/jdd-editor-0.2.30.tgz",
"integrity": "sha512-b+f5xR4FrGc8MLSDKB9AJhehQX8c9NLP7M+towfNUIQNVVVEbecKgJuidmGtSOwrPuahcjRxF8nZqXpFEGdBlQ==",
"version": "0.2.31",
"resolved": "https://registry.npmjs.org/@sealcode/jdd-editor/-/jdd-editor-0.2.31.tgz",
"integrity": "sha512-mPxrh+YGN5q9zSSZU9fjp/M+kTnDb99MGkwEAlcB17dDPJr5enjQJHLMFanC+v/cV5+H7Qxfyj5Yi3+b7ICciQ==",
"dependencies": {
"@koa/router": "^13.1.0",
"@sealcode/jdd": "^0.8.10",
@ -16016,9 +16016,9 @@
}
},
"@sealcode/jdd-editor": {
"version": "0.2.30",
"resolved": "https://registry.npmjs.org/@sealcode/jdd-editor/-/jdd-editor-0.2.30.tgz",
"integrity": "sha512-b+f5xR4FrGc8MLSDKB9AJhehQX8c9NLP7M+towfNUIQNVVVEbecKgJuidmGtSOwrPuahcjRxF8nZqXpFEGdBlQ==",
"version": "0.2.31",
"resolved": "https://registry.npmjs.org/@sealcode/jdd-editor/-/jdd-editor-0.2.31.tgz",
"integrity": "sha512-mPxrh+YGN5q9zSSZU9fjp/M+kTnDb99MGkwEAlcB17dDPJr5enjQJHLMFanC+v/cV5+H7Qxfyj5Yi3+b7ICciQ==",
"requires": {
"@koa/router": "^13.1.0",
"@sealcode/jdd": "^0.8.10",

View File

@ -98,7 +98,7 @@
"@sealcode/add-to-head": "^1.0.0",
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.8.10",
"@sealcode/jdd-editor": "^0.2.30",
"@sealcode/jdd-editor": "^0.2.31",
"@sealcode/monaco-wrapper": "^0.0.11",
"@sealcode/sealgen": "^0.19.21",
"@sealcode/show-first-row": "^0.1.0",

View File

@ -1,9 +1,15 @@
.button {
--icon-width: 32px;
--gap: 8px;
padding: 8px 16px;
display: inline-block;
display: inline-flex;
flex-flow: row;
align-items: center;
gap: 8px;
text-decoration: none;
margin: 4px;
cursor: pointer;
color: var(--button-fg-color);
&:hover {
filter: brightness(1.1);
@ -13,14 +19,25 @@
opacity: 0.5;
pointer-events: none;
}
svg * {
fill: var(--button-fg-color) !important;
}
}
.button--centered {
span {
text-align: center;
margin-right: calc(var(--gap) + var(--icon-width));
}
}
.button--color-accent1 {
background-color: var(--color-brand-accent);
color: var(--color-brand-text-on-accent);
--button-fg-color: var(--color-brand-text-on-accent);
}
.button--color-accent2 {
background-color: var(--color-brand-accent2);
color: var(--color-brand-text-on-accent2);
--button-fg-color: var(--color-brand-text-on-accent2);
}

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