@coffer-org/web-ui 5.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 (50) 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 +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/layout.js +3 -3
  6. package/dist/registry.d.ts +2 -1
  7. package/dist/registry.js +1 -0
  8. package/dist/render/activate.d.ts +4 -0
  9. package/dist/render/activate.js +11 -0
  10. package/dist/render/blocks/balance.js +3 -2
  11. package/dist/render/blocks/block-shell.d.ts +1 -0
  12. package/dist/render/blocks/block-shell.js +4 -0
  13. package/dist/render/blocks/chart.js +2 -1
  14. package/dist/render/blocks/compare.js +2 -1
  15. package/dist/render/blocks/countdown.js +2 -1
  16. package/dist/render/blocks/idcard.js +2 -1
  17. package/dist/render/blocks/journal.js +2 -1
  18. package/dist/render/blocks/ledger.js +2 -2
  19. package/dist/render/blocks/manifest.js +2 -1
  20. package/dist/render/blocks/masthead.js +2 -1
  21. package/dist/render/blocks/meter.js +2 -2
  22. package/dist/render/blocks/nutrition.js +2 -2
  23. package/dist/render/blocks/plaque.js +2 -1
  24. package/dist/render/blocks/receipt.js +2 -2
  25. package/dist/render/blocks/score.js +2 -2
  26. package/dist/render/blocks/stats.js +3 -2
  27. package/dist/render/blocks/table.js +5 -4
  28. package/dist/render/chrome.d.ts +21 -3
  29. package/dist/render/chrome.js +46 -6
  30. package/dist/render/core/dimensions.js +1 -1
  31. package/dist/render/core/embed.js +8 -1
  32. package/dist/render/core/money.js +1 -1
  33. package/dist/render/core/range.js +1 -1
  34. package/dist/render/core/reminder.js +1 -1
  35. package/dist/render/core/text.js +57 -3
  36. package/dist/render/core/weight.js +1 -1
  37. package/dist/render/datum.d.ts +4 -0
  38. package/dist/render/datum.js +16 -0
  39. package/dist/render/dispatch.js +4 -2
  40. package/dist/render/field-frame.d.ts +1 -1
  41. package/dist/render/field-frame.js +20 -9
  42. package/dist/render/popover-field.js +10 -19
  43. package/dist/render/widgets/display.js +2 -1
  44. package/dist/render/widgets/edit.js +2 -2
  45. package/dist/render/widgets/fixed-collection.js +2 -1
  46. package/dist/render/widgets/group-widgets.js +1 -1
  47. package/dist/render/widgets/group.js +2 -1
  48. package/dist/render/widgets/table-group.js +4 -3
  49. package/dist/types.d.ts +2 -0
  50. package/package.json +2 -2
@@ -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
  }
@@ -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 }, }) {
@@ -15,7 +15,7 @@ import { Plus } from 'lucide-react';
15
15
  import { RecordInline } from "../../components/RecordInline.js";
16
16
  import { RemoveBtn, AddBtn, MultipleChips, CheckLegend, colsToCheckRow, checkRowToCols, Dash, } from "../shared.js";
17
17
  import { FieldSlot } from "../dispatch.js";
18
- import { FieldLabel } from "../chrome.js";
18
+ import { FieldLabel, CHROME_LABEL_CLS } from "../chrome.js";
19
19
  import { measuredChipContent, measuredUnitLabel } from "./display.js";
20
20
  import { ChecklistRow } from "./checklist-row.js";
21
21
  import { ChecklistContent, clientChecklistContentFields } from "./checklist-content.js";
@@ -238,7 +238,7 @@ export function KeyValueWidget({ value, onChange, multiple = true, option, keyLa
238
238
  const canAdd = multiple || rows.length < 1;
239
239
  return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(FlowRow, { items: rows.map((item, idx) => ({
240
240
  key: String(idx),
241
- node: (_jsxs("div", { className: "flex flex-col gap-0.5 w-56 max-w-full min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1 min-w-0", children: [_jsx(Input, { placeholder: kLabel, value: item.key, className: "h-6 px-1 min-w-0 text-[11px] font-semibold uppercase text-muted", autoComplete: "off", onChange: (e) => save(rows.map((x, i) => (i === idx ? { ...x, key: e.target.value } : x))) }), _jsx(RemoveBtn, { onClick: () => save(rows.filter((_, i) => i !== idx)) })] }), _jsx(FieldSlot, { bare: true, field: option, value: item.value, mode: "edit", onChange: (v) => updatePairValue(idx, String(v)), recordCtx: recordCtx })] })),
241
+ node: (_jsxs("div", { className: "flex flex-col gap-0.5 w-56 max-w-full min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1 min-w-0", children: [_jsx(Input, { placeholder: kLabel, value: item.key, className: `h-6 px-1 ${CHROME_LABEL_CLS}`, autoComplete: "off", onChange: (e) => save(rows.map((x, i) => (i === idx ? { ...x, key: e.target.value } : x))) }), _jsx(RemoveBtn, { onClick: () => save(rows.filter((_, i) => i !== idx)) })] }), _jsx(FieldSlot, { bare: true, field: option, value: item.value, mode: "edit", onChange: (v) => updatePairValue(idx, String(v)), recordCtx: recordCtx })] })),
242
242
  })) }), canAdd && (_jsxs(AddBtn, { onClick: () => setRows((r) => [...r, { key: '', value: '' }]), children: [kLabel, "/", vLabel] }))] }));
