@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.
- package/README.md +1 -1
- 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/Modal.svelte +32 -11
- package/dist/components/ui/Modal.svelte.d.ts +17 -1
- 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/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
|
@@ -1,52 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
}:
|
|
21
|
+
}: LinkEditProps = $props();
|
|
58
22
|
|
|
59
23
|
const adapter = getEditAdapter();
|
|
60
|
-
const
|
|
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
|
-
|
|
154
|
-
|
|
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 {
|
|
2
|
-
|
|
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 {};
|
package/dist/edit/context.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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;
|
package/dist/edit/context.js
CHANGED
|
@@ -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
|
|
5
|
-
* calls this is read-only by construction: without
|
|
6
|
-
* affordance renders as plain content, byte-identical
|
|
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
|
-
|
|
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
|
}
|
package/dist/edit/index.d.ts
CHANGED
|
@@ -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 './
|
|
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
|
|
11
|
+
export type { EditFrameSpec } from './chrome-props.js';
|
|
10
12
|
export { default as EditPanel } from './chrome/EditPanel.svelte';
|
|
11
|
-
export { default as EditPopover } from './
|
|
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';
|
package/dist/edit/index.js
CHANGED
|
@@ -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 './
|
|
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 './
|
|
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>
|