@vit-foundation/ui 0.25.0 → 0.27.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 (70) hide show
  1. package/README.md +2 -2
  2. package/dist/community.d.ts +12 -3
  3. package/dist/community.js +7 -3
  4. package/dist/components/pages/ProjectPage.svelte +3 -9
  5. package/dist/components/pages/ProjectPage.svelte.d.ts +3 -5
  6. package/dist/components/pages/WeeklyPage.svelte +7 -12
  7. package/dist/components/pages/WeeklyPage.svelte.d.ts +7 -9
  8. package/dist/components/ui/Icon.svelte +4 -0
  9. package/dist/components/ui/Icon.svelte.d.ts +1 -0
  10. package/dist/components/ui/Modal.svelte +32 -11
  11. package/dist/components/ui/Modal.svelte.d.ts +17 -1
  12. package/dist/components/ui/SelectField.svelte +207 -0
  13. package/dist/components/ui/SelectField.svelte.d.ts +76 -0
  14. package/dist/config/context.d.ts +0 -6
  15. package/dist/config/context.js +1 -1
  16. package/dist/config/index.d.ts +2 -2
  17. package/dist/config/index.js +1 -1
  18. package/dist/content-components.d.ts +6 -3
  19. package/dist/content-components.js +5 -1
  20. package/dist/contract.d.ts +8 -4
  21. package/dist/contract.js +7 -3
  22. package/dist/edit/Editable.svelte +17 -161
  23. package/dist/edit/Editable.svelte.d.ts +2 -50
  24. package/dist/edit/chrome/AddSlot.svelte +11 -78
  25. package/dist/edit/chrome/AddSlot.svelte.d.ts +2 -15
  26. package/dist/edit/chrome/EditFrame.svelte +15 -158
  27. package/dist/edit/chrome/EditFrame.svelte.d.ts +2 -31
  28. package/dist/edit/chrome/EditPanel.svelte +14 -22
  29. package/dist/edit/chrome/EditPanel.svelte.d.ts +2 -13
  30. package/dist/edit/chrome/LinkEdit.svelte +13 -277
  31. package/dist/edit/chrome/LinkEdit.svelte.d.ts +2 -48
  32. package/dist/edit/chrome-props.d.ts +96 -0
  33. package/dist/edit/chrome-props.js +1 -0
  34. package/dist/edit/context.d.ts +38 -5
  35. package/dist/edit/context.js +18 -7
  36. package/dist/edit/index.d.ts +6 -4
  37. package/dist/edit/index.js +4 -3
  38. package/dist/edit/live/AddSlot.svelte +83 -0
  39. package/dist/edit/live/AddSlot.svelte.d.ts +4 -0
  40. package/dist/edit/live/EditFrame.svelte +153 -0
  41. package/dist/edit/live/EditFrame.svelte.d.ts +4 -0
  42. package/dist/edit/live/EditPanel.svelte +25 -0
  43. package/dist/edit/live/EditPanel.svelte.d.ts +4 -0
  44. package/dist/edit/live/Editable.svelte +162 -0
  45. package/dist/edit/live/Editable.svelte.d.ts +4 -0
  46. package/dist/edit/live/LinkEdit.svelte +276 -0
  47. package/dist/edit/live/LinkEdit.svelte.d.ts +4 -0
  48. package/dist/edit/{chrome → live}/PropertyRow.svelte +1 -1
  49. package/dist/edit/{commit.svelte.d.ts → live/commit.svelte.d.ts} +2 -2
  50. package/dist/edit/live/index.d.ts +10 -0
  51. package/dist/edit/live/index.js +14 -0
  52. package/dist/forms/index.d.ts +1 -0
  53. package/dist/forms/index.js +1 -0
  54. package/dist/styles/base.css +1 -1
  55. package/dist/styles/tokens.css +4 -0
  56. package/package.json +1 -1
  57. package/dist/components/account/index.d.ts +0 -3
  58. package/dist/components/account/index.js +0 -2
  59. package/dist/components/auth/index.d.ts +0 -7
  60. package/dist/components/auth/index.js +0 -4
  61. package/dist/components/contact/index.d.ts +0 -2
  62. package/dist/components/contact/index.js +0 -1
  63. package/dist/content/index.d.ts +0 -6
  64. package/dist/content/index.js +0 -3
  65. /package/dist/edit/{chrome → live}/ConfirmDialog.svelte +0 -0
  66. /package/dist/edit/{chrome → live}/ConfirmDialog.svelte.d.ts +0 -0
  67. /package/dist/edit/{chrome → live}/EditPopover.svelte +0 -0
  68. /package/dist/edit/{chrome → live}/EditPopover.svelte.d.ts +0 -0
  69. /package/dist/edit/{chrome → live}/PropertyRow.svelte.d.ts +0 -0
  70. /package/dist/edit/{commit.svelte.js → live/commit.svelte.js} +0 -0
