@coffer-org/web-ui 4.0.0 → 6.0.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.
Files changed (53) hide show
  1. package/dist/components/ui/copy-button.d.ts +6 -0
  2. package/dist/components/ui/copy-button.js +28 -0
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js +2 -1
  5. package/dist/internal-link.d.ts +15 -0
  6. package/dist/internal-link.js +66 -0
  7. package/dist/layout.js +3 -3
  8. package/dist/registry.d.ts +2 -1
  9. package/dist/registry.js +1 -0
  10. package/dist/render/activate.d.ts +4 -0
  11. package/dist/render/activate.js +11 -0
  12. package/dist/render/blocks/balance.js +3 -2
  13. package/dist/render/blocks/block-shell.d.ts +1 -0
  14. package/dist/render/blocks/block-shell.js +4 -0
  15. package/dist/render/blocks/chart.js +2 -1
  16. package/dist/render/blocks/compare.js +2 -1
  17. package/dist/render/blocks/countdown.js +2 -1
  18. package/dist/render/blocks/idcard.js +2 -1
  19. package/dist/render/blocks/journal.js +2 -1
  20. package/dist/render/blocks/ledger.js +2 -2
  21. package/dist/render/blocks/manifest.js +2 -1
  22. package/dist/render/blocks/masthead.js +2 -1
  23. package/dist/render/blocks/meter.js +2 -2
  24. package/dist/render/blocks/nutrition.js +2 -2
  25. package/dist/render/blocks/plaque.js +2 -1
  26. package/dist/render/blocks/receipt.js +2 -2
  27. package/dist/render/blocks/score.js +2 -2
  28. package/dist/render/blocks/stats.js +3 -2
  29. package/dist/render/blocks/table.js +5 -4
  30. package/dist/render/chrome.d.ts +21 -3
  31. package/dist/render/chrome.js +46 -6
  32. package/dist/render/core/dimensions.js +1 -1
  33. package/dist/render/core/embed.js +8 -1
  34. package/dist/render/core/money.js +1 -1
  35. package/dist/render/core/range.js +1 -1
  36. package/dist/render/core/reminder.js +1 -1
  37. package/dist/render/core/text.js +57 -3
  38. package/dist/render/core/weight.js +1 -1
  39. package/dist/render/datum.d.ts +4 -0
  40. package/dist/render/datum.js +16 -0
  41. package/dist/render/dispatch.js +4 -2
  42. package/dist/render/field-frame.d.ts +1 -1
  43. package/dist/render/field-frame.js +20 -9
  44. package/dist/render/markdown-view.js +33 -4
  45. package/dist/render/popover-field.js +10 -19
  46. package/dist/render/widgets/display.js +2 -1
  47. package/dist/render/widgets/edit.js +2 -2
  48. package/dist/render/widgets/fixed-collection.js +2 -1
  49. package/dist/render/widgets/group-widgets.js +1 -1
  50. package/dist/render/widgets/group.js +2 -1
  51. package/dist/render/widgets/table-group.js +4 -3
  52. package/dist/types.d.ts +2 -0
  53. package/package.json +2 -2
@@ -0,0 +1,6 @@
1
+ import { type ButtonProps } from './button.tsx';
2
+ export declare function CopyButton({ getText, className, size, }: {
3
+ getText: () => string;
4
+ className?: string;
5
+ size?: ButtonProps['size'];
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Check, Copy } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Button } from "./button.js";
6
+ const CONFIRM_MS = 1500;
7
+ export function CopyButton({ getText, className, size = 'iconXs', }) {
8
+ const { t } = useTranslation();
9
+ const [copied, setCopied] = useState(false);
10
+ const timer = useRef(undefined);
11
+ useEffect(() => () => clearTimeout(timer.current), []);
12
+ async function handleCopy() {
13
+ const text = getText();
14
+ if (!text || !navigator.clipboard)
15
+ return;
16
+ try {
17
+ await navigator.clipboard.writeText(text);
18
+ }
19
+ catch {
20
+ return;
21
+ }
22
+ setCopied(true);
23
+ clearTimeout(timer.current);
24
+ timer.current = setTimeout(() => setCopied(false), CONFIRM_MS);
25
+ }
26
+ const label = t(copied ? 'actions.copied' : 'actions.copy');
27
+ return (_jsx(Button, { type: "button", variant: "outline", size: size, className: className, onClick: () => void handleCopy(), title: label, "aria-label": label, children: copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }) }));
28
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './host-services.ts';
5
5
  export * from './list-view.ts';
6
6
  export * from './plugin-ui.ts';
7
7
  export * from './site-links.ts';
8
+ export * from './internal-link.ts';
8
9
  export * from './lib/utils.ts';
9
10
  export * from './lib/date-format.ts';
10
11
  export * from './hooks/use-as-ref.ts';
@@ -56,7 +57,7 @@ export * from './hooks/use-media-query.ts';
56
57
  export * from './render/dispatch.tsx';
57
58
  export { type Commit } from './render/write.ts';
58
59
  export { type SlotHandle, useSlot, useSlotOptional, useSlotRender, useSlots, SlotProvider, SlotScope, Slot, SlotEditor, SlotValue, } from './render/slot.tsx';
59
- export { DEFAULT_SECTION_ICON, PAGE_STACK, FieldLabel as RenderFieldLabel, FieldError } from './render/chrome.tsx';
60
+ export { DEFAULT_SECTION_ICON, PAGE_STACK, FieldLabel as RenderFieldLabel, FieldError, CHROME_LABEL_CLS, ChromeLabel, } from './render/chrome.tsx';
60
61
  export * from './render/field-errors.tsx';
61
62
  export * from './render/field-frame.tsx';
62
63
  export * from './render/form-types.ts';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from "./host-services.js";
5
5
  export * from "./list-view.js";
6
6
  export * from "./plugin-ui.js";
7
7
  export * from "./site-links.js";
8
+ export * from "./internal-link.js";
8
9
  export * from "./lib/utils.js";
9
10
  export * from "./lib/date-format.js";
10
11
  export * from "./hooks/use-as-ref.js";
@@ -55,7 +56,7 @@ export * from "./lib/use-record-labels.js";
55
56
  export * from "./hooks/use-media-query.js";
56
57
  export * from "./render/dispatch.js";
57
58
  export { useSlot, useSlotOptional, useSlotRender, useSlots, SlotProvider, SlotScope, Slot, SlotEditor, SlotValue, } from "./render/slot.js";
58
- export { DEFAULT_SECTION_ICON, PAGE_STACK, FieldLabel as RenderFieldLabel, FieldError } from "./render/chrome.js";
59
+ export { DEFAULT_SECTION_ICON, PAGE_STACK, FieldLabel as RenderFieldLabel, FieldError, CHROME_LABEL_CLS, ChromeLabel, } from "./render/chrome.js";
59
60
  export * from "./render/field-errors.js";
60
61
  export * from "./render/field-frame.js";
61
62
  export * from "./render/form-types.js";
