@vit-foundation/ui 0.10.1 → 0.12.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 (46) hide show
  1. package/dist/components/contact/ContactForm.svelte +44 -14
  2. package/dist/components/jobs/JobList.svelte +73 -12
  3. package/dist/components/jobs/JobList.svelte.d.ts +19 -2
  4. package/dist/components/projects/ProjectCard.svelte +78 -33
  5. package/dist/components/projects/ProjectCard.svelte.d.ts +11 -2
  6. package/dist/components/team/CollaboratorList.svelte +87 -9
  7. package/dist/components/team/CollaboratorList.svelte.d.ts +26 -0
  8. package/dist/components/team/TeamMemberCard.svelte +41 -19
  9. package/dist/components/team/TeamMemberCard.svelte.d.ts +8 -4
  10. package/dist/components/timeline/Timeline.svelte +47 -2
  11. package/dist/components/timeline/Timeline.svelte.d.ts +8 -0
  12. package/dist/components/timeline/TimelineMilestone.svelte +94 -38
  13. package/dist/components/timeline/TimelineMilestone.svelte.d.ts +16 -2
  14. package/dist/components/ui/RichText.svelte +12 -6
  15. package/dist/components/ui/SearchInput.svelte +31 -10
  16. package/dist/components/ui/SearchInput.svelte.d.ts +7 -0
  17. package/dist/components/weeklies/SortSelect.svelte +41 -11
  18. package/dist/components/weeklies/SortSelect.svelte.d.ts +8 -0
  19. package/dist/components/weeklies/WeeklieCard.svelte +44 -19
  20. package/dist/components/weeklies/WeeklieCard.svelte.d.ts +9 -2
  21. package/dist/config/context.js +6 -1
  22. package/dist/config/edit-messages.d.ts +47 -0
  23. package/dist/config/edit-messages.js +24 -0
  24. package/dist/config/index.d.ts +2 -0
  25. package/dist/config/index.js +1 -0
  26. package/dist/config/types.d.ts +8 -0
  27. package/dist/content/types.d.ts +4 -0
  28. package/dist/edit/Editable.svelte +5 -3
  29. package/dist/edit/chrome/AddSlot.svelte +89 -0
  30. package/dist/edit/chrome/AddSlot.svelte.d.ts +17 -0
  31. package/dist/edit/chrome/ConfirmDialog.svelte +65 -0
  32. package/dist/edit/chrome/ConfirmDialog.svelte.d.ts +18 -0
  33. package/dist/edit/chrome/EditFrame.svelte +171 -0
  34. package/dist/edit/chrome/EditFrame.svelte.d.ts +33 -0
  35. package/dist/edit/chrome/EditPanel.svelte +29 -0
  36. package/dist/edit/chrome/EditPanel.svelte.d.ts +15 -0
  37. package/dist/edit/chrome/EditPopover.svelte +57 -0
  38. package/dist/edit/chrome/EditPopover.svelte.d.ts +17 -0
  39. package/dist/edit/chrome/PropertyRow.svelte +241 -0
  40. package/dist/edit/chrome/PropertyRow.svelte.d.ts +17 -0
  41. package/dist/edit/helpers.d.ts +19 -1
  42. package/dist/edit/helpers.js +21 -0
  43. package/dist/edit/index.d.ts +8 -2
  44. package/dist/edit/index.js +6 -1
  45. package/dist/edit/types.d.ts +82 -2
  46. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The editor chrome's own strings — status announcements, panel labels, the
