@vit-foundation/ui 0.25.0 → 0.26.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 (62) hide show
  1. package/README.md +1 -1
  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/Modal.svelte +32 -11
  9. package/dist/components/ui/Modal.svelte.d.ts +17 -1
  10. package/dist/config/context.d.ts +0 -6
  11. package/dist/config/context.js +1 -1
  12. package/dist/config/index.d.ts +2 -2
  13. package/dist/config/index.js +1 -1
  14. package/dist/content-components.d.ts +6 -3
  15. package/dist/content-components.js +5 -1
  16. package/dist/contract.d.ts +8 -4
  17. package/dist/contract.js +7 -3
  18. package/dist/edit/Editable.svelte +17 -161
  19. package/dist/edit/Editable.svelte.d.ts +2 -50
  20. package/dist/edit/chrome/AddSlot.svelte +11 -78
  21. package/dist/edit/chrome/AddSlot.svelte.d.ts +2 -15
  22. package/dist/edit/chrome/EditFrame.svelte +15 -158
  23. package/dist/edit/chrome/EditFrame.svelte.d.ts +2 -31
  24. package/dist/edit/chrome/EditPanel.svelte +14 -22
  25. package/dist/edit/chrome/EditPanel.svelte.d.ts +2 -13
  26. package/dist/edit/chrome/LinkEdit.svelte +13 -277
  27. package/dist/edit/chrome/LinkEdit.svelte.d.ts +2 -48
  28. package/dist/edit/chrome-props.d.ts +96 -0
  29. package/dist/edit/chrome-props.js +1 -0
  30. package/dist/edit/context.d.ts +38 -5
  31. package/dist/edit/context.js +18 -7
  32. package/dist/edit/index.d.ts +6 -4
  33. package/dist/edit/index.js +4 -3
  34. package/dist/edit/live/AddSlot.svelte +83 -0
  35. package/dist/edit/live/AddSlot.svelte.d.ts +4 -0
  36. package/dist/edit/live/EditFrame.svelte +153 -0
  37. package/dist/edit/live/EditFrame.svelte.d.ts +4 -0
  38. package/dist/edit/live/EditPanel.svelte +25 -0
  39. package/dist/edit/live/EditPanel.svelte.d.ts +4 -0
  40. package/dist/edit/live/Editable.svelte +162 -0
  41. package/dist/edit/live/Editable.svelte.d.ts +4 -0
  42. package/dist/edit/live/LinkEdit.svelte +276 -0
  43. package/dist/edit/live/LinkEdit.svelte.d.ts +4 -0
  44. package/dist/edit/{chrome → live}/PropertyRow.svelte +1 -1
  45. package/dist/edit/{commit.svelte.d.ts → live/commit.svelte.d.ts} +2 -2
  46. package/dist/edit/live/index.d.ts +10 -0
  47. package/dist/edit/live/index.js +14 -0
  48. package/package.json +1 -1
  49. package/dist/components/account/index.d.ts +0 -3
  50. package/dist/components/account/index.js +0 -2
  51. package/dist/components/auth/index.d.ts +0 -7
  52. package/dist/components/auth/index.js +0 -4
  53. package/dist/components/contact/index.d.ts +0 -2
  54. package/dist/components/contact/index.js +0 -1
  55. package/dist/content/index.d.ts +0 -6
  56. package/dist/content/index.js +0 -3
  57. /package/dist/edit/{chrome → live}/ConfirmDialog.svelte +0 -0
  58. /package/dist/edit/{chrome → live}/ConfirmDialog.svelte.d.ts +0 -0
  59. /package/dist/edit/{chrome → live}/EditPopover.svelte +0 -0
  60. /package/dist/edit/{chrome → live}/EditPopover.svelte.d.ts +0 -0
  61. /package/dist/edit/{chrome → live}/PropertyRow.svelte.d.ts +0 -0
  62. /package/dist/edit/{commit.svelte.js → live/commit.svelte.js} +0 -0
