@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
@@ -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;
@@ -0,0 +1,153 @@
1
+ <script lang="ts">
2
+ import IconButton from '../../components/ui/IconButton.svelte';
3
+ import { getUiConfig } from '../../config/context.js';
4
+ import { getEditAdapter } from '../context.js';
5
+ import type { EditFrameProps } from '../chrome-props.js';
6
+ import ConfirmDialog from './ConfirmDialog.svelte';
7
+ import EditPopover from './EditPopover.svelte';
8
+
9
+ /**
10
+ * The page-builder wrapper: a corner toolbar (revealed on hover AND
11
+ * :focus-within — never hover-only) with a gear that opens the property
12
+ * panel and a trash that confirms, then applies the remove op.
13
+ *
14
+ * Triple-gated per affordance: spec present ∧ adapter editing ∧ the
15
+ * capability method present. With nothing to offer it renders the children
16
+ * ALONE — zero wrapper element, so an unadapted app's DOM is byte-identical
17
+ * (pinned by test). Components mount it INSIDE their own root element, so
18
+ * the wrapper never disturbs the parent's flex/grid.
19
+ */
20
+ let { spec, panel, children }: EditFrameProps = $props();
21
+
22
+ const adapter = getEditAdapter();
23
+ const config = getUiConfig();
24
+
25
+ const editing = $derived(spec !== undefined && (adapter?.isEditing ?? false));
26
+ const showGear = $derived(
27
+ editing && spec?.hasPanel === true && panel !== undefined && adapter?.saveProperty !== undefined
28
+ );
29
+ const showTrash = $derived(
30
+ editing && spec?.removeOp !== undefined && adapter?.applyOp !== undefined
31
+ );
32
+ const framed = $derived(showGear || showTrash);
33
+
34
+ let panelOpen = $state(false);
35
+ let confirming = $state(false);
36
+ let removing = $state(false);
37
+ let announcement = $state('');
38
+ let toolbar: HTMLDivElement | undefined = $state();
39
+
40
+ function closePanel(): void {
41
+ panelOpen = false;
42
+ // The popover owned focus; hand it back to the control that opened it.
43
+ toolbar?.querySelector('button')?.focus();
44
+ }
45
+
46
+ async function remove(): Promise<void> {
47
+ if (!adapter?.applyOp || !spec?.removeOp) return;
48
+ removing = true;
49
+ try {
50
+ await adapter.applyOp(spec.removeOp);
51
+ // The host's refresh unmounts this frame with the removed item.
52
+ confirming = false;
53
+ } catch {
54
+ confirming = false;
55
+ announcement = config.editMessages.edit_saveError();
56
+ } finally {
57
+ removing = false;
58
+ }
59
+ }
60
+ </script>
61
+
62
+ {#if framed && spec}
63
+ <div class="vit-edit-frame">
64
+ {@render children()}
65
+ <div class="toolbar" bind:this={toolbar}>
66
+ {#if showGear}
67
+ <IconButton
68
+ icon="gear"
69
+ label={config.editMessages.edit_properties({ label: spec.label })}
70
+ onclick={() => (panelOpen = !panelOpen)}
71
+ />
72
+ {/if}
73
+ {#if showTrash}
74
+ <IconButton
75
+ icon="trash"
76
+ label={config.editMessages.edit_remove()}
77
+ onclick={() => (confirming = true)}
78
+ />
79
+ {/if}
80
+ </div>
81
+ {#if panelOpen && panel}
82
+ <EditPopover
83
+ label={config.editMessages.edit_properties({ label: spec.label })}
84
+ onclose={closePanel}
85
+ >
86
+ {@render panel()}
87
+ </EditPopover>
88
+ {/if}
89
+ {#if showTrash}
90
+ <ConfirmDialog
91
+ open={confirming}
92
+ title={config.editMessages.edit_remove()}
93
+ message={config.editMessages.edit_removeConfirm({ label: spec.label })}
94
+ confirmLabel={config.editMessages.edit_remove()}
95
+ pending={removing}
96
+ onconfirm={() => void remove()}
97
+ oncancel={() => (confirming = false)}
98
+ />
99
+ {/if}
100
+ <span class="status" role="status">{announcement}</span>
101
+ </div>
102
+ {:else}
103
+ {@render children()}
104
+ {/if}
105
+
106
+ <style>
107
+ .vit-edit-frame {
108
+ position: relative;
109
+ border-radius: var(--radius);
110
+ outline: 1px dashed transparent;
111
+ outline-offset: 4px;
112
+ transition: outline-color var(--transition-fast);
113
+ }
114
+
115
+ .vit-edit-frame:hover,
116
+ .vit-edit-frame:focus-within {
117
+ outline-color: var(--color-brand);
118
+ }
119
+
120
+ .toolbar {
121
+ position: absolute;
122
+ top: calc(-1 * var(--space-2));
123
+ right: 0;
124
+ display: flex;
125
+ gap: 2px;
126
+ z-index: var(--z-raised);
127
+ background: var(--color-surface);
128
+ border: 1px solid var(--color-hairline);
129
+ border-radius: var(--radius);
130
+ box-shadow: var(--shadow-1);
131
+ opacity: 0;
132
+ pointer-events: none;
133
+ transition: opacity var(--transition-fast);
134
+ }
135
+
136
+ .vit-edit-frame:hover .toolbar,
137
+ .vit-edit-frame:focus-within .toolbar {
138
+ opacity: 1;
139
+ pointer-events: auto;
140
+ }
141
+
142
+ .status {
143
+ position: absolute;
144
+ width: 1px;
145
+ height: 1px;
146
+ padding: 0;
147
+ margin: -1px;
148
+ overflow: hidden;
149
+ clip-path: inset(50%);
150
+ white-space: nowrap;
151
+ border: 0;
152
+ }
153
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { EditFrameProps } from '../chrome-props.js';
2
+ declare const EditFrame: import("svelte").Component<EditFrameProps, {}, "">;
3
+ type EditFrame = ReturnType<typeof EditFrame>;
4
+ export default EditFrame;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import type { EditPanelProps } from '../chrome-props.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
+ let { rows }: EditPanelProps = $props();
11
+ </script>
12
+
13
+ <div class="panel">
14
+ {#each rows as row (JSON.stringify(row.descriptor.ref) + row.descriptor.label)}
15
+ <PropertyRow descriptor={row.descriptor} value={row.value} />
16
+ {/each}
17
+ </div>
18
+
19
+ <style>
20
+ .panel {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: var(--space-3);
24
+ }
25
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { EditPanelProps } from '../chrome-props.js';
2
+ declare const EditPanel: import("svelte").Component<EditPanelProps, {}, "">;
3
+ type EditPanel = ReturnType<typeof EditPanel>;
4
+ export default EditPanel;
@@ -0,0 +1,162 @@
1
+ <script lang="ts">
2
+ import { getUiConfig } from '../../config/context.js';
3
+ import { getEditAdapter } from '../context.js';
4
+ import { commitState } from './commit.svelte.js';
5
+ import type { EditableAttrs, EditableProps } from '../chrome-props.js';
6
+
7
+ /**
8
+ * The inline-edit primitive. It renders no element of its own: the child
9
+ * snippet receives the text and a bag of attributes, and spreads both onto
10
+ * whatever semantic element the call site owns —
11
+ *
12
+ * <Editable edit={descriptor} value={title}>
13
+ * {#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
14
+ * </Editable>
15
+ *
16
+ * — the same contract Field uses for its control attrs, and for the same
17
+ * reason: the element (h1, p, span) is part of the document outline and
18
+ * belongs to the caller, not behind a prop.
19
+ *
20
+ * Inactive — no `edit` descriptor, no adapter in context (a read-only
21
+ * app), or the adapter's `isEditing` off — it renders the child with an
22
+ * empty attribute bag: no listeners, no wrapper, no cost.
23
+ *
24
+ * Active, it makes the element a plain-text `contenteditable` textbox.
25
+ * The DOM owns the draft while the reader types; this component reads it
26
+ * back on commit. Enter commits single-line fields (Cmd/Ctrl+Enter for
27
+ * 'multiline'), Escape reverts to the last saved value, blur commits a
28
+ * dirty draft. A failed save keeps the draft on screen in the error state
29
+ * so nothing typed is lost. State is announced through a visually hidden
30
+ * `role="status"` region, the pattern ShareRow set for its copy action.
31
+ *
32
+ * Styling for the affordance lives in base.css (`[data-vit-editing]`):
33
+ * the element is rendered by the *caller's* markup, which scoped styles
34
+ * here could never reach.
35
+ */
36
+ let { edit, value, children }: EditableProps = $props();
37
+
38
+ const adapter = getEditAdapter();
39
+ const config = getUiConfig();
40
+
41
+ /**
42
+ * The commit lifecycle — the four states, the announcements, the last
43
+ * persisted value and the follow-the-prop rule — is shared with
44
+ * `PropertyRow` (see `./commit.svelte.js`). What stays here is where the
45
+ * DRAFT lives: in the DOM, because this is a contenteditable.
46
+ */
47
+ // svelte-ignore state_referenced_locally
48
+ const commit_ = commitState(value, config.editMessages);
49
+ /** What the child snippet renders. Only rewritten while the DOM is not
50
+ * being typed in — Svelte and the reader must not fight over the node. */
51
+ // svelte-ignore state_referenced_locally
52
+ let renderText = $state(value);
53
+
54
+ $effect(() => {
55
+ const adopted = commit_.follow(value);
56
+ if (adopted !== null) renderText = adopted;
57
+ });
58
+
59
+ const active = $derived(edit !== undefined && (adapter?.isEditing ?? false));
60
+ const multiline = $derived(edit?.format === 'multiline' || edit?.format === 'richtext');
61
+
62
+ /** contenteditable text, innerText keeps line breaks; NBSPs become spaces. */
63
+ function textOf(element: HTMLElement): string {
64
+ return (element.innerText ?? element.textContent ?? '').replace(/\u00a0/g, ' ');
65
+ }
66
+
67
+ function handleBeforeInput(event: InputEvent): void {
68
+ // 'plaintext-only' is not universal (Firefox); strip rich insertions.
69
+ if (event.inputType === 'insertFromPaste' || event.inputType === 'insertFromDrop') return;
70
+ if (!multiline && event.inputType === 'insertParagraph') event.preventDefault();
71
+ }
72
+
73
+ function handlePaste(event: ClipboardEvent): void {
74
+ // Plain text only, whatever the clipboard holds.
75
+ event.preventDefault();
76
+ const text = event.clipboardData?.getData('text/plain') ?? '';
77
+ const selection = window.getSelection();
78
+ if (!selection || selection.rangeCount === 0) return;
79
+ const range = selection.getRangeAt(0);
80
+ range.deleteContents();
81
+ range.insertNode(document.createTextNode(multiline ? text : text.replace(/\s*\n\s*/g, ' ')));
82
+ selection.collapseToEnd();
83
+ commit_.markDirty();
84
+ }
85
+
86
+ function handleInput(): void {
87
+ commit_.markDirty();
88
+ }
89
+
90
+ async function commit(element: HTMLElement): Promise<void> {
91
+ if (!edit || !adapter) return;
92
+ const draft = textOf(element).trim();
93
+ if (draft === commit_.saved) {
94
+ commit_.settle();
95
+ return;
96
+ }
97
+ // A failed save leaves the draft in the DOM; the reader decides whether
98
+ // to retry.
99
+ await commit_.commit(draft, () => adapter.save(edit, draft));
100
+ }
101
+
102
+ function handleBlur(event: FocusEvent): void {
103
+ const status = commit_.status;
104
+ if (status === 'dirty' || status === 'error') void commit(event.currentTarget as HTMLElement);
105
+ }
106
+
107
+ function handleKeydown(event: KeyboardEvent): void {
108
+ const element = event.currentTarget as HTMLElement;
109
+ if (event.key === 'Escape') {
110
+ // Svelte's cached text still equals renderText, so a state write
111
+ // alone cannot repaint a node the reader has mutated: restore the
112
+ // DOM directly, then settle.
113
+ element.textContent = commit_.saved;
114
+ commit_.revert();
115
+ element.blur();
116
+ return;
117
+ }
118
+ const commitKey = multiline
119
+ ? event.key === 'Enter' && (event.metaKey || event.ctrlKey)
120
+ : event.key === 'Enter';
121
+ if (commitKey) {
122
+ event.preventDefault();
123
+ void commit(element);
124
+ }
125
+ }
126
+
127
+ const attrs = $derived<EditableAttrs>(
128
+ active
129
+ ? {
130
+ contenteditable: 'plaintext-only',
131
+ role: 'textbox',
132
+ 'aria-label': edit?.label,
133
+ 'aria-multiline': multiline ? 'true' : undefined,
134
+ 'data-vit-editing': commit_.status,
135
+ onbeforeinput: handleBeforeInput,
136
+ oninput: handleInput,
137
+ onblur: handleBlur,
138
+ onkeydown: handleKeydown,
139
+ onpaste: handlePaste
140
+ }
141
+ : {}
142
+ );
143
+ </script>
144
+
145
+ {@render children(renderText, attrs)}
146
+ {#if active}
147
+ <span class="status" role="status">{commit_.announcement}</span>
148
+ {/if}
149
+
150
+ <style>
151
+ .status {
152
+ position: absolute;
153
+ width: 1px;
154
+ height: 1px;
155
+ padding: 0;
156
+ margin: -1px;
157
+ overflow: hidden;
158
+ clip-path: inset(50%);
159
+ white-space: nowrap;
160
+ border: 0;
161
+ }
162
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { EditableProps } from '../chrome-props.js';
2
+ declare const Editable: import("svelte").Component<EditableProps, {}, "">;
3
+ type Editable = ReturnType<typeof Editable>;
4
+ export default Editable;