3
+ * confirm-delete wording. A separate object from `UiMessages` on purpose:
4
+ * that interface mirrors the website's wording catalog (its keys are
5
+ * `ui_messages` rows a CMS edits), while these strings belong to the EDITOR
6
+ * a CMS host renders — they must never leak into the site's catalog, and
7
+ * they must never themselves become editable chrome.
8
+ *
9
+ * Same message-function style as `UiMessages`, and the same all-or-nothing
10
+ * override: a host replaces the whole object or keeps the Catalan defaults.
11
+ */
12
+ export interface EditMessages {
13
+ /** Inline + panel status announcements. */
14
+ edit_saving(): string;
15
+ edit_saved(): string;
16
+ edit_saveError(): string;
17
+ /** RichText's source editor. */
18
+ edit_save(): string;
19
+ edit_cancel(): string;
20
+ edit_edit(): string;
21
+ edit_preview(): string;
22
+ edit_editBody(): string;
23
+ /** The frame toolbar and its panel. */
24
+ edit_properties(params: {
25
+ label: string;
26
+ }): string;
27
+ edit_remove(): string;
28
+ edit_removeConfirm(params: {
29
+ label: string;
30
+ }): string;
31
+ edit_close(): string;
32
+ /** Collections. */
33
+ edit_add(params: {
34
+ label: string;
35
+ }): string;
36
+ edit_addFailed(): string;
37
+ /** Property rows. */
38
+ edit_uploadImage(): string;
39
+ edit_clearValue(): string;
40
+ edit_emptyRequired(): string;
41
+ }
42
+ /**
43
+ * Catalan defaults. The first three and the RichText strings reproduce the
44
+ * previously hardcoded copy byte-identically — swapping the source of a
45
+ * string must not repaint a single test or page.
46
+ */
47
+ export declare const defaultEditMessages: EditMessages;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Catalan defaults. The first three and the RichText strings reproduce the
3
+ * previously hardcoded copy byte-identically — swapping the source of a
4
+ * string must not repaint a single test or page.
5
+ */
6
+ export const defaultEditMessages = {
7
+ edit_saving: () => 'Desant…',
8
+ edit_saved: () => 'Desat',
9
+ edit_saveError: () => 'Error en desar',
10
+ edit_save: () => 'Desa',
11
+ edit_cancel: () => 'Cancel·la',
12
+ edit_edit: () => 'Edita',
13
+ edit_preview: () => 'Vista prèvia',
14
+ edit_editBody: () => 'Edita el contingut',
15
+ edit_properties: ({ label }) => `Propietats: ${label}`,
16
+ edit_remove: () => 'Elimina',
17
+ edit_removeConfirm: ({ label }) => `Segur que vols eliminar «${label}»? Es pot desfer des de l’historial.`,
18
+ edit_close: () => 'Tanca',
19
+ edit_add: ({ label }) => `Afegeix ${label}`,
20
+ edit_addFailed: () => 'No s’ha pogut afegir',
21
+ edit_uploadImage: () => 'Puja una imatge',
22
+ edit_clearValue: () => 'Buida el valor',
23
+ edit_emptyRequired: () => 'Aquest camp no pot quedar buit'
24
+ };
@@ -1,4 +1,6 @@
1
1
  export { DEFAULT_UI_CONFIG, getUiConfig, setUiConfig } from './context.js';
2
+ export { defaultEditMessages } from './edit-messages.js';
3
+ export type { EditMessages } from './edit-messages.js';
2
4
  export { defaultMessages } from './messages.js';
3
5
  export { default as UiProvider } from './UiProvider.svelte';
4
6
  export { BASE_LOCALE, LOCALES } from './types.js';
@@ -1,4 +1,5 @@
1
1
  export { DEFAULT_UI_CONFIG, getUiConfig, setUiConfig } from './context.js';
2
+ export { defaultEditMessages } from './edit-messages.js';
2
3
  export { defaultMessages } from './messages.js';
3
4
  export { default as UiProvider } from './UiProvider.svelte';
4
5
  export { BASE_LOCALE, LOCALES } from './types.js';
@@ -1,4 +1,5 @@
1
1
  import type { EditDescriptor } from '../edit/types.js';
2
+ import type { EditMessages } from './edit-messages.js';
2
3
  /**
3
4
  * The site's locales and their canonical member. Fixed rather than generic:
4
5
  * `ca` is load-bearing — it is the required key of every localized column
@@ -157,6 +158,12 @@ export interface UiConfig {
157
158
  /** Appended to document titles and shown as the wordmark. */
158
159
  siteName: string;
159
160
  messages: UiMessages;
161
+ /**
162
+ * The editor chrome's own strings (status announcements, panel wording) —
163
+ * resolved, Catalan by default. NOT part of `messages`: see
164
+ * `./edit-messages.js` for why the two catalogs must not mix.
165
+ */
166
+ editMessages: EditMessages;
160
167
  /**
161
168
  * Edit descriptor for one of THIS config's message strings, by catalog
162
169
  * key — the interface-wording half of edit mode. Optional and undefined by
@@ -175,5 +182,6 @@ export interface UiConfigInput {
175
182
  canonicalPathname?: UiConfig['canonicalPathname'];
176
183
  siteName?: string;
177
184
  messages?: UiMessages;
185
+ editMessages?: EditMessages;
178
186
  messageEdit?: UiConfig['messageEdit'];
179
187
  }
@@ -59,11 +59,15 @@ export interface TeamMemberData {
59
59
  isBoard: boolean;
60
60
  }
61
61
  export interface CollaboratorData {
62
+ /** Present only where the list is editable — remove ops need an identity. */
63
+ id?: string | number;
62
64
  personName: string;
63
65
  affiliation: string;
64
66
  url: string | null;
65
67
  }