@@ -1,52 +1,16 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import Button from '../../components/ui/Button.svelte';
4
- import Icon from '../../components/ui/Icon.svelte';
5
- import Modal from '../../components/ui/Modal.svelte';
6
- import { getUiConfig } from '../../config/context.js';
7
- import type { ParameterlessKey } from '../../config/types.js';
8
- import { getEditAdapter } from '../context.js';
9
- import { propertyValue } from '../commit.svelte.js';
10
- import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../types.js';
11
- import ConfirmDialog from './ConfirmDialog.svelte';
2
+ import type { LinkEditProps } from '../chrome-props.js';
3
+ import { getEditAdapter, getEditChrome } from '../context.js';
12
4
 
13
5
  /**
14
- * A LINK edited whole: one modal for its text and its destination, instead
15
- * of an inline label swap plus a separate property panel — a link's two
16
- * halves belong to one gesture.
17
- *
18
- * While the adapter is editing and a text descriptor is supplied, the
19
- * control is REPLACED by a button wearing the label (the ActionLabel rule:
20
- * a caret or a click inside a live link would navigate mid-edit) that
21
- * opens the modal. Desa commits ONLY the halves that changed — the text
22
- * through `adapter.save`, the destination through `adapter.saveProperty` —
23
- * so a failure on one keeps the other's success and the modal stays open
24
- * with the reason. Extra rows (a menu link's Ordre) and a confirmed
25
- * Elimina ride in the same modal when the host supplies them. Without an
6
+ * A LINK edited whole — the GATE. While the adapter is editing and a text
7
+ * descriptor is supplied, the control is REPLACED by the live half
8
+ * (`edit/live/LinkEdit.svelte`): a button wearing the label that opens one
9
+ * modal for the link's text and destination, the ActionLabel rule (a
10
+ * caret or a click inside a live link would navigate mid-edit). Without an
26
11
  * adapter or a descriptor the control renders untouched, byte-identical to
27
- * a read-only build.
12
+ * a read-only build, and the modal's code is not in that build.
28
13
  */
29
- interface Props {
30
- /** Inline-label half; undefined leaves the control alone. */
31
- text: { edit: EditDescriptor | undefined; value: string };
32
- /** Destination half; undefined hides the Adreça field. */
33
- href: { descriptor: PropertyDescriptor | undefined; value: string };
34
- /**
35
- * Extra property rows under Adreça — a menu link's Ordre. Labelled from
36
- * their descriptors, committed on Desa only when changed, and rendered
37
- * BY TYPE: this used to be a bare text input whatever the descriptor
38
- * said, which made it a second renderer that would silently miss any
39
- * property type the package grew. An `image` row has no place in a link
40
- * modal and is the one type refused here.
41
- */
42
- extras?: { descriptor: PropertyDescriptor; value: string | null }[];
43
- /** With `adapter.applyOp`, the modal gains a confirmed Elimina action. */
44
- removeOp?: EntityOp;
45
- /** Accessible name for the modal, e.g. the link's current text. */
46
- label?: string;
47
- control: Snippet;
48
- }
49
-
50
14
  let {
51
15
  text,
52
16
  href,
@@ -54,245 +18,17 @@
54
18
  removeOp = undefined,
55
19
  label = undefined,
56
20
  control
57
- }: Props = $props();
21
+ }: LinkEditProps = $props();
58
22
 
59
23
  const adapter = getEditAdapter();
60
- const config = getUiConfig();
61
- const messages = $derived(config.editMessages);
24
+ const chrome = getEditChrome();
62
25
 
63
26
  const editing = $derived(text.edit !== undefined && (adapter?.isEditing ?? false));