243
243
  }
244
244
  function parseMeasuredItems(value) {
@@ -8,6 +8,7 @@ import { FlowRow } from "./flow-row.js";
8
8
  import { subFull } from "./group.js";
9
9
  import { useGroupEdit } from "./group-edit.js";
10
10
  import { useHybridScope } from "../hybrid-scope.js";
11
+ import { datumClass } from "../datum.js";
11
12
  export function FixedCollection({ group, value, onChange, recordCtx, mode = 'edit', }) {
12
13
  const { t } = useTranslation();
13
14
  const depth = useGroupDepth();
@@ -29,6 +30,6 @@ export function FixedCollection({ group, value, onChange, recordCtx, mode = 'edi
29
30
  return (_jsxs("fieldset", { className: `${groupScopeClass(depth)} flex flex-col min-w-0 ${STACK}`, children: [_jsx(GroupTitle, { as: "legend", label: t(group.label), icon: groupIcon(group), actions: live ? null : g.actions }), _jsx("div", { className: `flex flex-col ${STACK}`, children: rows.map((row, idx) => (_jsx(Rail, { children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsxs("div", { className: "flex flex-col gap-0.5 min-w-0 shrink-0", children: [by.type.label ? _jsx(FieldLabel, { label: t(by.type.label) }) : null, _jsx("span", { className: "text-sm px-1 py-2", children: fmtOptionLabel(t, { options: byOpts }, String(row[by.key] ?? '')) })] }), _jsx("div", { className: "flex-1 min-w-0", children: _jsx(FlowRow, { items: valueFields.map((sub) => ({
30
31
  key: sub.key,
31
32
  full: subFull(sub.type),
32
- node: editing ? (_jsx("div", { className: "flex flex-col gap-0.5 min-w-0", children: _jsx(FieldSlot, { field: sub.type, value: row[sub.key] ?? '', onChange: (v) => update(idx, sub.key, v), label: sub.type.label ? t(sub.type.label) : undefined, mode: "edit", recordCtx: recordCtx }) })) : (_jsxs("div", { className: "flex flex-col gap-0.5 min-w-0", children: [sub.type.label ? _jsx(FieldLabel, { label: t(sub.type.label) }) : null, _jsx(FieldSlot, { field: sub.type, fieldKey: live ? sub.key : undefined, value: row[sub.key] ?? '', onChange: live ? (v) => update(idx, sub.key, v) : undefined, mode: "view", bare: !live, recordCtx: recordCtx })] })),
33
+ node: editing ? (_jsx("div", { className: "flex flex-col gap-0.5 min-w-0", children: _jsx(FieldSlot, { field: sub.type, value: row[sub.key] ?? '', onChange: (v) => update(idx, sub.key, v), label: sub.type.label ? t(sub.type.label) : undefined, mode: "edit", recordCtx: recordCtx }) })) : (_jsxs("div", { className: `flex flex-col gap-0.5 min-w-0 ${datumClass(sub.type)}`.trim(), children: [sub.type.label ? _jsx(FieldLabel, { label: t(sub.type.label) }) : null, _jsx(FieldSlot, { field: sub.type, fieldKey: live ? sub.key : undefined, value: row[sub.key] ?? '', onChange: live ? (v) => update(idx, sub.key, v) : undefined, mode: "view", bare: !live, recordCtx: recordCtx })] })),
33
34
  })) }) })] }) }, String(row[by.key])))) })] }));
34
35
  }
@@ -37,5 +37,5 @@ function ChecklistPage({ el, value, recordCtx, mode }) {
37
37
  return (_jsx(ChecklistGroup, { group: el, value: value, onChange: (v) => void write(v), recordCtx: recordCtx, mode: mode }));
38
38
  }