@@ -17,9 +17,13 @@
17
17
  * is what makes it a promise rather than a description: adding a `.svelte`
18
18
  * anywhere beneath it fails here rather than in a consumer's server build.
19
19
  *
20
- * It adds nothing. Every name below is already exported from a semantic
21
- * subpath and stays there — this is a second door into the same rooms, not a
22
- * new floor, and the barrel remains the surface a component host reads.
20
+ * It adds almost nothing. Every name below is already exported from a
21
+ * semantic subpath and stays there — this is a second door into the same
22
+ * rooms, not a new floor, and the barrel remains the surface a component host
23
+ * reads. `contract-names.test.ts` holds that to be true, name by name and from
24
+ * the same source module; its `CONTRACT_ONLY` list names the three exceptions
25
+ * (the two destination classifiers and the title format), each with its
26
+ * reason, so a fourth cannot arrive unexplained.
23
27
  *
24
28
  * It was cut to fit ONE consumer, and the second one did not fit through it.
25
29
  * vit-brain's four restatements were the motive, so the first version carried
@@ -38,7 +42,7 @@
38
42
  * the guard walks them like the rest.
39
43
  */
40
44
  export { LOCALES, BASE_LOCALE } from './config/types.js';
41
- export type { Locale, NotParameterized, ParameterizedKey, ParameterlessKey, SiteLink, UiMessages } from './config/types.js';
45
+ export type { Locale, NotParameterized, ParameterlessKey, SiteLink, UiMessages } from './config/types.js';
42
46
  export { localize } from './edit/types.js';
43
47
  export type { CollectionRef, ContentRef, EditableEntity, EditDescriptor, EntityOp, LocalizedText, PropertyDescriptor, PropertyOption, PropertyType, PropertyValue } from './edit/types.js';
44
48
  export { REACTIONS } from './content/types.js';
package/dist/contract.js CHANGED
@@ -17,9 +17,13 @@
17
17
  * is what makes it a promise rather than a description: adding a `.svelte`
18
18
  * anywhere beneath it fails here rather than in a consumer's server build.
19
19
  *
20
- * It adds nothing. Every name below is already exported from a semantic
21
- * subpath and stays there — this is a second door into the same rooms, not a
22
- * new floor, and the barrel remains the surface a component host reads.
20
+ * It adds almost nothing. Every name below is already exported from a
21
+ * semantic subpath and stays there — this is a second door into the same
22
+ * rooms, not a new floor, and the barrel remains the surface a component host
23
+ * reads. `contract-names.test.ts` holds that to be true, name by name and from
24
+ * the same source module; its `CONTRACT_ONLY` list names the three exceptions
25
+ * (the two destination classifiers and the title format), each with its
26
+ * reason, so a fourth cannot arrive unexplained.
23
27
  *
24
28
  * It was cut to fit ONE consumer, and the second one did not fit through it.
25
29
  * vit-brain's four restatements were the motive, so the first version carried
@@ -1,14 +1,11 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { getUiConfig } from '../config/context.js';
4
- import { getEditAdapter } from './context.js';
5
- import { commitState } from './commit.svelte.js';
6
- import type { EditDescriptor } from './types.js';
2
+ import type { EditableProps } from './chrome-props.js';
3
+ import { getEditAdapter, getEditChrome } from './context.js';
7
4
 