64
- // Every field beyond the text needs both a place and a verb to save with.
65
- const editsHref = $derived(href.descriptor !== undefined && adapter?.saveProperty !== undefined);
66
- const editableExtras = $derived(
67
- adapter?.saveProperty !== undefined
68
- ? (extras ?? []).filter((extra) => extra.descriptor.type !== 'image')
69
- : []
70
- );
71
-
72
- /** A flag state's wording, from the same catalog the site renders. */
73
- const wording = (key: ParameterlessKey): string => config.messages[key]?.() ?? key;
74
- const removable = $derived(removeOp !== undefined && adapter?.applyOp !== undefined);
75
-
76
- let open = $state(false);
77
- let draftText = $state('');
78
- let draftHref = $state('');
79
- let draftExtras = $state<string[]>([]);
80
- let saving = $state(false);
81
- let confirmingRemove = $state(false);
82
- let removing = $state(false);
83
- let error = $state<string | null>(null);
84
-
85
- function show(): void {
86
- draftText = text.value;
87
- draftHref = href.value;
88
- draftExtras = editableExtras.map((extra) => extra.value ?? '');
89
- error = null;
90
- open = true;
91
- }
92
-
93
- /** Escape/Cancel·la/backdrop: discard the drafts, keep the stored values. */
94
- function cancel(): void {
95
- if (saving || removing) return;
96
- open = false;
97
- }
98
-
99
- async function save(): Promise<void> {
100
- if (!adapter || text.edit === undefined) return;
101
- const nextText = draftText.trim();
102
- const nextHref = draftHref.trim();
103
- saving = true;
104
- error = null;
105
- try {
106
- // Sequential on purpose: if the text lands and the address fails, the
107
- // reseeded draft no longer differs, so a retry commits only what is
108
- // still pending.
109
- if (nextText !== text.value) {
110
- await adapter.save(text.edit, nextText);
111
- draftText = nextText;
112
- }
113
- if (editsHref && href.descriptor && nextHref !== href.value) {
114
- await adapter.saveProperty!(href.descriptor, nextHref);
115
- draftHref = nextHref;
116
- }
117
- for (const [index, extra] of editableExtras.entries()) {
118
- const next = draftExtras[index].trim();
119
- if (next === (extra.value ?? '')) continue;
120
- // A flag extra hands the adapter a BOOLEAN, like a panel row does.
121
- await adapter.saveProperty!(extra.descriptor, propertyValue(extra.descriptor, next));
122
- draftExtras[index] = next;
123
- }
124
- open = false;
125
- } catch (thrown) {
126
- error =
127
- thrown instanceof Error && thrown.message ? thrown.message : messages.edit_saveError();
128
- } finally {
129
- saving = false;
130
- }
131
- }
132
-
133
- /** Confirmed removal closes everything; a failure reports in THIS modal. */
134
- async function remove(): Promise<void> {
135
- if (!adapter?.applyOp || removeOp === undefined) return;
136
- removing = true;
137
- error = null;
138
- try {
139
- await adapter.applyOp(removeOp);
140
- confirmingRemove = false;
141
- open = false;
142
- } catch (thrown) {
143
- confirmingRemove = false;
144
- error =
145
- thrown instanceof Error && thrown.message ? thrown.message : messages.edit_saveError();
146
- } finally {
147
- removing = false;
148
- }
149
- }
150
27
  </script>
151
28
 