39
39
  registerRenderer({ kinds: ['per-weekday'], layout: 'block', Page: WeekdayGroupPage, editor: 'custom' });
40
- registerRenderer({ kinds: ['url'], layout: 'block', Page: UrlGroupPage });
40
+ registerRenderer({ kinds: ['url'], layout: 'block', Page: UrlGroupPage, activate: 'value' });
41
41
  registerRenderer({ kinds: ['checklist'], layout: 'block', Page: ChecklistPage, editor: 'custom' });
@@ -12,6 +12,7 @@ import { useFieldErrors } from "../field-errors.js";
12
12
  import { GroupField } from "./group-field.js";
13
13
  import { useGroupEdit } from "./group-edit.js";
14
14
  import { useHybridScope } from "../hybrid-scope.js";
15
+ import { datumClass } from "../datum.js";
15
16
  import { FlowRow } from "./flow-row.js";
16
17
  import { fieldLayout } from "../../registry.js";
17
18
  export const subFull = (f) => fieldLayout(f) === 'block';
@@ -75,7 +76,7 @@ function CollectionCell({ sub, row, group, recordCtx, onChange, }) {
75
76
  const { t } = useTranslation();
76
77
  const live = useHybridScope() && Boolean(onChange);
77
78
  const wired = live ? onChange : undefined;
78
- return (_jsxs("div", { className: "flex flex-col gap-0.5 min-w-0", children: [sub.type.label ? _jsx(FieldLabel, { label: t(sub.type.label) }) : null, _jsx(FieldSlot, { field: sub.type, fieldKey: live ? sub.key : undefined, value: row[sub.key] ?? '', mode: "view", bare: !live, recordCtx: recordCtx, onChange: wired })] }));
79
+ return (_jsxs("div", { className: `flex flex-col gap-0.5 min-w-0 ${datumClass(sub.type)}`.trim(), children: [sub.type.label ? _jsx(FieldLabel, { label: t(sub.type.label) }) : null, _jsx(FieldSlot, { field: sub.type, fieldKey: live ? sub.key : undefined, value: row[sub.key] ?? '', mode: "view", bare: !live, recordCtx: recordCtx, onChange: wired })] }));
79
80
  }