8
5
  /**
9
- * The inline-edit primitive. It renders no element of its own: the child
10
- * snippet receives the text and a bag of attributes, and spreads both onto
11
- * whatever semantic element the call site owns —
6
+ * The inline-edit primitive's GATE. It renders no element of its own: the
7
+ * child snippet receives the text and a bag of attributes, and spreads
8
+ * both onto whatever semantic element the call site owns —
12
9
  *
13
10
  * <Editable edit={descriptor} value={title}>
14
11
  * {#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
@@ -20,164 +17,23 @@
20
17
  *
21
18
  * Inactive — no `edit` descriptor, no adapter in context (a read-only
22
19
  * app), or the adapter's `isEditing` off — it renders the child with an
23
- * empty attribute bag: no listeners, no wrapper, no cost.
24
- *
25
- * Active, it makes the element a plain-text `contenteditable` textbox.
26
- * The DOM owns the draft while the reader types; this component reads it
27
- * back on commit. Enter commits single-line fields (Cmd/Ctrl+Enter for
28
- * 'multiline'), Escape reverts to the last saved value, blur commits a
29
- * dirty draft. A failed save keeps the draft on screen in the error state
30
- * so nothing typed is lost. State is announced through a visually hidden
31
- * `role="status"` region, the pattern ShareRow set for its copy action.
32
- *
33
- * Styling for the affordance lives in base.css (`[data-vit-editing]`):
34
- * the element is rendered by the *caller's* markup, which scoped styles
35
- * here could never reach.
20
+ * empty attribute bag: no listeners, no wrapper, no cost. Active, it
21
+ * mounts the LIVE half the host installed (`edit/live/Editable.svelte`,
22
+ * through `setEditAdapter`'s chrome), which owns the contenteditable, the
23
+ * commit lifecycle and the announcements. This file imports none of that,
24
+ * so a renderer that imports it ships none of that (`chrome-props.ts`).
36
25
  */
37
- interface EditableAttrs {
38
- contenteditable?: 'plaintext-only' | 'true';
39
- role?: 'textbox';
40
- 'aria-label'?: string;
41
- 'aria-multiline'?: 'true';
42
- 'data-vit-editing'?: 'idle' | 'dirty' | 'saving' | 'error';
43
- onbeforeinput?: (event: InputEvent) => void;
44
- oninput?: (event: Event) => void;
45
- onblur?: (event: FocusEvent) => void;
46
- onkeydown?: (event: KeyboardEvent) => void;
47
- onpaste?: (event: ClipboardEvent) => void;
48
- }
49
-
50
- interface Props {
51
- /** Identity of the string. Omit it and this is a passthrough. */
52
- edit?: EditDescriptor;
53
- value: string;
54
- children: Snippet<[string, EditableAttrs]>;
55
- }
56
-
57
- let { edit, value, children }: Props = $props();
26
+ let { edit, value, children }: EditableProps = $props();
58
27
 
59
28
  const adapter = getEditAdapter();
60
- const config = getUiConfig();
61
-
62
- /**
63
- * The commit lifecycle — the four states, the announcements, the last
64
- * persisted value and the follow-the-prop rule — is shared with
65
- * `PropertyRow` (see `./commit.svelte.js`). What stays here is where the
66
- * DRAFT lives: in the DOM, because this is a contenteditable.
67
- */
68
- // svelte-ignore state_referenced_locally
69
- const commit_ = commitState(value, config.editMessages);
70
- /** What the child snippet renders. Only rewritten while the DOM is not
71
- * being typed in — Svelte and the reader must not fight over the node. */
72
- // svelte-ignore state_referenced_locally
73
- let renderText = $state(value);
74
-
75
- $effect(() => {
76
- const adopted = commit_.follow(value);
77
- if (adopted !== null) renderText = adopted;
78
- });
29
+ const chrome = getEditChrome();
79
30
 
80
31
  const active = $derived(edit !== undefined && (adapter?.isEditing ?? false));