66
68
  export interface JobOpeningData {
69
+ /** Present only where the list is editable — remove ops need an identity. */
70
+ id?: string | number;
67
71
  slug: string;
68
72
  title: string;
69
73
  description: string | null;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { getUiConfig } from '../config/context.js';
3
4
  import { getEditAdapter } from './context.js';
4
5
  import type { EditDescriptor } from './types.js';
5
6
 
@@ -55,6 +56,7 @@
55
56
  let { edit, value, children }: Props = $props();
56
57
 
57
58
  const adapter = getEditAdapter();
59
+ const config = getUiConfig();
58
60
 
59
61
  let status = $state<'idle' | 'dirty' | 'saving' | 'error'>('idle');
60
62
  /** Last persisted value: what Escape restores and blur diffs against. */
@@ -121,16 +123,16 @@
121
123
  return;
122
124
  }
123
125
  status = 'saving';
124
- announcement = 'Desant…';
126
+ announcement = config.editMessages.edit_saving();
125
127
  try {
126
128
  await adapter.save(edit, draft);
127
129
  savedValue = draft;
128
130
  status = 'idle';
129
- announcement = 'Desat';
131
+ announcement = config.editMessages.edit_saved();
130
132
  } catch {
131
133
  // Draft stays in the DOM; the reader decides whether to retry.
132
134
  status = 'error';
133
- announcement = 'Error en desar';
135
+ announcement = config.editMessages.edit_saveError();
134
136
  }
135
137
  }
136
138
 
