@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.
- package/README.md +2 -2
- package/dist/community.d.ts +12 -3
- package/dist/community.js +7 -3
- package/dist/components/pages/ProjectPage.svelte +3 -9
- package/dist/components/pages/ProjectPage.svelte.d.ts +3 -5
- package/dist/components/pages/WeeklyPage.svelte +7 -12
- package/dist/components/pages/WeeklyPage.svelte.d.ts +7 -9
- package/dist/components/ui/Icon.svelte +4 -0
- package/dist/components/ui/Icon.svelte.d.ts +1 -0
- package/dist/components/ui/Modal.svelte +32 -11
- package/dist/components/ui/Modal.svelte.d.ts +17 -1
- package/dist/components/ui/SelectField.svelte +207 -0
- package/dist/components/ui/SelectField.svelte.d.ts +76 -0
- package/dist/config/context.d.ts +0 -6
- package/dist/config/context.js +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +1 -1
- package/dist/content-components.d.ts +6 -3
- package/dist/content-components.js +5 -1
- package/dist/contract.d.ts +8 -4
- package/dist/contract.js +7 -3
- package/dist/edit/Editable.svelte +17 -161
- package/dist/edit/Editable.svelte.d.ts +2 -50
- package/dist/edit/chrome/AddSlot.svelte +11 -78
- package/dist/edit/chrome/AddSlot.svelte.d.ts +2 -15
- package/dist/edit/chrome/EditFrame.svelte +15 -158
- package/dist/edit/chrome/EditFrame.svelte.d.ts +2 -31
- package/dist/edit/chrome/EditPanel.svelte +14 -22
- package/dist/edit/chrome/EditPanel.svelte.d.ts +2 -13
- package/dist/edit/chrome/LinkEdit.svelte +13 -277
- package/dist/edit/chrome/LinkEdit.svelte.d.ts +2 -48
- package/dist/edit/chrome-props.d.ts +96 -0
- package/dist/edit/chrome-props.js +1 -0
- package/dist/edit/context.d.ts +38 -5
- package/dist/edit/context.js +18 -7
- package/dist/edit/index.d.ts +6 -4
- package/dist/edit/index.js +4 -3
- package/dist/edit/live/AddSlot.svelte +83 -0
- package/dist/edit/live/AddSlot.svelte.d.ts +4 -0
- package/dist/edit/live/EditFrame.svelte +153 -0
- package/dist/edit/live/EditFrame.svelte.d.ts +4 -0
- package/dist/edit/live/EditPanel.svelte +25 -0
- package/dist/edit/live/EditPanel.svelte.d.ts +4 -0
- package/dist/edit/live/Editable.svelte +162 -0
- package/dist/edit/live/Editable.svelte.d.ts +4 -0
- package/dist/edit/live/LinkEdit.svelte +276 -0
- package/dist/edit/live/LinkEdit.svelte.d.ts +4 -0
- package/dist/edit/{chrome → live}/PropertyRow.svelte +1 -1
- package/dist/edit/{commit.svelte.d.ts → live/commit.svelte.d.ts} +2 -2
- package/dist/edit/live/index.d.ts +10 -0
- package/dist/edit/live/index.js +14 -0
- package/dist/forms/index.d.ts +1 -0
- package/dist/forms/index.js +1 -0
- package/dist/styles/base.css +1 -1
- package/dist/styles/tokens.css +4 -0
- package/package.json +1 -1
- package/dist/components/account/index.d.ts +0 -3
- package/dist/components/account/index.js +0 -2
- package/dist/components/auth/index.d.ts +0 -7
- package/dist/components/auth/index.js +0 -4
- package/dist/components/contact/index.d.ts +0 -2
- package/dist/components/contact/index.js +0 -1
- package/dist/content/index.d.ts +0 -6
- package/dist/content/index.js +0 -3
- /package/dist/edit/{chrome → live}/ConfirmDialog.svelte +0 -0
- /package/dist/edit/{chrome → live}/ConfirmDialog.svelte.d.ts +0 -0
- /package/dist/edit/{chrome → live}/EditPopover.svelte +0 -0
- /package/dist/edit/{chrome → live}/EditPopover.svelte.d.ts +0 -0
- /package/dist/edit/{chrome → live}/PropertyRow.svelte.d.ts +0 -0
- /package/dist/edit/{commit.svelte.js → live/commit.svelte.js} +0 -0
package/dist/contract.d.ts
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
|
|
21
|
-
* subpath and stays there — this is a second door into the same
|
|
22
|
-
* new floor, and the barrel remains the surface a component host
|
|
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,
|
|
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
|
|
21
|
-
* subpath and stays there — this is a second door into the same
|
|
22
|
-
* new floor, and the barrel remains the surface a component host
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
|
10
|
-
* snippet receives the text and a bag of attributes, and spreads
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
167
|
-
{
|
|
168
|
-
<
|
|
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 {
|
|
2
|
-
|
|
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
|
|
3
|
-
import {
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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 {
|
|
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 {
|
|
3
|
-
import
|
|
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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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 {
|
|
2
|
-
|
|
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;
|