@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
@@ -7,10 +7,11 @@ import { registerRenderer } from "../../registry.js";
7
7
  import { Icon } from "../../components/Icon.js";
8
8
  import { AddBtn } from "../shared.js";
9
9
  import { RemoveBtn } from "../multiple-chips.js";
10
- import { groupScopeClass, useGroupDepth, GroupDepthProvider } from "../chrome.js";
10
+ import { groupScopeClass, useGroupDepth, GroupDepthProvider, CHROME_LABEL_CLS } from "../chrome.js";
11
11
  import { useGroupEdit } from "../widgets/group-edit.js";
12
12
  import { useHybridScope } from "../hybrid-scope.js";
13
13
  import { FieldSlot, fieldKeys } from "../dispatch.js";
14
+ import { datumClass } from "../datum.js";
14
15
  import { useFieldErrors } from "../field-errors.js";
15
16
  import { sortByField } from "../sort-rows.js";
16
17
  import { useCollectionRows } from "../use-collection-rows.js";
@@ -123,12 +124,12 @@ function TablePage({ el, value, recordCtx, mode }) {
123
124
  const totalRow = (bucketRows, testId) => (_jsxs("tr", { "data-testid": testId, className: "text-xs text-muted", children: [numbered && _jsx("td", { className: "border-b border-border py-1.5 pr-3" }), cols.map((f, i) => {
124
125
  const kind = totals?.[f.key];
125
126
  const n = kind && colOf(f.key) ? aggregate(kind, f, bucketRows) : undefined;
126
- return (_jsxs("td", { className: "border-b border-border py-1.5 pr-4", children: [i === 0 && !totals?.[f.key] ? t('table.total') : null, n === undefined ? null : (_jsx(FieldSlot, { el: f, field: f.type, value: totalValue(f, bucketRows, n), mode: "view", bare: true, recordCtx: recordCtx }))] }, f.key));
127
+ return (_jsxs("td", { className: `border-b border-border py-1.5 pr-4 ${datumClass(f.type)}`.trim(), children: [i === 0 && !totals?.[f.key] ? t('table.total') : null, n === undefined ? null : (_jsx(FieldSlot, { el: f, field: f.type, value: totalValue(f, bucketRows, n), mode: "view", bare: true, recordCtx: recordCtx }))] }, f.key));
127
128
  }), rowTools && _jsx("td", { className: "border-b border-border" })] }));
128
129
  const { rowId, updateRow, removeRow, addRow } = useCollectionRows(rows, (v) => g.onChange(v));
129
130
  const errorFor = (idx, key) => isStorageGroup(group) ? errorAt([group.key, idx, key]) : undefined;
130
- const bodyRow = (row, idx, seq) => (_jsxs("tr", { "data-testid": `table-row-${idx}`, children: [numbered && _jsx("td", { className: "border-b border-border py-2 pr-3 text-xs text-muted", children: seq }), cols.map((f) => (_jsx("td", { className: "border-b border-border py-2 pr-4 align-top", children: cellNode(f, row, editing, live, (v) => updateRow(idx, f.key, v), recordCtx, errorFor(idx, f.key)) }, f.key))), rowTools && (_jsx("td", { className: "border-b border-border py-2 align-top", children: _jsx(RemoveBtn, { className: "mt-0.5", onClick: () => removeRow(idx) }) }))] }, rowId(row)));
131
- const content = (_jsxs("div", { className: "flex flex-col gap-2 min-w-0", children: [_jsx("div", { className: "overflow-auto max-h-[60vh]", children: _jsxs("table", { className: "w-full border-separate border-spacing-0 text-sm", "data-testid": "table-block", children: [_jsx("thead", { className: "eink-solid sticky top-0 z-10 bg-bg/95 backdrop-blur", children: _jsxs("tr", { children: [numbered && _jsx("th", { className: "border-b border-border py-1.5 pr-3" }), cols.map((f) => (_jsx("th", { className: "border-b border-border py-1.5 pr-4 text-left text-xs font-normal text-muted", children: _jsxs("button", { type: "button", className: "inline-flex items-center gap-1 focus-visible:outline-2 focus-visible:outline-accent", onClick: () => cycle(f.key), children: [t(f.type.label), !editing && sort?.key === f.key && (_jsx(Icon, { name: sort.dir === 'asc' ? 'lucide:arrow-up' : 'lucide:arrow-down', className: "size-3" }))] }) }, f.key))), rowTools && _jsx("th", { className: "border-b border-border" })] }) }), buckets.map((b) => (_jsxs("tbody", { className: "tabular-nums", children: [b.key !== null && (_jsx("tr", { "data-testid": `table-group-${b.key}`, children: _jsxs("th", { colSpan: cols.length + (numbered ? 1 : 0) + (rowTools ? 1 : 0), className: "border-b border-border pt-4 pb-1 text-left text-xs font-medium text-muted", children: [b.key === NO_VALUE || !groupFld ? (b.label) : (_jsx(FieldSlot, { el: groupFld, field: groupFld.type, value: b.rows[0]?.[groupKey ?? ''], mode: "view", bare: true, recordCtx: recordCtx })), ' ', _jsx("span", { className: "text-muted/70", children: b.rows.length })] }) })), b.rows.map((row, i) => bodyRow(row, indexOf.get(row) ?? -1, i + 1)), hasTotals && b.key !== null && totalRow(b.rows, `table-subtotal-${b.key}`)] }, b.key ?? 'all'))), hasTotals && _jsx("tfoot", { children: totalRow(rows, 'table-total') })] }) }), rowTools && _jsx(AddBtn, { onClick: addRow, children: t('form.addRow') })] }));
131
+ const bodyRow = (row, idx, seq) => (_jsxs("tr", { "data-testid": `table-row-${idx}`, children: [numbered && _jsx("td", { className: "border-b border-border py-2 pr-3 text-xs text-muted", children: seq }), cols.map((f) => (_jsx("td", { className: `border-b border-border py-2 pr-4 align-top ${datumClass(f.type)}`.trim(), children: cellNode(f, row, editing, live, (v) => updateRow(idx, f.key, v), recordCtx, errorFor(idx, f.key)) }, f.key))), rowTools && (_jsx("td", { className: "border-b border-border py-2 align-top", children: _jsx(RemoveBtn, { className: "mt-0.5", onClick: () => removeRow(idx) }) }))] }, rowId(row)));
132
+ const content = (_jsxs("div", { className: "flex flex-col gap-2 min-w-0", children: [_jsx("div", { className: "overflow-auto max-h-[60vh]", children: _jsxs("table", { className: "w-full border-separate border-spacing-0 text-sm", "data-testid": "table-block", children: [_jsx("thead", { className: "eink-solid sticky top-0 z-10 bg-bg/95 backdrop-blur", children: _jsxs("tr", { children: [numbered && _jsx("th", { className: "border-b border-border py-1.5 pr-3" }), cols.map((f) => (_jsx("th", { className: `border-b border-border py-1.5 pr-4 text-left ${CHROME_LABEL_CLS}`, children: _jsxs("button", { type: "button", className: "inline-flex items-center gap-1 focus-visible:outline-2 focus-visible:outline-accent", onClick: () => cycle(f.key), children: [t(f.type.label), !editing && sort?.key === f.key && (_jsx(Icon, { name: sort.dir === 'asc' ? 'lucide:arrow-up' : 'lucide:arrow-down', className: "size-3" }))] }) }, f.key))), rowTools && _jsx("th", { className: "border-b border-border" })] }) }), buckets.map((b) => (_jsxs("tbody", { children: [b.key !== null && (_jsx("tr", { "data-testid": `table-group-${b.key}`, children: _jsxs("th", { colSpan: cols.length + (numbered ? 1 : 0) + (rowTools ? 1 : 0), className: `border-b border-border pt-4 pb-1 text-left ${CHROME_LABEL_CLS} ${groupFld ? datumClass(groupFld.type) : ''}`.trim(), children: [b.key === NO_VALUE || !groupFld ? (b.label) : (_jsx(FieldSlot, { el: groupFld, field: groupFld.type, value: b.rows[0]?.[groupKey ?? ''], mode: "view", bare: true, recordCtx: recordCtx })), ' ', _jsx("span", { className: "text-muted/70", children: b.rows.length })] }) })), b.rows.map((row, i) => bodyRow(row, indexOf.get(row) ?? -1, i + 1)), hasTotals && b.key !== null && totalRow(b.rows, `table-subtotal-${b.key}`)] }, b.key ?? 'all'))), hasTotals && _jsx("tfoot", { children: totalRow(rows, 'table-total') })] }) }), rowTools && _jsx(AddBtn, { onClick: addRow, children: t('form.addRow') })] }));
132
133
  if (mode !== 'view')
133
134
  return content;
134
135
  return (_jsxs("section", { className: `${groupScopeClass(depth)} flex flex-col gap-1 min-w-0`, "data-testid": "table", 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,4 +1,4 @@
1
- import { type ReactNode } from 'react';
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
2
  import type { BoxGeom } from '../types.ts';
3
3
  import type { FieldMeta, ContainerNode } from '@coffer-org/sdk/fields';
4
4
  export declare const STACK = "gap-3.5";
@@ -14,6 +14,10 @@ export declare function FieldLabel({ label, required, actions, reserveAction, }:
14
14
  reserveAction?: boolean;
15
15
  htmlFor?: string;
16
16
  }): import("react").JSX.Element;
17
+ export declare const CHROME_LABEL_CLS = "min-w-0 text-[11px] font-medium tracking-wide text-muted";
18
+ export declare function ChromeLabel({ children, ...rest }: {
19
+ children: ReactNode;
20
+ } & HTMLAttributes<HTMLSpanElement>): import("react").JSX.Element;
17
21
  export declare function FieldLabelText({ label, required }: {
18
22
  label: string;
19
23
  required?: boolean;
@@ -41,11 +45,18 @@ export declare function groupScopeClass(depth: number): string;
41
45
  export declare function groupRevealClass(depth: number): string;
42
46
  export declare const ACTION_CHIP = "h-7 w-7 rounded-md border border-border bg-surface/80 text-muted hover:border-accent/60 hover:text-accent";
43
47
  export declare const ACTION_CHIP_ACCENT = "h-7 w-7 rounded-md border border-accent/40 bg-accent/10 text-accent hover:border-accent hover:bg-accent/20 hover:text-accent";
44
- export declare function EditToggle({ onEdit, variant, scope, }: {
48
+ export declare function EditToggle({ onEdit, scope }: {
45
49
  onEdit: () => void;
46
- variant?: 'inline' | 'overlay';
47
50
  scope?: 'field' | 'group';
48
51
  }): import("react").JSX.Element;
52
+ export declare function ValueActions({ getText, onEdit }: {
53
+ getText: () => string;
54
+ onEdit: () => void;
55
+ }): import("react").JSX.Element;
56
+ export declare function ValueActionsOverlay({ onEdit, children }: {
57
+ onEdit: () => void;
58
+ children: ReactNode;
59
+ }): import("react").JSX.Element;
49
60
  export declare function SaveCancel({ size, busy, disabled, onSave, onCancel, }: {
50
61
  size: 'xs' | 'sm';
51
62
  busy: boolean;
@@ -59,6 +70,13 @@ export declare function FieldBox({ geom, field, children }: {
59
70
  field: FieldMeta;
60
71
  children: ReactNode;
61
72
  }): import("react").JSX.Element;
73
+ export declare function TriggerBox({ expanded, label, onOpen, haspopup, children, }: {
74
+ expanded?: boolean;
75
+ label?: string;
76
+ onOpen: () => void;
77
+ haspopup?: 'dialog';
78
+ children: ReactNode;
79
+ }): import("react").JSX.Element;
62
80
  export declare function FieldError({ error }: {
63
81
  error?: string;
64
82
  }): import("react").JSX.Element | null;
@@ -1,11 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, useContext } from 'react';
2
+ import { createContext, useContext, useLayoutEffect, useRef, useState, } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { Check, Pencil, X } from 'lucide-react';
5
5
  import { Button } from "../components/ui/button.js";
6
+ import { CopyButton } from "../components/ui/copy-button.js";
6
7
  import { Icon } from "../components/Icon.js";
7
8
  import { resolveBox, boxClass, FIELD_ROW_CLS } from "./box.js";
8
9
  import { isCollectionGroup } from '@coffer-org/sdk/fields';
10
+ import { datumClass } from "./datum.js";
9
11
  export const STACK = 'gap-3.5';
10
12
  export const LAYOUT_STACK = 'gap-6';
11
13
  export const PAGE_STACK = 'gap-6';
@@ -15,8 +17,12 @@ export function Rail({ children }) {
15
17
  export function FieldLabel({ label, required, actions, reserveAction, }) {
16
18
  return (_jsxs("div", { className: `flex items-center gap-2 mb-0.5 min-h-8 ${reserveAction ? 'pr-24' : ''}`, children: [_jsx(FieldLabelText, { label: label, required: required }), actions && _jsx("div", { className: "ml-auto flex shrink-0 items-center gap-1.5", children: actions })] }));
17
19
  }
20
+ export const CHROME_LABEL_CLS = 'min-w-0 text-[11px] font-medium tracking-wide text-muted';
21
+ export function ChromeLabel({ children, ...rest }) {
22
+ return (_jsx("span", { ...rest, className: CHROME_LABEL_CLS, children: children }));
23
+ }
18
24
  export function FieldLabelText({ label, required }) {
19
- return (_jsxs("span", { className: "min-w-0 text-[11px] font-medium text-muted", children: [label, required && _jsx("span", { className: "text-danger ml-0.5", children: "*" })] }));
25
+ return (_jsxs(ChromeLabel, { children: [label, required && _jsx("span", { className: "text-danger ml-0.5", children: "*" })] }));
20
26
  }
21
27
  const GROUP_TITLE_BASE = 'flex items-center gap-2 font-semibold';
22
28
  export const GROUP_TITLE_CLS = `${GROUP_TITLE_BASE} text-[13px] text-accent`;
@@ -64,12 +70,19 @@ export function groupRevealClass(depth) {
64
70
  }
65
71
  export const ACTION_CHIP = 'h-7 w-7 rounded-md border border-border bg-surface/80 text-muted hover:border-accent/60 hover:text-accent';
66
72
  export const ACTION_CHIP_ACCENT = 'h-7 w-7 rounded-md border border-accent/40 bg-accent/10 text-accent hover:border-accent hover:bg-accent/20 hover:text-accent';
67
- export function EditToggle({ onEdit, variant = 'inline', scope = 'field', }) {
73
+ export function EditToggle({ onEdit, scope = 'field' }) {
68
74
  const { t } = useTranslation();
69
75
  const depth = useGroupDepth();
70
76
  const reveal = scope === 'group' ? groupRevealClass(depth) : EDIT_REVEAL;
71
- const pos = variant === 'overlay' ? 'absolute right-1 top-0 z-10' : '';
72
- return (_jsx(Button, { type: "button", variant: "outline", size: "iconXs", className: `${pos} ${ACTION_CHIP} ${reveal}`, onClick: onEdit, title: t('actions.edit'), children: _jsx(Pencil, { size: 13 }) }));
77
+ const label = t('actions.edit');
78
+ return (_jsx(Button, { type: "button", variant: "outline", size: "iconXs", className: `${ACTION_CHIP} ${reveal}`, onClick: onEdit, title: label, "aria-label": label, children: _jsx(Pencil, { size: 13 }) }));
79
+ }
80
+ export function ValueActions({ getText, onEdit }) {
81
+ return (_jsxs("span", { className: "inline-flex shrink-0 items-center gap-1", children: [_jsx(CopyButton, { getText: getText, className: `${ACTION_CHIP} ${EDIT_REVEAL}` }), _jsx(EditToggle, { onEdit: onEdit })] }));
82
+ }
83
+ export function ValueActionsOverlay({ onEdit, children }) {
84
+ const ref = useRef(null);
85
+ return (_jsxs("div", { className: "relative flex min-w-0 max-w-full items-center pr-16", children: [_jsx("div", { ref: ref, className: "contents", children: children }), _jsx("div", { className: "absolute right-1 top-1/2 z-10 -translate-y-1/2", children: _jsx(ValueActions, { getText: () => ref.current?.textContent ?? '', onEdit: onEdit }) })] }));
73
86
  }
74
87
  export function SaveCancel({ size, busy, disabled, onSave, onCancel, }) {
75
88
  const { t } = useTranslation();
@@ -83,7 +96,34 @@ export function FieldBox({ geom, field, children }) {
83
96
  const box = resolveBox(field, geom);
84
97
  if (box === 'none')
85
98
  return _jsx(_Fragment, { children: children });
86
- return _jsx("div", { className: boxClass(box), children: children });
99
+ return _jsx("div", { className: `${boxClass(box)} ${datumClass(field)}`.trim(), children: children });
100
+ }
101
+ const TRIGGER_DESCENDANT_SEL = 'a,button,[role="button"],audio,video,input,label,summary,select,textarea,[contenteditable]';
102
+ function ownedByDescendant(target, currentTarget) {
103
+ if (!(target instanceof Element))
104
+ return false;
105
+ const inner = target.closest(TRIGGER_DESCENDANT_SEL);
106
+ return inner !== null && inner !== currentTarget;
107
+ }
108
+ export function TriggerBox({ expanded, label, onOpen, haspopup, children, }) {
109
+ const textRef = useRef(null);
110
+ const [valueText, setValueText] = useState('');
111
+ useLayoutEffect(() => {
112
+ setValueText(textRef.current?.textContent ?? '');
113
+ });
114
+ const accessibleName = label && valueText ? `${label}: ${valueText}` : label;
115
+ return (_jsx("div", { ref: textRef, role: "button", tabIndex: 0, "aria-haspopup": haspopup, "aria-expanded": expanded, "aria-label": accessibleName, className: "relative min-w-0 cursor-pointer rounded-lg outline-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: (e) => {
116
+ if (ownedByDescendant(e.target, e.currentTarget))
117
+ return;
118
+ onOpen();
119
+ }, onKeyDown: (e) => {
120
+ if (e.key !== 'Enter' && e.key !== ' ')
121
+ return;
122
+ if (ownedByDescendant(e.target, e.currentTarget))
123
+ return;
124
+ e.preventDefault();
125
+ onOpen();
126
+ }, children: children }));
87
127
  }
88
128
  export function FieldError({ error }) {
89
129
  if (!error)
@@ -33,7 +33,7 @@ function DimInput({ value, onChange }) {
33
33
  }
34
34
  function DimView({ value }) {
35
35
  const s = fmtDimensions(value);
36
- return s ? _jsx("span", { className: "tabular-nums", children: s }) : _jsx(Dash, {});
36
+ return s ? _jsx("span", { children: s }) : _jsx(Dash, {});
37
37
  }
38
38
  function DimPage(p) {
39
39
  return (_jsx(FieldFrame, { field: p.field, value: p.value, label: p.label, required: p.required, error: p.error, mode: p.mode, geom: "inline", width: "w-fit", bare: p.bare, view: (v) => _jsx(DimView, { value: v }), edit: (value, onChange) => _jsx(DimInput, { value: value, onChange: onChange }) }));
@@ -96,4 +96,11 @@ export function EmbedInline(p) {
96
96
  const first = list[0];
97
97
  return (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1", children: [playable(first) ? _jsx(Play, { size: 12 }) : _jsx(Link2, { size: 12 }), _jsx("span", { className: "truncate", children: first.title ?? getHostname(first.url) }), list.length > 1 && _jsxs("span", { className: "text-muted", children: ["+", list.length - 1] })] }));
98
98
  }
99
- registerRenderer({ kinds: ['embed'], layout: 'block', Page: EmbedPage, Inline: EmbedInline, pickerSize: 'large' });
99
+ registerRenderer({
100
+ kinds: ['embed'],
101
+ layout: 'block',
102
+ Page: EmbedPage,
103
+ Inline: EmbedInline,
104
+ pickerSize: 'large',
105
+ activate: 'value',
106
+ });
@@ -24,7 +24,7 @@ export function fmtMoney(value, currency) {
24
24
  }
25
25
  function MoneyView({ value, field }) {
26
26
  const s = fmtMoney(value, currencyOf(field));
27
- return s ? _jsx("span", { className: "tabular-nums", children: s }) : _jsx(Dash, {});
27
+ return s ? _jsx("span", { children: s }) : _jsx(Dash, {});
28
28
  }
29
29
  function MoneyInput({ field, value, onChange }) {
30
30
  const cur = currencyOf(field);
@@ -44,7 +44,7 @@ function RangeView({ value, field }) {
44
44
  const r = parseRange(value, field);
45
45
  if (r.from == null && r.to == null)
46
46
  return _jsx(Dash, {});
47
- return (_jsxs("span", { className: "tabular-nums", children: [r.from ?? '…', "\u2013", r.to ?? '…'] }));
47
+ return (_jsxs("span", { children: [r.from ?? '…', "\u2013", r.to ?? '…'] }));
48
48
  }
49
49
  function RangePage(p) {
50
50
  return (_jsx(FieldFrame, { field: p.field, value: p.value, label: p.label, required: p.required, error: p.error, mode: p.mode, geom: "inline", width: "w-48", bare: p.bare, view: (v) => _jsx(RangeView, { value: v, field: p.field }), edit: (value, onChange) => (_jsx(RangeInput, { field: p.field, value: value, onChange: onChange })) }));
@@ -45,7 +45,7 @@ function ReminderView({ field, value }) {
45
45
  : tone === 'soon'
46
46
  ? t('reminder.inDays', { days: diff })
47
47
  : null;
48
- return (_jsxs("span", { className: `tabular-nums ${TONE_CLS[tone]}`, children: [isoToDisplay(s), note ? _jsx("span", { className: "ml-2 text-xs", children: note }) : null] }));
48
+ return (_jsxs("span", { className: TONE_CLS[tone], children: [isoToDisplay(s), note ? _jsx("span", { className: "ml-2 text-xs", children: note }) : null] }));
49
49
  }
50
50
  function ReminderPage(p) {
51
51
  return (_jsx(FieldFrame, { field: p.field, value: p.value, label: p.label, required: p.required, error: p.error, mode: p.mode, geom: "inline", width: "w-40", bare: p.bare, view: (v) => _jsx(ReminderView, { field: p.field, value: v }), edit: (value, onChange) => (_jsx(Input, { type: "date", value: String(value ?? ''), onChange: (e) => onChange(e.target.value) })) }));
@@ -72,22 +72,75 @@ function TextInline({ field, value }) {
72
72
  return _jsx("span", { className: "truncate", children: trunc(fmtOptionLabel(t, field, value)) });
73
73
  }
74
74
  registerRenderer({
75
- kinds: ['text', 'email', 'slug', 'unit'],
75
+ kinds: ['text', 'slug', 'unit'],
76
76
  Page: TextPage,
77
77
  Inline: TextInline,
78
78
  Value: PlainValue,
79
79
  keys: optionKeys,
80
80
  });
81
+ function telHref(s) {
82
+ const digits = s.replace(/\D/g, '');
83
+ if (!digits)
84
+ return undefined;
85
+ return (s.trim().startsWith('+') ? '+' : '') + digits;
86
+ }
87
+ function TelAnchor({ value }) {
88
+ const s = String(value ?? '');
89
+ if (!hasValue(s))
90
+ return _jsx(Dash, {});
91
+ const href = telHref(s);
92
+ if (!href)
93
+ return _jsx("span", { className: "truncate", children: s });
94
+ return (_jsx("a", { href: `tel:${href}`, className: "inline-block max-w-full truncate align-bottom text-primary underline hover:text-primary/80 transition-colors", title: s, children: s }));
95
+ }
81
96
  function TelPage(p) {
82
- return (_jsx(FieldFrame, { field: p.field, value: p.value, label: p.label, required: p.required, error: p.error, mode: p.mode, geom: "inline", width: "w-48", bare: p.bare, view: (v) => (hasValue(v) ? _jsx("span", { children: String(v) }) : _jsx(Dash, {})), edit: (value, onChange) => (_jsx(Suspense, { fallback: _jsx("div", { className: "text-sm text-muted", children: "\u2026" }), children: _jsx(PhoneInput, { value: String(value), onChange: (v) => onChange(v ?? '') }) })) }));
97
+ return (_jsx(FieldFrame, { field: p.field, value: p.value, label: p.label, required: p.required, error: p.error, mode: p.mode, geom: "inline", width: "w-48", bare: p.bare, view: (v) => _jsx(TelAnchor, { value: v }), edit: (value, onChange) => (_jsx(Suspense, { fallback: _jsx("div", { className: "text-sm text-muted", children: "\u2026" }), children: _jsx(PhoneInput, { value: String(value), onChange: (v) => onChange(v ?? '') }) })) }));
83
98
  }
84
99
  function TelInline({ value }) {
85
- return _jsx("span", { className: "truncate", children: trunc(String(value ?? '')) });
100
+ return _jsx(TelAnchor, { value: value });
86
101
  }
87
102
  registerRenderer({
88
103
  kinds: ['tel'],
89
104
  Page: TelPage,
90
105
  Inline: TelInline,
106
+ activate: 'value',
107
+ });
108
+ const EMAIL_HREF_RE = /^[^\s@]+@[^\s@]+$/;
109
+ function mailtoHref(s) {
110
+ const trimmed = s.trim();
111
+ return EMAIL_HREF_RE.test(trimmed) ? trimmed : undefined;
112
+ }
113
+ function EmailAnchor({ value }) {
114
+ const s = String(value ?? '');
115
+ if (!hasValue(s))
116
+ return _jsx(Dash, {});
117
+ const href = mailtoHref(s);
118
+ if (!href)
119
+ return _jsx("span", { className: "truncate", children: s });
120
+ return (_jsx("a", { href: `mailto:${href}`, className: "inline-block max-w-full truncate align-bottom text-primary underline hover:text-primary/80 transition-colors", title: s, children: s }));
121
+ }
122
+ function EmailDisplay({ field, value }) {
123
+ return multiView(field, value, (i) => _jsx(EmailAnchor, { value: i }), undefined, optionChipProps(field));
124
+ }
125
+ function EmailPage(p) {
126
+ return (_jsx(FieldFrame, { field: p.field, value: p.value, label: p.label, required: p.required, error: p.error, mode: p.mode, geom: "inline", bare: p.bare, view: (v) => _jsx(EmailDisplay, { field: p.field, value: v }), edit: (value, onChange) => (_jsx(TextInput, { name: `field-${p.field.label}`, field: p.field, value: value, onChange: onChange, selectOptions: p.selectOptions, excludeValues: p.excludeValues })) }));
127
+ }
128
+ function EmailInline({ field, value }) {
129
+ if (field.hints['multiple'] === true) {
130
+ const items = Array.isArray(value) ? value : parseStrArray(value);
131
+ if (!items.length)
132
+ return _jsx("span", { className: "truncate", children: "\u2014" });
133
+ return (_jsx("span", { className: "truncate", children: items.map((v, i) => (_jsxs(React.Fragment, { children: [i > 0 ? ', ' : '', _jsx(EmailAnchor, { value: v })] }, i))) }));
134
+ }
135
+ return _jsx(EmailAnchor, { value: value });
136
+ }
137
+ registerRenderer({
138
+ kinds: ['email'],
139
+ Page: EmailPage,
140
+ Inline: EmailInline,
141
+ Value: ({ value }) => _jsx(EmailAnchor, { value: value }),
142
+ keys: optionKeys,
143
+ activate: 'value',
91
144
  });
92
145
  const hrefOf = (s) => (/^[a-z][a-z0-9+.-]*:\/\//i.test(s) ? s : `https://${s}`);
93
146
  function LinkAnchor({ value }) {
@@ -104,6 +157,7 @@ registerRenderer({
104
157
  Page: LinkPage,
105
158
  Inline: ({ value }) => _jsx(LinkAnchor, { value: value }),
106
159
  Value: ({ value }) => _jsx(LinkAnchor, { value: value }),
160
+ activate: 'value',
107
161
  });
108
162
  function PasswordInput({ name, field, value, onChange }) {
109
163
  if (field.hints['multiple'] === true) {
@@ -16,7 +16,7 @@ function WeightView({ value }) {
16
16
  const p = weightParts(value);
17
17
  if (!p)
18
18
  return _jsx(Dash, {});
19
- return (_jsxs("span", { className: "tabular-nums", children: [p.n, " ", t(`core.units.${p.unit}`)] }));
19
+ return (_jsxs("span", { children: [p.n, " ", t(`core.units.${p.unit}`)] }));
20
20
  }
21
21
  function ScaleToggle({ unit, onPick, label }) {
22
22
  const { t } = useTranslation();
@@ -0,0 +1,4 @@
1
+ import type { FieldClient } from '@coffer-org/sdk/fields';
2
+ export declare const DATUM_KINDS: ReadonlySet<string>;
3
+ export declare const DATUM_PRIMS: ReadonlySet<string>;
4
+ export declare function datumClass(field: Pick<FieldClient, 'kind' | 'prim'>): string;
@@ -0,0 +1,16 @@
1
+ const TABULAR_NUMS = 'tabular-nums';
2
+ export const DATUM_KINDS = new Set(['tel', 'slug', 'tag', 'dimensions']);
3
+ export const DATUM_PRIMS = new Set([
4
+ 'number',
5
+ 'date',
6
+ 'time',
7
+ 'datetime',
8
+ 'measured',
9
+ 'geo',
10
+ 'period',
11
+ 'numberRange',
12
+ 'realRange',
13
+ ]);
14
+ export function datumClass(field) {
15
+ return DATUM_KINDS.has(field.kind) || DATUM_PRIMS.has(field.prim) ? TABULAR_NUMS : '';
16
+ }
@@ -4,6 +4,7 @@ import { wrapKey } from '@coffer-org/sdk/fields';
4
4
  import { resolveRenderer } from "../registry.js";
5
5
  import { parseTagValues, isMultiValued } from "../lib/multi-value.js";
6
6
  import { FieldAddressProvider } from "./field-address.js";
7
+ import { datumClass } from "./datum.js";
7
8
  import { SlotProvider } from "./slot.js";
8
9
  export function FieldSlot(p) {
9
10
  const { onChange, ...rest } = p;
@@ -25,9 +26,10 @@ export function FieldSlot(p) {
25
26
  export function FieldCell({ field, value, height }) {
26
27
  const r = resolveRenderer(field);
27
28
  const inner = r?.Inline ? (_jsx(r.Inline, { field: field, value: value, height: height })) : (_jsx("span", { className: "truncate", children: String(value ?? '') }));
29
+ const datum = datumClass(field);
28
30
  if (!height)
29
- return inner;
30
- return (_jsx("span", { className: "inline-flex min-w-0 items-center font-medium", style: { fontSize: Math.round(height * 0.7), lineHeight: 1.15 }, children: inner }));
31
+ return datum ? _jsx("span", { className: datum, children: inner }) : inner;
32
+ return (_jsx("span", { className: `inline-flex min-w-0 items-center font-medium ${datum}`.trim(), style: { fontSize: Math.round(height * 0.7), lineHeight: 1.15 }, children: inner }));
31
33
  }
32
34
  export function fieldKeys(field, value, ctx) {
33
35
  const r = resolveRenderer(field);
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
  import type { BoxGeom, FieldMode } from '../types.ts';
3
3
  import type { FieldMeta, OptionItem } from '@coffer-org/sdk/fields';
4
4
  export declare function focusInPopup(related: EventTarget | null): boolean;
@@ -1,13 +1,16 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
2
3
  import { useFieldEdit, normalizeValue } from "./use-field-edit.js";
3
- import { FieldLabel, FieldBox, FieldError, EditToggle, SaveCancel } from "./chrome.js";
4
+ import { FieldLabel, FieldBox, FieldError, SaveCancel, TriggerBox, ValueActions, ValueActionsOverlay, } from "./chrome.js";
4
5
  import { useInFlow } from "./widgets/flow-row.js";
5
6
  import { editorMode } from "./editor-mode.js";
7
+ import { activateMode } from "./activate.js";
6
8
  import { useFieldAddress } from "./field-address.js";
7
9
  import { useHybridScope } from "./hybrid-scope.js";
8
10
  import { useFieldWidth } from "./field-width.js";
9
11
  import { PopoverField } from "./popover-field.js";
10
12
  import { useSlotOptional, useSlotRender } from "./slot.js";
13
+ import { datumClass } from "./datum.js";
11
14
  const POPUP_SEL = '[data-slot="combobox-content"],[data-slot="popover-content"],[role="listbox"],[role="dialog"],[role="menu"],[data-scope="date-picker"][data-part="content"]';
12
15
  export function focusInPopup(related) {
13
16
  const el = related instanceof Element ? related : typeof document !== 'undefined' ? document.activeElement : null;
@@ -27,6 +30,7 @@ function FieldFrameBody(props) {
27
30
  const address = useFieldAddress();
28
31
  const onRecordPage = useHybridScope();
29
32
  const width = useFieldWidth() ?? ownWidth;
33
+ const valueRef = useRef(null);
30
34
  const emphasis = field.hints?.['emphasis'];
31
35
  const noLabel = field.hints?.['noLabel'] === true || label === undefined;
32
36
  const display = (val, commit) => {
@@ -59,20 +63,27 @@ function FieldFrameBody(props) {
59
63
  fe.cancel();
60
64
  }
61
65
  }, children: content(true, fe.draft, fe.setDraft) }));
62
- const viewLayer = (_jsx(FieldBox, { geom: geom, field: field, children: content(false, value, () => { }, write) }));
66
+ const activatable = Boolean(write) && activateMode(field) === 'edit';
67
+ const valueActivated = Boolean(write) && activateMode(field) === 'value';
68
+ const wrapValueText = (node) => valueActivated ? (_jsx("span", { ref: valueRef, className: "contents", children: node })) : (node);
69
+ const getValueText = () => valueRef.current?.textContent ?? '';
70
+ const closedContent = (_jsx(FieldBox, { geom: geom, field: field, children: content(false, value, () => { }, write) }));
71
+ const viewLayer = activatable ? (_jsx(TriggerBox, { label: label, expanded: fe.editing, onOpen: fe.beginEdit, children: closedContent })) : valueActivated && !noLabel ? (_jsx(ValueActionsOverlay, { onEdit: fe.beginEdit, children: closedContent })) : (wrapValueText(closedContent));
63
72
  const zone = inFlow ? (_jsxs("div", { className: "grid min-w-0", children: [_jsx("div", { className: "col-start-1 row-start-1 min-w-0", "aria-hidden": !fe.editing, inert: !fe.editing ? true : undefined, style: { visibility: fe.editing ? 'visible' : 'hidden' }, children: editLayer }), _jsx("div", { className: "col-start-1 row-start-1 min-w-0", style: { visibility: fe.editing ? 'hidden' : 'visible' }, children: viewLayer })] })) : fe.editing ? (editLayer) : (viewLayer);
64
- const pencilOverlay = write && !fe.editing ? _jsx(EditToggle, { variant: "overlay", onEdit: fe.beginEdit }) : null;
65
73
  const editControls = write && fe.editing ? (_jsx("div", { className: "absolute right-1 top-0 z-10 flex gap-1", children: _jsx(SaveCancel, { size: "xs", busy: fe.busy, onSave: () => fe.commit(), onCancel: fe.cancel }) })) : null;
66
74
  if (emphasis === 'hero') {
67
- const heroAction = write ? (fe.editing ? (_jsx(SaveCancel, { size: "sm", busy: fe.busy, onSave: () => fe.commit(), onCancel: fe.cancel })) : (_jsx(EditToggle, { onEdit: fe.beginEdit }))) : null;
68
- return (_jsxs("div", { className: "group/field flex items-center gap-2 min-w-0", children: [_jsx("div", { className: "text-lg font-semibold text-text min-w-0", children: fe.editing ? editLayer : content(false, value, () => { }, write) }), heroAction, _jsx(FieldError, { error: slot?.error ?? error })] }));
75
+ const heroAction = write && fe.editing ? (_jsx(SaveCancel, { size: "sm", busy: fe.busy, onSave: () => fe.commit(), onCancel: fe.cancel })) : valueActivated ? (_jsx(ValueActions, { getText: getValueText, onEdit: fe.beginEdit })) : null;
76
+ const heroContent = wrapValueText(content(false, value, () => { }, write));
77
+ return (_jsxs("div", { className: "group/field flex items-center gap-2 min-w-0", children: [_jsx("div", { className: `text-lg font-semibold text-text min-w-0 ${fe.editing ? '' : datumClass(field)}`.trim(), children: fe.editing ? (editLayer) : activatable ? (_jsx(TriggerBox, { label: label, expanded: fe.editing, onOpen: fe.beginEdit, children: heroContent })) : (heroContent) }), heroAction, _jsx(FieldError, { error: slot?.error ?? error })] }));
69
78
  }
70
79
  if (emphasis === 'muted') {
71
- const mutedAction = write ? (fe.editing ? (_jsx(SaveCancel, { size: "xs", busy: fe.busy, onSave: () => fe.commit(), onCancel: fe.cancel })) : (_jsx(EditToggle, { onEdit: fe.beginEdit }))) : null;
72
- return (_jsxs("div", { className: "group/field grid grid-cols-[110px_1fr] items-center gap-x-3 text-xs text-muted min-w-0", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [label, mutedAction] }), _jsx("span", { className: "min-w-0", children: fe.editing ? editLayer : content(false, value, () => { }, write) }), _jsx(FieldError, { error: slot?.error ?? error })] }));
80
+ const mutedAction = write && fe.editing ? (_jsx(SaveCancel, { size: "xs", busy: fe.busy, onSave: () => fe.commit(), onCancel: fe.cancel })) : valueActivated ? (_jsx(ValueActions, { getText: getValueText, onEdit: fe.beginEdit })) : null;
81
+ const mutedContent = wrapValueText(content(false, value, () => { }, write));
82
+ return (_jsxs("div", { className: "group/field grid grid-cols-[110px_1fr] items-center gap-x-3 text-xs text-muted min-w-0", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [label, mutedAction] }), _jsx("span", { className: `min-w-0 ${fe.editing ? '' : datumClass(field)}`.trim(), children: fe.editing ? (editLayer) : activatable ? (_jsx(TriggerBox, { label: label, expanded: fe.editing, onOpen: fe.beginEdit, children: mutedContent })) : (mutedContent) }), _jsx(FieldError, { error: slot?.error ?? error })] }));
73
83
  }
74
84
  if (noLabel) {
75
- return (_jsxs("div", { className: "group/field relative min-w-0 pr-8", children: [zone, pencilOverlay, editControls, _jsx(FieldError, { error: slot?.error ?? error })] }));
85
+ const valueActions = valueActivated && !fe.editing ? (_jsx("div", { className: "absolute right-1 top-1/2 z-10 -translate-y-1/2", children: _jsx(ValueActions, { getText: getValueText, onEdit: fe.beginEdit }) })) : null;
86
+ return (_jsxs("div", { className: "group/field relative min-w-0 pr-16", children: [zone, editControls, valueActions, _jsx(FieldError, { error: slot?.error ?? error })] }));
76
87
  }
77
- return (_jsxs("div", { className: `group/field relative ${width}`, children: [_jsx(FieldLabel, { label: label, required: required, reserveAction: Boolean(write) }), zone, pencilOverlay, editControls, _jsx(FieldError, { error: slot?.error ?? error })] }));
88
+ return (_jsxs("div", { className: `group/field relative ${width}`, children: [_jsx(FieldLabel, { label: label, required: required, reserveAction: Boolean(write) }), zone, editControls, _jsx(FieldError, { error: slot?.error ?? error })] }));
78
89
  }
@@ -1,17 +1,46 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { marked } from 'marked';
3
3
  import DOMPurify from 'dompurify';
4
+ import { useInRouterContext, useNavigate } from 'react-router-dom';
5
+ import { classifySiteHref } from "../internal-link.js";
6
+ const INTERNAL_ATTR = 'data-site-link';
4
7
  DOMPurify.addHook('afterSanitizeAttributes', (node) => {
5
- if (node.tagName === 'A') {
6
- node.setAttribute('target', '_blank');
7
- node.setAttribute('rel', 'noopener noreferrer');
8
+ if (node.tagName !== 'A')
9
+ return;
10
+ const kind = classifySiteHref(node.getAttribute('href') ?? '');
11
+ if (kind.kind === 'internal') {
12
+ node.setAttribute('href', kind.to);
13
+ node.setAttribute(INTERNAL_ATTR, '');
14
+ node.removeAttribute('target');
15
+ node.removeAttribute('rel');
16
+ return;
8
17
  }
18
+ node.setAttribute('target', '_blank');
19
+ node.setAttribute('rel', 'noopener noreferrer');
9
20
  });
10
21
  export function markdownToSafeHtml(src) {
11
22
  const raw = marked.parse(src, { async: false });
12
23
  return DOMPurify.sanitize(raw);
13
24
  }
25
+ function Prose({ value, onClick }) {
26
+ return (_jsx("div", { className: "md-static", onClick: onClick, dangerouslySetInnerHTML: { __html: markdownToSafeHtml(value) } }));
27
+ }
28
+ function RoutedProse({ value }) {
29
+ const navigate = useNavigate();
30
+ return (_jsx(Prose, { value: value, onClick: (event) => {
31
+ const anchor = event.target.closest?.(`a[${INTERNAL_ATTR}]`);
32
+ if (!anchor)
33
+ return;
34
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
35
+ return;
36
+ const to = anchor.getAttribute('href');
37
+ if (!to)
38
+ return;
39
+ event.preventDefault();
40
+ navigate(to);
41
+ } }));
42
+ }
14
43
  export function MarkdownView({ value }) {
15
- return _jsx("div", { className: "md-static", dangerouslySetInnerHTML: { __html: markdownToSafeHtml(value) } });
44
+ return useInRouterContext() ? _jsx(RoutedProse, { value: value }) : _jsx(Prose, { value: value });
16
45
  }
17
46
  export default MarkdownView;
@@ -9,11 +9,12 @@ import { Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle } from "..
9
9
  import { Button } from "../components/ui/button.js";
10
10
  import { Plus } from 'lucide-react';
11
11
  import { useMinWidth } from "../hooks/use-media-query.js";
12
- import { FieldLabel, FieldError, FieldBox } from "./chrome.js";
12
+ import { FieldLabel, FieldError, FieldBox, TriggerBox, ValueActionsOverlay } from "./chrome.js";
13
13
  import { makeIssueText } from "./field-errors.js";
14
14
  import { normalizeValue } from "./use-field-edit.js";
15
15
  import { focusInPopup } from "./field-frame.js";
16
16
  import { editorMode } from "./editor-mode.js";
17
+ import { activateMode } from "./activate.js";
17
18
  import { useRegisterPicker } from "./picker-scrim.js";
18
19
  import { useFieldAddress } from "./field-address.js";
19
20
  import { PickerSurface } from "./picker-surface.js";
@@ -23,7 +24,7 @@ import { hasValue } from "./shared.js";
23
24
  import { parseJSON } from "../lib/json.js";
24
25
  import { makeWriter } from "./write.js";
25
26
  import { SlotProvider } from "./slot.js";
26
- function Picker({ field, validateWith, trigger, seed, options, listMultiple, pickDecides, showInput, size, label, edit, build, onCommit, onDelete, removeLabel, }) {
27
+ function Picker({ field, validateWith, trigger, valueActivated, seed, options, listMultiple, pickDecides, showInput, size, label, edit, build, onCommit, onDelete, removeLabel, }) {
27
28
  const { t } = useTranslation();
28
29
  const wide = useMinWidth('sm', 640);
29
30
  const [open, setOpen] = useState(false);
@@ -72,26 +73,15 @@ function Picker({ field, validateWith, trigger, seed, options, listMultiple, pic
72
73
  edit(normalizeValue(field, draft), setDraft), options && (_jsx(OptionList, { field: field, options: options, value: draft, multiple: listMultiple, onPick: pick }))] })) }), _jsx(FieldError, { error: localError }), !Custom && (showInput || onDelete) && (_jsxs("div", { className: "mt-2 flex items-center justify-end gap-2 border-t border-border pt-2", children: [onDelete && (_jsx(Button, { type: "button", variant: "destructiveOutline", size: "sm", className: "mr-auto", disabled: busy, onClick: () => void write(onDelete()), children: removeLabel })), showInput && (_jsx(Button, { type: "button", size: "sm", disabled: busy, onClick: () => void write(build(draft)), children: t('actions.save') }))] }))] }));
73
74
  const asWindow = size === 'modal' || !wide;
74
75
  const panelWidth = size === 'large' ? 'w-[min(32rem,calc(100vw-2rem))]' : 'w-72';
75
- const anchored = (_jsxs(Popover, { open: open, onOpenChange: (o) => (o ? start() : cancel()), children: [_jsxs(TriggerBox, { open: open, label: label, onOpen: start, children: [trigger, _jsx(PopoverAnchor, { asChild: true, children: _jsx("span", { "aria-hidden": true, className: `pointer-events-none absolute inset-y-0 left-0 max-w-full ${panelWidth}` }) })] }), _jsxs(PopoverContent, { align: "start", className: `${panelWidth} p-2`, onInteractOutside: (e) => {
76
+ const anchor = (_jsx(PopoverAnchor, { asChild: true, children: _jsx("span", { "aria-hidden": true, className: `pointer-events-none absolute inset-y-0 left-0 max-w-full ${panelWidth}` }) }));
77
+ const valueDisplay = (children) => (_jsxs(ValueActionsOverlay, { onEdit: start, children: [trigger, children] }));
78
+ const anchored = (_jsxs(Popover, { open: open, onOpenChange: (o) => (o ? start() : cancel()), children: [valueActivated ? (valueDisplay(anchor)) : (_jsxs(TriggerBox, { expanded: open, label: label, onOpen: start, haspopup: "dialog", children: [trigger, anchor] })), _jsxs(PopoverContent, { align: "start", className: `${panelWidth} p-2`, onInteractOutside: (e) => {
76
79
  if (focusInPopup(e.target))
77
80
  e.preventDefault();
78
81
  }, onEscapeKeyDown: cancel, children: [_jsx(PopoverArrow, {}), body] })] }));
79
82
  if (!asWindow)
80
83
  return anchored;
81
- return (_jsxs(_Fragment, { children: [_jsx(TriggerBox, { open: open, label: label, onOpen: start, children: trigger }), _jsx(Dialog, { open: open, onOpenChange: (o) => (o ? start() : cancel()), children: _jsxs(DialogContent, { onEscapeKeyDown: cancel, className: "h-[88dvh] md:h-[min(80dvh,52rem)] md:max-w-5xl md:overflow-hidden", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: label ?? '' }) }), _jsx(DialogBody, { children: body })] }) })] }));
82
- }
83
- function TriggerBox({ open, label, onOpen, children, ...rest }) {
84
- return (_jsx("div", { ...rest, role: "button", tabIndex: 0, "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": label, className: "relative min-w-0 cursor-pointer rounded-lg border border-transparent focus-visible:border-accent focus-visible:outline-none", onClick: (e) => {
85
- const inner = e.target.closest('a,button,[role="button"],audio,video,input,label');
86
- if (inner && inner !== e.currentTarget)
87
- return;
88
- onOpen();
89
- }, onKeyDown: (e) => {
90
- if (e.key === 'Enter' || e.key === ' ') {
91
- e.preventDefault();
92
- onOpen();
93
- }
94
- }, children: children }));
84
+ return (_jsxs(_Fragment, { children: [valueActivated ? (valueDisplay()) : (_jsx(TriggerBox, { expanded: open, label: label, onOpen: start, haspopup: "dialog", children: trigger })), _jsx(Dialog, { open: open, onOpenChange: (o) => (o ? start() : cancel()), children: _jsxs(DialogContent, { onEscapeKeyDown: cancel, className: "h-[88dvh] md:h-[min(80dvh,52rem)] md:max-w-5xl md:overflow-hidden", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: label ?? '' }) }), _jsx(DialogBody, { children: body })] }) })] }));
95
85
  }
96
86
  export function PopoverField({ field, value, label, required, error, geom, view, edit, pickerOptions, onCommit, width = 'w-64', }) {
97
87
  const { t } = useTranslation();
@@ -107,6 +97,7 @@ export function PopoverField({ field, value, label, required, error, geom, view,
107
97
  const showInput = !(options && field.strict);
108
98
  const Value = R?.Value;
109
99
  const perValue = multi && Value;
100
+ const valueActivated = activateMode(field) === 'value';
110
101
  const address = useFieldAddress();
111
102
  const scalarField = useMemo(() => ({ ...field, hints: { ...field.hints, multiple: false }, json: undefined }), [field]);
112
103
  const freeField = useMemo(() => ({ ...field, options: undefined, hints: { ...field.hints, source: null, suggestions: undefined } }), [field]);
@@ -146,8 +137,8 @@ export function PopoverField({ field, value, label, required, error, geom, view,
146
137
  label,
147
138
  removeLabel: t('actions.delete'),
148
139
  };
149
- return shell(_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: [items.map((item, idx) => (_createElement(Picker, { ...one, key: `${String(item)}-${idx}`, validateWith: field, seed: item, build: (d) => at(idx, d), onDelete: () => without(idx), onCommit: onCommit, trigger: _jsx("span", { className: "inline-flex min-w-0 items-center gap-1.5 rounded-full border border-border px-2 py-0.5 text-sm", children: _jsx(Value, { field: field, value: item }) }) }))), _jsx(Picker, { ...one, validateWith: field, seed: "", build: (d) => [...items, d], onCommit: onCommit, trigger: _jsx("span", { className: "inline-flex items-center gap-1 rounded-full border border-dashed border-border px-2 py-0.5 text-sm text-muted", title: t('actions.add'), children: _jsx(Plus, { size: 14 }) }) })] }));
140
+ return shell(_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: [items.map((item, idx) => (_createElement(Picker, { ...one, key: `${String(item)}-${idx}`, validateWith: field, valueActivated: valueActivated, seed: item, build: (d) => at(idx, d), onDelete: () => without(idx), onCommit: onCommit, trigger: _jsx("span", { className: "inline-flex min-w-0 items-center gap-1.5 rounded-full border border-border px-2 py-0.5 text-sm", children: _jsx(Value, { field: field, value: item }) }) }))), _jsx(Picker, { ...one, validateWith: field, valueActivated: false, seed: "", build: (d) => [...items, d], onCommit: onCommit, trigger: _jsx("span", { className: "inline-flex items-center gap-1 rounded-full border border-dashed border-border px-2 py-0.5 text-sm text-muted", title: t('actions.add'), children: _jsx(Plus, { size: 14 }) }) })] }));
150
141
  }
151
142
  const clearable = !multi && !field.required && !required && hasValue(value);
152
- return shell(_jsx(Picker, { field: field, label: label, size: size, options: options, listMultiple: multi, pickDecides: !multi, showInput: showInput, seed: value, edit: inputEdit, build: (d) => d, onCommit: onCommit, onDelete: clearable ? () => '' : undefined, removeLabel: t('actions.clear'), trigger: _jsx(FieldBox, { geom: geom, field: field, children: triggerDisplay(value, (v) => void onCommit(v)) }) }));
143
+ return shell(_jsx(Picker, { field: field, valueActivated: valueActivated, label: label, size: size, options: options, listMultiple: multi, pickDecides: !multi, showInput: showInput, seed: value, edit: inputEdit, build: (d) => d, onCommit: onCommit, onDelete: clearable ? () => '' : undefined, removeLabel: t('actions.clear'), trigger: _jsx(FieldBox, { geom: geom, field: field, children: triggerDisplay(value, (v) => void onCommit(v)) }) }));
153
144
  }
@@ -9,6 +9,7 @@ import { getShelf } from "../../schema-registry.js";
9
9
  import { RecordInline } from "../../components/RecordInline.js";
10
10
  import { Dash, hasValue, shortId, CheckLegend, colsToCheckRow } from "../shared.js";
11
11
  import { FieldSlot } from "../dispatch.js";
12
+ import { CHROME_LABEL_CLS } from "../chrome.js";
12
13
  import { FlowRow } from "./flow-row.js";
13
14
  import { ChecklistRow } from "./checklist-row.js";
14
15
  import { ChecklistContent, clientChecklistContentFields } from "./checklist-content.js";
@@ -18,7 +19,7 @@ export function KeyValueList({ value, option, recordCtx = { library: '', shelf:
18
19
  return _jsx(Dash, {});
19
20
  return (_jsx(FlowRow, { items: list.map((p, i) => ({
20
21
  key: String(i),
21
- node: (_jsxs("div", { children: [_jsx("dt", { className: "text-[11px] font-semibold text-muted uppercase", children: p.key }), _jsx("dd", { className: "text-sm text-text break-all", children: _jsx(FieldSlot, { bare: true, field: option, value: p.value, mode: "view", recordCtx: recordCtx }) })] })),
22
+ node: (_jsxs("div", { children: [_jsx("dt", { className: CHROME_LABEL_CLS, children: p.key }), _jsx("dd", { className: "text-sm text-text break-all", children: _jsx(FieldSlot, { bare: true, field: option, value: p.value, mode: "view", recordCtx: recordCtx }) })] })),
22
23
  })) }));
23
24
  }
24
25
  export function CheckDisplay({ value, slots = [], fields, recordCtx = { library: '', shelf: '', recordId: null }, }) {