81
- const multiline = $derived(edit?.format === 'multiline' || edit?.format === 'richtext');
82
-
83
- /** contenteditable text, innerText keeps line breaks; NBSPs become spaces. */
84
- function textOf(element: HTMLElement): string {
85
- return (element.innerText ?? element.textContent ?? '').replace(/\u00a0/g, ' ');
86
- }
87
-
88
- function handleBeforeInput(event: InputEvent): void {
89
- // 'plaintext-only' is not universal (Firefox); strip rich insertions.
90
- if (event.inputType === 'insertFromPaste' || event.inputType === 'insertFromDrop') return;
91
- if (!multiline && event.inputType === 'insertParagraph') event.preventDefault();
92
- }
93
-
94
- function handlePaste(event: ClipboardEvent): void {
95
- // Plain text only, whatever the clipboard holds.
96
- event.preventDefault();
97
- const text = event.clipboardData?.getData('text/plain') ?? '';
98
- const selection = window.getSelection();
99
- if (!selection || selection.rangeCount === 0) return;
100
- const range = selection.getRangeAt(0);
101
- range.deleteContents();
102
- range.insertNode(document.createTextNode(multiline ? text : text.replace(/\s*\n\s*/g, ' ')));
103
- selection.collapseToEnd();
104
- commit_.markDirty();
105
- }
106
-
107
- function handleInput(): void {
108
- commit_.markDirty();
109
- }
110
-
111
- async function commit(element: HTMLElement): Promise<void> {
112
- if (!edit || !adapter) return;
113
- const draft = textOf(element).trim();
114
- if (draft === commit_.saved) {
115
- commit_.settle();
116
- return;
117
- }
118
- // A failed save leaves the draft in the DOM; the reader decides whether
119
- // to retry.
120
- await commit_.commit(draft, () => adapter.save(edit, draft));
121
- }
122
-
123
- function handleBlur(event: FocusEvent): void {
124
- const status = commit_.status;
125
- if (status === 'dirty' || status === 'error') void commit(event.currentTarget as HTMLElement);
126
- }
127
-
128
- function handleKeydown(event: KeyboardEvent): void {
129
- const element = event.currentTarget as HTMLElement;
130
- if (event.key === 'Escape') {
131
- // Svelte's cached text still equals renderText, so a state write
132
- // alone cannot repaint a node the reader has mutated: restore the
133
- // DOM directly, then settle.
134
- element.textContent = commit_.saved;
135
- commit_.revert();
136
- element.blur();
137
- return;
138
- }
139
- const commitKey = multiline
140
- ? event.key === 'Enter' && (event.metaKey || event.ctrlKey)
141
- : event.key === 'Enter';
142
- if (commitKey) {
143
- event.preventDefault();
144
- void commit(element);
145
- }
146
- }
147
-
148
- const attrs = $derived<EditableAttrs>(
149
- active
150
- ? {
151
- contenteditable: 'plaintext-only',
152
- role: 'textbox',
153
- 'aria-label': edit?.label,
154
- 'aria-multiline': multiline ? 'true' : undefined,
155
- 'data-vit-editing': commit_.status,
156
- onbeforeinput: handleBeforeInput,
157
- oninput: handleInput,
158
- onblur: handleBlur,
159
- onkeydown: handleKeydown,
160
- onpaste: handlePaste
161
- }
162
- : {}
163
- );
164
32
  </script>
165
33
 