@@ -0,0 +1,15 @@
1
+ import { type SiteShelf } from './site-links.ts';
2
+ export type HrefKind = {
3
+ kind: 'internal';
4
+ to: string;
5
+ } | {
6
+ kind: 'external';
7
+ href: string;
8
+ } | {
9
+ kind: 'text';
10
+ };
11
+ export type SiteShelfPair = SiteShelf;
12
+ export declare function classifyHref(href: string, origin: string, routes: {
13
+ pattern: string;
14
+ }[], pairs?: SiteShelfPair[]): HrefKind;
15
+ export declare function classifySiteHref(href: string): HrefKind;
@@ -0,0 +1,66 @@
1
+ import { getSiteLinks, getSiteShelves } from "./site-links.js";
2
+ import { getPluginRoutes } from "./plugin-ui.js";
3
+ const SEGMENT = /^[A-Za-z0-9._~%-]+$/;
4
+ function matchesPattern(pathname, pattern) {
5
+ const got = pathname.split('/').filter(Boolean);
6
+ const want = pattern.split('/').filter(Boolean);
7
+ const captured = {};
8
+ for (let i = 0; i < want.length; i++) {
9
+ const w = want[i];
10
+ if (w === '*')
11
+ return got.length >= i ? captured : null;
12
+ const g = got[i];
13
+ if (g === undefined)
14
+ return null;
15
+ if (w.startsWith(':')) {
16
+ if (!SEGMENT.test(g))
17
+ return null;
18
+ captured[w.slice(1)] = g;
19
+ }
20
+ else if (w !== g)
21
+ return null;
22
+ }
23
+ return got.length === want.length ? captured : null;
24
+ }
25
+ function hostOf(origin) {
26
+ try {
27
+ return new URL(origin).hostname;
28
+ }
29
+ catch {
30
+ return '';
31
+ }
32
+ }
33
+ export function classifyHref(href, origin, routes, pairs = []) {
34
+ const raw = href.trim();
35
+ if (!raw || raw.startsWith('//'))
36
+ return { kind: 'text' };
37
+ const isPath = raw.startsWith('/');
38
+ if (!isPath && !/^https?:\/\//i.test(raw))
39
+ return { kind: 'text' };
40
+ let url;
41
+ try {
42
+ url = new URL(raw, origin);
43
+ }
44
+ catch {
45
+ return { kind: 'text' };
46
+ }
47
+ if (url.hostname !== hostOf(origin))
48
+ return { kind: 'external', href: url.href };
49
+ for (const r of routes) {
50
+ const captured = matchesPattern(url.pathname, r.pattern);
51
+ if (!captured)
52
+ continue;
53
+ if (pairs.length && captured['library'] !== undefined && captured['shelf'] !== undefined) {
54
+ const known = pairs.some((p) => p.library === captured['library'] && p.shelf === captured['shelf']);
55
+ if (!known)
56
+ return { kind: 'text' };
57
+ }
58
+ return { kind: 'internal', to: `${url.pathname}${url.search}${url.hash}` };
59
+ }
60
+ return { kind: 'text' };
61
+ }
62
+ export function classifySiteHref(href) {
63
+ const routes = [...getSiteLinks(), ...getPluginRoutes().map((r) => ({ pattern: `/${r.path}` }))];
64
+ const origin = typeof window === 'undefined' ? '' : window.location.origin;
65
+ return classifyHref(href, origin, routes, getSiteShelves());
66
+ }
package/dist/layout.js CHANGED
@@ -8,7 +8,7 @@ import { fieldOptions } from "./lib/format.js";
8
8
  import { withScopeValues, ParentValuesProvider } from "./render/field-scope.js";
9
9
  import { parseJSON } from "./lib/json.js";
10
10
  import { FlowRow, FlowCtx } from "./render/widgets/flow-row.js";
11
- import { STACK, LAYOUT_STACK, GroupTitle, Rail, groupIcon } from "./render/chrome.js";
11
+ import { STACK, LAYOUT_STACK, GroupTitle, Rail, groupIcon, CHROME_LABEL_CLS, } from "./render/chrome.js";
12
12
  import { focusInPopup } from "./render/field-frame.js";
13
13
  import { FieldAddressProvider } from "./render/field-address.js";
14
14
  import { SlotProvider } from "./render/slot.js";
@@ -193,7 +193,7 @@ function GroupSection({ group, children }) {
193
193
  }
194
194
  function RowSection({ group, children }) {
195
195
  const { t } = useTranslation();
196
- return (_jsxs("div", { className: "flex flex-col gap-1 min-w-0", children: [group.label && _jsx("span", { className: "text-xs text-muted", children: t(group.label) }), children] }));
196
+ return (_jsxs("div", { className: "flex flex-col gap-1 min-w-0", children: [group.label && _jsx("span", { className: CHROME_LABEL_CLS, children: t(group.label) }), children] }));
197
197
  }
198
198
  function SheetSection({ group, renderCell, }) {
199
199
  const { t } = useTranslation();
@@ -234,7 +234,7 @@ function renderNodes(nodes, keyPrefix) {
234
234
  }
235
235
  export function LabelledDivider({ label }) {
236
236
  const { t } = useTranslation();
237
- return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "shrink-0 text-xs uppercase tracking-wider text-muted", children: t(label) }), _jsx("div", { className: "flex-1 border-t border-border" })] }));
237
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `shrink-0 ${CHROME_LABEL_CLS}`, children: t(label) }), _jsx("div", { className: "flex-1 border-t border-border" })] }));
238
238
  }
