@elytracms/studio 0.0.77 → 0.0.79

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 (90) hide show
  1. package/convex/contract-parity.test.ts +238 -0
  2. package/convex/tsconfig.json +3 -1
  3. package/dist/components/app-views/default-modules.js +5 -1
  4. package/dist/components/app-views/default-modules.js.map +1 -1
  5. package/dist/components/assets/asset-thumb.js +12 -11
  6. package/dist/components/assets/asset-thumb.js.map +1 -1
  7. package/dist/components/builder/blocks-panel.d.ts +11 -5
  8. package/dist/components/builder/blocks-panel.js +18 -24
  9. package/dist/components/builder/blocks-panel.js.map +1 -1
  10. package/dist/components/builder/canvas-render-context.d.ts +22 -0
  11. package/dist/components/builder/canvas-render-context.js +65 -9
  12. package/dist/components/builder/canvas-render-context.js.map +1 -1
  13. package/dist/components/builder/reuse.d.ts +25 -0
  14. package/dist/components/builder/reuse.js +11 -2
  15. package/dist/components/builder/reuse.js.map +1 -1
  16. package/dist/components/cms/asset-field.js +52 -19
  17. package/dist/components/cms/asset-field.js.map +1 -1
  18. package/dist/components/cms/asset-picker-core.d.ts +40 -0
  19. package/dist/components/cms/asset-picker-core.js +21 -0
  20. package/dist/components/cms/asset-picker-core.js.map +1 -1
  21. package/dist/components/cms/asset-picker.d.ts +11 -2
  22. package/dist/components/cms/asset-picker.js +34 -13
  23. package/dist/components/cms/asset-picker.js.map +1 -1
  24. package/dist/components/cms/cell.js +2 -1
  25. package/dist/components/cms/cell.js.map +1 -1
  26. package/dist/components/cms/cms-store.d.ts +8 -1
  27. package/dist/components/cms/cms-store.js +8 -7
  28. package/dist/components/cms/cms-store.js.map +1 -1
  29. package/dist/components/cms/collection-table.js +8 -2
  30. package/dist/components/cms/collection-table.js.map +1 -1
  31. package/dist/components/cms/date-input-core.d.ts +64 -0
  32. package/dist/components/cms/date-input-core.js +78 -0
  33. package/dist/components/cms/date-input-core.js.map +1 -0
  34. package/dist/components/cms/date-input.d.ts +43 -0
  35. package/dist/components/cms/date-input.js +109 -0
  36. package/dist/components/cms/date-input.js.map +1 -0
  37. package/dist/components/cms/detail-editor.js.map +1 -1
  38. package/dist/components/cms/document-diff-view.js +2 -1
  39. package/dist/components/cms/document-diff-view.js.map +1 -1
  40. package/dist/components/cms/document-staging.d.ts +15 -4
  41. package/dist/components/cms/document-staging.js +26 -15
  42. package/dist/components/cms/document-staging.js.map +1 -1
  43. package/dist/components/cms/editable-cell.js +39 -5
  44. package/dist/components/cms/editable-cell.js.map +1 -1
  45. package/dist/components/cms/field-control.js +38 -7
  46. package/dist/components/cms/field-control.js.map +1 -1
  47. package/dist/components/cms/scalar-value-editor.js +4 -1
  48. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  49. package/dist/components/cms/slug-auto-derive.d.ts +41 -0
  50. package/dist/components/cms/slug-auto-derive.js +14 -0
  51. package/dist/components/cms/slug-auto-derive.js.map +1 -0
  52. package/dist/components/components/transfer-dialog.js +3 -1
  53. package/dist/components/components/transfer-dialog.js.map +1 -1
  54. package/dist/components/project-session.d.ts +44 -22
  55. package/dist/components/project-session.js +72 -36
  56. package/dist/components/project-session.js.map +1 -1
  57. package/dist/lib/assets/asset-url.d.ts +20 -0
  58. package/dist/lib/assets/asset-url.js +25 -0
  59. package/dist/lib/assets/asset-url.js.map +1 -0
  60. package/dist/lib/assets/browser-blob-storage.d.ts +80 -0
  61. package/dist/lib/assets/browser-blob-storage.js +86 -0
  62. package/dist/lib/assets/browser-blob-storage.js.map +1 -0
  63. package/dist/lib/assets/index.d.ts +2 -0
  64. package/dist/lib/assets/index.js +2 -0
  65. package/dist/lib/assets/index.js.map +1 -1
  66. package/dist/lib/data/collection-content.d.ts +20 -1
  67. package/dist/lib/data/collection-content.js +51 -1
  68. package/dist/lib/data/collection-content.js.map +1 -1
  69. package/dist/lib/data/content-session-refresh.d.ts +74 -0
  70. package/dist/lib/data/content-session-refresh.js +138 -0
  71. package/dist/lib/data/content-session-refresh.js.map +1 -0
  72. package/dist/lib/data/index.d.ts +2 -0
  73. package/dist/lib/data/index.js +2 -0
  74. package/dist/lib/data/index.js.map +1 -1
  75. package/dist/lib/data/playground-unload-guard.d.ts +22 -0
  76. package/dist/lib/data/playground-unload-guard.js +73 -0
  77. package/dist/lib/data/playground-unload-guard.js.map +1 -0
  78. package/dist/lib/i18n/messages.de.js +3 -1
  79. package/dist/lib/i18n/messages.de.js.map +1 -1
  80. package/dist/lib/i18n/messages.en.d.ts +2 -0
  81. package/dist/lib/i18n/messages.en.js +5 -1
  82. package/dist/lib/i18n/messages.en.js.map +1 -1
  83. package/dist/lib/persistence/convex-adapter.js +3 -3
  84. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  85. package/dist/lib/persistence/index.js +6 -1
  86. package/dist/lib/persistence/index.js.map +1 -1
  87. package/dist/ui/input.d.ts +8 -3
  88. package/dist/ui/input.js +6 -1
  89. package/dist/ui/input.js.map +1 -1
  90. package/package.json +3 -2
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Input } from '@elytracms/studio/ui';
4
+ import { dateCommitDecision, dateInputValue, parseDateEntry, storedDateValue, } from './date-input-core.js';
5
+ /**
6
+ * EC-463 — the studio's ONE `date` control: the document form, the inline table
7
+ * cell, and the filter/bulk scalar editor all render this, so the correctness
8
+ * rules below hold everywhere a date is entered.
9
+ *
10
+ * It is still a native `<input type="date">` (calendar picker, localized segment
11
+ * order, native keyboard handling — no bespoke date picker to maintain), wrapped
12
+ * with the three things the raw element does not give a CMS:
13
+ *
14
+ * 1. **A local draft.** The input is driven by its own state, not straight off
15
+ * the document. An autosave round-trip, a query invalidation or any other
16
+ * re-render arriving mid-entry can no longer rewrite `value` and blow away the
17
+ * half-typed segments (live-reproduced: `2026-01-18` snapping back to
18
+ * `2026-01-02` when a re-render landed between two keystrokes). External
19
+ * updates — locale switch, version restore, an AI draft apply — still land,
20
+ * they just wait until the control is not focused.
21
+ *
22
+ * 2. **Settle-gated commits.** Chrome fires `change` on every segment
23
+ * transition, so typing a year emits the complete dates `0002-…`, `0020-…`,
24
+ * `0202-…` before `2026-…`, and clearing a segment emits `""`. Those never
25
+ * reach the document now ({@link dateCommitDecision}); blur and Enter commit
26
+ * whatever is there, so nothing is unreachable.
27
+ *
28
+ * 3. **An insert path that works.** A native date input ignores `insertText`
29
+ * entirely, which is why pasting `2026-09-18` does nothing and why every
30
+ * documented automation verb (`fill`, `type`, `keyboard inserttext`,
31
+ * `find label … fill`) reported success while silently clearing or ignoring
32
+ * the field. Chrome still DISPATCHES `beforeinput`/`paste`, so this control
33
+ * listens and applies an unambiguous ISO date itself — humans get working
34
+ * paste, agent editors get a working `fill`.
35
+ */
36
+ export function DateInput({ id, value, onChange, disabled = false, className, ariaLabel, testId, autoFocus = false, }) {
37
+ const incoming = dateInputValue(value);
38
+ const [draft, setDraft] = useState(incoming);
39
+ const ref = useRef(null);
40
+ const focusedRef = useRef(false);
41
+ // What we last handed to `onChange`, so a re-render carrying our own value
42
+ // back does not look like an external change (and we never re-emit it).
43
+ const committedRef = useRef(incoming);
44
+ const draftRef = useRef(draft);
45
+ draftRef.current = draft;
46
+ const onChangeRef = useRef(onChange);
47
+ onChangeRef.current = onChange;
48
+ // Accept external updates only while the editor is not mid-entry.
49
+ useEffect(() => {
50
+ if (focusedRef.current)
51
+ return;
52
+ if (incoming === committedRef.current)
53
+ return;
54
+ committedRef.current = incoming;
55
+ setDraft(incoming);
56
+ }, [incoming]);
57
+ function commit(next, reason) {
58
+ if (!dateCommitDecision({ draft: next, reason }).commit)
59
+ return;
60
+ if (next === committedRef.current)
61
+ return;
62
+ committedRef.current = next;
63
+ onChangeRef.current(storedDateValue(next));
64
+ }
65
+ // `beforeinput` / `paste` are dispatched on a date input but never applied by
66
+ // it. Native listeners (not React's synthetic `onBeforeInput`) so the
67
+ // `preventDefault` and the value write are unambiguous.
68
+ useEffect(() => {
69
+ const el = ref.current;
70
+ if (!el)
71
+ return;
72
+ function apply(text, event) {
73
+ if (!text)
74
+ return;
75
+ const parsed = parseDateEntry(text);
76
+ if (parsed === null)
77
+ return;
78
+ event.preventDefault();
79
+ const input = ref.current;
80
+ if (input)
81
+ input.value = parsed;
82
+ setDraft(parsed);
83
+ // A whole value arriving at once is settled by definition.
84
+ commit(parsed, 'settle');
85
+ }
86
+ const onBeforeInput = (event) => apply(event.data, event);
87
+ const onPaste = (event) => apply(event.clipboardData?.getData('text'), event);
88
+ el.addEventListener('beforeinput', onBeforeInput);
89
+ el.addEventListener('paste', onPaste);
90
+ return () => {
91
+ el.removeEventListener('beforeinput', onBeforeInput);
92
+ el.removeEventListener('paste', onPaste);
93
+ };
94
+ // `commit` reads only refs, so one subscription lasts the control's life.
95
+ }, []);
96
+ return (_jsx(Input, { ref: ref, id: id, type: "date", disabled: disabled, autoFocus: autoFocus, "aria-label": ariaLabel, "data-testid": testId, className: className, value: draft, onFocus: () => {
97
+ focusedRef.current = true;
98
+ }, onChange: (event) => {
99
+ setDraft(event.target.value);
100
+ commit(event.target.value, 'edit');
101
+ }, onBlur: () => {
102
+ focusedRef.current = false;
103
+ commit(draftRef.current, 'settle');
104
+ }, onKeyDown: (event) => {
105
+ if (event.key === 'Enter')
106
+ commit(draftRef.current, 'settle');
107
+ } }));
108
+ }
109
+ //# sourceMappingURL=date-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date-input.js","sourceRoot":"","sources":["../../../src/components/cms/date-input.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport { Input } from '@elytracms/studio/ui'\nimport {\n dateCommitDecision,\n dateInputValue,\n parseDateEntry,\n storedDateValue,\n} from './date-input-core'\nimport type { DateCommitReason } from './date-input-core'\n\n/**\n * EC-463 — the studio's ONE `date` control: the document form, the inline table\n * cell, and the filter/bulk scalar editor all render this, so the correctness\n * rules below hold everywhere a date is entered.\n *\n * It is still a native `<input type=\"date\">` (calendar picker, localized segment\n * order, native keyboard handling — no bespoke date picker to maintain), wrapped\n * with the three things the raw element does not give a CMS:\n *\n * 1. **A local draft.** The input is driven by its own state, not straight off\n * the document. An autosave round-trip, a query invalidation or any other\n * re-render arriving mid-entry can no longer rewrite `value` and blow away the\n * half-typed segments (live-reproduced: `2026-01-18` snapping back to\n * `2026-01-02` when a re-render landed between two keystrokes). External\n * updates — locale switch, version restore, an AI draft apply — still land,\n * they just wait until the control is not focused.\n *\n * 2. **Settle-gated commits.** Chrome fires `change` on every segment\n * transition, so typing a year emits the complete dates `0002-…`, `0020-…`,\n * `0202-…` before `2026-…`, and clearing a segment emits `\"\"`. Those never\n * reach the document now ({@link dateCommitDecision}); blur and Enter commit\n * whatever is there, so nothing is unreachable.\n *\n * 3. **An insert path that works.** A native date input ignores `insertText`\n * entirely, which is why pasting `2026-09-18` does nothing and why every\n * documented automation verb (`fill`, `type`, `keyboard inserttext`,\n * `find label … fill`) reported success while silently clearing or ignoring\n * the field. Chrome still DISPATCHES `beforeinput`/`paste`, so this control\n * listens and applies an unambiguous ISO date itself — humans get working\n * paste, agent editors get a working `fill`.\n */\nexport function DateInput({\n id,\n value,\n onChange,\n disabled = false,\n className,\n ariaLabel,\n testId,\n autoFocus = false,\n}: {\n id?: string\n /** The stored value (an ISO instant, a date-only string, or absent). */\n value: unknown\n /** Called with the storage-shaped value, or `undefined` when cleared. */\n onChange: (stored: string | undefined) => void\n disabled?: boolean\n className?: string\n ariaLabel?: string\n testId?: string\n autoFocus?: boolean\n}) {\n const incoming = dateInputValue(value)\n const [draft, setDraft] = useState(incoming)\n const ref = useRef<HTMLInputElement>(null)\n const focusedRef = useRef(false)\n // What we last handed to `onChange`, so a re-render carrying our own value\n // back does not look like an external change (and we never re-emit it).\n const committedRef = useRef(incoming)\n const draftRef = useRef(draft)\n draftRef.current = draft\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n\n // Accept external updates only while the editor is not mid-entry.\n useEffect(() => {\n if (focusedRef.current) return\n if (incoming === committedRef.current) return\n committedRef.current = incoming\n setDraft(incoming)\n }, [incoming])\n\n function commit(next: string, reason: DateCommitReason) {\n if (!dateCommitDecision({ draft: next, reason }).commit) return\n if (next === committedRef.current) return\n committedRef.current = next\n onChangeRef.current(storedDateValue(next))\n }\n\n // `beforeinput` / `paste` are dispatched on a date input but never applied by\n // it. Native listeners (not React's synthetic `onBeforeInput`) so the\n // `preventDefault` and the value write are unambiguous.\n useEffect(() => {\n const el = ref.current\n if (!el) return\n function apply(text: string | null | undefined, event: Event): void {\n if (!text) return\n const parsed = parseDateEntry(text)\n if (parsed === null) return\n event.preventDefault()\n const input = ref.current\n if (input) input.value = parsed\n setDraft(parsed)\n // A whole value arriving at once is settled by definition.\n commit(parsed, 'settle')\n }\n const onBeforeInput = (event: Event) => apply((event as InputEvent).data, event)\n const onPaste = (event: Event) =>\n apply((event as ClipboardEvent).clipboardData?.getData('text'), event)\n el.addEventListener('beforeinput', onBeforeInput)\n el.addEventListener('paste', onPaste)\n return () => {\n el.removeEventListener('beforeinput', onBeforeInput)\n el.removeEventListener('paste', onPaste)\n }\n // `commit` reads only refs, so one subscription lasts the control's life.\n }, [])\n\n return (\n <Input\n ref={ref}\n id={id}\n type=\"date\"\n disabled={disabled}\n autoFocus={autoFocus}\n aria-label={ariaLabel}\n data-testid={testId}\n className={className}\n value={draft}\n onFocus={() => {\n focusedRef.current = true\n }}\n onChange={(event) => {\n setDraft(event.target.value)\n commit(event.target.value, 'edit')\n }}\n onBlur={() => {\n focusedRef.current = false\n commit(draftRef.current, 'settle')\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter') commit(draftRef.current, 'settle')\n }}\n />\n )\n}\n"]}