166
- {@render children(renderText, attrs)}
167
- {#if active}
168
- <span class="status" role="status">{commit_.announcement}</span>
34
+ {#if active && chrome}
35
+ {@const Live = chrome.Editable}
36
+ <Live {edit} {value} {children} />
37
+ {:else}
38
+ {@render children(value, {})}
169
39
  {/if}
170
-
171
- <style>
172
- .status {
173
- position: absolute;
174
- width: 1px;
175
- height: 1px;
176
- padding: 0;
177
- margin: -1px;
178
- overflow: hidden;
179
- clip-path: inset(50%);
180
- white-space: nowrap;
181
- border: 0;
182
- }
183
- </style>
@@ -1,52 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- import type { EditDescriptor } from './types.js';
3
- /**
4
- * The inline-edit primitive. It renders no element of its own: the child
5
- * snippet receives the text and a bag of attributes, and spreads both onto
6
- * whatever semantic element the call site owns —
7
- *
8
- * <Editable edit={descriptor} value={title}>
9
- * {#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
10
- * </Editable>
11
- *
12
- * — the same contract Field uses for its control attrs, and for the same
13
- * reason: the element (h1, p, span) is part of the document outline and
14
- * belongs to the caller, not behind a prop.
15
- *
16
- * Inactive — no `edit` descriptor, no adapter in context (a read-only
17
- * app), or the adapter's `isEditing` off — it renders the child with an
18
- * empty attribute bag: no listeners, no wrapper, no cost.
19
- *
20
- * Active, it makes the element a plain-text `contenteditable` textbox.
21
- * The DOM owns the draft while the reader types; this component reads it
22
- * back on commit. Enter commits single-line fields (Cmd/Ctrl+Enter for
23
- * 'multiline'), Escape reverts to the last saved value, blur commits a
24
- * dirty draft. A failed save keeps the draft on screen in the error state
25
- * so nothing typed is lost. State is announced through a visually hidden
26
- * `role="status"` region, the pattern ShareRow set for its copy action.
27
- *
28
- * Styling for the affordance lives in base.css (`[data-vit-editing]`):
29
- * the element is rendered by the *caller's* markup, which scoped styles
30
- * here could never reach.
31
- */
32
- interface EditableAttrs {
33
- contenteditable?: 'plaintext-only' | 'true';
34
- role?: 'textbox';
35
- 'aria-label'?: string;
36
- 'aria-multiline'?: 'true';
37
- 'data-vit-editing'?: 'idle' | 'dirty' | 'saving' | 'error';
38
- onbeforeinput?: (event: InputEvent) => void;
39
- oninput?: (event: Event) => void;
40
- onblur?: (event: FocusEvent) => void;
41
- onkeydown?: (event: KeyboardEvent) => void;
42
- onpaste?: (event: ClipboardEvent) => void;
43
- }
44
- interface Props {
45
- /** Identity of the string. Omit it and this is a passthrough. */
46
- edit?: EditDescriptor;
47
- value: string;
48
- children: Snippet<[string, EditableAttrs]>;
49
- }
50
- declare const Editable: import("svelte").Component<Props, {}, "">;
1
+ import type { EditableProps } from './chrome-props.js';
2
+ declare const Editable: import("svelte").Component<EditableProps, {}, "">;
51
3
  type Editable = ReturnType<typeof Editable>;
52
4
  export default Editable;
@@ -1,89 +1,22 @@
1
1
  <script lang="ts">
2
- import Icon from '../../components/ui/Icon.svelte';
3
- import { getUiConfig } from '../../config/context.js';
4
- import { getEditAdapter } from '../context.js';
5
- import type { EntityOp } from '../types.js';
2
+ import type { AddSlotProps } from '../chrome-props.js';
3
+ import { getEditAdapter, getEditChrome } from '../context.js';
6
4
 
7
5
  /**
8
- * The "+" affordance of an editable collection. Renders ONLY while the
9
- * adapter is editing AND implements `applyOp` — otherwise nothing, the
10
- * byte-identical invariant. The op resolves server-side and the host's
11
- * refresh brings the new row in; this control only shows pending/error.
6
+ * The "+" affordance's GATE. Renders ONLY while the adapter is editing
7
+ * AND implements `applyOp` — otherwise nothing, the byte-identical
8
+ * invariant. The control itself, its pending state and its announcement
9
+ * live in `edit/live/AddSlot.svelte`, installed by the host.
12
10
  */
13
- interface Props {
14
- op: Extract<EntityOp, { kind: 'create' }>;
15
- /** The entity noun for the label, e.g. "una fita". */
16
- label?: string;
17
- }
18
-
19
- let { op, label = '' }: Props = $props();
11
+ let { op, label = '' }: AddSlotProps = $props();
20
12
 
21
13
  const adapter = getEditAdapter();
22
- const config = getUiConfig();
23
-
24
- let pending = $state(false);
25
- let announcement = $state('');
14
+ const chrome = getEditChrome();
26
15
 
27
16
  const active = $derived((adapter?.isEditing ?? false) && adapter?.applyOp !== undefined);
28
- const text = $derived(config.editMessages.edit_add({ label }).trim());
29
-
30
- async function add(): Promise<void> {
31
- if (!adapter?.applyOp || pending) return;
32
- pending = true;
33
- announcement = '';
34
- try {
35
- await adapter.applyOp(op);
36
- } catch {
37
- announcement = config.editMessages.edit_addFailed();
38
- } finally {
39
- pending = false;
40
- }
41
- }
42
17
  </script>
43
18
 
44
- {#if active}
45
- <button type="button" class="add" disabled={pending} onclick={() => void add()}>
46
- <Icon name="plus" size={16} />
47
- {text}
48
- </button>
49
- <span class="status" role="status">{announcement}</span>
19
+ {#if active && chrome}
20
+ {@const Live = chrome.AddSlot}
21
+ <Live {op} {label} />
50
22
  {/if}
51
-
52
- <style>
53
- .add {
54
- display: inline-flex;
55
- align-items: center;
56
- gap: var(--space-1);
57
- padding: var(--space-1) var(--space-3);
58
- border: 1px dashed var(--color-ink-muted);
59
- border-radius: 999px;
60
- background: transparent;
61
- color: var(--color-ink);
62
- font: inherit;
63
- font-size: var(--text-sm);
64
- cursor: pointer;
65
- transition: border-color var(--transition-fast);
66
- }
67
-
68
- .add:hover:not(:disabled) {
69
- border-color: var(--color-brand);
70
- color: var(--color-brand);
71
- }
72
-
73
- .add:disabled {
74
- opacity: 0.5;
75
- cursor: default;
76
- }
77
-
78
- .status {
79
- position: absolute;
80
- width: 1px;
81
- height: 1px;
82
- padding: 0;
83
- margin: -1px;
84
- overflow: hidden;
85
- clip-path: inset(50%);
86
- white-space: nowrap;
87
- border: 0;
88
- }
89
- </style>
@@ -1,17 +1,4 @@
1
- import type { EntityOp } from '../types.js';
2
- /**
3
- * The "+" affordance of an editable collection. Renders ONLY while the
4
- * adapter is editing AND implements `applyOp` — otherwise nothing, the
5
- * byte-identical invariant. The op resolves server-side and the host's
6
- * refresh brings the new row in; this control only shows pending/error.
7
- */
8
- interface Props {
9
- op: Extract<EntityOp, {
10
- kind: 'create';
11
- }>;
12
- /** The entity noun for the label, e.g. "una fita". */
13
- label?: string;
14
- }
15
- declare const AddSlot: import("svelte").Component<Props, {}, "">;
1
+ import type { AddSlotProps } from '../chrome-props.js';
2
+ declare const AddSlot: import("svelte").Component<AddSlotProps, {}, "">;
16
3
  type AddSlot = ReturnType<typeof AddSlot>;
17
4
  export default AddSlot;
@@ -1,171 +1,28 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import IconButton from '../../components/ui/IconButton.svelte';
4
- import { getUiConfig } from '../../config/context.js';
5
- import { getEditAdapter } from '../context.js';
6
- import type { EntityOp } from '../types.js';
7
- import ConfirmDialog from './ConfirmDialog.svelte';
8
- import EditPopover from './EditPopover.svelte';
9
-
10
- /** What one framed component tells the frame about itself. */
11
- export interface EditFrameSpec {
12
- /** Human name of the thing, e.g. "Fita: Neix la fundació". */
13
- label: string;
14
- /** Gates the gear — true when the panel snippet has rows to show. */
15
- hasPanel?: boolean;
16
- /** Gates the trash: removing this item from its collection. */
17
- removeOp?: Extract<EntityOp, { kind: 'remove' }>;
18
- }
2
+ import type { EditFrameProps } from '../chrome-props.js';
3
+ import { getEditAdapter, getEditChrome } from '../context.js';
19
4
 
20
5
  /**
21
- * The page-builder wrapper: a corner toolbar (revealed on hover AND
22
- * :focus-within — never hover-only) with a gear that opens the property
23
- * panel and a trash that confirms, then applies the remove op.
24
- *
25
- * Triple-gated per affordance: spec present ∧ adapter editing ∧ the
26
- * capability method present. With nothing to offer it renders the children
27
- * ALONE — zero wrapper element, so an unadapted app's DOM is byte-identical
28
- * (pinned by test). Components mount it INSIDE their own root element, so
29
- * the wrapper never disturbs the parent's flex/grid.
6
+ * The page-builder wrapper's GATE. With no spec, no adapter or editing
7
+ * off it renders the children ALONE — zero wrapper element, so an
8
+ * unadapted app's DOM is byte-identical (pinned by test). Editing, it
9
+ * mounts the live frame the host installed (`edit/live/EditFrame.svelte`),
10
+ * which owns the corner toolbar, the property popover and the confirmed
11
+ * remove, and which applies the finer per-affordance gates itself.
12
+ * Components mount this INSIDE their own root element, so the wrapper
13
+ * never disturbs the parent's flex/grid.
30
14
  */
31
- interface Props {
32
- spec?: EditFrameSpec;
33
- /** The panel content — an EditPanel with this component's rows. */
34
- panel?: Snippet;
35
- children: Snippet;
36
- }
37
-
38
- let { spec, panel, children }: Props = $props();
15
+ let { spec, panel, children }: EditFrameProps = $props();
39
16
 
40
17
  const adapter = getEditAdapter();
41
- const config = getUiConfig();
18
+ const chrome = getEditChrome();
42
19
 
43
20
  const editing = $derived(spec !== undefined && (adapter?.isEditing ?? false));
44
- const showGear = $derived(
45
- editing && spec?.hasPanel === true && panel !== undefined && adapter?.saveProperty !== undefined
46
- );
47
- const showTrash = $derived(
48
- editing && spec?.removeOp !== undefined && adapter?.applyOp !== undefined
49
- );
50
- const framed = $derived(showGear || showTrash);
51
-
52
- let panelOpen = $state(false);
53
- let confirming = $state(false);
54
- let removing = $state(false);
55
- let announcement = $state('');
56
- let toolbar: HTMLDivElement | undefined = $state();
57
-
58
- function closePanel(): void {
59
- panelOpen = false;
60
- // The popover owned focus; hand it back to the control that opened it.
61
- toolbar?.querySelector('button')?.focus();
62
- }
63
-
64
- async function remove(): Promise<void> {
65
- if (!adapter?.applyOp || !spec?.removeOp) return;
66
- removing = true;
67
- try {
68
- await adapter.applyOp(spec.removeOp);
69
- // The host's refresh unmounts this frame with the removed item.
70
- confirming = false;
71
- } catch {
72
- confirming = false;
73
- announcement = config.editMessages.edit_saveError();
74
- } finally {
75
- removing = false;
76
- }
77
- }
78
21
  </script>
79
22
 
80
- {#if framed && spec}
81
- <div class="vit-edit-frame">
82
- {@render children()}
83
- <div class="toolbar" bind:this={toolbar}>
84
- {#if showGear}
85
- <IconButton
86
- icon="gear"
87
- label={config.editMessages.edit_properties({ label: spec.label })}
88
- onclick={() => (panelOpen = !panelOpen)}
89
- />
90
- {/if}
91
- {#if showTrash}
92
- <IconButton
93
- icon="trash"
94
- label={config.editMessages.edit_remove()}
95
- onclick={() => (confirming = true)}
96
- />
97
- {/if}
98
- </div>
99
- {#if panelOpen && panel}
100
- <EditPopover
101
- label={config.editMessages.edit_properties({ label: spec.label })}
102
- onclose={closePanel}
103
- >
104
- {@render panel()}
105
- </EditPopover>
106
- {/if}
107
- {#if showTrash}
108
- <ConfirmDialog
109
- open={confirming}
110
- title={config.editMessages.edit_remove()}
111
- message={config.editMessages.edit_removeConfirm({ label: spec.label })}
112
- confirmLabel={config.editMessages.edit_remove()}
113
- pending={removing}
114
- onconfirm={() => void remove()}
115
- oncancel={() => (confirming = false)}
116
- />
117
- {/if}
118
- <span class="status" role="status">{announcement}</span>
119
- </div>
23
+ {#if editing && chrome}
24
+ {@const Live = chrome.EditFrame}
25
+ <Live {spec} {panel} {children} />
120
26
  {:else}
121
27
  {@render children()}
122
28
  {/if}
123
-
124
- <style>
125
- .vit-edit-frame {
126
- position: relative;
127
- border-radius: var(--radius);
128
- outline: 1px dashed transparent;
129
- outline-offset: 4px;
130
- transition: outline-color var(--transition-fast);
131
- }
132
-
133
- .vit-edit-frame:hover,
134
- .vit-edit-frame:focus-within {
135
- outline-color: var(--color-brand);
136
- }
137
-
138
- .toolbar {
139
- position: absolute;
140
- top: calc(-1 * var(--space-2));
141
- right: 0;
142
- display: flex;
143
- gap: 2px;
144
- z-index: var(--z-raised);
145
- background: var(--color-surface);
146
- border: 1px solid var(--color-hairline);
147
- border-radius: var(--radius);
148
- box-shadow: var(--shadow-1);
149
- opacity: 0;
150
- pointer-events: none;
151
- transition: opacity var(--transition-fast);
152
- }
153
-
154
- .vit-edit-frame:hover .toolbar,
155
- .vit-edit-frame:focus-within .toolbar {
156
- opacity: 1;
157
- pointer-events: auto;
158
- }
159
-
160
- .status {
161
- position: absolute;
162
- width: 1px;
163
- height: 1px;
164
- padding: 0;
165
- margin: -1px;
166
- overflow: hidden;
167
- clip-path: inset(50%);
168
- white-space: nowrap;
169
- border: 0;
170
- }
171
- </style>
@@ -1,33 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- import type { EntityOp } from '../types.js';
3
- /** What one framed component tells the frame about itself. */
4
- export interface EditFrameSpec {
5
- /** Human name of the thing, e.g. "Fita: Neix la fundació". */
6
- label: string;
7
- /** Gates the gear — true when the panel snippet has rows to show. */
8
- hasPanel?: boolean;
9
- /** Gates the trash: removing this item from its collection. */
10
- removeOp?: Extract<EntityOp, {
11
- kind: 'remove';
12
- }>;
13
- }
14
- /**
15
- * The page-builder wrapper: a corner toolbar (revealed on hover AND
16
- * :focus-within — never hover-only) with a gear that opens the property
17
- * panel and a trash that confirms, then applies the remove op.
18
- *
19
- * Triple-gated per affordance: spec present ∧ adapter editing ∧ the
20
- * capability method present. With nothing to offer it renders the children
21
- * ALONE — zero wrapper element, so an unadapted app's DOM is byte-identical
22
- * (pinned by test). Components mount it INSIDE their own root element, so
23
- * the wrapper never disturbs the parent's flex/grid.
24
- */
25
- interface Props {
26
- spec?: EditFrameSpec;
27
- /** The panel content — an EditPanel with this component's rows. */
28
- panel?: Snippet;
29
- children: Snippet;
30
- }
31
- declare const EditFrame: import("svelte").Component<Props, {}, "">;
1
+ import type { EditFrameProps } from '../chrome-props.js';
2
+ declare const EditFrame: import("svelte").Component<EditFrameProps, {}, "">;
32
3
  type EditFrame = ReturnType<typeof EditFrame>;
33
4
  export default EditFrame;