239
239
  function renderDivider(label, key) {
240
240
  return label ? _jsx(LabelledDivider, { label: label }, key) : _jsx("hr", { className: "border-border" }, key);
@@ -1,5 +1,5 @@
1
1
  import type { FieldClient, FieldMeta, LayoutEl } from '@coffer-org/sdk/fields';
2
- import type { FieldRenderer, ElementRenderProps, FieldInlineProps, FieldValueProps, FieldPickerProps, PickerSize, EditorMode, BoxGeom, TemplateImporter } from './types.ts';
2
+ import type { FieldRenderer, ElementRenderProps, FieldInlineProps, FieldValueProps, FieldPickerProps, PickerSize, EditorMode, ActivateMode, BoxGeom, TemplateImporter } from './types.ts';
3
3
  import type { FC } from 'react';
4
4
  export declare const rendererRegistry: Record<string, FieldRenderer>;
5
5
  type NoWidenedProps<P> = Exclude<keyof P, keyof ElementRenderProps> extends never ? unknown : never;
@@ -14,6 +14,7 @@ export declare function registerRenderer<P extends ElementRenderProps>(r: {
14
14
  pickerSize?: PickerSize | ((field: FieldMeta) => PickerSize);
15
15
  Picker?: FC<FieldPickerProps>;
16
16
  editor?: EditorMode;
17
+ activate?: ActivateMode;
17
18
  keys?: FieldRenderer['keys'];
18
19
  } & NoWidenedProps<P>): void;
19
20
  export declare function resolveRenderer(field: Pick<FieldClient, 'kind'>): FieldRenderer | undefined;
package/dist/registry.js CHANGED
@@ -14,6 +14,7 @@ export function registerRenderer(r) {
14
14
  pickerSize: r.pickerSize,
15
15
  Picker: r.Picker,
16
16
  editor: r.editor,
17
+ activate: r.activate,
17
18
  keys: r.keys,
18
19
  };
19
20
  for (const kind of r.kinds) {
@@ -0,0 +1,4 @@
1
+ import type { FieldClient } from '@coffer-org/sdk/fields';
2
+ import type { ActivateMode } from '../types.ts';
3
+ export type { ActivateMode } from '../types.ts';
4
+ export declare function activateMode(field: Pick<FieldClient, 'kind' | 'hints'>): ActivateMode;
@@ -0,0 +1,11 @@
1
+ import { resolveRenderer } from "../registry.js";
2
+ const MODES = new Set(['edit', 'value']);
3
+ function isActivateMode(v) {
4
+ return typeof v === 'string' && MODES.has(v);
5
+ }
6
+ export function activateMode(field) {
7
+ const override = field.hints?.['activate'];
8
+ if (isActivateMode(override))
9
+ return override;
10
+ return resolveRenderer(field)?.activate ?? 'edit';
11
+ }
@@ -2,13 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { registerRenderer } from "../../registry.js";
4
4
  import { Slot } from "../slot.js";
5
- import { BlockShell } from "./block-shell.js";
5
+ import { CHROME_LABEL_CLS } from "../chrome.js";
6
+ import { BlockShell, fieldDatumClass } from "./block-shell.js";
6
7
  function BalancePage({ el }) {
7
8
  const g = el;
8
9
  const sourceName = g.view?.source;
9
10
  const { t } = useTranslation();
10
11
  const caption = g.view?.caption;
11
- const body = (_jsxs("div", { className: "flex flex-col gap-1", "data-testid": "balance", children: [_jsx(Slot, { name: sourceName, render: (_handle, defaultDisplay) => (_jsx("span", { className: "text-3xl font-semibold tabular-nums", "data-testid": "balance-figure", children: defaultDisplay })) }), caption && (_jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-muted", "data-testid": "balance-caption", children: t(caption) }))] }));
12
+ const body = (_jsxs("div", { className: "flex flex-col gap-1", "data-testid": "balance", children: [_jsx(Slot, { name: sourceName, render: (handle, defaultDisplay) => (_jsx("span", { className: `text-3xl font-semibold ${fieldDatumClass(handle)}`.trim(), "data-testid": "balance-figure", children: defaultDisplay })) }), caption && (_jsx("div", { className: CHROME_LABEL_CLS, "data-testid": "balance-caption", children: t(caption) }))] }));
12
13
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
13
14
  }
14
15
  registerRenderer({ kinds: ['balance'], layout: 'block', editor: 'custom', Page: BalancePage });
@@ -13,3 +13,4 @@ export declare function BlockShell({ label, icon, children, }: {
13
13
  children: ReactNode;
14
14
  }): ReactElement;
15
15
  export declare function fieldLabel(handle: SlotHandle, t: TFn): string | undefined;
16
+ export declare function fieldDatumClass(handle: SlotHandle): string;
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { isNamedField } from '@coffer-org/sdk/fields';
4
4
  import { SectionShell } from "../../layout.js";
5
+ import { datumClass } from "../datum.js";
5
6
  export function findSlot(slots, name) {
6
7
  return name !== undefined ? slots.find((s) => s.name === name) : undefined;
7
8
  }
@@ -18,3 +19,6 @@ export function fieldLabel(handle, t) {
18
19
  const key = handle.field.label ?? (isNamedField(handle.field) ? handle.field.type.label : undefined);
19
20
  return key ? t(key) : undefined;
20
21
  }
22
+ export function fieldDatumClass(handle) {
23
+ return isNamedField(handle.field) ? datumClass(handle.field.type) : '';
24
+ }
@@ -6,6 +6,7 @@ import { useGroupEdit } from "../widgets/group-edit.js";
6
6
  import { GroupField } from "../widgets/group-field.js";
7
7
  import { FieldSlot } from "../dispatch.js";
8
8
  import { useSlot } from "../slot.js";
9
+ import { datumClass } from "../datum.js";
9
10
  import { toSeries, sparkline, seriesDelta } from "./series.js";
10
11
  import { partField } from "../use-collection-rows.js";
11
12
  const BOX = { width: 100, height: 28, pad: 2 };
@@ -37,7 +38,7 @@ function ChartPage({ el, value, recordCtx, mode }) {
37
38
  const reading = (p) => (_jsx(FieldSlot, { el: valueFld, field: valueFld.type, value: p.row?.[valueKey] ?? p.value, mode: "view", bare: true, recordCtx: recordCtx }));
38
39
  const lowest = points.length ? points.reduce((a, b) => (b.value < a.value ? b : a)) : undefined;
39
40
  const highest = points.length ? points.reduce((a, b) => (b.value > a.value ? b : a)) : undefined;
40
- const chart = last === undefined ? (_jsx("p", { className: "py-1 text-sm text-muted", "data-testid": "chart-empty", children: t('form.noSeries', 'No readings yet') })) : (_jsxs("div", { className: "flex flex-col gap-1.5", "data-testid": "chart-body", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("div", { className: "min-w-0 text-2xl font-semibold tabular-nums text-text", "data-testid": "chart-last", children: reading(last) }), delta !== undefined && (_jsx("span", { className: `text-sm font-medium tabular-nums ${delta > 0 ? 'text-accent' : delta < 0 ? 'text-danger' : 'text-muted'}`, "data-testid": "chart-delta", children: fmtDelta(delta) }))] }), _jsx("svg", { className: "h-12 w-full text-accent", viewBox: `0 0 ${BOX.width} ${BOX.height}`, preserveAspectRatio: "none", "aria-hidden": "true", "data-testid": "chart-spark", children: _jsx("path", { d: spark.path, "data-testid": "chart-path", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" }) }), _jsxs("div", { className: "flex items-baseline gap-1 text-[10px] tabular-nums text-muted", "data-testid": "chart-range", children: [lowest && reading(lowest), _jsx("span", { "aria-hidden": "true", children: "\u2013" }), highest && reading(highest)] })] }));
41
+ const chart = last === undefined ? (_jsx("p", { className: "py-1 text-sm text-muted", "data-testid": "chart-empty", children: t('form.noSeries', 'No readings yet') })) : (_jsxs("div", { className: "flex flex-col gap-1.5", "data-testid": "chart-body", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("div", { className: `min-w-0 text-2xl font-semibold text-text ${datumClass(valueFld.type)}`.trim(), "data-testid": "chart-last", children: reading(last) }), delta !== undefined && (_jsx("span", { className: `text-sm font-medium tabular-nums ${delta > 0 ? 'text-accent' : delta < 0 ? 'text-danger' : 'text-muted'}`, "data-testid": "chart-delta", children: fmtDelta(delta) }))] }), _jsx("svg", { className: "h-12 w-full text-accent", viewBox: `0 0 ${BOX.width} ${BOX.height}`, preserveAspectRatio: "none", "aria-hidden": "true", "data-testid": "chart-spark", children: _jsx("path", { d: spark.path, "data-testid": "chart-path", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" }) }), _jsxs("div", { className: `flex items-baseline gap-1 text-[10px] text-muted ${datumClass(valueFld.type)}`.trim(), "data-testid": "chart-range", children: [lowest && reading(lowest), _jsx("span", { "aria-hidden": "true", children: "\u2013" }), highest && reading(highest)] })] }));
41
42
  const content = editing ? editor : chart;
42
43
  if (mode !== 'view')
43
44
  return content;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { registerRenderer } from "../../registry.js";
4
4
  import { Slot } from "../slot.js";
5
+ import { CHROME_LABEL_CLS } from "../chrome.js";
5
6
  import { BlockShell } from "./block-shell.js";
6
7
  function ComparePage({ el }) {
7
8
  const g = el;
@@ -10,7 +11,7 @@ function ComparePage({ el }) {
10
11
  const right = g.view?.right ?? [];
11
12
  const leftLabel = g.view?.leftLabel;
12
13
  const rightLabel = g.view?.rightLabel;
13
- const body = (_jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", "data-testid": "compare", children: [_jsxs("div", { className: "min-w-0 space-y-3", "data-testid": "compare-left", children: [leftLabel && _jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-muted", children: t(leftLabel) }), left.map((n) => (_jsx(Slot, { name: n }, n)))] }), _jsxs("div", { className: "min-w-0 space-y-3 sm:border-l sm:border-border sm:pl-4", "data-testid": "compare-right", children: [rightLabel && _jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-muted", children: t(rightLabel) }), right.map((n) => (_jsx(Slot, { name: n }, n)))] })] }));
14
+ const body = (_jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", "data-testid": "compare", children: [_jsxs("div", { className: "min-w-0 space-y-3", "data-testid": "compare-left", children: [leftLabel && _jsx("div", { className: CHROME_LABEL_CLS, children: t(leftLabel) }), left.map((n) => (_jsx(Slot, { name: n }, n)))] }), _jsxs("div", { className: "min-w-0 space-y-3 sm:border-l sm:border-border sm:pl-4", "data-testid": "compare-right", children: [rightLabel && _jsx("div", { className: CHROME_LABEL_CLS, children: t(rightLabel) }), right.map((n) => (_jsx(Slot, { name: n }, n)))] })] }));
14
15
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
15
16
  }
16
17
  registerRenderer({ kinds: ['compare'], layout: 'block', editor: 'custom', Page: ComparePage });
@@ -3,6 +3,7 @@ import { ArrowUp, ArrowDown, Minus } from 'lucide-react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { registerRenderer } from "../../registry.js";
5
5
  import { Slot } from "../slot.js";
6
+ import { CHROME_LABEL_CLS } from "../chrome.js";
6
7
  import { BlockShell } from "./block-shell.js";
7
8
  const MS_PER_DAY = 86_400_000;
8
9
  export function countdownDays(value, today) {
@@ -30,7 +31,7 @@ function CountdownPage({ el }) {
30
31
  const direction = days > 0 ? 'remaining' : days < 0 ? 'elapsed' : 'today';
31
32
  const Ico = days > 0 ? ArrowUp : days < 0 ? ArrowDown : Minus;
32
33
  return (_jsxs("span", { className: "inline-flex items-baseline gap-3", "data-testid": "countdown-figure", "data-direction": direction, children: [_jsxs("span", { className: "inline-flex items-baseline gap-1.5", children: [_jsx(Ico, { size: 16, className: "shrink-0 self-center text-muted", "aria-hidden": "true" }), _jsx("span", { className: "text-3xl font-bold tabular-nums", children: Math.abs(days) })] }), _jsx("span", { className: "text-sm text-muted", "data-testid": "countdown-date", children: defaultDisplay })] }));
33
- } }), caption && (_jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-muted", "data-testid": "countdown-caption", children: t(caption) }))] }));
34
+ } }), caption && (_jsx("div", { className: CHROME_LABEL_CLS, "data-testid": "countdown-caption", children: t(caption) }))] }));
34
35
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
35
36
  }
36
37
  registerRenderer({ kinds: ['countdown'], layout: 'block', editor: 'custom', Page: CountdownPage });
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { registerRenderer } from "../../registry.js";
3
3
  import { Slot, useSlots } from "../slot.js";
4
+ import { CHROME_LABEL_CLS } from "../chrome.js";
4
5
  import { findSlot, findSlots } from "./block-shell.js";
5
6
  function groupedNumber(raw) {
6
7
  const compact = raw.replace(/\s+/g, '');
@@ -19,7 +20,7 @@ function IdcardPage({ el }) {
19
20
  const slots = useSlots();
20
21
  const overline = findSlot(slots, overlineName);
21
22
  const metaVisible = findSlots(slots, metaNames).filter((s) => !s.handle.hidden);
22
- return (_jsxs("div", { className: "space-y-1", "data-testid": "idcard", children: [overline && !overline.handle.hidden && (_jsx("div", { className: "min-w-0 text-xs font-medium uppercase tracking-widest text-muted", "data-testid": "idcard-overline", children: _jsx(Slot, { name: overlineName }) })), _jsx("div", { className: "min-w-0", children: _jsx(Slot, { name: numberName, render: (handle, defaultDisplay) => {
23
+ return (_jsxs("div", { className: "space-y-1", "data-testid": "idcard", children: [overline && !overline.handle.hidden && (_jsx("div", { className: CHROME_LABEL_CLS, "data-testid": "idcard-overline", children: _jsx(Slot, { name: overlineName }) })), _jsx("div", { className: "min-w-0", children: _jsx(Slot, { name: numberName, render: (handle, defaultDisplay) => {
23
24
  const raw = typeof handle.value === 'string' || typeof handle.value === 'number' ? String(handle.value) : '';
24
25
  const grouped = groupedNumber(raw);
25
26
  if (!grouped)
@@ -13,6 +13,7 @@ import { ChecklistContent } from "../widgets/checklist-content.js";
13
13
  import { useFieldErrors } from "../field-errors.js";
14
14
  import { useCollectionRows, partField } from "../use-collection-rows.js";
15
15
  import { useSlot } from "../slot.js";
16
+ import { datumClass } from "../datum.js";
16
17
  function cellNode(f, row, key, editing, live, onCommit, recordCtx, error) {
17
18
  if (editing) {
18
19
  return (_jsx(FieldSlot, { el: f, field: f.type, fieldKey: key, value: row[key] ?? '', onChange: onCommit, mode: "edit", bare: true, recordCtx: recordCtx, error: error }));
@@ -42,7 +43,7 @@ function JournalPage({ el, value, recordCtx, mode }) {
42
43
  .filter((f) => isNamedField(f) && f.key !== dateKey && f.key !== textKey)
43
44
  .map((f) => ({ key: f.key, type: f.type }));
44
45
  const errorFor = (idx, key) => isStorageGroup(group) ? errorAt([group.key, idx, key]) : undefined;
45
- const content = (_jsxs("div", { className: `flex flex-col ${STACK}`, children: [rows.map((row, idx) => (_jsx(Rail, { children: _jsxs("div", { className: "flex items-start gap-3", "data-testid": `journal-entry-${idx}`, children: [_jsx("div", { className: "w-20 shrink-0 text-xs text-muted tabular-nums", "data-testid": `journal-date-${idx}`, children: cellNode(dateFld, row, dateKey, editing, live, (v) => updateRow(idx, dateKey, v), recordCtx, errorFor(idx, dateKey)) }), _jsx("div", { className: "min-w-0 flex-1 text-sm", "data-testid": `journal-text-${idx}`, children: cellNode(textFld, row, textKey, editing, live, (v) => updateRow(idx, textKey, v), recordCtx, errorFor(idx, textKey)) }), restFields.length > 0 && (_jsx(ChecklistContent, { fields: restFields, row: row, editing: editing, onChange: (key, v) => updateRow(idx, key, v), recordCtx: recordCtx, error: (key) => errorFor(idx, key) })), rowTools && _jsx(RemoveBtn, { className: "mt-0.5", onClick: () => removeRow(idx) })] }) }, rowId(row)))), rowTools && _jsx(AddBtn, { onClick: addRow, children: t('form.addRow') })] }));
46
+ const content = (_jsxs("div", { className: `flex flex-col ${STACK}`, children: [rows.map((row, idx) => (_jsx(Rail, { children: _jsxs("div", { className: "flex items-start gap-3", "data-testid": `journal-entry-${idx}`, children: [_jsx("div", { className: `w-20 shrink-0 text-xs text-muted ${datumClass(dateFld.type)}`.trim(), "data-testid": `journal-date-${idx}`, children: cellNode(dateFld, row, dateKey, editing, live, (v) => updateRow(idx, dateKey, v), recordCtx, errorFor(idx, dateKey)) }), _jsx("div", { className: "min-w-0 flex-1 text-sm", "data-testid": `journal-text-${idx}`, children: cellNode(textFld, row, textKey, editing, live, (v) => updateRow(idx, textKey, v), recordCtx, errorFor(idx, textKey)) }), restFields.length > 0 && (_jsx(ChecklistContent, { fields: restFields, row: row, editing: editing, onChange: (key, v) => updateRow(idx, key, v), recordCtx: recordCtx, error: (key) => errorFor(idx, key) })), rowTools && _jsx(RemoveBtn, { className: "mt-0.5", onClick: () => removeRow(idx) })] }) }, rowId(row)))), rowTools && _jsx(AddBtn, { onClick: addRow, children: t('form.addRow') })] }));
46
47
  if (mode !== 'view')
47
48
  return content;
48
49
  return (_jsxs("section", { className: `${groupScopeClass(depth)} flex flex-col gap-1 min-w-0`, "data-testid": "journal", children: [group.label && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h3", { className: "text-[13px] font-semibold text-accent", children: t(group.label) }), !live && g.actions && _jsx("span", { className: "ml-auto flex shrink-0 items-center gap-1.5", children: g.actions })] })), _jsx(GroupDepthProvider, { depth: depth + 1, children: content })] }));
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { registerRenderer } from "../../registry.js";
4
4
  import { Slot, useSlots } from "../slot.js";
5
- import { BlockShell, fieldLabel } from "./block-shell.js";
5
+ import { BlockShell, fieldLabel, fieldDatumClass } from "./block-shell.js";
6
6
  function LedgerPage({ el }) {
7
7
  const g = el;
8
8
  const { t } = useTranslation();
@@ -10,7 +10,7 @@ function LedgerPage({ el }) {
10
10
  const body = (_jsx("div", { className: "flex flex-col divide-y divide-border", "data-testid": "ledger", children: slots.map((s, i) => {
11
11
  if (s.handle.hidden)
12
12
  return null;
13
- return (_jsx("div", { className: "min-w-0 py-2", "data-testid": `ledger-row-${i}`, children: _jsx(Slot, { name: s.name, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline gap-2", "data-testid": "ledger-leader", children: [_jsx("span", { className: "shrink-0", children: fieldLabel(handle, t) }), _jsx("span", { className: "min-w-0 flex-1 border-b border-dotted border-border", "data-testid": "ledger-dots" }), _jsx("span", { className: "shrink-0 text-right font-medium tabular-nums", children: defaultDisplay })] })) }) }, s.name));
13
+ return (_jsx("div", { className: "min-w-0 py-2", "data-testid": `ledger-row-${i}`, children: _jsx(Slot, { name: s.name, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline gap-2", "data-testid": "ledger-leader", children: [_jsx("span", { className: "shrink-0", children: fieldLabel(handle, t) }), _jsx("span", { className: "min-w-0 flex-1 border-b border-dotted border-border", "data-testid": "ledger-dots" }), _jsx("span", { className: `shrink-0 text-right font-medium ${fieldDatumClass(handle)}`.trim(), children: defaultDisplay })] })) }) }, s.name));
14
14
  }) }));
15
15
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
16
16
  }
@@ -11,6 +11,7 @@ import { FieldSlot } from "../dispatch.js";
11
11
  import { GroupField } from "../widgets/group-field.js";
12
12
  import { ChecklistContent } from "../widgets/checklist-content.js";
13
13
  import { useFieldErrors } from "../field-errors.js";
14
+ import { datumClass } from "../datum.js";
14
15
  import { useCollectionRows, partField } from "../use-collection-rows.js";
15
16
  import { useSlot } from "../slot.js";
16
17
  function cellNode(f, row, key, editing, live, onCommit, recordCtx, error) {
@@ -42,7 +43,7 @@ function ManifestPage({ el, value, recordCtx, mode }) {
42
43
  .filter((f) => isNamedField(f) && f.key !== qtyKey && f.key !== itemKey)
43
44
  .map((f) => ({ key: f.key, type: f.type }));
44
45
  const errorFor = (idx, key) => isStorageGroup(group) ? errorAt([group.key, idx, key]) : undefined;
45
- const content = (_jsxs("div", { className: `flex flex-col ${STACK}`, children: [rows.map((row, idx) => (_jsx(Rail, { children: _jsxs("div", { className: "flex items-center gap-3", "data-testid": `manifest-row-${idx}`, children: [_jsx("div", { className: "w-12 shrink-0 text-right text-sm tabular-nums", "data-testid": `manifest-quantity-${idx}`, children: cellNode(qtyFld, row, qtyKey, editing, live, (v) => updateRow(idx, qtyKey, v), recordCtx, errorFor(idx, qtyKey)) }), _jsx("div", { className: "min-w-0 flex-1 text-sm", "data-testid": `manifest-item-${idx}`, children: cellNode(itemFld, row, itemKey, editing, live, (v) => updateRow(idx, itemKey, v), recordCtx, errorFor(idx, itemKey)) }), restFields.length > 0 && (_jsx(ChecklistContent, { fields: restFields, row: row, editing: editing, onChange: (key, v) => updateRow(idx, key, v), recordCtx: recordCtx, error: (key) => errorFor(idx, key) })), rowTools && _jsx(RemoveBtn, { onClick: () => removeRow(idx) })] }) }, rowId(row)))), rowTools && _jsx(AddBtn, { onClick: addRow, children: t('form.addRow') })] }));
46
+ const content = (_jsxs("div", { className: `flex flex-col ${STACK}`, children: [rows.map((row, idx) => (_jsx(Rail, { children: _jsxs("div", { className: "flex items-center gap-3", "data-testid": `manifest-row-${idx}`, children: [_jsx("div", { className: `w-12 shrink-0 text-right text-sm ${datumClass(qtyFld.type)}`.trim(), "data-testid": `manifest-quantity-${idx}`, children: cellNode(qtyFld, row, qtyKey, editing, live, (v) => updateRow(idx, qtyKey, v), recordCtx, errorFor(idx, qtyKey)) }), _jsx("div", { className: "min-w-0 flex-1 text-sm", "data-testid": `manifest-item-${idx}`, children: cellNode(itemFld, row, itemKey, editing, live, (v) => updateRow(idx, itemKey, v), recordCtx, errorFor(idx, itemKey)) }), restFields.length > 0 && (_jsx(ChecklistContent, { fields: restFields, row: row, editing: editing, onChange: (key, v) => updateRow(idx, key, v), recordCtx: recordCtx, error: (key) => errorFor(idx, key) })), rowTools && _jsx(RemoveBtn, { onClick: () => removeRow(idx) })] }) }, rowId(row)))), rowTools && _jsx(AddBtn, { onClick: addRow, children: t('form.addRow') })] }));
46
47
  if (mode !== 'view')
47
48
  return content;
48
49
  return (_jsxs("section", { className: `${groupScopeClass(depth)} flex flex-col gap-1 min-w-0`, "data-testid": "manifest", children: [group.label && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h3", { className: "text-[13px] font-semibold text-accent", children: t(group.label) }), !live && g.actions && _jsx("span", { className: "ml-auto flex shrink-0 items-center gap-1.5", children: g.actions })] })), _jsx(GroupDepthProvider, { depth: depth + 1, children: content })] }));
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { registerRenderer } from "../../registry.js";
3
3
  import { Slot, useSlots } from "../slot.js";
4
+ import { CHROME_LABEL_CLS } from "../chrome.js";
4
5
  import { findSlot, findSlots } from "./block-shell.js";
5
6
  function MastheadPage({ el }) {
6
7
  const g = el;
@@ -10,6 +11,6 @@ function MastheadPage({ el }) {
10
11
  const slots = useSlots();
11
12
  const overline = findSlot(slots, overlineName);
12
13
  const metaVisible = findSlots(slots, metaNames).filter((s) => !s.handle.hidden);
13
- return (_jsxs("div", { className: "space-y-1", "data-testid": "masthead", children: [overline && !overline.handle.hidden && (_jsx("div", { className: "min-w-0 text-xs font-medium uppercase tracking-widest text-muted", "data-testid": "masthead-overline", children: _jsx(Slot, { name: overlineName }) })), _jsx("div", { className: "min-w-0 text-2xl leading-tight font-bold", "data-testid": "masthead-title", children: _jsx(Slot, { name: titleName }) }), metaVisible.length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-x-1 border-t border-b border-border py-1.5 text-sm text-muted", "data-testid": "masthead-meta", children: metaVisible.map((s, i) => (_jsx("span", { className: `inline-flex min-w-0 items-center ${i > 0 ? "before:mr-2 before:text-muted before:content-['·']" : ''}`, "data-testid": `masthead-meta-${i}`, children: _jsx(Slot, { name: s.name }) }, s.name))) }))] }));
14
+ return (_jsxs("div", { className: "space-y-1", "data-testid": "masthead", children: [overline && !overline.handle.hidden && (_jsx("div", { className: CHROME_LABEL_CLS, "data-testid": "masthead-overline", children: _jsx(Slot, { name: overlineName }) })), _jsx("div", { className: "min-w-0 text-2xl leading-tight font-bold", "data-testid": "masthead-title", children: _jsx(Slot, { name: titleName }) }), metaVisible.length > 0 && (_jsx("div", { className: "flex flex-wrap items-center gap-x-1 border-t border-b border-border py-1.5 text-sm text-muted", "data-testid": "masthead-meta", children: metaVisible.map((s, i) => (_jsx("span", { className: `inline-flex min-w-0 items-center ${i > 0 ? "before:mr-2 before:text-muted before:content-['·']" : ''}`, "data-testid": `masthead-meta-${i}`, children: _jsx(Slot, { name: s.name }) }, s.name))) }))] }));
14
15
  }
15
16
  registerRenderer({ kinds: ['masthead'], layout: 'block', editor: 'custom', Page: MastheadPage });
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { registerRenderer } from "../../registry.js";
3
3
  import { Slot, useSlots } from "../slot.js";
4
- import { BlockShell, findSlot } from "./block-shell.js";
4
+ import { BlockShell, findSlot, fieldDatumClass } from "./block-shell.js";
5
5
  function resolveRange(view, field) {
6
6
  const min = Number(view?.min ?? field.hints['min'] ?? 0);
7
7
  const maxRaw = view?.max ?? field.hints['max'];
@@ -25,7 +25,7 @@ function MeterPage({ el }) {
25
25
  const { min, max } = resolveRange(view, handle.field.type);
26
26
  const n = magnitudeOf(handle.value);
27
27
  const ratio = Number.isFinite(n) ? Math.max(0, Math.min(1, (n - min) / (max - min))) : 0;
28
- return (_jsxs("span", { className: "flex min-w-0 flex-col gap-1", "data-testid": "meter-figure", children: [_jsx("span", { className: "text-2xl font-semibold tabular-nums", children: defaultDisplay }), _jsx("span", { role: "progressbar", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : min, className: "h-2 w-full overflow-hidden rounded-full bg-border", "data-testid": "meter-bar", children: _jsx("span", { className: "block h-full rounded-full bg-accent transition-[width]", style: { width: `${ratio * 100}%` }, "data-testid": "meter-fill" }) })] }));
28
+ return (_jsxs("span", { className: "flex min-w-0 flex-col gap-1", "data-testid": "meter-figure", children: [_jsx("span", { className: `text-2xl font-semibold ${fieldDatumClass(handle)}`.trim(), children: defaultDisplay }), _jsx("span", { role: "progressbar", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : min, className: "h-2 w-full overflow-hidden rounded-full bg-border", "data-testid": "meter-bar", children: _jsx("span", { className: "block h-full rounded-full bg-accent transition-[width]", style: { width: `${ratio * 100}%` }, "data-testid": "meter-fill" }) })] }));
29
29
  } }) }), of && !of.handle.hidden && (_jsx("div", { className: "min-w-0 text-sm text-muted", "data-testid": "meter-of", children: _jsx(Slot, { name: ofName }) }))] }));
30
30
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
31
31
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { registerRenderer } from "../../registry.js";
4
4
  import { Slot, useSlots } from "../slot.js";
5
- import { BlockShell, fieldLabel, findSlot, findSlots } from "./block-shell.js";
5
+ import { BlockShell, fieldLabel, fieldDatumClass, findSlot, findSlots } from "./block-shell.js";
6
6
  function NutritionPage({ el }) {
7
7
  const g = el;
8
8
  const { t } = useTranslation();
@@ -15,7 +15,7 @@ function NutritionPage({ el }) {
15
15
  const visibleRows = rows.filter((r) => !r.handle.hidden);
16
16
  if (!energyVisible && visibleRows.length === 0)
17
17
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: null }));
18
- const body = (_jsxs("div", { className: "w-full max-w-xs border-2 border-text p-3", "data-testid": "nutrition", children: [energyVisible && (_jsx("div", { className: "mb-2 border-b-4 border-text pb-2", "data-testid": "nutrition-energy-row", children: _jsx(Slot, { name: energyName, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline justify-between gap-3", "data-testid": "nutrition-energy", children: [_jsx("span", { className: "min-w-0 text-base font-bold uppercase tracking-wide", children: fieldLabel(handle, t) }), _jsx("span", { className: "shrink-0 text-2xl font-black tabular-nums", children: defaultDisplay })] })) }) })), _jsx("div", { className: "flex flex-col divide-y divide-border", children: rows.map((r, i) => r.handle.hidden ? null : (_jsx("div", { className: "min-w-0 py-1", "data-testid": `nutrition-row-${i}`, children: _jsx(Slot, { name: r.name, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline justify-between gap-3 text-sm", children: [_jsx("span", { className: "min-w-0", children: fieldLabel(handle, t) }), _jsx("span", { className: "shrink-0 text-right font-medium tabular-nums", children: defaultDisplay })] })) }) }, r.name))) })] }));
18
+ const body = (_jsxs("div", { className: "w-full max-w-xs border-2 border-text p-3", "data-testid": "nutrition", children: [energyVisible && (_jsx("div", { className: "mb-2 border-b-4 border-text pb-2", "data-testid": "nutrition-energy-row", children: _jsx(Slot, { name: energyName, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline justify-between gap-3", "data-testid": "nutrition-energy", children: [_jsx("span", { className: "min-w-0 text-base font-bold tracking-wide", children: fieldLabel(handle, t) }), _jsx("span", { className: `shrink-0 text-2xl font-black ${fieldDatumClass(handle)}`.trim(), children: defaultDisplay })] })) }) })), _jsx("div", { className: "flex flex-col divide-y divide-border", children: rows.map((r, i) => r.handle.hidden ? null : (_jsx("div", { className: "min-w-0 py-1", "data-testid": `nutrition-row-${i}`, children: _jsx(Slot, { name: r.name, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline justify-between gap-3 text-sm", children: [_jsx("span", { className: "min-w-0", children: fieldLabel(handle, t) }), _jsx("span", { className: `shrink-0 text-right font-medium ${fieldDatumClass(handle)}`.trim(), children: defaultDisplay })] })) }) }, r.name))) })] }));
19
19
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
20
20
  }
21
21
  registerRenderer({
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { registerRenderer } from "../../registry.js";
3
3
  import { Slot, useSlots } from "../slot.js";
4
+ import { CHROME_LABEL_CLS } from "../chrome.js";
4
5
  import { BlockShell, findSlot, findSlots } from "./block-shell.js";
5
6
  function PlaquePage({ el }) {
6
7
  const g = el;
@@ -9,7 +10,7 @@ function PlaquePage({ el }) {
9
10
  const slots = useSlots();
10
11
  const title = findSlot(slots, titleName);
11
12
  const subtitles = findSlots(slots, subtitleNames);
12
- const body = (_jsxs("div", { className: "flex flex-col items-center gap-3 py-4 text-center", "data-testid": "plaque", children: [title && !title.handle.hidden && (_jsx("div", { className: "min-w-0 w-full border-b border-border pb-3", "data-testid": "plaque-item-0", children: _jsx(Slot, { name: titleName, render: (_handle, defaultDisplay) => (_jsx("div", { className: "text-base font-semibold text-text", "data-testid": "plaque-title", children: defaultDisplay })) }) })), subtitles.map((s, i) => s.handle.hidden ? null : (_jsx("div", { className: "min-w-0", "data-testid": `plaque-item-${i + 1}`, children: _jsx(Slot, { name: s.name, render: (_handle, defaultDisplay) => (_jsx("div", { className: "text-xs font-medium uppercase tracking-wider text-muted", "data-testid": "plaque-subtitle", children: defaultDisplay })) }) }, s.name)))] }));
13
+ const body = (_jsxs("div", { className: "flex flex-col items-center gap-3 py-4 text-center", "data-testid": "plaque", children: [title && !title.handle.hidden && (_jsx("div", { className: "min-w-0 w-full border-b border-border pb-3", "data-testid": "plaque-item-0", children: _jsx(Slot, { name: titleName, render: (_handle, defaultDisplay) => (_jsx("div", { className: "text-base font-semibold text-text", "data-testid": "plaque-title", children: defaultDisplay })) }) })), subtitles.map((s, i) => s.handle.hidden ? null : (_jsx("div", { className: "min-w-0", "data-testid": `plaque-item-${i + 1}`, children: _jsx(Slot, { name: s.name, render: (_handle, defaultDisplay) => (_jsx("div", { className: CHROME_LABEL_CLS, "data-testid": "plaque-subtitle", children: defaultDisplay })) }) }, s.name)))] }));
13
14
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
14
15
  }
15
16
  registerRenderer({ kinds: ['plaque'], layout: 'block', editor: 'custom', Page: PlaquePage });
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { registerRenderer } from "../../registry.js";
4
4
  import { Slot, useSlots } from "../slot.js";
5
- import { BlockShell, fieldLabel, findSlot, findSlots } from "./block-shell.js";
5
+ import { BlockShell, fieldLabel, fieldDatumClass, findSlot, findSlots } from "./block-shell.js";
6
6
  function ReceiptPage({ el }) {
7
7
  const g = el;
8
8
  const { t } = useTranslation();
@@ -11,7 +11,7 @@ function ReceiptPage({ el }) {
11
11
  const slots = useSlots();
12
12
  const lines = findSlots(slots, lineNames);
13
13
  const total = findSlot(slots, totalName);
14
- const body = (_jsxs("div", { className: "flex flex-col", "data-testid": "receipt", children: [_jsx("div", { className: "flex flex-col divide-y divide-dashed divide-border", children: lines.map((line, i) => line.handle.hidden ? null : (_jsx("div", { className: "min-w-0 py-1.5", "data-testid": `receipt-line-${i}`, children: _jsx(Slot, { name: line.name, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline justify-between gap-3", "data-testid": "receipt-line", children: [_jsx("span", { className: "min-w-0 text-muted", children: fieldLabel(handle, t) }), _jsx("span", { className: "shrink-0 text-right tabular-nums", children: defaultDisplay })] })) }) }, line.name))) }), total && !total.handle.hidden && (_jsx("div", { className: "mt-2 min-w-0 border-t-2 border-border pt-2", "data-testid": "receipt-total-row", children: _jsx(Slot, { name: totalName, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline justify-between gap-3 text-lg font-bold", "data-testid": "receipt-total", children: [_jsx("span", { className: "min-w-0", children: fieldLabel(handle, t) }), _jsx("span", { className: "shrink-0 text-right tabular-nums", children: defaultDisplay })] })) }) }))] }));
14
+ const body = (_jsxs("div", { className: "flex flex-col", "data-testid": "receipt", children: [_jsx("div", { className: "flex flex-col divide-y divide-dashed divide-border", children: lines.map((line, i) => line.handle.hidden ? null : (_jsx("div", { className: "min-w-0 py-1.5", "data-testid": `receipt-line-${i}`, children: _jsx(Slot, { name: line.name, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline justify-between gap-3", "data-testid": "receipt-line", children: [_jsx("span", { className: "min-w-0 text-muted", children: fieldLabel(handle, t) }), _jsx("span", { className: `shrink-0 text-right ${fieldDatumClass(handle)}`.trim(), children: defaultDisplay })] })) }) }, line.name))) }), total && !total.handle.hidden && (_jsx("div", { className: "mt-2 min-w-0 border-t-2 border-border pt-2", "data-testid": "receipt-total-row", children: _jsx(Slot, { name: totalName, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex w-full min-w-0 items-baseline justify-between gap-3 text-lg font-bold", "data-testid": "receipt-total", children: [_jsx("span", { className: "min-w-0", children: fieldLabel(handle, t) }), _jsx("span", { className: `shrink-0 text-right ${fieldDatumClass(handle)}`.trim(), children: defaultDisplay })] })) }) }))] }));
15
15
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
16
16
  }
17
17
  registerRenderer({
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { registerRenderer } from "../../registry.js";
3
3
  import { Slot, useSlots } from "../slot.js";
4
- import { BlockShell, findSlot } from "./block-shell.js";
4
+ import { BlockShell, findSlot, fieldDatumClass } from "./block-shell.js";
5
5
  function ScorePage({ el }) {
6
6
  const g = el;
7
7
  const sourceName = g.view?.source;
@@ -9,7 +9,7 @@ function ScorePage({ el }) {
9
9
  const max = g.view?.max;
10
10
  const slots = useSlots();
11
11
  const verdict = findSlot(slots, verdictName);
12
- const body = (_jsxs("div", { className: "flex flex-wrap items-baseline gap-3", "data-testid": "score", children: [_jsx("div", { className: "min-w-0", children: _jsx(Slot, { name: sourceName, render: (_handle, defaultDisplay) => (_jsxs("span", { className: "inline-flex items-baseline gap-0.5 text-3xl font-bold tabular-nums", "data-testid": "score-figure", children: [defaultDisplay, max != null && _jsxs("span", { className: "text-lg font-normal text-muted", children: ["/", max] })] })) }) }), verdict && !verdict.handle.hidden && (_jsx("div", { className: "min-w-0 text-muted", "data-testid": "score-verdict", children: _jsx(Slot, { name: verdictName }) }))] }));
12
+ const body = (_jsxs("div", { className: "flex flex-wrap items-baseline gap-3", "data-testid": "score", children: [_jsx("div", { className: "min-w-0", children: _jsx(Slot, { name: sourceName, render: (handle, defaultDisplay) => (_jsxs("span", { className: `inline-flex items-baseline gap-0.5 text-3xl font-bold ${fieldDatumClass(handle)}`.trim(), "data-testid": "score-figure", children: [defaultDisplay, max != null && _jsxs("span", { className: "text-lg font-normal text-muted", children: ["/", max] })] })) }) }), verdict && !verdict.handle.hidden && (_jsx("div", { className: "min-w-0 text-muted", "data-testid": "score-verdict", children: _jsx(Slot, { name: verdictName }) }))] }));
13
13
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
14
14
  }
15
15
  registerRenderer({ kinds: ['score'], layout: 'block', editor: 'custom', Page: ScorePage });
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { registerRenderer } from "../../registry.js";
4
4
  import { Slot, useSlots } from "../slot.js";
5
- import { BlockShell, fieldLabel } from "./block-shell.js";
5
+ import { CHROME_LABEL_CLS } from "../chrome.js";
6
+ import { BlockShell, fieldLabel, fieldDatumClass } from "./block-shell.js";
6
7
  function StatsPage({ el }) {
7
8
  const g = el;
8
9
  const { t } = useTranslation();
@@ -10,7 +11,7 @@ function StatsPage({ el }) {
10
11
  const body = (_jsx("div", { className: "flex flex-wrap gap-x-8 gap-y-4 sm:flex-nowrap sm:divide-x sm:divide-border py-1", "data-testid": "stats", children: slots.map((s, i) => {
11
12
  if (s.handle.hidden)
12
13
  return null;
13
- return (_jsx("div", { className: "min-w-0 flex-1 sm:px-5 sm:first:pl-0", "data-testid": `stats-item-${i}`, children: _jsx(Slot, { name: s.name, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex min-w-0 flex-col items-start gap-0.5 py-1", "data-testid": "stats-figure", children: [_jsx("span", { className: "min-w-0 text-2xl font-semibold tabular-nums text-text", children: defaultDisplay }), _jsx("span", { className: "min-w-0 text-[11px] font-medium uppercase tracking-wider text-muted", children: fieldLabel(handle, t) })] })) }) }, s.name));
14
+ return (_jsx("div", { className: "min-w-0 flex-1 sm:px-5 sm:first:pl-0", "data-testid": `stats-item-${i}`, children: _jsx(Slot, { name: s.name, ownLabel: true, render: (handle, defaultDisplay) => (_jsxs("span", { className: "flex min-w-0 flex-col items-start gap-0.5 py-1", "data-testid": "stats-figure", children: [_jsx("span", { className: `min-w-0 text-2xl font-semibold text-text ${fieldDatumClass(handle)}`.trim(), children: defaultDisplay }), _jsx("span", { className: CHROME_LABEL_CLS, children: fieldLabel(handle, t) })] })) }) }, s.name));
14
15
  }) }));
15
16
  return (_jsx(BlockShell, { label: g.label, icon: g.icon, children: body }));
16
17
  }