@@ -0,0 +1,89 @@
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';
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
+ 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();
20
+
21
+ const adapter = getEditAdapter();
22
+ const config = getUiConfig();
23
+
24
+ let pending = $state(false);
25
+ let announcement = $state('');
26
+
27
+ 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
+ </script>
43
+
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>
50
+ {/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>
@@ -0,0 +1,17 @@
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, {}, "">;
16
+ type AddSlot = ReturnType<typeof AddSlot>;
17
+ export default AddSlot;
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import Button from '../../components/ui/Button.svelte';
3
+ import Modal from '../../components/ui/Modal.svelte';
4
+ import { getUiConfig } from '../../config/context.js';
5
+
6
+ /**
7
+ * Destructive confirmation over the Modal primitive: native <dialog>
8
+ * handles top layer, backdrop and Escape. The confirm button turns
9
+ * destructive by overriding the tokens Button's `primary` reads — a theme
10
+ * decision expressed as token values, not a new Button variant.
11
+ */
12
+ interface Props {
13
+ open: boolean;
14
+ title: string;
15
+ message: string;
16
+ confirmLabel: string;
17
+ pending?: boolean;
18
+ onconfirm: () => void;
19
+ oncancel: () => void;
20
+ }
21
+
22
+ let {
23
+ open,
24
+ title,
25
+ message,
26
+ confirmLabel,
27
+ pending = false,
28
+ onconfirm,
29
+ oncancel
30
+ }: Props = $props();
31
+
32
+ const config = getUiConfig();
33
+ </script>
34
+
35
+ <Modal {open} {title} onclose={oncancel} closeLabel={config.editMessages.edit_close()}>
36
+ <p class="message">{message}</p>
37
+ <div class="actions">
38
+ <span class="danger">
39
+ <Button pending={pending ? 1 : 0} onclick={onconfirm}>{confirmLabel}</Button>
40
+ </span>
41
+ <Button variant="ghost" pending={null} onclick={oncancel}>
42
+ {config.editMessages.edit_cancel()}
43
+ </Button>
44
+ </div>
45
+ </Modal>
46
+
47
+ <style>
48
+ .message {
49
+ margin: 0;
50
+ max-width: 44ch;
51
+ }
52
+
53
+ .actions {
54
+ display: flex;
55
+ gap: var(--space-2);
56
+ }
57
+
58
+ /* Button's primary fill reads --color-brand (hover --color-navy); the
59
+ error hue is --series-8, so overriding both here IS the destructive
60
+ variant — no second styling path inside Button. */
61
+ .danger {
62
+ --color-brand: var(--series-8);
63
+ --color-navy: color-mix(in srgb, var(--series-8) 80%, var(--color-ink));
64
+ }
65
+ </style>
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Destructive confirmation over the Modal primitive: native <dialog>
3
+ * handles top layer, backdrop and Escape. The confirm button turns
4
+ * destructive by overriding the tokens Button's `primary` reads — a theme
5
+ * decision expressed as token values, not a new Button variant.
6
+ */
7
+ interface Props {
8
+ open: boolean;
9
+ title: string;
10
+ message: string;
11
+ confirmLabel: string;
12
+ pending?: boolean;
13
+ onconfirm: () => void;
14
+ oncancel: () => void;
15
+ }
16
+ declare const ConfirmDialog: import("svelte").Component<Props, {}, "">;
17
+ type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
18
+ export default ConfirmDialog;
@@ -0,0 +1,171 @@
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
+ }
19
+
20
+ /**
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.
30
+ */
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();
39
+
40
+ const adapter = getEditAdapter();
41
+ const config = getUiConfig();
42
+
43
+ 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
+ </script>
79
+
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>
120
+ {:else}
121
+ {@render children()}
122
+ {/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>
@@ -0,0 +1,33 @@
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, {}, "">;
32
+ type EditFrame = ReturnType<typeof EditFrame>;
33
+ export default EditFrame;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import type { PropertyDescriptor } from '../types.js';
3
+ import PropertyRow from './PropertyRow.svelte';
4
+
5
+ /**
6
+ * The property form an EditFrame's popover holds: one PropertyRow per
7
+ * entry, each with its own independent commit lifecycle. The component
8
+ * knows nothing about the entity — the descriptors say everything.
9
+ */
10
+ interface Props {
11
+ rows: { descriptor: PropertyDescriptor; value: string | null }[];
12
+ }
13
+
14
+ let { rows }: Props = $props();
15
+ </script>
16
+
17
+ <div class="panel">
18
+ {#each rows as row (JSON.stringify(row.descriptor.ref) + row.descriptor.label)}
19
+ <PropertyRow descriptor={row.descriptor} value={row.value} />
20
+ {/each}
21
+ </div>
22
+
23
+ <style>
24
+ .panel {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--space-3);
28
+ }
29
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { PropertyDescriptor } from '../types.js';
2
+ /**
3
+ * The property form an EditFrame's popover holds: one PropertyRow per
4
+ * entry, each with its own independent commit lifecycle. The component
5
+ * knows nothing about the entity — the descriptors say everything.
6
+ */
7
+ interface Props {
8
+ rows: {
9
+ descriptor: PropertyDescriptor;
10
+ value: string | null;
11
+ }[];
12
+ }
13
+ declare const EditPanel: import("svelte").Component<Props, {}, "">;
14
+ type EditPanel = ReturnType<typeof EditPanel>;
15
+ export default EditPanel;
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ /**
5
+ * The floating layer an EditFrame opens its panel in. Absolutely
6
+ * positioned INSIDE the frame (no portal: in-flow positioning needs no
7
+ * SSR-unfriendly machinery, and modality is only for destructive confirm,
8
+ * which native <dialog> covers). Escape and outside pointerdown close it;
9
+ * the OPENER owns focus return, because only it knows its own element —
10
+ * the Nav menu pattern.
11
+ */
12
+ interface Props {
13
+ label: string;
14
+ onclose: () => void;
15
+ children: Snippet;
16
+ }
17
+
18
+ let { label, onclose, children }: Props = $props();
19
+
20
+ let popover: HTMLDivElement | undefined = $state();
21
+
22
+ // Focus the first control so the keyboard lands where the caret goes.
23
+ $effect(() => {
24
+ popover?.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]')?.focus();
25
+ });
26
+
27
+ function onWindowKeydown(event: KeyboardEvent): void {
28
+ if (event.key === 'Escape') onclose();
29
+ }
30
+
31
+ function onWindowPointerdown(event: PointerEvent): void {
32
+ // The opening click's pointerdown fired before this listener existed,
33
+ // so only genuinely outside presses land here.
34
+ if (popover && !popover.contains(event.target as Node)) onclose();
35
+ }
36
+ </script>
37
+
38
+ <svelte:window onkeydown={onWindowKeydown} onpointerdown={onWindowPointerdown} />
39
+
40
+ <div class="popover" role="dialog" aria-label={label} bind:this={popover}>
41
+ {@render children()}
42
+ </div>
43
+
44
+ <style>
45
+ .popover {
46
+ position: absolute;
47
+ top: var(--space-2);
48
+ right: var(--space-2);
49
+ width: min(20rem, 90vw);
50
+ z-index: var(--z-overlay);
51
+ background: var(--color-surface);
52
+ border: 1px solid var(--color-hairline);
53
+ border-radius: var(--radius);
54
+ box-shadow: var(--shadow-2);
55
+ padding: var(--space-3);
56
+ }
57
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * The floating layer an EditFrame opens its panel in. Absolutely
4
+ * positioned INSIDE the frame (no portal: in-flow positioning needs no
5
+ * SSR-unfriendly machinery, and modality is only for destructive confirm,
6
+ * which native <dialog> covers). Escape and outside pointerdown close it;
7
+ * the OPENER owns focus return, because only it knows its own element —
8
+ * the Nav menu pattern.
9
+ */
10
+ interface Props {
11
+ label: string;
12
+ onclose: () => void;
13
+ children: Snippet;
14
+ }
15
+ declare const EditPopover: import("svelte").Component<Props, {}, "">;
16
+ type EditPopover = ReturnType<typeof EditPopover>;
17
+ export default EditPopover;