tsquare 0.1.0 → 0.2.0
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 +15 -3
- package/assets/mark.png +0 -0
- package/bin/tsquare.js +2 -6
- package/dist/catalog.d.ts +1021 -0
- package/dist/chunks/chunk-L6GTIZQH.js +2029 -0
- package/{src/cli.ts → dist/cli.js} +28 -24
- package/dist/colors.d.ts +29 -0
- package/dist/compile.d.ts +24 -0
- package/dist/components.d.ts +83 -0
- package/dist/icons.d.ts +12 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +38 -0
- package/dist/layout.d.ts +66 -0
- package/dist/playground/index.d.ts +27 -0
- package/dist/playground/index.html +501 -0
- package/dist/playground/index.js +376 -0
- package/dist/playground/reference.d.ts +11 -0
- package/dist/print.d.ts +2 -0
- package/dist/prompt-example.d.ts +3 -0
- package/dist/prompt.d.ts +22 -0
- package/dist/render.d.ts +20 -0
- package/dist/schema.d.ts +33 -0
- package/dist/suggest.d.ts +5 -0
- package/dist/text.d.ts +45 -0
- package/examples/notes-mobile.tsq +84 -0
- package/examples/notes-web.tsq +54 -0
- package/package.json +23 -7
- package/skill/tsquare/SKILL.md +3 -3
- package/skill/tsquare/reference.md +2 -2
- package/src/catalog.ts +0 -329
- package/src/colors.ts +0 -82
- package/src/compile.ts +0 -64
- package/src/components.tsx +0 -1057
- package/src/icons.ts +0 -54
- package/src/index.ts +0 -11
- package/src/layout.ts +0 -63
- package/src/print.ts +0 -73
- package/src/prompt-example.ts +0 -43
- package/src/prompt.ts +0 -121
- package/src/render.ts +0 -178
- package/src/schema.ts +0 -43
- package/src/suggest.ts +0 -34
- package/src/text.ts +0 -333
package/src/components.tsx
DELETED
|
@@ -1,1057 +0,0 @@
|
|
|
1
|
-
import React, { Children, cloneElement, isValidElement, type CSSProperties, type ReactNode } from "react";
|
|
2
|
-
import { paletteFor, TONES, type Palette } from "./colors";
|
|
3
|
-
import { iconNode } from "./icons";
|
|
4
|
-
import type { ComponentRenderProps } from "@json-render/image";
|
|
5
|
-
import {
|
|
6
|
-
BOARD_GAP,
|
|
7
|
-
BOARD_PADDING,
|
|
8
|
-
LABEL_H,
|
|
9
|
-
NOTE_WIDTH,
|
|
10
|
-
TITLE_H,
|
|
11
|
-
screenSize,
|
|
12
|
-
theme as t,
|
|
13
|
-
} from "./layout";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Satori renders a subset of CSS. Rules followed here:
|
|
17
|
-
* - every element that has more than one child sets display:flex
|
|
18
|
-
* - absolute positioning is relative to the direct parent (Yoga), so overlays
|
|
19
|
-
* are lifted out and placed by Screen rather than nested in the body
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
type Dir = "row" | "column";
|
|
23
|
-
type Props<P = Record<string, any>> = ComponentRenderProps<P> & { dir?: Dir; stretch?: boolean; aligned?: boolean; colors?: Palette };
|
|
24
|
-
|
|
25
|
-
/** The board's colors, passed to every component by `withPalette`. Grayscale when there's no accent. */
|
|
26
|
-
const GRAY = paletteFor();
|
|
27
|
-
|
|
28
|
-
/** A registry whose components all receive this board's palette as `colors`. */
|
|
29
|
-
export function withPalette(colors: Palette): typeof registry {
|
|
30
|
-
return Object.fromEntries(
|
|
31
|
-
Object.entries(registry).map(([name, C]) => [name, (props: Props) => (C as any)({ ...props, colors })]),
|
|
32
|
-
) as typeof registry;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const flexAlign = { start: "flex-start", center: "center", end: "flex-end", stretch: "stretch" } as const;
|
|
36
|
-
const flexJustify = {
|
|
37
|
-
start: "flex-start",
|
|
38
|
-
center: "center",
|
|
39
|
-
end: "flex-end",
|
|
40
|
-
between: "space-between",
|
|
41
|
-
around: "space-around",
|
|
42
|
-
} as const;
|
|
43
|
-
|
|
44
|
-
function Box({ style, children }: { style?: CSSProperties; children?: ReactNode }) {
|
|
45
|
-
// Satori chokes on undefined style values, so drop them.
|
|
46
|
-
const clean: Record<string, unknown> = { display: "flex" };
|
|
47
|
-
for (const [k, v] of Object.entries(style ?? {})) if (v !== undefined && v !== null) clean[k] = v;
|
|
48
|
-
return <div style={clean as CSSProperties}>{children}</div>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Tell each child which way its parent lays out, so leaves can avoid stretching. */
|
|
52
|
-
/** `aligned`: the parent column sets align=start/center/end, so leaves follow it instead of hugging the left edge. */
|
|
53
|
-
function withDir(children: ReactNode, dir: Dir, stretch = false, aligned = false) {
|
|
54
|
-
return Children.toArray(children).map((c) => (isValidElement(c) ? cloneElement(c as any, { dir, stretch, aligned }) : c));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Containers in a row fill its height (sidebars, side-by-side cards) unless the
|
|
59
|
-
* row sets `align` explicitly. Leaves keep the row's centering.
|
|
60
|
-
*/
|
|
61
|
-
function fillRow(inRow?: boolean, edge = true): CSSProperties {
|
|
62
|
-
if (!inRow) return {};
|
|
63
|
-
return { alignSelf: edge ? "stretch" : "flex-start" };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* `grow` fills the parent's leftover space. In a row, growing siblings split it
|
|
68
|
-
* equally whatever their content (two growing cards → equal columns); with the
|
|
69
|
-
* default flex-basis the one with more content would get more.
|
|
70
|
-
*/
|
|
71
|
-
function grow(on: unknown, dir?: Dir): CSSProperties {
|
|
72
|
-
if (!on) return { flexGrow: 0 };
|
|
73
|
-
return dir === "row" ? { flexGrow: 1, flexBasis: "0px" } : { flexGrow: 1 };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Leaves like buttons shouldn't stretch across a column, unless the column sets its own alignment. */
|
|
77
|
-
function hug(dir?: Dir, aligned = false): CSSProperties {
|
|
78
|
-
return dir === "row" || aligned ? {} : { alignSelf: "flex-start" };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ── Icons ───────────────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
export function IconGlyph({ name, size = 20, color = t.ink, strokeWidth = 2 }: {
|
|
84
|
-
name?: string | null;
|
|
85
|
-
size?: number;
|
|
86
|
-
color?: string;
|
|
87
|
-
strokeWidth?: number;
|
|
88
|
-
}) {
|
|
89
|
-
const node = name ? iconNode(name) : null;
|
|
90
|
-
return (
|
|
91
|
-
<svg
|
|
92
|
-
width={size}
|
|
93
|
-
height={size}
|
|
94
|
-
viewBox="0 0 24 24"
|
|
95
|
-
fill="none"
|
|
96
|
-
stroke={color}
|
|
97
|
-
strokeWidth={strokeWidth}
|
|
98
|
-
strokeLinecap="round"
|
|
99
|
-
strokeLinejoin="round"
|
|
100
|
-
style={{ flexShrink: 0 }}
|
|
101
|
-
>
|
|
102
|
-
{node
|
|
103
|
-
? node.map(([tag, attrs]: [string, Record<string, string>], i: number) =>
|
|
104
|
-
React.createElement(tag, { key: i, ...attrs }),
|
|
105
|
-
)
|
|
106
|
-
: <circle cx="12" cy="12" r="9" />}
|
|
107
|
-
</svg>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ── Canvas ──────────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
function Board({ element, children }: Props) {
|
|
114
|
-
const p = element.props;
|
|
115
|
-
const gap = p.gap ?? BOARD_GAP;
|
|
116
|
-
const items = Children.toArray(children);
|
|
117
|
-
const perRow = p.layout === "grid" ? Math.max(1, p.columns ?? 3) : Math.max(1, items.length);
|
|
118
|
-
const rows: ReactNode[][] = [];
|
|
119
|
-
for (let i = 0; i < items.length; i += perRow) rows.push(items.slice(i, i + perRow));
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<Box
|
|
123
|
-
style={{
|
|
124
|
-
flexDirection: "column",
|
|
125
|
-
width: "100%",
|
|
126
|
-
height: "100%",
|
|
127
|
-
padding: p.padding ?? BOARD_PADDING,
|
|
128
|
-
backgroundColor: t.board,
|
|
129
|
-
fontFamily: t.font,
|
|
130
|
-
color: t.ink,
|
|
131
|
-
}}
|
|
132
|
-
>
|
|
133
|
-
{p.title ? (
|
|
134
|
-
<Box style={{ height: TITLE_H, fontSize: 28, fontWeight: 600, color: t.ink }}>{p.title}</Box>
|
|
135
|
-
) : null}
|
|
136
|
-
<Box style={{ flexDirection: "column", gap }}>
|
|
137
|
-
{rows.map((row, i) => (
|
|
138
|
-
<Box key={i} style={{ flexDirection: "row", alignItems: "flex-start", gap }}>
|
|
139
|
-
{row}
|
|
140
|
-
</Box>
|
|
141
|
-
))}
|
|
142
|
-
</Box>
|
|
143
|
-
</Box>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function StatusBar() {
|
|
148
|
-
return (
|
|
149
|
-
<Box
|
|
150
|
-
style={{
|
|
151
|
-
height: 34,
|
|
152
|
-
flexShrink: 0,
|
|
153
|
-
padding: "0 26px",
|
|
154
|
-
alignItems: "center",
|
|
155
|
-
justifyContent: "space-between",
|
|
156
|
-
fontSize: 14,
|
|
157
|
-
fontWeight: 600,
|
|
158
|
-
}}
|
|
159
|
-
>
|
|
160
|
-
<Box>9:41</Box>
|
|
161
|
-
<Box style={{ gap: 5, alignItems: "center" }}>
|
|
162
|
-
<IconGlyph name="signal" size={15} strokeWidth={2.4} />
|
|
163
|
-
<IconGlyph name="wifi" size={15} strokeWidth={2.4} />
|
|
164
|
-
<IconGlyph name="battery-full" size={17} strokeWidth={2} />
|
|
165
|
-
</Box>
|
|
166
|
-
</Box>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function BrowserBar() {
|
|
171
|
-
return (
|
|
172
|
-
<Box
|
|
173
|
-
style={{
|
|
174
|
-
height: 40,
|
|
175
|
-
flexShrink: 0,
|
|
176
|
-
padding: "0 14px",
|
|
177
|
-
gap: 16,
|
|
178
|
-
alignItems: "center",
|
|
179
|
-
backgroundColor: t.fill,
|
|
180
|
-
borderBottom: `1px solid ${t.line}`,
|
|
181
|
-
}}
|
|
182
|
-
>
|
|
183
|
-
<Box style={{ gap: 7 }}>
|
|
184
|
-
{[0, 1, 2].map((i) => (
|
|
185
|
-
<Box key={i} style={{ width: 11, height: 11, borderRadius: 6, backgroundColor: t.fill2, border: `1px solid ${t.lineStrong}` }} />
|
|
186
|
-
))}
|
|
187
|
-
</Box>
|
|
188
|
-
<Box style={{ flexGrow: 1, justifyContent: "center" }}>
|
|
189
|
-
<Box style={{ width: 420, height: 24, borderRadius: 12, backgroundColor: t.paper, border: `1px solid ${t.line}` }} />
|
|
190
|
-
</Box>
|
|
191
|
-
<Box style={{ width: 57 }} />
|
|
192
|
-
</Box>
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function Screen({ element, children }: Props) {
|
|
197
|
-
const p = element.props;
|
|
198
|
-
const s = screenSize(p);
|
|
199
|
-
const top: ReactNode[] = [];
|
|
200
|
-
const body: ReactNode[] = [];
|
|
201
|
-
const bottom: ReactNode[] = [];
|
|
202
|
-
const overlays: ReactNode[] = [];
|
|
203
|
-
for (const c of Children.toArray(children)) {
|
|
204
|
-
const type = isValidElement(c) ? (c.props as any).element?.type : null;
|
|
205
|
-
if (type === "NavBar") top.push(c);
|
|
206
|
-
else if (type === "TabBar") bottom.push(c);
|
|
207
|
-
else if (type === "Modal" || type === "Drawer") overlays.push(c);
|
|
208
|
-
else body.push(c);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
<Box style={{ flexDirection: "column", flexShrink: 0 }}>
|
|
213
|
-
<Box style={{ height: LABEL_H, fontSize: 16, fontWeight: 600, color: t.text }}>{p.name ?? ""}</Box>
|
|
214
|
-
<Box
|
|
215
|
-
style={{
|
|
216
|
-
width: s.width,
|
|
217
|
-
height: s.height,
|
|
218
|
-
flexDirection: "column",
|
|
219
|
-
backgroundColor: t.paper,
|
|
220
|
-
border: `1.5px solid ${t.lineStrong}`,
|
|
221
|
-
borderRadius: s.radius,
|
|
222
|
-
overflow: "hidden",
|
|
223
|
-
boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
|
|
224
|
-
color: t.ink,
|
|
225
|
-
fontSize: 15,
|
|
226
|
-
}}
|
|
227
|
-
>
|
|
228
|
-
{s.chrome && s.device === "phone" ? <StatusBar /> : null}
|
|
229
|
-
{s.chrome && s.device === "desktop" ? <BrowserBar /> : null}
|
|
230
|
-
<Box style={{ position: "relative", flexGrow: 1, flexDirection: "column", overflow: "hidden" }}>
|
|
231
|
-
{top}
|
|
232
|
-
<Box
|
|
233
|
-
style={{
|
|
234
|
-
flexGrow: 1,
|
|
235
|
-
flexDirection: "column",
|
|
236
|
-
padding: s.padding,
|
|
237
|
-
gap: p.gap ?? 14,
|
|
238
|
-
overflow: "hidden",
|
|
239
|
-
}}
|
|
240
|
-
>
|
|
241
|
-
{withDir(body, "column")}
|
|
242
|
-
</Box>
|
|
243
|
-
{bottom}
|
|
244
|
-
{overlays}
|
|
245
|
-
</Box>
|
|
246
|
-
</Box>
|
|
247
|
-
</Box>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function Note({ element, dir, aligned }: Props) {
|
|
252
|
-
const p = element.props;
|
|
253
|
-
const onBoard = dir === undefined; // Board children don't get a dir
|
|
254
|
-
return (
|
|
255
|
-
<Box
|
|
256
|
-
style={{
|
|
257
|
-
width: p.width ?? (onBoard ? NOTE_WIDTH : undefined),
|
|
258
|
-
marginTop: onBoard ? LABEL_H : 0,
|
|
259
|
-
flexShrink: 0,
|
|
260
|
-
padding: 14,
|
|
261
|
-
backgroundColor: t.notes[(p.color ?? "yellow") as keyof typeof t.notes] ?? t.notes.yellow,
|
|
262
|
-
boxShadow: "0 2px 6px rgba(0,0,0,0.12)",
|
|
263
|
-
fontSize: 14,
|
|
264
|
-
lineHeight: 1.5,
|
|
265
|
-
color: t.ink,
|
|
266
|
-
...(onBoard ? {} : hug(dir, aligned)),
|
|
267
|
-
}}
|
|
268
|
-
>
|
|
269
|
-
{p.text}
|
|
270
|
-
</Box>
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ── Layout ──────────────────────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
const borderSide = (side?: string | null): CSSProperties => {
|
|
277
|
-
const b = `1px solid ${t.line}`;
|
|
278
|
-
switch (side) {
|
|
279
|
-
case "all": return { border: b };
|
|
280
|
-
case "left": return { borderLeft: b };
|
|
281
|
-
case "right": return { borderRight: b };
|
|
282
|
-
case "top": return { borderTop: b };
|
|
283
|
-
case "bottom": return { borderBottom: b };
|
|
284
|
-
default: return {};
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
function Stack({ element, children, dir: parentDir, stretch }: Props) {
|
|
289
|
-
const p = element.props;
|
|
290
|
-
const dir: Dir = p.direction ?? "column";
|
|
291
|
-
return (
|
|
292
|
-
<Box
|
|
293
|
-
style={{
|
|
294
|
-
flexDirection: dir,
|
|
295
|
-
gap: p.gap ?? (dir === "row" ? 12 : 12),
|
|
296
|
-
padding: p.padding ?? 0,
|
|
297
|
-
alignItems: flexAlign[(p.align ?? (dir === "row" ? "center" : "stretch")) as keyof typeof flexAlign],
|
|
298
|
-
justifyContent: flexJustify[(p.justify ?? "start") as keyof typeof flexJustify],
|
|
299
|
-
flexWrap: p.wrap ? "wrap" : "nowrap",
|
|
300
|
-
...grow(p.grow, parentDir),
|
|
301
|
-
flexShrink: p.width ? 0 : 1,
|
|
302
|
-
width: p.width ?? undefined,
|
|
303
|
-
minHeight: 0,
|
|
304
|
-
backgroundColor: p.fill ? t.fill : undefined,
|
|
305
|
-
...borderSide(p.border),
|
|
306
|
-
// stacks with a visible edge (sidebars, panels) fill the row's height; others sit at the top
|
|
307
|
-
...fillRow(stretch, p.width != null || !!p.fill || (!!p.border && p.border !== "none")),
|
|
308
|
-
}}
|
|
309
|
-
>
|
|
310
|
-
{withDir(children, dir, dir === "row" && p.align == null, dir === "column" && p.align != null && p.align !== "stretch")}
|
|
311
|
-
</Box>
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function Grid({ element, children }: Props) {
|
|
316
|
-
const p = element.props;
|
|
317
|
-
const gap = p.gap ?? 16;
|
|
318
|
-
const cols = Math.max(1, p.columns ?? 2);
|
|
319
|
-
return (
|
|
320
|
-
<Box style={{ padding: p.padding ?? 0, flexDirection: "column" }}>
|
|
321
|
-
<Box style={{ flexDirection: "row", flexWrap: "wrap", margin: -gap / 2 }}>
|
|
322
|
-
{Children.toArray(children).map((c, i) => (
|
|
323
|
-
<Box key={i} style={{ width: `${100 / cols}%`, padding: gap / 2, flexDirection: "column" }}>
|
|
324
|
-
{withDir(c, "column")}
|
|
325
|
-
</Box>
|
|
326
|
-
))}
|
|
327
|
-
</Box>
|
|
328
|
-
</Box>
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function Card({ element, children, dir, stretch }: Props) {
|
|
333
|
-
const p = element.props;
|
|
334
|
-
const filled = p.variant === "filled";
|
|
335
|
-
return (
|
|
336
|
-
<Box
|
|
337
|
-
style={{
|
|
338
|
-
flexDirection: "column",
|
|
339
|
-
gap: p.gap ?? 10,
|
|
340
|
-
padding: p.padding ?? 16,
|
|
341
|
-
borderRadius: 10,
|
|
342
|
-
border: filled ? "none" : `1.5px solid ${t.line}`,
|
|
343
|
-
backgroundColor: filled ? t.fill : t.paper,
|
|
344
|
-
...grow(p.grow, dir),
|
|
345
|
-
width: p.width ?? undefined,
|
|
346
|
-
flexShrink: p.width ? 0 : 1,
|
|
347
|
-
...fillRow(stretch),
|
|
348
|
-
}}
|
|
349
|
-
>
|
|
350
|
-
{p.title ? <Box style={{ fontSize: 15, fontWeight: 600 }}>{p.title}</Box> : null}
|
|
351
|
-
{withDir(children, "column")}
|
|
352
|
-
</Box>
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function Divider({ element, dir }: Props) {
|
|
357
|
-
const vertical = element.props.vertical ?? dir === "row";
|
|
358
|
-
return vertical ? (
|
|
359
|
-
<Box style={{ width: 1, alignSelf: "stretch", backgroundColor: t.line, flexShrink: 0 }} />
|
|
360
|
-
) : (
|
|
361
|
-
<Box style={{ height: 1, alignSelf: "stretch", backgroundColor: t.line, flexShrink: 0 }} />
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function Spacer({ element }: Props) {
|
|
366
|
-
const size = element.props.size;
|
|
367
|
-
return size != null ? (
|
|
368
|
-
<Box style={{ flexBasis: `${size}px`, flexGrow: 0, flexShrink: 0 }} />
|
|
369
|
-
) : (
|
|
370
|
-
<Box style={{ flexGrow: 1 }} />
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ── Content ─────────────────────────────────────────────────────────────
|
|
375
|
-
|
|
376
|
-
function Heading({ element }: Props) {
|
|
377
|
-
const p = element.props;
|
|
378
|
-
const size = { 1: 26, 2: 20, 3: 16 }[(p.level ?? 1) as 1 | 2 | 3] ?? 26;
|
|
379
|
-
return (
|
|
380
|
-
<Box
|
|
381
|
-
style={{
|
|
382
|
-
fontSize: size,
|
|
383
|
-
fontWeight: 600,
|
|
384
|
-
lineHeight: 1.25,
|
|
385
|
-
color: t.ink,
|
|
386
|
-
justifyContent: p.align === "center" ? "center" : p.align === "right" ? "flex-end" : "flex-start",
|
|
387
|
-
textAlign: p.align ?? "left",
|
|
388
|
-
}}
|
|
389
|
-
>
|
|
390
|
-
{p.text}
|
|
391
|
-
</Box>
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const LINE_WIDTHS = [100, 94, 97, 89, 95, 91];
|
|
396
|
-
|
|
397
|
-
function PlaceholderLines({ count, height = 8, gap = 9, color = t.fill2, last = 62 }: {
|
|
398
|
-
count: number;
|
|
399
|
-
height?: number;
|
|
400
|
-
gap?: number;
|
|
401
|
-
color?: string;
|
|
402
|
-
last?: number;
|
|
403
|
-
}) {
|
|
404
|
-
return (
|
|
405
|
-
<Box style={{ flexDirection: "column", gap, alignSelf: "stretch" }}>
|
|
406
|
-
{Array.from({ length: count }, (_, i) => (
|
|
407
|
-
<Box
|
|
408
|
-
key={i}
|
|
409
|
-
style={{
|
|
410
|
-
height,
|
|
411
|
-
borderRadius: height / 2,
|
|
412
|
-
backgroundColor: color,
|
|
413
|
-
width: `${i === count - 1 && count > 1 ? last : LINE_WIDTHS[i % LINE_WIDTHS.length]}%`,
|
|
414
|
-
}}
|
|
415
|
-
/>
|
|
416
|
-
))}
|
|
417
|
-
</Box>
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function Text({ element }: Props) {
|
|
422
|
-
const p = element.props;
|
|
423
|
-
const size = { sm: 13, md: 15, lg: 18 }[(p.size ?? "md") as "sm" | "md" | "lg"] ?? 15;
|
|
424
|
-
if (p.text == null) {
|
|
425
|
-
return <PlaceholderLines count={p.lines ?? 2} height={Math.round(size * 0.55)} gap={Math.round(size * 0.6)} />;
|
|
426
|
-
}
|
|
427
|
-
return (
|
|
428
|
-
<Box
|
|
429
|
-
style={{
|
|
430
|
-
fontSize: size,
|
|
431
|
-
lineHeight: 1.45,
|
|
432
|
-
color: p.muted ? t.muted : t.text,
|
|
433
|
-
fontWeight: p.bold ? 600 : 400,
|
|
434
|
-
justifyContent: p.align === "center" ? "center" : p.align === "right" ? "flex-end" : "flex-start",
|
|
435
|
-
textAlign: p.align ?? "left",
|
|
436
|
-
}}
|
|
437
|
-
>
|
|
438
|
-
{p.text}
|
|
439
|
-
</Box>
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function Image({ element, dir }: Props) {
|
|
444
|
-
const p = element.props;
|
|
445
|
-
const h = p.height ?? 160;
|
|
446
|
-
const w = p.width ?? (dir === "row" ? h : "100%");
|
|
447
|
-
return (
|
|
448
|
-
<Box
|
|
449
|
-
style={{
|
|
450
|
-
position: "relative",
|
|
451
|
-
width: w,
|
|
452
|
-
height: h,
|
|
453
|
-
flexShrink: 0,
|
|
454
|
-
backgroundColor: t.fill,
|
|
455
|
-
border: `1.5px solid ${t.line}`,
|
|
456
|
-
borderRadius: p.rounded ? 12 : 4,
|
|
457
|
-
overflow: "hidden",
|
|
458
|
-
alignItems: "center",
|
|
459
|
-
justifyContent: "center",
|
|
460
|
-
}}
|
|
461
|
-
>
|
|
462
|
-
<svg
|
|
463
|
-
width="100%"
|
|
464
|
-
height="100%"
|
|
465
|
-
viewBox="0 0 100 100"
|
|
466
|
-
preserveAspectRatio="none"
|
|
467
|
-
style={{ position: "absolute", top: 0, left: 0 }}
|
|
468
|
-
>
|
|
469
|
-
<line x1="0" y1="0" x2="100" y2="100" stroke={t.line} strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
|
|
470
|
-
<line x1="100" y1="0" x2="0" y2="100" stroke={t.line} strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
|
|
471
|
-
</svg>
|
|
472
|
-
{p.label ? (
|
|
473
|
-
<Box
|
|
474
|
-
style={{
|
|
475
|
-
padding: "3px 8px",
|
|
476
|
-
fontSize: 12,
|
|
477
|
-
color: t.muted,
|
|
478
|
-
backgroundColor: t.fill,
|
|
479
|
-
borderRadius: 4,
|
|
480
|
-
}}
|
|
481
|
-
>
|
|
482
|
-
{p.label}
|
|
483
|
-
</Box>
|
|
484
|
-
) : null}
|
|
485
|
-
</Box>
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function Icon({ element, dir, aligned }: Props) {
|
|
490
|
-
const p = element.props;
|
|
491
|
-
return (
|
|
492
|
-
<Box style={{ flexShrink: 0, ...hug(dir, aligned) }}>
|
|
493
|
-
<IconGlyph name={p.name} size={p.size ?? 20} />
|
|
494
|
-
</Box>
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function AvatarCircle({ initials, size = 40 }: { initials?: string | null; size?: number }) {
|
|
499
|
-
return (
|
|
500
|
-
<Box
|
|
501
|
-
style={{
|
|
502
|
-
width: size,
|
|
503
|
-
height: size,
|
|
504
|
-
borderRadius: size / 2,
|
|
505
|
-
backgroundColor: t.fill2,
|
|
506
|
-
alignItems: "center",
|
|
507
|
-
justifyContent: "center",
|
|
508
|
-
flexShrink: 0,
|
|
509
|
-
fontSize: Math.round(size * 0.38),
|
|
510
|
-
fontWeight: 600,
|
|
511
|
-
color: t.text,
|
|
512
|
-
overflow: "hidden",
|
|
513
|
-
}}
|
|
514
|
-
>
|
|
515
|
-
{initials ? initials.slice(0, 2).toUpperCase() : <IconGlyph name="user" size={Math.round(size * 0.55)} color={t.muted} />}
|
|
516
|
-
</Box>
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function Avatar({ element, dir, aligned }: Props) {
|
|
521
|
-
return (
|
|
522
|
-
<Box style={hug(dir, aligned)}>
|
|
523
|
-
<AvatarCircle initials={element.props.initials} size={element.props.size ?? 40} />
|
|
524
|
-
</Box>
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function BadgePill({ label, variant, tone, c = GRAY }: { label: string; variant?: string | null; tone?: string | null; c?: Palette }) {
|
|
529
|
-
const solid = variant !== "outline";
|
|
530
|
-
const toneColor = tone && tone !== "neutral" ? TONES[tone as keyof typeof TONES] : undefined;
|
|
531
|
-
return (
|
|
532
|
-
<Box
|
|
533
|
-
style={{
|
|
534
|
-
height: 22,
|
|
535
|
-
padding: "0 9px",
|
|
536
|
-
borderRadius: 11,
|
|
537
|
-
alignItems: "center",
|
|
538
|
-
fontSize: 12,
|
|
539
|
-
fontWeight: 600,
|
|
540
|
-
flexShrink: 0,
|
|
541
|
-
backgroundColor: solid ? toneColor ?? c.accent : "transparent",
|
|
542
|
-
color: solid ? (toneColor ? "#ffffff" : c.onAccent) : toneColor ?? t.text,
|
|
543
|
-
border: solid ? "none" : `1.5px solid ${toneColor ?? t.lineStrong}`,
|
|
544
|
-
}}
|
|
545
|
-
>
|
|
546
|
-
{label}
|
|
547
|
-
</Box>
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function Badge({ element, dir, aligned, colors }: Props) {
|
|
552
|
-
return (
|
|
553
|
-
<Box style={hug(dir, aligned)}>
|
|
554
|
-
<BadgePill label={element.props.label} variant={element.props.variant} tone={element.props.tone} c={colors} />
|
|
555
|
-
</Box>
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// ── Controls ────────────────────────────────────────────────────────────
|
|
560
|
-
|
|
561
|
-
function Button({ element, dir, aligned, colors: c = GRAY }: Props) {
|
|
562
|
-
const p = element.props;
|
|
563
|
-
const variant = p.variant ?? "primary";
|
|
564
|
-
const h = { sm: 32, md: 42, lg: 50 }[(p.size ?? "md") as "sm" | "md" | "lg"] ?? 42;
|
|
565
|
-
const fg = variant === "primary" ? c.onAccent : variant === "ghost" ? c.accentText : t.ink;
|
|
566
|
-
return (
|
|
567
|
-
<Box
|
|
568
|
-
style={{
|
|
569
|
-
height: h,
|
|
570
|
-
padding: `0 ${Math.round(h * 0.45)}px`,
|
|
571
|
-
gap: 8,
|
|
572
|
-
borderRadius: 8,
|
|
573
|
-
alignItems: "center",
|
|
574
|
-
justifyContent: "center",
|
|
575
|
-
flexShrink: 0,
|
|
576
|
-
fontSize: h < 36 ? 13 : 15,
|
|
577
|
-
fontWeight: 600,
|
|
578
|
-
color: fg,
|
|
579
|
-
backgroundColor: variant === "primary" ? c.accent : "transparent",
|
|
580
|
-
border: variant === "secondary" ? `1.5px solid ${t.lineStrong}` : "none",
|
|
581
|
-
...(p.fullWidth ? { alignSelf: "stretch" } : hug(dir, aligned)),
|
|
582
|
-
}}
|
|
583
|
-
>
|
|
584
|
-
{p.icon ? <IconGlyph name={p.icon} size={h < 36 ? 16 : 18} color={fg} /> : null}
|
|
585
|
-
{p.label ? <Box>{p.label}</Box> : null}
|
|
586
|
-
</Box>
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** Fields fill a column; in a row they take the given width or share the space. */
|
|
591
|
-
function fieldWidth(width: unknown, dir?: Dir): CSSProperties {
|
|
592
|
-
if (width != null) return { width: width as number, flexShrink: 0 };
|
|
593
|
-
return dir === "row" ? { flexGrow: 1, flexBasis: "0px", minWidth: 120 } : {};
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function FieldLabel({ text }: { text?: string | null }) {
|
|
597
|
-
return text ? <Box style={{ fontSize: 13, fontWeight: 600, color: t.text }}>{text}</Box> : null;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function Field({ children, height = 42, top = false, error = false }: { children?: ReactNode; height?: number; top?: boolean; error?: boolean }) {
|
|
601
|
-
return (
|
|
602
|
-
<Box
|
|
603
|
-
style={{
|
|
604
|
-
height,
|
|
605
|
-
padding: top ? "10px 12px" : "0 12px",
|
|
606
|
-
gap: 8,
|
|
607
|
-
alignItems: top ? "flex-start" : "center",
|
|
608
|
-
border: `1.5px solid ${error ? TONES.danger : t.lineStrong}`,
|
|
609
|
-
borderRadius: 8,
|
|
610
|
-
backgroundColor: t.paper,
|
|
611
|
-
fontSize: 15,
|
|
612
|
-
}}
|
|
613
|
-
>
|
|
614
|
-
{children}
|
|
615
|
-
</Box>
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function Input({ element, dir }: Props) {
|
|
620
|
-
const p = element.props;
|
|
621
|
-
const rows = p.multiline ?? 1;
|
|
622
|
-
const isPassword = p.type === "password";
|
|
623
|
-
const shown = p.value ?? null;
|
|
624
|
-
const content =
|
|
625
|
-
isPassword && shown ? "•".repeat(Math.min(shown.length, 12)) : shown;
|
|
626
|
-
return (
|
|
627
|
-
<Box style={{ flexDirection: "column", gap: 6, ...fieldWidth(p.width, dir), ...(p.grow ? { flexGrow: 1 } : {}) }}>
|
|
628
|
-
<FieldLabel text={p.label} />
|
|
629
|
-
<Field height={rows > 1 ? rows * 22 + 20 : 42} top={rows > 1} error={!!p.error}>
|
|
630
|
-
{p.type === "search" ? <IconGlyph name="search" size={18} color={t.muted} /> : null}
|
|
631
|
-
<Box style={{ flexGrow: 1, color: content ? t.ink : t.muted }}>
|
|
632
|
-
{content ?? p.placeholder ?? (isPassword ? "••••••••" : "")}
|
|
633
|
-
</Box>
|
|
634
|
-
{isPassword ? <IconGlyph name="eye-off" size={18} color={t.muted} /> : null}
|
|
635
|
-
</Field>
|
|
636
|
-
{p.helper ? <Box style={{ fontSize: 12, color: p.error ? TONES.danger : t.muted }}>{p.helper}</Box> : null}
|
|
637
|
-
</Box>
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function Select({ element, dir }: Props) {
|
|
642
|
-
const p = element.props;
|
|
643
|
-
return (
|
|
644
|
-
<Box style={{ flexDirection: "column", gap: 6, ...fieldWidth(p.width, dir) }}>
|
|
645
|
-
<FieldLabel text={p.label} />
|
|
646
|
-
<Field>
|
|
647
|
-
<Box style={{ flexGrow: 1, color: p.value ? t.ink : t.muted }}>{p.value ?? p.placeholder ?? "Select…"}</Box>
|
|
648
|
-
<IconGlyph name="chevron-down" size={18} color={t.text} />
|
|
649
|
-
</Field>
|
|
650
|
-
</Box>
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function CheckMark({ checked, round = false, c = GRAY }: { checked?: boolean | null; round?: boolean; c?: Palette }) {
|
|
655
|
-
return (
|
|
656
|
-
<Box
|
|
657
|
-
style={{
|
|
658
|
-
width: 20,
|
|
659
|
-
height: 20,
|
|
660
|
-
flexShrink: 0,
|
|
661
|
-
borderRadius: round ? 10 : 5,
|
|
662
|
-
border: `1.5px solid ${checked && !round ? c.accent : t.lineStrong}`,
|
|
663
|
-
backgroundColor: checked && !round ? c.accent : t.paper,
|
|
664
|
-
alignItems: "center",
|
|
665
|
-
justifyContent: "center",
|
|
666
|
-
}}
|
|
667
|
-
>
|
|
668
|
-
{checked ? (
|
|
669
|
-
round ? (
|
|
670
|
-
<Box style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: c.accent }} />
|
|
671
|
-
) : (
|
|
672
|
-
<IconGlyph name="check" size={14} color={c.onAccent} strokeWidth={3} />
|
|
673
|
-
)
|
|
674
|
-
) : null}
|
|
675
|
-
</Box>
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function Checkbox({ element, dir, aligned, colors }: Props) {
|
|
680
|
-
const p = element.props;
|
|
681
|
-
return (
|
|
682
|
-
<Box style={{ gap: 10, alignItems: "center", ...hug(dir, aligned) }}>
|
|
683
|
-
<CheckMark checked={p.checked} c={colors} />
|
|
684
|
-
{p.label ? <Box style={{ color: t.text }}>{p.label}</Box> : null}
|
|
685
|
-
</Box>
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function Radio({ element, dir, aligned, colors }: Props) {
|
|
690
|
-
const p = element.props;
|
|
691
|
-
return (
|
|
692
|
-
<Box style={{ gap: 10, alignItems: "center", ...hug(dir, aligned) }}>
|
|
693
|
-
<CheckMark checked={p.checked} round c={colors} />
|
|
694
|
-
{p.label ? <Box style={{ color: t.text }}>{p.label}</Box> : null}
|
|
695
|
-
</Box>
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function Switch({ on, c = GRAY }: { on?: boolean | null; c?: Palette }) {
|
|
700
|
-
return (
|
|
701
|
-
<Box
|
|
702
|
-
style={{
|
|
703
|
-
width: 42,
|
|
704
|
-
height: 24,
|
|
705
|
-
borderRadius: 12,
|
|
706
|
-
padding: 3,
|
|
707
|
-
flexShrink: 0,
|
|
708
|
-
backgroundColor: on ? c.accent : t.fill2,
|
|
709
|
-
justifyContent: on ? "flex-end" : "flex-start",
|
|
710
|
-
}}
|
|
711
|
-
>
|
|
712
|
-
<Box style={{ width: 18, height: 18, borderRadius: 9, backgroundColor: t.paper }} />
|
|
713
|
-
</Box>
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function Toggle({ element, colors }: Props) {
|
|
718
|
-
const p = element.props;
|
|
719
|
-
return (
|
|
720
|
-
<Box style={{ gap: 12, alignItems: "center", justifyContent: "space-between" }}>
|
|
721
|
-
{p.label ? <Box style={{ color: t.text }}>{p.label}</Box> : null}
|
|
722
|
-
<Switch on={p.on} c={colors} />
|
|
723
|
-
</Box>
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// ── Navigation & data ───────────────────────────────────────────────────
|
|
728
|
-
|
|
729
|
-
const LEADING_ICON: Record<string, string> = { menu: "menu", back: "arrow-left", close: "x" };
|
|
730
|
-
|
|
731
|
-
function NavBar({ element }: Props) {
|
|
732
|
-
const p = element.props;
|
|
733
|
-
const leading =
|
|
734
|
-
p.leading === "logo" ? (
|
|
735
|
-
<Box style={{ width: 30, height: 30, borderRadius: 8, backgroundColor: t.fill2 }} />
|
|
736
|
-
) : p.leading && p.leading !== "none" ? (
|
|
737
|
-
<IconGlyph name={LEADING_ICON[p.leading]} size={22} />
|
|
738
|
-
) : null;
|
|
739
|
-
const actions = (
|
|
740
|
-
<Box style={{ gap: 18, alignItems: "center", justifyContent: "flex-end" }}>
|
|
741
|
-
{(p.actions ?? []).map((a: string, i: number) => (
|
|
742
|
-
<IconGlyph key={i} name={a} size={22} />
|
|
743
|
-
))}
|
|
744
|
-
</Box>
|
|
745
|
-
);
|
|
746
|
-
const title = p.title ? <Box style={{ fontSize: 17, fontWeight: 600 }}>{p.title}</Box> : null;
|
|
747
|
-
const centered = p.align === "center";
|
|
748
|
-
return (
|
|
749
|
-
<Box
|
|
750
|
-
style={{
|
|
751
|
-
height: 56,
|
|
752
|
-
flexShrink: 0,
|
|
753
|
-
padding: "0 16px",
|
|
754
|
-
gap: 14,
|
|
755
|
-
alignItems: "center",
|
|
756
|
-
borderBottom: `1px solid ${t.line}`,
|
|
757
|
-
backgroundColor: t.paper,
|
|
758
|
-
}}
|
|
759
|
-
>
|
|
760
|
-
{centered
|
|
761
|
-
? [
|
|
762
|
-
<Box key="l" style={{ width: 90, alignItems: "center" }}>{leading}</Box>,
|
|
763
|
-
<Box key="c" style={{ flexGrow: 1, justifyContent: "center" }}>{title}</Box>,
|
|
764
|
-
<Box key="r" style={{ width: 90, justifyContent: "flex-end" }}>{actions}</Box>,
|
|
765
|
-
]
|
|
766
|
-
: [
|
|
767
|
-
leading ? <Box key="l">{leading}</Box> : null,
|
|
768
|
-
title ? <Box key="t">{title}</Box> : null,
|
|
769
|
-
<Box key="s" style={{ flexGrow: 1 }} />,
|
|
770
|
-
<Box key="a">{actions}</Box>,
|
|
771
|
-
]}
|
|
772
|
-
</Box>
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function TabBar({ element, colors: c = GRAY }: Props) {
|
|
777
|
-
const p = element.props;
|
|
778
|
-
const active = p.active ?? 0;
|
|
779
|
-
return (
|
|
780
|
-
<Box
|
|
781
|
-
style={{
|
|
782
|
-
height: 76,
|
|
783
|
-
flexShrink: 0,
|
|
784
|
-
padding: "8px 0 18px",
|
|
785
|
-
borderTop: `1px solid ${t.line}`,
|
|
786
|
-
backgroundColor: t.paper,
|
|
787
|
-
}}
|
|
788
|
-
>
|
|
789
|
-
{(p.items ?? []).map((item: { label: string; icon?: string }, i: number) => {
|
|
790
|
-
const color = i === active ? c.accentText : t.muted;
|
|
791
|
-
return (
|
|
792
|
-
<Box
|
|
793
|
-
key={i}
|
|
794
|
-
style={{ flexGrow: 1, flexBasis: "0px", flexDirection: "column", alignItems: "center", gap: 4, color, fontSize: 11, fontWeight: i === active ? 600 : 400 }}
|
|
795
|
-
>
|
|
796
|
-
<IconGlyph name={item.icon} size={22} color={color} />
|
|
797
|
-
<Box>{item.label}</Box>
|
|
798
|
-
</Box>
|
|
799
|
-
);
|
|
800
|
-
})}
|
|
801
|
-
</Box>
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function Tabs({ element, colors: c = GRAY }: Props) {
|
|
806
|
-
const p = element.props;
|
|
807
|
-
const active = p.active ?? 0;
|
|
808
|
-
return (
|
|
809
|
-
<Box style={{ borderBottom: `1px solid ${t.line}`, gap: 4 }}>
|
|
810
|
-
{(p.items ?? []).map((label: string, i: number) => (
|
|
811
|
-
<Box
|
|
812
|
-
key={i}
|
|
813
|
-
style={{
|
|
814
|
-
padding: "10px 14px",
|
|
815
|
-
marginBottom: -1,
|
|
816
|
-
fontSize: 14,
|
|
817
|
-
fontWeight: i === active ? 600 : 400,
|
|
818
|
-
color: i === active ? c.accentText : t.muted,
|
|
819
|
-
borderBottom: i === active ? `2.5px solid ${c.accentText}` : "2.5px solid transparent",
|
|
820
|
-
}}
|
|
821
|
-
>
|
|
822
|
-
{label}
|
|
823
|
-
</Box>
|
|
824
|
-
))}
|
|
825
|
-
</Box>
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function List({ element, children, dir, stretch }: Props) {
|
|
830
|
-
const dividers = element.props.dividers ?? true;
|
|
831
|
-
const items = withDir(children, "column");
|
|
832
|
-
return (
|
|
833
|
-
<Box style={{ flexDirection: "column", ...grow(element.props.grow, dir), ...fillRow(stretch, false) }}>
|
|
834
|
-
{items.map((c, i) => (
|
|
835
|
-
<Box key={i} style={{ flexDirection: "column" }}>
|
|
836
|
-
{i > 0 && dividers ? <Box style={{ height: 1, backgroundColor: t.line }} /> : null}
|
|
837
|
-
{c}
|
|
838
|
-
</Box>
|
|
839
|
-
))}
|
|
840
|
-
</Box>
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function ListItem({ element, colors }: Props) {
|
|
845
|
-
const p = element.props;
|
|
846
|
-
const leading = (() => {
|
|
847
|
-
switch (p.leading) {
|
|
848
|
-
case "icon":
|
|
849
|
-
return (
|
|
850
|
-
<Box style={{ width: 38, height: 38, borderRadius: 10, backgroundColor: t.fill, alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
|
851
|
-
<IconGlyph name={p.icon} size={20} />
|
|
852
|
-
</Box>
|
|
853
|
-
);
|
|
854
|
-
case "avatar":
|
|
855
|
-
return <AvatarCircle size={40} />;
|
|
856
|
-
case "image":
|
|
857
|
-
return (
|
|
858
|
-
<Box style={{ width: 48, height: 48, borderRadius: 6, backgroundColor: t.fill, border: `1.5px solid ${t.line}`, flexShrink: 0 }} />
|
|
859
|
-
);
|
|
860
|
-
case "checkbox":
|
|
861
|
-
return <CheckMark c={colors} />;
|
|
862
|
-
default:
|
|
863
|
-
return null;
|
|
864
|
-
}
|
|
865
|
-
})();
|
|
866
|
-
const trailing = (() => {
|
|
867
|
-
switch (p.trailing) {
|
|
868
|
-
case "chevron":
|
|
869
|
-
return <IconGlyph name="chevron-right" size={20} color={t.muted} />;
|
|
870
|
-
case "toggle":
|
|
871
|
-
return <Switch on c={colors} />;
|
|
872
|
-
case "text":
|
|
873
|
-
return <Box style={{ fontSize: 13, color: t.muted, flexShrink: 0 }}>{p.trailingText ?? ""}</Box>;
|
|
874
|
-
case "badge":
|
|
875
|
-
return <BadgePill label={p.trailingText ?? "1"} c={colors} />;
|
|
876
|
-
case "icon":
|
|
877
|
-
return <IconGlyph name={p.trailingIcon ?? "more-horizontal"} size={20} color={t.muted} />;
|
|
878
|
-
default:
|
|
879
|
-
return null;
|
|
880
|
-
}
|
|
881
|
-
})();
|
|
882
|
-
return (
|
|
883
|
-
<Box style={{ minHeight: 60, padding: "10px 0", gap: 14, alignItems: "center" }}>
|
|
884
|
-
{leading}
|
|
885
|
-
<Box style={{ flexGrow: 1, flexShrink: 1, flexDirection: "column", gap: 4 }}>
|
|
886
|
-
{p.title ? (
|
|
887
|
-
<Box style={{ fontSize: 15, fontWeight: 600, color: t.ink }}>{p.title}</Box>
|
|
888
|
-
) : (
|
|
889
|
-
<Box style={{ height: 9, width: "55%", borderRadius: 5, backgroundColor: t.fill2 }} />
|
|
890
|
-
)}
|
|
891
|
-
{p.subtitle ? (
|
|
892
|
-
<Box style={{ fontSize: 13, color: t.muted }}>{p.subtitle}</Box>
|
|
893
|
-
) : p.title ? null : (
|
|
894
|
-
<Box style={{ height: 7, width: "80%", borderRadius: 4, backgroundColor: t.fill, marginTop: 4 }} />
|
|
895
|
-
)}
|
|
896
|
-
</Box>
|
|
897
|
-
{trailing}
|
|
898
|
-
</Box>
|
|
899
|
-
);
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const CELL_WIDTHS = [70, 45, 60, 38, 52];
|
|
903
|
-
|
|
904
|
-
function Table({ element }: Props) {
|
|
905
|
-
const p = element.props;
|
|
906
|
-
const cols: string[] = p.columns ?? [];
|
|
907
|
-
const data: string[][] | null = p.data ?? null;
|
|
908
|
-
const rowCount = data ? data.length : p.rows ?? 4;
|
|
909
|
-
return (
|
|
910
|
-
<Box style={{ flexDirection: "column", border: `1.5px solid ${t.line}`, borderRadius: 8, overflow: "hidden", fontSize: 14 }}>
|
|
911
|
-
<Box style={{ backgroundColor: t.fill }}>
|
|
912
|
-
{cols.map((c, i) => (
|
|
913
|
-
<Box key={i} style={{ flexGrow: 1, flexBasis: "0px", padding: "10px 14px", fontSize: 13, fontWeight: 600, color: t.text }}>
|
|
914
|
-
{c}
|
|
915
|
-
</Box>
|
|
916
|
-
))}
|
|
917
|
-
</Box>
|
|
918
|
-
{Array.from({ length: rowCount }, (_, r) => (
|
|
919
|
-
<Box key={r} style={{ borderTop: `1px solid ${t.line}`, minHeight: 42, alignItems: "center" }}>
|
|
920
|
-
{cols.map((_, c) => (
|
|
921
|
-
<Box key={c} style={{ flexGrow: 1, flexBasis: "0px", padding: "10px 14px", color: t.text }}>
|
|
922
|
-
{data ? (
|
|
923
|
-
data[r]?.[c] ?? ""
|
|
924
|
-
) : (
|
|
925
|
-
<Box
|
|
926
|
-
style={{
|
|
927
|
-
height: 8,
|
|
928
|
-
borderRadius: 4,
|
|
929
|
-
backgroundColor: c === 0 ? t.fill2 : t.fill,
|
|
930
|
-
width: `${CELL_WIDTHS[(r + c * 2) % CELL_WIDTHS.length]}%`,
|
|
931
|
-
}}
|
|
932
|
-
/>
|
|
933
|
-
)}
|
|
934
|
-
</Box>
|
|
935
|
-
))}
|
|
936
|
-
</Box>
|
|
937
|
-
))}
|
|
938
|
-
</Box>
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// ── Overlays ────────────────────────────────────────────────────────────
|
|
943
|
-
|
|
944
|
-
function Scrim({ show, children, style }: { show: boolean; children?: ReactNode; style?: CSSProperties }) {
|
|
945
|
-
return (
|
|
946
|
-
<Box
|
|
947
|
-
style={{
|
|
948
|
-
position: "absolute",
|
|
949
|
-
top: 0,
|
|
950
|
-
left: 0,
|
|
951
|
-
right: 0,
|
|
952
|
-
bottom: 0,
|
|
953
|
-
backgroundColor: show ? t.scrim : "transparent",
|
|
954
|
-
...style,
|
|
955
|
-
}}
|
|
956
|
-
>
|
|
957
|
-
{children}
|
|
958
|
-
</Box>
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function Modal({ element, children }: Props) {
|
|
963
|
-
const p = element.props;
|
|
964
|
-
return (
|
|
965
|
-
<Scrim show={p.scrim ?? true} style={{ alignItems: "center", justifyContent: "center", padding: 24 }}>
|
|
966
|
-
<Box
|
|
967
|
-
style={{
|
|
968
|
-
width: p.width ?? 320,
|
|
969
|
-
flexDirection: "column",
|
|
970
|
-
gap: 14,
|
|
971
|
-
padding: 22,
|
|
972
|
-
borderRadius: 14,
|
|
973
|
-
backgroundColor: t.paper,
|
|
974
|
-
boxShadow: "0 8px 30px rgba(0,0,0,0.18)",
|
|
975
|
-
}}
|
|
976
|
-
>
|
|
977
|
-
{p.title ? <Box style={{ fontSize: 18, fontWeight: 600 }}>{p.title}</Box> : null}
|
|
978
|
-
{withDir(children, "column")}
|
|
979
|
-
</Box>
|
|
980
|
-
</Scrim>
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function Drawer({ element, children }: Props) {
|
|
985
|
-
const p = element.props;
|
|
986
|
-
const side: "left" | "right" | "bottom" = p.side ?? "left";
|
|
987
|
-
const bottom = side === "bottom";
|
|
988
|
-
const size = p.size ?? (bottom ? 380 : 300);
|
|
989
|
-
const panel: CSSProperties = bottom
|
|
990
|
-
? { left: 0, right: 0, bottom: 0, minHeight: size, maxHeight: "92%", borderRadius: "18px 18px 0 0", boxShadow: "0 -6px 24px rgba(0,0,0,0.14)" }
|
|
991
|
-
: {
|
|
992
|
-
top: 0,
|
|
993
|
-
bottom: 0,
|
|
994
|
-
[side]: 0,
|
|
995
|
-
width: size,
|
|
996
|
-
boxShadow: side === "left" ? "6px 0 24px rgba(0,0,0,0.14)" : "-6px 0 24px rgba(0,0,0,0.14)",
|
|
997
|
-
};
|
|
998
|
-
return (
|
|
999
|
-
<Scrim show={p.scrim ?? true}>
|
|
1000
|
-
<Box
|
|
1001
|
-
style={{
|
|
1002
|
-
position: "absolute",
|
|
1003
|
-
flexDirection: "column",
|
|
1004
|
-
backgroundColor: t.paper,
|
|
1005
|
-
padding: bottom ? "10px 20px 20px" : 20,
|
|
1006
|
-
gap: 14,
|
|
1007
|
-
overflow: "hidden",
|
|
1008
|
-
...panel,
|
|
1009
|
-
}}
|
|
1010
|
-
>
|
|
1011
|
-
{bottom && (p.handle ?? true) ? (
|
|
1012
|
-
<Box style={{ justifyContent: "center", paddingBottom: 4 }}>
|
|
1013
|
-
<Box style={{ width: 40, height: 5, borderRadius: 3, backgroundColor: t.fill2 }} />
|
|
1014
|
-
</Box>
|
|
1015
|
-
) : null}
|
|
1016
|
-
{p.title ? (
|
|
1017
|
-
<Box style={{ alignItems: "center", justifyContent: "space-between" }}>
|
|
1018
|
-
<Box style={{ fontSize: 18, fontWeight: 600 }}>{p.title}</Box>
|
|
1019
|
-
<IconGlyph name="x" size={22} color={t.muted} />
|
|
1020
|
-
</Box>
|
|
1021
|
-
) : null}
|
|
1022
|
-
{withDir(children, "column")}
|
|
1023
|
-
</Box>
|
|
1024
|
-
</Scrim>
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
export const registry = {
|
|
1029
|
-
Board,
|
|
1030
|
-
Screen,
|
|
1031
|
-
Note,
|
|
1032
|
-
Stack,
|
|
1033
|
-
Grid,
|
|
1034
|
-
Card,
|
|
1035
|
-
Divider,
|
|
1036
|
-
Spacer,
|
|
1037
|
-
Heading,
|
|
1038
|
-
Text,
|
|
1039
|
-
Image,
|
|
1040
|
-
Icon,
|
|
1041
|
-
Avatar,
|
|
1042
|
-
Badge,
|
|
1043
|
-
Button,
|
|
1044
|
-
Input,
|
|
1045
|
-
Checkbox,
|
|
1046
|
-
Radio,
|
|
1047
|
-
Toggle,
|
|
1048
|
-
Select,
|
|
1049
|
-
NavBar,
|
|
1050
|
-
TabBar,
|
|
1051
|
-
Tabs,
|
|
1052
|
-
List,
|
|
1053
|
-
ListItem,
|
|
1054
|
-
Table,
|
|
1055
|
-
Modal,
|
|
1056
|
-
Drawer,
|
|
1057
|
-
};
|