80
81
  export function CollectionView({ group, value, onChange, recordCtx, actions, editable = false, }) {
81
82
  const { t } = useTranslation();
@@ -6,10 +6,11 @@ import { hasValue, hasChildren, isNamedField, isStorageGroup } from '@coffer-org
6
6
  import { FieldSlot } from "../dispatch.js";
7
7
  import { useFieldErrors } from "../field-errors.js";
8
8
  import { AddBtn, usedElsewhere } from "../shared.js";
9
- import { GroupTitle, groupIcon, useGroupDepth, GroupDepthProvider, groupScopeClass } from "../chrome.js";
9
+ import { GroupTitle, groupIcon, useGroupDepth, GroupDepthProvider, groupScopeClass, ChromeLabel } from "../chrome.js";
10
10
  import { Button } from "../../components/ui/button.js";
11
11
  import { useGroupEdit } from "./group-edit.js";
12
12
  import { useHybridScope } from "../hybrid-scope.js";
13
+ import { datumClass } from "../datum.js";
13
14
  export function TableGroup({ group, value, onChange, recordCtx, mode, }) {
14
15
  const { t } = useTranslation();
15
16
  const errorAt = useFieldErrors();
@@ -49,8 +50,8 @@ export function TableGroup({ group, value, onChange, recordCtx, mode, }) {
49
50
  };
50
51
  const addRow = () => (live ? setPending((ps) => [...ps, {}]) : setRows([...rows, {}]));
51
52
  const rowTools = isEdit || live;
52
- const header = (_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border/60 bg-muted/15", children: [subs.map((s, i) => (_jsx("th", { className: `text-left text-[11px] font-semibold text-muted uppercase tracking-wider px-3.5 py-2.5 ${isGrowCol(s, i) ? 'w-full min-w-[140px]' : 'whitespace-nowrap min-w-[90px]'}`, children: colLabel(s) ? t(colLabel(s)) : '' }, colKey(s)))), rowTools && _jsx("th", { className: "w-10 px-2", "aria-hidden": true })] }) }));
53
- const body = (_jsx("tbody", { className: "divide-y divide-border/30", children: rows.map((row, idx) => (_jsxs("tr", { className: "align-top hover:bg-muted/10 transition-colors", children: [subs.map((s, i) => (_jsx("td", { className: `px-3.5 py-2.5 min-h-10 ${isGrowCol(s, i) ? 'min-w-[140px]' : 'min-w-[90px]'}`, children: _jsx(FieldSlot, { ...colSlot(s), value: row[colKey(s)] ?? (hasValue(s) ? '' : null), excludeValues: usedElsewhere(rows, unique, colKey(s), idx), onChange: (v) => update(idx, colKey(s), v), fieldKey: live ? colKey(s) : undefined, mode: isEdit ? 'edit' : 'view', bare: !live, recordCtx: recordCtx, error: isStorageGroup(group) ? errorAt([group.key, idx, colKey(s)]) : undefined }) }, colKey(s)))), rowTools && (_jsx("td", { className: "px-2 py-2 min-h-10 w-10", children: _jsx(Button, { type: "button", variant: "outline", size: "iconSm", onClick: () => removeRow(idx), title: t('actions.delete'), className: "rounded-lg text-muted hover:border-danger hover:text-danger", children: _jsx(Trash2, { size: 14 }) }) }))] }, idx))) }));
53
+ const header = (_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border/60 bg-muted/15", children: [subs.map((s, i) => (_jsx("th", { className: `text-left px-3.5 py-2.5 ${isGrowCol(s, i) ? 'w-full min-w-[140px]' : 'whitespace-nowrap min-w-[90px]'}`, children: _jsx(ChromeLabel, { children: colLabel(s) ? t(colLabel(s)) : '' }) }, colKey(s)))), rowTools && _jsx("th", { className: "w-10 px-2", "aria-hidden": true })] }) }));
54
+ const body = (_jsx("tbody", { className: "divide-y divide-border/30", children: rows.map((row, idx) => (_jsxs("tr", { className: "align-top hover:bg-muted/10 transition-colors", children: [subs.map((s, i) => (_jsx("td", { className: `px-3.5 py-2.5 min-h-10 ${isGrowCol(s, i) ? 'min-w-[140px]' : 'min-w-[90px]'} ${datumClass(colSlot(s).field)}`.trim(), children: _jsx(FieldSlot, { ...colSlot(s), value: row[colKey(s)] ?? (hasValue(s) ? '' : null), excludeValues: usedElsewhere(rows, unique, colKey(s), idx), onChange: (v) => update(idx, colKey(s), v), fieldKey: live ? colKey(s) : undefined, mode: isEdit ? 'edit' : 'view', bare: !live, recordCtx: recordCtx, error: isStorageGroup(group) ? errorAt([group.key, idx, colKey(s)]) : undefined }) }, colKey(s)))), rowTools && (_jsx("td", { className: "px-2 py-2 min-h-10 w-10", children: _jsx(Button, { type: "button", variant: "outline", size: "iconSm", onClick: () => removeRow(idx), title: t('actions.delete'), className: "rounded-lg text-muted hover:border-danger hover:text-danger", children: _jsx(Trash2, { size: 14 }) }) }))] }, idx))) }));
54
55
  const tableEl = (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("div", { className: "overflow-x-auto rounded-lg border border-border/60 bg-card/20 shadow-2xs", children: _jsxs("table", { className: "w-full border-collapse text-sm", children: [header, body] }) }), rowTools && _jsx(AddBtn, { onClick: addRow, children: t('form.addRow') })] }));
55
56
  if (mode !== 'view')
56
57
  return tableEl;
package/dist/types.d.ts CHANGED
@@ -48,6 +48,7 @@ export interface FieldValueProps {
48
48
  }
49
49
  export type PickerSize = 'small' | 'large' | 'modal';
50
50
  export type EditorMode = 'popover' | 'modal' | 'inplace' | 'custom';
51
+ export type ActivateMode = 'edit' | 'value';
51
52
  export interface FieldPickerProps {
52
53
  field: FieldMeta;
53
54
  draft: unknown;
@@ -80,5 +81,6 @@ export interface FieldRenderer {
80
81
  pickerSize?: PickerSize | ((field: FieldMeta) => PickerSize);
81
82
  Picker?: FC<FieldPickerProps>;
82
83
  editor?: EditorMode;
84
+ activate?: ActivateMode;
83
85
  keys?: (value: unknown, field: FieldMeta, ctx: FieldKeyCtx) => FieldKey[];
84
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/web-ui",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -25,7 +25,7 @@
25
25
  "postpack": "node ../../scripts/swap-exports.mjs src"
26
26
  },
27
27
  "dependencies": {
28
- "@coffer-org/sdk": "^5.0.0",
28
+ "@coffer-org/sdk": "^6.0.0",
29
29
  "@ark-ui/react": "^5.37.2",
30
30
  "@base-ui/react": "^1.5.0",
31
31
  "@icons-pack/react-simple-icons": "^13.13.0",