@struq/skill 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -100
- package/package.json +5 -3
- package/skills/struq/SKILL.md +168 -165
- package/skills/struq/blocks/CATALOG.md +49 -46
- package/skills/struq/blocks/backgrounds/background-aurora.tsx +2 -1
- package/skills/struq/blocks/backgrounds/background-facets.tsx +5 -3
- package/skills/struq/blocks/backgrounds/background-silk.tsx +6 -2
- package/skills/struq/blocks/catalog.json +50 -5
- package/skills/struq/blocks/film.json +11 -7
- package/skills/struq/blocks/footers/footer-contact.tsx +8 -78
- package/skills/struq/blocks/headers/header-bar.tsx +144 -29
- package/skills/struq/blocks/headers/menu-curtain.tsx +36 -19
- package/skills/struq/blocks/heroes/hero-poster.tsx +36 -10
- package/skills/struq/blocks/kit/HeroCanvas.tsx +6 -2
- package/skills/struq/blocks/kit/SplitHeadline.tsx +2 -1
- package/skills/struq/blocks/kit/accent.ts +29 -3
- package/skills/struq/blocks/kit/blocks.css +530 -6
- package/skills/struq/blocks/kit/feel.ts +183 -0
- package/skills/struq/blocks/kit/gsap.ts +9 -6
- package/skills/struq/blocks/kit/middle/PlaceHours.tsx +254 -0
- package/skills/struq/blocks/kit/middle/ProcessStrip.tsx +428 -0
- package/skills/struq/blocks/kit/middle/ServicesList.tsx +496 -0
- package/skills/struq/blocks/kit/middle/entrance.ts +98 -0
- package/skills/struq/blocks/kit/middle/hours.ts +154 -0
- package/skills/struq/blocks/kit/middle/index.ts +14 -0
- package/skills/struq/blocks/kit/motion/HoverLink.tsx +33 -0
- package/skills/struq/blocks/kit/motion/ImageCurtain.tsx +79 -0
- package/skills/struq/blocks/kit/motion/MaskWipe.tsx +91 -0
- package/skills/struq/blocks/kit/motion/Pressable.tsx +58 -0
- package/skills/struq/blocks/kit/motion/RevealText.tsx +103 -0
- package/skills/struq/blocks/kit/motion/Rise.tsx +77 -0
- package/skills/struq/blocks/kit/motion/ScrubLine.tsx +108 -0
- package/skills/struq/blocks/kit/motion/index.ts +11 -0
- package/skills/struq/blocks/kit/quiet/Close.tsx +178 -52
- package/skills/struq/blocks/kit/quiet/Statement.tsx +3 -2
- package/skills/struq/blocks/kit/subject/AppScreen.tsx +388 -0
- package/skills/struq/blocks/kit/subject/Document.tsx +742 -0
- package/skills/struq/blocks/kit/subject/Mark.tsx +44 -0
- package/skills/struq/blocks/kit/subject/PatternPaper.tsx +410 -0
- package/skills/struq/blocks/kit/subject/SectionDrawing.tsx +349 -0
- package/skills/struq/blocks/kit/subject/Ticket.tsx +354 -0
- package/skills/struq/blocks/kit/subject/build.ts +66 -0
- package/skills/struq/blocks/kit/subject/draw.ts +196 -0
- package/skills/struq/blocks/kit/subject/drawings.ts +373 -0
- package/skills/struq/blocks/kit/subject/format.ts +122 -0
- package/skills/struq/blocks/kit/subject/index.ts +11 -0
- package/skills/struq/blocks/kit/subject/patterns.ts +204 -0
- package/skills/struq/blocks/preloaders/preloader-stack.tsx +75 -22
- package/skills/struq/blocks/showpieces/hero-opener.tsx +55 -7
- package/skills/struq/blocks/showpieces/image-to-fullbleed.tsx +64 -6
- package/skills/struq/blocks/showpieces/manifesto-scrub.tsx +56 -3
- package/skills/struq/blocks/showpieces/panel-wipe.tsx +93 -11
- package/skills/struq/blocks/showpieces/wordmark-finale.tsx +38 -5
- package/skills/struq/canon/canon.json +1 -1
- package/skills/struq/references/art-direction.md +51 -0
- package/skills/struq/scripts/blocks.mjs +56 -20
- package/skills/struq/scripts/canon.mjs +36 -7
- package/skills/struq/scripts/check.mjs +417 -11
- package/skills/struq/scripts/draw.mjs +49 -0
- package/skills/struq/scripts/film-check.mjs +28 -4
- package/skills/struq/scripts/gate.mjs +13 -3
- package/skills/struq/scripts/hand.mjs +222 -0
- package/skills/struq/scripts/images.mjs +12 -3
- package/skills/struq/scripts/look.mjs +1184 -0
- package/skills/struq/scripts/stamp.mjs +47 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// components/struq/kit/subject/AppScreen.tsx (Struq recipe)
|
|
3
|
+
// One designed screen of the client's own software, never a generic dashboard: a window or a phone drawn in the
|
|
4
|
+
// page's palette (no borrowed OS chrome, no brand marks but the client's), composed from a few typed slots: a list, a
|
|
5
|
+
// detail, a timeline, a form state, or any piece passed as children (a Document, set on paper="scene"). It reads as
|
|
6
|
+
// the client's product because it is their nouns, their statuses and their tokens.
|
|
7
|
+
//
|
|
8
|
+
// <AppScreen product="Kraan Werkbon" nav={["Werkbonnen", "Facturen", "Klanten", "Planning"]} active={1}
|
|
9
|
+
// title="Facturen" action="Nieuwe factuur" columns={["Klant", "Bedrag", "Status"]}
|
|
10
|
+
// list={[{ title: "Familie de Wit", meta: "2026-0142", value: "€ 213,57", status: "Verstuurd" }]}
|
|
11
|
+
// statuses={{ Verstuurd: "open", Betaald: "done", "Te laat": "late" }} change={{ item: 0, to: "Betaald" }}
|
|
12
|
+
// detail={{ title: "Familie de Wit", meta: "Lindelaan 4, Rotterdam", fields: [{ label: "Factuur", value: "2026-0142" }] }} />
|
|
13
|
+
//
|
|
14
|
+
// Props
|
|
15
|
+
// - product: the software's name, beside its mark in the top bar. logo: optional image src (otherwise a monogram).
|
|
16
|
+
// - frame: "window" (default) or "phone".
|
|
17
|
+
// - nav: up to six section names; active: the index shown as current. A window shows them in a side column, a
|
|
18
|
+
// phone in its tab bar (the first four).
|
|
19
|
+
// - title: the screen's own heading. action: optional primary button label. user: optional name for the account disc.
|
|
20
|
+
// - list: { title, meta?, value?, status? }[], the screen's records. selected: the index shown open in the detail.
|
|
21
|
+
// columns: optional head labels for the list's three columns (title, value, status).
|
|
22
|
+
// - detail: { title, meta?, fields?: { label, value }[], text? }: the selected record.
|
|
23
|
+
// - timeline: { time, text, done? }[]: the record's history, newest last.
|
|
24
|
+
// - form: { title?, fields: { label, value?, placeholder?, state?: "focus" | "error", hint? }[], submit }: a form
|
|
25
|
+
// caught mid-fill (one field focused, one perhaps in error).
|
|
26
|
+
// - children: anything else for the detail pane, a <Document paper="scene" /> above all.
|
|
27
|
+
// - statuses: how each status word reads: "done" (a filled accent dot), "open" (a ring), "late" (a filled ink dot,
|
|
28
|
+
// the word in medium weight) or "quiet". Words not listed read as "quiet".
|
|
29
|
+
// - change: { item, to }: the one state change. As the screen scrolls up past the middle of the view, the list's
|
|
30
|
+
// item `item` moves from its status to `to` (Sent to Paid). One per screen, at most.
|
|
31
|
+
// - caption: a line under the frame, in the scene's ink. build: "scroll" (default) runs the change; "static" shows
|
|
32
|
+
// the screen after it.
|
|
33
|
+
// - className: on the figure.
|
|
34
|
+
//
|
|
35
|
+
// Mounting: inside any section. A window wants eight or more of twelve columns on a wide screen and reflows on a
|
|
36
|
+
// phone (no side column, the panes stacked); a phone frame wants four. The root is a <figure data-subject="app">.
|
|
37
|
+
//
|
|
38
|
+
// Motion: one state change and nothing else: the row's status crossfades to its new word and dot while the row's
|
|
39
|
+
// ground warms for a moment, played forward as the screen passes the middle of the view and back when it leaves
|
|
40
|
+
// downward. Reduced motion, build="static" or no scripts: the screen as it stands after the change.
|
|
41
|
+
import { useRef, type ReactNode } from "react";
|
|
42
|
+
import { gsap, useGSAP } from "../gsap";
|
|
43
|
+
import { FIGURES } from "./format";
|
|
44
|
+
import { Mark } from "./Mark";
|
|
45
|
+
|
|
46
|
+
type Tone = "done" | "open" | "late" | "quiet";
|
|
47
|
+
type Item = { title: string; meta?: string; value?: string; status?: string };
|
|
48
|
+
type Detail = { title: string; meta?: string; fields?: { label: string; value: string }[]; text?: string };
|
|
49
|
+
type Moment = { time: string; text: string; done?: boolean };
|
|
50
|
+
type FormField = { label: string; value?: string; placeholder?: string; state?: "focus" | "error"; hint?: string };
|
|
51
|
+
type Form = { title?: string; fields: FormField[]; submit: string };
|
|
52
|
+
|
|
53
|
+
type Props = {
|
|
54
|
+
product: string;
|
|
55
|
+
logo?: string;
|
|
56
|
+
frame?: "window" | "phone";
|
|
57
|
+
nav?: string[];
|
|
58
|
+
active?: number;
|
|
59
|
+
title: string;
|
|
60
|
+
action?: string;
|
|
61
|
+
user?: string;
|
|
62
|
+
list?: Item[];
|
|
63
|
+
selected?: number;
|
|
64
|
+
columns?: [string, string?, string?];
|
|
65
|
+
detail?: Detail;
|
|
66
|
+
timeline?: Moment[];
|
|
67
|
+
form?: Form;
|
|
68
|
+
children?: ReactNode;
|
|
69
|
+
statuses?: Record<string, Tone>;
|
|
70
|
+
change?: { item: number; to: string };
|
|
71
|
+
caption?: string;
|
|
72
|
+
build?: "scroll" | "static";
|
|
73
|
+
className?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const SMALL = "text-[0.75rem] leading-[1.45]";
|
|
77
|
+
const BODY = "text-[0.8125rem] leading-[1.5] @3xl:text-[0.875rem]";
|
|
78
|
+
|
|
79
|
+
function Status({ word, tone }: { word: string; tone: Tone }) {
|
|
80
|
+
return (
|
|
81
|
+
<span className={`inline-flex items-center gap-1.5 whitespace-nowrap ${SMALL} ${tone === "late" ? "font-medium text-ink" : tone === "quiet" ? "text-ink-muted" : "text-ink"}`}>
|
|
82
|
+
<span
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
className={`size-[0.4375rem] shrink-0 rounded-full ${
|
|
85
|
+
tone === "done" ? "bg-accent" : tone === "late" ? "bg-[color:var(--ink)]" : tone === "open" ? "border border-[color:var(--ink-muted)]" : "bg-[color:var(--line)]"
|
|
86
|
+
}`}
|
|
87
|
+
/>
|
|
88
|
+
{word}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function AppScreen({
|
|
94
|
+
product,
|
|
95
|
+
logo,
|
|
96
|
+
frame = "window",
|
|
97
|
+
nav = [],
|
|
98
|
+
active = 0,
|
|
99
|
+
title,
|
|
100
|
+
action,
|
|
101
|
+
user,
|
|
102
|
+
list,
|
|
103
|
+
selected,
|
|
104
|
+
columns,
|
|
105
|
+
detail,
|
|
106
|
+
timeline,
|
|
107
|
+
form,
|
|
108
|
+
children,
|
|
109
|
+
statuses = {},
|
|
110
|
+
change,
|
|
111
|
+
caption,
|
|
112
|
+
build = "scroll",
|
|
113
|
+
className = "",
|
|
114
|
+
}: Props) {
|
|
115
|
+
const root = useRef<HTMLElement>(null);
|
|
116
|
+
const toneOf = (word?: string): Tone => (word ? (statuses[word] ?? "quiet") : "quiet");
|
|
117
|
+
const pick = selected ?? (change ? change.item : 0);
|
|
118
|
+
|
|
119
|
+
useGSAP(
|
|
120
|
+
() => {
|
|
121
|
+
const el = root.current;
|
|
122
|
+
if (!el || !change || build !== "scroll") return;
|
|
123
|
+
const row = el.querySelector<HTMLElement>("[data-app-change]");
|
|
124
|
+
const before = row?.querySelector<HTMLElement>("[data-app-before]");
|
|
125
|
+
const after = row?.querySelector<HTMLElement>("[data-app-after]");
|
|
126
|
+
if (!row || !before || !after) return;
|
|
127
|
+
const mm = gsap.matchMedia();
|
|
128
|
+
mm.add("(prefers-reduced-motion: no-preference)", () => {
|
|
129
|
+
gsap.set(after, { autoAlpha: 0, y: 6 });
|
|
130
|
+
gsap.set(before, { autoAlpha: 1, y: 0 });
|
|
131
|
+
const tl = gsap.timeline({
|
|
132
|
+
paused: true,
|
|
133
|
+
defaults: { duration: 0.42, ease: "power2.out" },
|
|
134
|
+
scrollTrigger: { trigger: row, start: "top 55%", toggleActions: "play none none reverse" },
|
|
135
|
+
});
|
|
136
|
+
tl.to(before, { autoAlpha: 0, y: -6 })
|
|
137
|
+
.to(after, { autoAlpha: 1, y: 0 }, "<0.12")
|
|
138
|
+
.fromTo(row, { "--app-glow": "0%" }, { "--app-glow": "9%", duration: 0.3 }, "<")
|
|
139
|
+
.to(row, { "--app-glow": "0%", duration: 1.1, ease: "power1.inOut" }, ">0.15");
|
|
140
|
+
});
|
|
141
|
+
return () => mm.revert();
|
|
142
|
+
},
|
|
143
|
+
{ scope: root, dependencies: [change?.item, change?.to, build] },
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const rows = list ? (
|
|
147
|
+
<ListPane items={list} columns={columns} pick={pick} change={change} toneOf={toneOf} motion={build === "scroll"} />
|
|
148
|
+
) : null;
|
|
149
|
+
const side =
|
|
150
|
+
children ??
|
|
151
|
+
(detail || timeline || form ? (
|
|
152
|
+
<div className="grid gap-7">
|
|
153
|
+
{detail ? <DetailPane detail={detail} /> : null}
|
|
154
|
+
{timeline ? <TimelinePane moments={timeline} /> : null}
|
|
155
|
+
{form ? <FormPane form={form} /> : null}
|
|
156
|
+
</div>
|
|
157
|
+
) : null);
|
|
158
|
+
|
|
159
|
+
if (frame === "phone") {
|
|
160
|
+
const tabs = nav.slice(0, 4);
|
|
161
|
+
return (
|
|
162
|
+
<figure ref={root} data-subject="app" data-frame-kind="phone" className={`@container m-0 ${className}`}>
|
|
163
|
+
<div className="mx-auto w-full max-w-[22rem] rounded-[2.75rem] border border-line bg-surface-2 p-[0.5rem]">
|
|
164
|
+
<div className="relative flex aspect-[9/19] flex-col overflow-hidden rounded-[2.3rem] bg-surface-0 text-ink">
|
|
165
|
+
<div aria-hidden="true" className="mx-auto mt-2.5 h-[1.375rem] w-[5.5rem] shrink-0 rounded-full bg-surface-2" />
|
|
166
|
+
<div className="flex items-center justify-between px-5 pt-4">
|
|
167
|
+
<div className="flex items-center gap-2">
|
|
168
|
+
<Mark name={product} logo={logo} size={1.5} />
|
|
169
|
+
<span className={`${SMALL} font-medium`}>{product}</span>
|
|
170
|
+
</div>
|
|
171
|
+
{user ? <Mark name={user} size={1.5} shape="round" className="opacity-90" /> : null}
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex items-end justify-between gap-3 px-5 pt-5">
|
|
174
|
+
<p className="m-0 font-display text-[1.75rem] leading-[1.05] tracking-[-0.01em]">{title}</p>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="min-h-0 flex-1 overflow-hidden px-5 pt-4">
|
|
177
|
+
{rows}
|
|
178
|
+
{side ? <div className={rows ? "mt-6" : ""}>{side}</div> : null}
|
|
179
|
+
</div>
|
|
180
|
+
{action ? (
|
|
181
|
+
<div className="px-5 pb-3 pt-3">
|
|
182
|
+
<span className={`block rounded-[0.625rem] bg-[color:var(--ink)] py-2.5 text-center ${SMALL} font-medium text-surface-0`}>{action}</span>
|
|
183
|
+
</div>
|
|
184
|
+
) : null}
|
|
185
|
+
{tabs.length ? (
|
|
186
|
+
<nav aria-label={product} className="grid shrink-0 border-t border-line px-2 pb-5 pt-2.5" style={{ gridTemplateColumns: `repeat(${tabs.length}, 1fr)` }}>
|
|
187
|
+
{tabs.map((tab, i) => (
|
|
188
|
+
<span key={tab} className={`flex flex-col items-center gap-1 text-center ${SMALL} ${i === active ? "font-medium text-ink" : "text-ink-muted"}`}>
|
|
189
|
+
<span aria-hidden="true" className={`size-[0.3125rem] rounded-full ${i === active ? "bg-accent" : "bg-transparent"}`} />
|
|
190
|
+
<span className="max-w-full truncate">{tab}</span>
|
|
191
|
+
</span>
|
|
192
|
+
))}
|
|
193
|
+
</nav>
|
|
194
|
+
) : null}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
{caption ? <figcaption className="mx-auto mt-4 max-w-[22rem] font-body text-sheet-label text-ink-muted">{caption}</figcaption> : null}
|
|
198
|
+
</figure>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<figure ref={root} data-subject="app" data-frame-kind="window" className={`@container m-0 ${className}`}>
|
|
204
|
+
<div className="overflow-hidden rounded-[0.875rem] border border-line bg-surface-1 text-ink">
|
|
205
|
+
<div className="flex items-center justify-between gap-4 border-b border-line px-4 py-3 @3xl:px-5">
|
|
206
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
207
|
+
<Mark name={product} logo={logo} size={1.625} shape="square" />
|
|
208
|
+
<span className={`truncate ${BODY} font-medium`}>{product}</span>
|
|
209
|
+
</div>
|
|
210
|
+
{user ? (
|
|
211
|
+
<span className="flex items-center gap-2">
|
|
212
|
+
<span className={`hidden ${SMALL} text-ink-muted @2xl:inline`}>{user}</span>
|
|
213
|
+
<Mark name={user} size={1.625} />
|
|
214
|
+
</span>
|
|
215
|
+
) : null}
|
|
216
|
+
</div>
|
|
217
|
+
<div className="grid @3xl:grid-cols-[11.5rem_1fr]">
|
|
218
|
+
{nav.length ? (
|
|
219
|
+
<nav aria-label={product} className="hidden border-r border-line px-3 py-5 @3xl:block">
|
|
220
|
+
<ul className="m-0 grid list-none gap-0.5 p-0">
|
|
221
|
+
{nav.map((item, i) => (
|
|
222
|
+
<li
|
|
223
|
+
key={item}
|
|
224
|
+
className={`rounded-[0.4375rem] px-3 py-2 ${BODY} ${i === active ? "bg-surface-2 font-medium text-ink" : "text-ink-muted"}`}
|
|
225
|
+
>
|
|
226
|
+
{item}
|
|
227
|
+
</li>
|
|
228
|
+
))}
|
|
229
|
+
</ul>
|
|
230
|
+
</nav>
|
|
231
|
+
) : null}
|
|
232
|
+
<div className="min-w-0 bg-surface-0">
|
|
233
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-line px-5 py-4 @3xl:px-7 @3xl:py-5">
|
|
234
|
+
<p className="m-0 font-display text-[1.5rem] leading-[1.1] tracking-[-0.01em] @3xl:text-[1.875rem]">{title}</p>
|
|
235
|
+
{action ? (
|
|
236
|
+
<span className={`rounded-[0.4375rem] bg-[color:var(--ink)] px-3.5 py-2 ${SMALL} font-medium text-surface-0`}>{action}</span>
|
|
237
|
+
) : null}
|
|
238
|
+
</div>
|
|
239
|
+
<div className={`grid ${rows && side ? "@4xl:grid-cols-[minmax(0,1fr)_minmax(0,0.9fr)]" : ""}`}>
|
|
240
|
+
{rows ? <div className="min-w-0 px-5 py-4 @3xl:px-7 @3xl:py-5">{rows}</div> : null}
|
|
241
|
+
{side ? (
|
|
242
|
+
<div className={`min-w-0 px-5 py-5 @3xl:px-7 @3xl:py-6 ${rows ? "border-t border-line @4xl:border-l @4xl:border-t-0" : ""}`}>{side}</div>
|
|
243
|
+
) : null}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
{caption ? <figcaption className="mt-4 font-body text-sheet-label text-ink-muted">{caption}</figcaption> : null}
|
|
249
|
+
</figure>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function ListPane({
|
|
254
|
+
items,
|
|
255
|
+
columns,
|
|
256
|
+
pick,
|
|
257
|
+
change,
|
|
258
|
+
toneOf,
|
|
259
|
+
motion,
|
|
260
|
+
}: {
|
|
261
|
+
items: Item[];
|
|
262
|
+
columns?: [string, string?, string?];
|
|
263
|
+
pick: number;
|
|
264
|
+
change?: { item: number; to: string };
|
|
265
|
+
toneOf: (word?: string) => Tone;
|
|
266
|
+
motion: boolean;
|
|
267
|
+
}) {
|
|
268
|
+
return (
|
|
269
|
+
<div>
|
|
270
|
+
{columns ? (
|
|
271
|
+
<div className={`grid grid-cols-[1fr_auto] gap-x-4 border-b border-line pb-2 ${SMALL} text-ink-muted @xl:grid-cols-[1fr_7rem_6.5rem]`}>
|
|
272
|
+
<span>{columns[0]}</span>
|
|
273
|
+
<span className="hidden text-right @xl:block">{columns[1]}</span>
|
|
274
|
+
<span className="text-right @xl:text-left">{columns[2]}</span>
|
|
275
|
+
</div>
|
|
276
|
+
) : null}
|
|
277
|
+
<ul className="m-0 list-none p-0">
|
|
278
|
+
{items.map((item, i) => {
|
|
279
|
+
const changes = change?.item === i;
|
|
280
|
+
const status = changes && !motion ? change.to : item.status;
|
|
281
|
+
return (
|
|
282
|
+
<li
|
|
283
|
+
key={`${item.title}-${i}`}
|
|
284
|
+
data-app-change={changes ? "" : undefined}
|
|
285
|
+
className={`subject-app-row -mx-2 grid grid-cols-[1fr_auto] items-center gap-x-4 rounded-[0.4375rem] border-b border-line px-2 py-3 last:border-b-0 @xl:grid-cols-[1fr_7rem_6.5rem] ${
|
|
286
|
+
i === pick ? "bg-surface-1" : ""
|
|
287
|
+
}`}
|
|
288
|
+
>
|
|
289
|
+
<span className="min-w-0">
|
|
290
|
+
<span className={`block truncate ${BODY} font-medium`}>{item.title}</span>
|
|
291
|
+
{item.meta || item.value ? (
|
|
292
|
+
<span className={`block truncate ${SMALL} text-ink-muted`}>
|
|
293
|
+
{item.meta}
|
|
294
|
+
{item.value ? <span className={`@xl:hidden ${FIGURES}`}>{item.meta ? `, ${item.value}` : item.value}</span> : null}
|
|
295
|
+
</span>
|
|
296
|
+
) : null}
|
|
297
|
+
</span>
|
|
298
|
+
<span className={`hidden text-right ${BODY} @xl:block ${FIGURES}`}>{item.value}</span>
|
|
299
|
+
<span className="grid justify-items-end @xl:justify-items-start">
|
|
300
|
+
{changes && motion ? (
|
|
301
|
+
<>
|
|
302
|
+
<span data-app-before="" className="col-start-1 row-start-1 invisible">
|
|
303
|
+
{item.status ? <Status word={item.status} tone={toneOf(item.status)} /> : null}
|
|
304
|
+
</span>
|
|
305
|
+
<span data-app-after="" className="col-start-1 row-start-1">
|
|
306
|
+
<Status word={change.to} tone={toneOf(change.to)} />
|
|
307
|
+
</span>
|
|
308
|
+
</>
|
|
309
|
+
) : status ? (
|
|
310
|
+
<Status word={status} tone={toneOf(status)} />
|
|
311
|
+
) : null}
|
|
312
|
+
</span>
|
|
313
|
+
</li>
|
|
314
|
+
);
|
|
315
|
+
})}
|
|
316
|
+
</ul>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function DetailPane({ detail }: { detail: Detail }) {
|
|
322
|
+
return (
|
|
323
|
+
<div>
|
|
324
|
+
<p className="m-0 font-display text-[1.25rem] leading-[1.2] @3xl:text-[1.5rem]">{detail.title}</p>
|
|
325
|
+
{detail.meta ? <p className={`m-0 mt-1 ${SMALL} text-ink-muted`}>{detail.meta}</p> : null}
|
|
326
|
+
{detail.fields?.length ? (
|
|
327
|
+
<dl className="m-0 mt-5 grid grid-cols-2 gap-x-6 gap-y-4">
|
|
328
|
+
{detail.fields.map((field) => (
|
|
329
|
+
<div key={field.label} className="min-w-0">
|
|
330
|
+
<dt className={`${SMALL} text-ink-muted`}>{field.label}</dt>
|
|
331
|
+
<dd className={`m-0 mt-0.5 ${BODY} ${/\d/.test(field.value) ? FIGURES : ""}`}>{field.value}</dd>
|
|
332
|
+
</div>
|
|
333
|
+
))}
|
|
334
|
+
</dl>
|
|
335
|
+
) : null}
|
|
336
|
+
{detail.text ? <p className={`m-0 mt-5 max-w-[48ch] ${BODY} text-ink-muted`}>{detail.text}</p> : null}
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function TimelinePane({ moments }: { moments: Moment[] }) {
|
|
342
|
+
return (
|
|
343
|
+
<ol className="m-0 list-none p-0">
|
|
344
|
+
{moments.map((moment, i) => (
|
|
345
|
+
<li key={`${moment.time}-${i}`} className="relative grid grid-cols-[0.75rem_1fr] gap-x-3 pb-4 last:pb-0">
|
|
346
|
+
{i < moments.length - 1 ? <span aria-hidden="true" className="absolute bottom-0 left-[0.34375rem] top-[1.1rem] w-px bg-[color:var(--line)]" /> : null}
|
|
347
|
+
<span
|
|
348
|
+
aria-hidden="true"
|
|
349
|
+
className={`mt-[0.4rem] size-[0.75rem] rounded-full border ${moment.done === false ? "border-[color:var(--ink-muted)] bg-surface-0" : "border-[color:var(--ink)] bg-[color:var(--ink)]"}`}
|
|
350
|
+
/>
|
|
351
|
+
<span>
|
|
352
|
+
<span className={`block ${BODY}`}>{moment.text}</span>
|
|
353
|
+
<span className={`block ${SMALL} text-ink-muted ${FIGURES}`}>{moment.time}</span>
|
|
354
|
+
</span>
|
|
355
|
+
</li>
|
|
356
|
+
))}
|
|
357
|
+
</ol>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function FormPane({ form }: { form: Form }) {
|
|
362
|
+
return (
|
|
363
|
+
<div>
|
|
364
|
+
{form.title ? <p className={`m-0 mb-4 ${BODY} font-medium`}>{form.title}</p> : null}
|
|
365
|
+
<div className="grid gap-4">
|
|
366
|
+
{form.fields.map((field) => (
|
|
367
|
+
<div key={field.label}>
|
|
368
|
+
<span className={`block ${SMALL} ${field.state === "error" ? "font-medium text-ink" : "text-ink-muted"}`}>{field.label}</span>
|
|
369
|
+
<span
|
|
370
|
+
className={`mt-1.5 flex min-h-[2.375rem] items-center rounded-[0.4375rem] bg-surface-0 px-3 ${BODY} ${
|
|
371
|
+
field.state === "focus"
|
|
372
|
+
? "border border-[color:var(--ink)] outline outline-2 outline-offset-0 outline-[color:var(--line)]"
|
|
373
|
+
: field.state === "error"
|
|
374
|
+
? "border-[1.5px] border-[color:var(--ink)]"
|
|
375
|
+
: "border border-line"
|
|
376
|
+
}`}
|
|
377
|
+
>
|
|
378
|
+
{field.value ? <span className="truncate">{field.value}</span> : <span className="truncate text-ink-muted">{field.placeholder}</span>}
|
|
379
|
+
{field.state === "focus" ? <span aria-hidden="true" className="subject-caret ml-px h-[1.1em] w-px bg-[color:var(--ink)]" /> : null}
|
|
380
|
+
</span>
|
|
381
|
+
{field.hint ? <span className={`mt-1 block ${SMALL} ${field.state === "error" ? "text-ink" : "text-ink-muted"}`}>{field.hint}</span> : null}
|
|
382
|
+
</div>
|
|
383
|
+
))}
|
|
384
|
+
<span className={`mt-1 justify-self-start rounded-[0.4375rem] bg-[color:var(--ink)] px-4 py-2.5 ${SMALL} font-medium text-surface-0`}>{form.submit}</span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|