152
- {#if editing}
153
- <button
154
- type="button"
155
- class="link-swap"
156
- data-vit-editing="idle"
157
- aria-label={messages.edit_editLink({ label: label ?? text.value })}
158
- onclick={show}
159
- >
160
- {text.value}
161
- <Icon name="pencil" size={13} />
162
- </button>
163
- <Modal
164
- {open}
165
- title={messages.edit_editLink({ label: label ?? text.value })}
166
- onclose={cancel}
167
- closeLabel={messages.edit_close()}
168
- >
169
- <div class="fields">
170
- <label>
171
- <span>{messages.edit_linkText()}</span>
172
- <input type="text" bind:value={draftText} disabled={saving} />
173
- </label>
174
- {#if editsHref}
175
- <label>
176
- <span>{messages.edit_linkUrl()}</span>
177
- <!-- type=text, not url: site destinations are relative paths. -->
178
- <input type="text" bind:value={draftHref} disabled={saving} />
179
- </label>
180
- {/if}
181
- {#each editableExtras as extra, index (index)}
182
- <label>
183
- <span>{extra.descriptor.label}</span>
184
- {#if extra.descriptor.type === 'select'}
185
- <select bind:value={draftExtras[index]} disabled={saving}>
186
- {#each extra.descriptor.options ?? [] as option (option.value)}
187
- <option value={option.value}>{option.label}</option>
188
- {/each}
189
- </select>
190
- {:else if extra.descriptor.type === 'flag'}
191
- <select bind:value={draftExtras[index]} disabled={saving}>
192
- <option value="true">{wording(extra.descriptor.on ?? 'status_published')}</option>
193
- <option value="false">{wording(extra.descriptor.off ?? 'status_draft')}</option>
194
- </select>
195
- {:else}
196
- <input
197
- type={extra.descriptor.type === 'date' ? 'date' : 'text'}
198
- placeholder={extra.descriptor.placeholder}
199
- bind:value={draftExtras[index]}
200
- disabled={saving}
201
- />
202
- {/if}
203
- </label>
204
- {/each}
205
- </div>
206
- {#if error}
207
- <p class="error" role="alert">{error}</p>
208
- {/if}
209
- <div class="actions">
210
- <Button pending={saving ? 1 : 0} onclick={() => void save()}>{messages.edit_save()}</Button>
211
- <Button variant="ghost" pending={null} onclick={cancel}>{messages.edit_cancel()}</Button>
212
- {#if removable}
213
- <span class="danger">
214
- <Button variant="ghost" pending={null} onclick={() => (confirmingRemove = true)}>
215
- {messages.edit_remove()}
216
- </Button>
217
- </span>
218
- {/if}
219
- </div>
220
- </Modal>
221
- {#if removable}
222
- <ConfirmDialog
223
- open={confirmingRemove}
224
- title={messages.edit_remove()}
225
- message={messages.edit_removeConfirm({ label: label ?? text.value })}
226
- confirmLabel={messages.edit_remove()}
227
- pending={removing}
228
- onconfirm={() => void remove()}
229
- oncancel={() => (confirmingRemove = false)}
230
- />
231
- {/if}
29
+ {#if editing && chrome}
30
+ {@const Live = chrome.LinkEdit}
31
+ <Live {text} {href} {extras} {removeOp} {label} {control} />
232
32
  {:else}
233
33
  {@render control()}
234
34
  {/if}
235
-
236
- <style>
237
- /* The ActionLabel footprint plus a pencil hint: the same dashed affordance
238
- (base.css paints [data-vit-editing]), but a click OPENS rather than
239
- focuses — this swap is a button, never a caret. */
240
- .link-swap {
241
- display: inline-flex;
242
- align-items: center;
243
- gap: var(--space-1);
244
- padding: 0.2em 0.4em;
245
- border: none;
246
- background: transparent;
247
- font: inherit;
248
- color: inherit;
249
- cursor: pointer;
250
- }
251
-
252
- .fields {
253
- display: flex;
254
- flex-direction: column;
255
- gap: var(--space-3);
256
- }
257
-
258
- label {
259
- display: flex;
260
- flex-direction: column;
261
- gap: var(--space-1);
262
- font-size: var(--text-sm);
263
- font-weight: 600;
264
- }
265
-
266
- /* PropertyRow's control look — the modal is the panel, relocated. */
267
- input {
268
- font: inherit;
269
- font-size: var(--text-sm);
270
- font-weight: 400;
271
- color: var(--color-ink);
272
- padding: var(--space-1) var(--space-2);
273
- border: 1px solid var(--color-hairline);
274
- border-radius: var(--radius);
275
- background: var(--color-surface);
276
- width: 100%;
277
- }
278
-
279
- .error {
280
- margin: var(--space-3) 0 0;
281
- color: var(--series-8);
282
- font-size: var(--text-sm);
283
- }
284
-
285
- .actions {
286
- display: flex;
287
- gap: var(--space-2);
288
- margin-top: var(--space-4);
289
- }
290
-
291
- /* Elimina sits apart from the commit pair, wearing the error hue — the
292
- ConfirmDialog token override, reused. */
293
- .danger {
294
- margin-left: auto;
295
- --color-brand: var(--series-8);
296
- --color-ink: var(--series-8);
297
- }
298
- </style>
@@ -1,50 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../types.js';
3
- /**
4
- * A LINK edited whole: one modal for its text and its destination, instead
5
- * of an inline label swap plus a separate property panel — a link's two
6
- * halves belong to one gesture.
7
- *
8
- * While the adapter is editing and a text descriptor is supplied, the
9
- * control is REPLACED by a button wearing the label (the ActionLabel rule:
10
- * a caret or a click inside a live link would navigate mid-edit) that
11
- * opens the modal. Desa commits ONLY the halves that changed — the text
12
- * through `adapter.save`, the destination through `adapter.saveProperty` —
13
- * so a failure on one keeps the other's success and the modal stays open
14
- * with the reason. Extra rows (a menu link's Ordre) and a confirmed
15
- * Elimina ride in the same modal when the host supplies them. Without an
16
- * adapter or a descriptor the control renders untouched, byte-identical to
17
- * a read-only build.
18
- */
19
- interface Props {
20
- /** Inline-label half; undefined leaves the control alone. */
21
- text: {
22
- edit: EditDescriptor | undefined;
23
- value: string;
24
- };
25
- /** Destination half; undefined hides the Adreça field. */
26
- href: {
27
- descriptor: PropertyDescriptor | undefined;
28
- value: string;
29
- };
30
- /**
31
- * Extra property rows under Adreça — a menu link's Ordre. Labelled from
32
- * their descriptors, committed on Desa only when changed, and rendered
33
- * BY TYPE: this used to be a bare text input whatever the descriptor
34
- * said, which made it a second renderer that would silently miss any
35
- * property type the package grew. An `image` row has no place in a link
36
- * modal and is the one type refused here.
37
- */
38
- extras?: {
39
- descriptor: PropertyDescriptor;
40
- value: string | null;
41
- }[];
42
- /** With `adapter.applyOp`, the modal gains a confirmed Elimina action. */
43
- removeOp?: EntityOp;
44
- /** Accessible name for the modal, e.g. the link's current text. */
45
- label?: string;
46
- control: Snippet;
47
- }
48
- declare const LinkEdit: import("svelte").Component<Props, {}, "">;
1
+ import type { LinkEditProps } from '../chrome-props.js';
2
+ declare const LinkEdit: import("svelte").Component<LinkEditProps, {}, "">;
49
3
  type LinkEdit = ReturnType<typeof LinkEdit>;
50
4
  export default LinkEdit;
@@ -0,0 +1,96 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { EditDescriptor, EntityOp, PropertyDescriptor, PropertyValue } from './types.js';
3
+ /**
4
+ * The props of the five edit-chrome modules, in one component-free file.
5
+ *
6
+ * Each of these modules is TWO components with one interface: a GATE at the
7
+ * name every renderer imports (`edit/Editable.svelte`, `edit/chrome/*`), and
8
+ * the LIVE half under `edit/live/`, which a host installs through
9
+ * `setEditAdapter(adapter, EDIT_CHROME)`. The gate decides whether editing is
10
+ * on; the live half is the editing. A read-only host never imports the live
11
+ * half, so it never ships it — the property the page tests used to assert on
12
+ * the DOM alone ("byte-identical without descriptors") while the site's bundle
13
+ * carried `dist/edit` regardless.
14
+ *
15
+ * The props live here rather than in either component so the two halves are
16
+ * typed against ONE declaration and `EditChrome` (in `./context.ts`) can name
17
+ * the live component's type without importing a `.svelte` file into a `.ts`
18
+ * one.
19
+ */
20
+ export interface EditableAttrs {
21
+ contenteditable?: 'plaintext-only' | 'true';
22
+ role?: 'textbox';
23
+ 'aria-label'?: string;
24
+ 'aria-multiline'?: 'true';
25
+ 'data-vit-editing'?: 'idle' | 'dirty' | 'saving' | 'error';
26
+ onbeforeinput?: (event: InputEvent) => void;
27
+ oninput?: (event: Event) => void;
28
+ onblur?: (event: FocusEvent) => void;
29
+ onkeydown?: (event: KeyboardEvent) => void;
30
+ onpaste?: (event: ClipboardEvent) => void;
31
+ }
32
+ export interface EditableProps {
33
+ /** Identity of the string. Omit it and this is a passthrough. */
34
+ edit?: EditDescriptor;
35
+ value: string;
36
+ children: Snippet<[string, EditableAttrs]>;
37
+ }
38
+ /** What one framed component tells the frame about itself. */
39
+ export interface EditFrameSpec {
40
+ /** Human name of the thing, e.g. "Fita: Neix la fundació". */
41
+ label: string;
42
+ /** Gates the gear — true when the panel snippet has rows to show. */
43
+ hasPanel?: boolean;
44
+ /** Gates the trash: removing this item from its collection. */
45
+ removeOp?: Extract<EntityOp, {
46
+ kind: 'remove';
47
+ }>;
48
+ }
49
+ export interface EditFrameProps {
50
+ spec?: EditFrameSpec;
51
+ /** The panel content — an EditPanel with this component's rows. */
52
+ panel?: Snippet;
53
+ children: Snippet;
54
+ }
55
+ export interface EditPanelProps {
56
+ rows: {
57
+ descriptor: PropertyDescriptor;
58
+ value: PropertyValue;
59
+ }[];
60
+ }
61
+ export interface AddSlotProps {
62
+ op: Extract<EntityOp, {
63
+ kind: 'create';
64
+ }>;
65
+ /** The entity noun for the label, e.g. "una fita". */
66
+ label?: string;
67
+ }
68
+ export interface LinkEditProps {
69
+ /** Inline-label half; undefined leaves the control alone. */
70
+ text: {
71
+ edit: EditDescriptor | undefined;
72
+ value: string;
73
+ };
74
+ /** Destination half; undefined hides the Adreça field. */
75
+ href: {
76
+ descriptor: PropertyDescriptor | undefined;
77
+ value: string;
78
+ };
79
+ /**
80
+ * Extra property rows under Adreça — a menu link's Ordre. Labelled from
81
+ * their descriptors, committed on Desa only when changed, and rendered
82
+ * BY TYPE: this used to be a bare text input whatever the descriptor
83
+ * said, which made it a second renderer that would silently miss any
84
+ * property type the package grew. An `image` row has no place in a link
85
+ * modal and is the one type refused here.
86
+ */
87
+ extras?: {
88
+ descriptor: PropertyDescriptor;
89
+ value: string | null;
90
+ }[];
91
+ /** With `adapter.applyOp`, the modal gains a confirmed Elimina action. */
92
+ removeOp?: EntityOp;
93
+ /** Accessible name for the modal, e.g. the link's current text. */
94
+ label?: string;
95
+ control: Snippet;
96
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,43 @@
1
+ import type { Component } from 'svelte';
2
+ import type { AddSlotProps, EditableProps, EditFrameProps, EditPanelProps, LinkEditProps } from './chrome-props.js';
1
3
  import type { EditAdapter } from './types.js';
2
4
  /**
3
- * Installs the app's persistence adapter for the subtree. An app that never
4
- * calls this is read-only by construction: without an adapter every edit
5
- * affordance renders as plain content, byte-identical to a build of the
6
- * components with no edit mode at all.
5
+ * The LIVE half of the edit chrome: the five components that turn a gate into
6
+ * an editing surface. `EDIT_CHROME` in `./live/index.js` is the package's
7
+ * table; a host installs it beside its adapter.
8
+ *
9
+ * Why the host passes it rather than the gates importing it. Every renderer
10
+ * imports the gates — `Editable`, `EditFrame`, `EditPanel`, `AddSlot`,
11
+ * `LinkEdit` — because a renderer that MIGHT be edited has to say where.
12
+ * When the gates imported the live components too, a page module's static
13
+ * import graph reached the contenteditable machinery, the property panel, the
14
+ * link modal and the Modal primitive, and the public site shipped all of it
15
+ * (`dist/edit`, 176 KB) to render nothing with it. Through this seam the
16
+ * import is the host's: brain names the table once at its composition root,
17
+ * the site never names it, and a bundler following the site's imports never
18
+ * sees `edit/live/`. `live-isolation.test.ts` pins that no renderer reaches it.
7
19
  */
8
- export declare function setEditAdapter(adapter: EditAdapter): EditAdapter;
20
+ export interface EditChrome {
21
+ Editable: Component<EditableProps>;
22
+ EditFrame: Component<EditFrameProps>;
23
+ EditPanel: Component<EditPanelProps>;
24
+ AddSlot: Component<AddSlotProps>;
25
+ LinkEdit: Component<LinkEditProps>;
26
+ }
27
+ /**
28
+ * Installs the app's persistence adapter, and the chrome it drives, for the
29
+ * subtree. An app that never calls this is read-only by construction: without
30
+ * an adapter every edit affordance renders as plain content, byte-identical
31
+ * to a build of the components with no edit mode at all — and, since 0.26,
32
+ * without the edit mode's code in its bundle either.
33
+ *
34
+ * Both arguments are required. An adapter with no chrome would be an editing
35
+ * host whose gates open onto nothing, and the failure would be a page that
36
+ * silently does not edit; a chrome with no adapter has nothing to save
37
+ * through. One call, two halves, so neither can be installed alone.
38
+ */
39
+ export declare function setEditAdapter(adapter: EditAdapter, chrome: EditChrome): EditAdapter;
9
40
  /** The nearest adapter, or undefined in a read-only app. */
10
41
  export declare function getEditAdapter(): EditAdapter | undefined;
42
+ /** The chrome installed beside the nearest adapter, or undefined in a read-only app. */
43
+ export declare function getEditChrome(): EditChrome | undefined;
@@ -1,15 +1,26 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  const KEY = Symbol.for('@vit-foundation/ui:edit-adapter');
3
3
  /**
4
- * Installs the app's persistence adapter for the subtree. An app that never
5
- * calls this is read-only by construction: without an adapter every edit
6
- * affordance renders as plain content, byte-identical to a build of the
7
- * components with no edit mode at all.
4
+ * Installs the app's persistence adapter, and the chrome it drives, for the
5
+ * subtree. An app that never calls this is read-only by construction: without
6
+ * an adapter every edit affordance renders as plain content, byte-identical
7
+ * to a build of the components with no edit mode at all — and, since 0.26,
8
+ * without the edit mode's code in its bundle either.
9
+ *
10
+ * Both arguments are required. An adapter with no chrome would be an editing
11
+ * host whose gates open onto nothing, and the failure would be a page that
12
+ * silently does not edit; a chrome with no adapter has nothing to save
13
+ * through. One call, two halves, so neither can be installed alone.
8
14
  */
9
- export function setEditAdapter(adapter) {
10
- return setContext(KEY, adapter);
15
+ export function setEditAdapter(adapter, chrome) {
16
+ setContext(KEY, { adapter, chrome });
17
+ return adapter;
11
18
  }
12
19
  /** The nearest adapter, or undefined in a read-only app. */
13
20
  export function getEditAdapter() {
14
- return getContext(KEY);
21
+ return getContext(KEY)?.adapter;
22
+ }
23
+ /** The chrome installed beside the nearest adapter, or undefined in a read-only app. */
24
+ export function getEditChrome() {
25
+ return getContext(KEY)?.chrome;
15
26
  }
@@ -1,14 +1,16 @@
1
- export { getEditAdapter, setEditAdapter } from './context.js';
1
+ export { getEditAdapter, getEditChrome, setEditAdapter } from './context.js';
2
+ export type { EditChrome } from './context.js';
3
+ export { EDIT_CHROME } from './live/index.js';
2
4
  export { collectionEditing } from './collection.svelte.js';
3
5
  export type { CollectionEditing, RemovableMap } from './collection.svelte.js';
4
6
  export { default as ActionLabel } from './ActionLabel.svelte';
5
7
  export { default as Editable } from './Editable.svelte';
6
8
  export { default as AddSlot } from './chrome/AddSlot.svelte';
7
- export { default as ConfirmDialog } from './chrome/ConfirmDialog.svelte';
9
+ export { default as ConfirmDialog } from './live/ConfirmDialog.svelte';
8
10
  export { default as EditFrame } from './chrome/EditFrame.svelte';
9
- export type { EditFrameSpec } from './chrome/EditFrame.svelte';
11
+ export type { EditFrameSpec } from './chrome-props.js';
10
12
  export { default as EditPanel } from './chrome/EditPanel.svelte';
11
- export { default as EditPopover } from './chrome/EditPopover.svelte';
13
+ export { default as EditPopover } from './live/EditPopover.svelte';
12
14
  export { default as LinkEdit } from './chrome/LinkEdit.svelte';
13
15
  export { chromeEdit, chromeProperty, collectionOf, entityEdit, entityProperty, pageCopyEdit } from './helpers.js';
14
16
  export { localize } from './types.js';
@@ -1,12 +1,13 @@
1
- export { getEditAdapter, setEditAdapter } from './context.js';
1
+ export { getEditAdapter, getEditChrome, setEditAdapter } from './context.js';
2
+ export { EDIT_CHROME } from './live/index.js';
2
3
  export { collectionEditing } from './collection.svelte.js';
3
4
  export { default as ActionLabel } from './ActionLabel.svelte';
4
5
  export { default as Editable } from './Editable.svelte';
5
6
  export { default as AddSlot } from './chrome/AddSlot.svelte';
6
- export { default as ConfirmDialog } from './chrome/ConfirmDialog.svelte';
7
+ export { default as ConfirmDialog } from './live/ConfirmDialog.svelte';
7
8
  export { default as EditFrame } from './chrome/EditFrame.svelte';
8
9
  export { default as EditPanel } from './chrome/EditPanel.svelte';
9
- export { default as EditPopover } from './chrome/EditPopover.svelte';
10
+ export { default as EditPopover } from './live/EditPopover.svelte';
10
11
  export { default as LinkEdit } from './chrome/LinkEdit.svelte';
11
12
  export { chromeEdit, chromeProperty, collectionOf, entityEdit, entityProperty, pageCopyEdit } from './helpers.js';
12
13
  export { localize } from './types.js';
@@ -0,0 +1,83 @@
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 { AddSlotProps } from '../chrome-props.js';
6
+
7
+ /**
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.
12
+ */
13
+ let { op, label = '' }: AddSlotProps = $props();
14
+
15
+ const adapter = getEditAdapter();
16
+ const config = getUiConfig();
17
+
18
+ let pending = $state(false);
19
+ let announcement = $state('');
20
+
21
+ const active = $derived((adapter?.isEditing ?? false) && adapter?.applyOp !== undefined);
22
+ const text = $derived(config.editMessages.edit_add({ label }).trim());
23
+
24
+ async function add(): Promise<void> {
25
+ if (!adapter?.applyOp || pending) return;
26
+ pending = true;
27
+ announcement = '';
28
+ try {
29
+ await adapter.applyOp(op);
30
+ } catch {
31
+ announcement = config.editMessages.edit_addFailed();
32
+ } finally {
33
+ pending = false;
34
+ }
35
+ }
36
+ </script>
37
+
38
+ {#if active}
39
+ <button type="button" class="add" disabled={pending} onclick={() => void add()}>
40
+ <Icon name="plus" size={16} />
41
+ {text}
42
+ </button>
43
+ <span class="status" role="status">{announcement}</span>
44
+ {/if}
45
+
46
+ <style>
47
+ .add {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ gap: var(--space-1);
51
+ padding: var(--space-1) var(--space-3);
52
+ border: 1px dashed var(--color-ink-muted);
53
+ border-radius: 999px;
54
+ background: transparent;
55
+ color: var(--color-ink);
56
+ font: inherit;
57
+ font-size: var(--text-sm);
58
+ cursor: pointer;
59
+ transition: border-color var(--transition-fast);
60
+ }
61
+
62
+ .add:hover:not(:disabled) {
63
+ border-color: var(--color-brand);
64
+ color: var(--color-brand);
65
+ }
66
+
67
+ .add:disabled {
68
+ opacity: 0.5;
69
+ cursor: default;
70
+ }
71
+
72
+ .status {
73
+ position: absolute;
74
+ width: 1px;
75
+ height: 1px;
76
+ padding: 0;
77
+ margin: -1px;
78
+ overflow: hidden;
79
+ clip-path: inset(50%);
80
+ white-space: nowrap;
81
+ border: 0;
82
+ }
83
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { AddSlotProps } from '../chrome-props.js';
2
+ declare const AddSlot: import("svelte").Component<AddSlotProps, {}, "">;
3
+ type AddSlot = ReturnType<typeof AddSlot>;
4
+ export default AddSlot;