@vit-foundation/ui 0.14.0 → 0.15.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/dist/components/layout/Footer.svelte +12 -27
- package/dist/components/layout/Footer.svelte.d.ts +1 -1
- package/dist/components/layout/Nav.svelte +19 -31
- package/dist/components/layout/Nav.svelte.d.ts +4 -3
- package/dist/components/layout/site-link-edit.d.ts +5 -4
- package/dist/edit/chrome/LinkEdit.svelte +85 -6
- package/dist/edit/chrome/LinkEdit.svelte.d.ts +15 -3
- package/package.json +1 -1
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getUiConfig } from '../../config/context.js';
|
|
3
3
|
import type { SiteLink } from '../../config/types.js';
|
|
4
|
-
import ActionLabel from '../../edit/ActionLabel.svelte';
|
|
5
4
|
import AddSlot from '../../edit/chrome/AddSlot.svelte';
|
|
6
|
-
import
|
|
7
|
-
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
5
|
+
import LinkEdit from '../../edit/chrome/LinkEdit.svelte';
|
|
8
6
|
import { getEditAdapter } from '../../edit/context.js';
|
|
9
7
|
import Editable from '../../edit/Editable.svelte';
|
|
10
8
|
import type { CollectionRef, EditDescriptor } from '../../edit/types.js';
|
|
@@ -20,7 +18,7 @@
|
|
|
20
18
|
links: SiteLink[];
|
|
21
19
|
/** Edit descriptors for the link labels — see Nav. */
|
|
22
20
|
editFor?: (link: SiteLink) => EditDescriptor | undefined;
|
|
23
|
-
/**
|
|
21
|
+
/** The modal's Adreça/Ordre rows and removal per link — see LinkEdit. */
|
|
24
22
|
propertiesFor?: (link: SiteLink) => SiteLinkEditMap | undefined;
|
|
25
23
|
/** Names the links' collection; with `applyOp`, turns on add/remove. */
|
|
26
24
|
collection?: CollectionRef;
|
|
@@ -42,13 +40,6 @@
|
|
|
42
40
|
return { ...map, removeOp: { kind: 'remove', collection, id: link.id } };
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
function rowsFor(link: SiteLink, map: SiteLinkEditMap | undefined) {
|
|
46
|
-
return [
|
|
47
|
-
map?.href && { descriptor: map.href, value: link.href },
|
|
48
|
-
map?.order && { descriptor: map.order, value: String(link.order ?? 0) }
|
|
49
|
-
].filter((row) => row !== undefined);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
43
|
const year = new Date().getFullYear();
|
|
53
44
|
</script>
|
|
54
45
|
|
|
@@ -60,25 +51,19 @@
|
|
|
60
51
|
<ul>
|
|
61
52
|
{#each links as link (link.href)}
|
|
62
53
|
{@const map = editMapFor(link)}
|
|
63
|
-
{@const rows = rowsFor(link, map)}
|
|
64
54
|
<li>
|
|
65
|
-
<!--
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
removeOp: map.removeOp
|
|
72
|
-
}
|
|
55
|
+
<!-- One gesture: the modal edits text, destination, order, removal. -->
|
|
56
|
+
<LinkEdit
|
|
57
|
+
text={{ edit: editFor?.(link), value: link.label }}
|
|
58
|
+
href={{ descriptor: map?.href, value: link.href }}
|
|
59
|
+
extras={map?.order
|
|
60
|
+
? [{ descriptor: map.order, value: String(link.order ?? 0) }]
|
|
73
61
|
: undefined}
|
|
62
|
+
removeOp={map?.removeOp}
|
|
63
|
+
label={map?.label ?? link.label}
|
|
74
64
|
>
|
|
75
|
-
{#snippet
|
|
76
|
-
|
|
77
|
-
{/snippet}
|
|
78
|
-
<ActionLabel edit={editFor?.(link)} value={link.label}>
|
|
79
|
-
{#snippet control()}<Link href={link.href}>{link.label}</Link>{/snippet}
|
|
80
|
-
</ActionLabel>
|
|
81
|
-
</EditFrame>
|
|
65
|
+
{#snippet control()}<Link href={link.href}>{link.label}</Link>{/snippet}
|
|
66
|
+
</LinkEdit>
|
|
82
67
|
</li>
|
|
83
68
|
{/each}
|
|
84
69
|
{#if structural && collection}
|
|
@@ -10,7 +10,7 @@ interface Props {
|
|
|
10
10
|
links: SiteLink[];
|
|
11
11
|
/** Edit descriptors for the link labels — see Nav. */
|
|
12
12
|
editFor?: (link: SiteLink) => EditDescriptor | undefined;
|
|
13
|
-
/**
|
|
13
|
+
/** The modal's Adreça/Ordre rows and removal per link — see LinkEdit. */
|
|
14
14
|
propertiesFor?: (link: SiteLink) => SiteLinkEditMap | undefined;
|
|
15
15
|
/** Names the links' collection; with `applyOp`, turns on add/remove. */
|
|
16
16
|
collection?: CollectionRef;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getUiConfig } from '../../config/context.js';
|
|
3
3
|
import type { SiteLink } from '../../config/types.js';
|
|
4
|
-
import ActionLabel from '../../edit/ActionLabel.svelte';
|
|
5
4
|
import AddSlot from '../../edit/chrome/AddSlot.svelte';
|
|
6
|
-
import
|
|
7
|
-
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
5
|
+
import LinkEdit from '../../edit/chrome/LinkEdit.svelte';
|
|
8
6
|
import { getEditAdapter } from '../../edit/context.js';
|
|
9
7
|
import type { CollectionRef, EditDescriptor } from '../../edit/types.js';
|
|
10
8
|
import { isPathUnder } from '../../utils/paths.js';
|
|
@@ -26,11 +24,12 @@
|
|
|
26
24
|
/**
|
|
27
25
|
* Edit descriptors for the link LABELS — a function because the host
|
|
28
26
|
* owns both the routes and the message keys behind them. While the
|
|
29
|
-
* adapter is editing, a link with a descriptor
|
|
30
|
-
*
|
|
27
|
+
* adapter is editing, a link with a descriptor swaps for a button that
|
|
28
|
+
* opens the LINK MODAL (see LinkEdit) — every half of the entry edits
|
|
29
|
+
* in that one gesture.
|
|
31
30
|
*/
|
|
32
31
|
editFor?: (link: SiteLink) => EditDescriptor | undefined;
|
|
33
|
-
/**
|
|
32
|
+
/** The modal's Adreça/Ordre rows and removal per link — see LinkEdit. */
|
|
34
33
|
propertiesFor?: (link: SiteLink) => SiteLinkEditMap | undefined;
|
|
35
34
|
/** Names the links' collection; with `applyOp`, turns on add/remove. */
|
|
36
35
|
collection?: CollectionRef;
|
|
@@ -59,12 +58,6 @@
|
|
|
59
58
|
return { ...map, removeOp: { kind: 'remove', collection, id: link.id } };
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
function rowsFor(link: SiteLink, map: SiteLinkEditMap | undefined) {
|
|
63
|
-
return [
|
|
64
|
-
map?.href && { descriptor: map.href, value: link.href },
|
|
65
|
-
map?.order && { descriptor: map.order, value: String(link.order ?? 0) }
|
|
66
|
-
].filter((row) => row !== undefined);
|
|
67
|
-
}
|
|
68
61
|
const msg = $derived(config.messages);
|
|
69
62
|
|
|
70
63
|
let isMenuOpen = $state(false);
|
|
@@ -121,29 +114,24 @@
|
|
|
121
114
|
<ul class="links">
|
|
122
115
|
{#each links as link (link.href)}
|
|
123
116
|
{@const map = editMapFor(link)}
|
|
124
|
-
{@const rows = rowsFor(link, map)}
|
|
125
117
|
<li>
|
|
126
|
-
<!--
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
118
|
+
<!-- One gesture: clicking the entry opens the modal that edits
|
|
119
|
+
its text, destination, order and removal together. -->
|
|
120
|
+
<LinkEdit
|
|
121
|
+
text={{ edit: editFor?.(link), value: link.label }}
|
|
122
|
+
href={{ descriptor: map?.href, value: link.href }}
|
|
123
|
+
extras={map?.order
|
|
124
|
+
? [{ descriptor: map.order, value: String(link.order ?? 0) }]
|
|
134
125
|
: undefined}
|
|
126
|
+
removeOp={map?.removeOp}
|
|
127
|
+
label={map?.label ?? link.label}
|
|
135
128
|
>
|
|
136
|
-
{#snippet
|
|
137
|
-
<
|
|
129
|
+
{#snippet control()}
|
|
130
|
+
<Link href={link.href} aria-current={isCurrent(link.href) ? 'page' : undefined}>
|
|
131
|
+
{link.label}
|
|
132
|
+
</Link>
|
|
138
133
|
{/snippet}
|
|
139
|
-
|
|
140
|
-
{#snippet control()}
|
|
141
|
-
<Link href={link.href} aria-current={isCurrent(link.href) ? 'page' : undefined}>
|
|
142
|
-
{link.label}
|
|
143
|
-
</Link>
|
|
144
|
-
{/snippet}
|
|
145
|
-
</ActionLabel>
|
|
146
|
-
</EditFrame>
|
|
134
|
+
</LinkEdit>
|
|
147
135
|
</li>
|
|
148
136
|
{/each}
|
|
149
137
|
{#if structural && collection}
|
|
@@ -17,11 +17,12 @@ interface Props {
|
|
|
17
17
|
/**
|
|
18
18
|
* Edit descriptors for the link LABELS — a function because the host
|
|
19
19
|
* owns both the routes and the message keys behind them. While the
|
|
20
|
-
* adapter is editing, a link with a descriptor
|
|
21
|
-
*
|
|
20
|
+
* adapter is editing, a link with a descriptor swaps for a button that
|
|
21
|
+
* opens the LINK MODAL (see LinkEdit) — every half of the entry edits
|
|
22
|
+
* in that one gesture.
|
|
22
23
|
*/
|
|
23
24
|
editFor?: (link: SiteLink) => EditDescriptor | undefined;
|
|
24
|
-
/**
|
|
25
|
+
/** The modal's Adreça/Ordre rows and removal per link — see LinkEdit. */
|
|
25
26
|
propertiesFor?: (link: SiteLink) => SiteLinkEditMap | undefined;
|
|
26
27
|
/** Names the links' collection; with `applyOp`, turns on add/remove. */
|
|
27
28
|
collection?: CollectionRef;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { EntityOp, PropertyDescriptor } from '../../edit/types.js';
|
|
2
2
|
/**
|
|
3
|
-
* What the CMS may edit on one nav/footer link beyond its LABEL
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `collection
|
|
3
|
+
* What the CMS may edit on one nav/footer link beyond its LABEL: the href and
|
|
4
|
+
* the display order join the label in ONE modal (LinkEdit — clicking the
|
|
5
|
+
* entry opens it), and removal arrives injected by the list from its
|
|
6
|
+
* `collection`. The modal is entered through the label's `editFor`
|
|
7
|
+
* descriptor, so these rows take effect alongside it.
|
|
7
8
|
*/
|
|
8
9
|
export interface SiteLinkEditMap {
|
|
9
10
|
href?: PropertyDescriptor;
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import Modal from '../../components/ui/Modal.svelte';
|
|
6
6
|
import { getUiConfig } from '../../config/context.js';
|
|
7
7
|
import { getEditAdapter } from '../context.js';
|
|
8
|
-
import type { EditDescriptor, PropertyDescriptor } from '../types.js';
|
|
8
|
+
import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../types.js';
|
|
9
|
+
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A LINK edited whole: one modal for its text and its destination, instead
|
|
@@ -18,45 +19,67 @@
|
|
|
18
19
|
* opens the modal. Desa commits ONLY the halves that changed — the text
|
|
19
20
|
* through `adapter.save`, the destination through `adapter.saveProperty` —
|
|
20
21
|
* so a failure on one keeps the other's success and the modal stays open
|
|
21
|
-
* with the reason.
|
|
22
|
-
*
|
|
22
|
+
* with the reason. Extra rows (a menu link's Ordre) and a confirmed
|
|
23
|
+
* Elimina ride in the same modal when the host supplies them. Without an
|
|
24
|
+
* adapter or a descriptor the control renders untouched, byte-identical to
|
|
25
|
+
* a read-only build.
|
|
23
26
|
*/
|
|
24
27
|
interface Props {
|
|
25
28
|
/** Inline-label half; undefined leaves the control alone. */
|
|
26
29
|
text: { edit: EditDescriptor | undefined; value: string };
|
|
27
30
|
/** Destination half; undefined hides the Adreça field. */
|
|
28
31
|
href: { descriptor: PropertyDescriptor | undefined; value: string };
|
|
32
|
+
/**
|
|
33
|
+
* Extra property rows under Adreça — a menu link's Ordre. Labelled from
|
|
34
|
+
* their descriptors, committed on Desa only when changed.
|
|
35
|
+
*/
|
|
36
|
+
extras?: { descriptor: PropertyDescriptor; value: string | null }[];
|
|
37
|
+
/** With `adapter.applyOp`, the modal gains a confirmed Elimina action. */
|
|
38
|
+
removeOp?: EntityOp;
|
|
29
39
|
/** Accessible name for the modal, e.g. the link's current text. */
|
|
30
40
|
label?: string;
|
|
31
41
|
control: Snippet;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
let {
|
|
44
|
+
let {
|
|
45
|
+
text,
|
|
46
|
+
href,
|
|
47
|
+
extras = undefined,
|
|
48
|
+
removeOp = undefined,
|
|
49
|
+
label = undefined,
|
|
50
|
+
control
|
|
51
|
+
}: Props = $props();
|
|
35
52
|
|
|
36
53
|
const adapter = getEditAdapter();
|
|
37
54
|
const config = getUiConfig();
|
|
38
55
|
const messages = $derived(config.editMessages);
|
|
39
56
|
|
|
40
57
|
const editing = $derived(text.edit !== undefined && (adapter?.isEditing ?? false));
|
|
41
|
-
//
|
|
58
|
+
// Every field beyond the text needs both a place and a verb to save with.
|
|
42
59
|
const editsHref = $derived(href.descriptor !== undefined && adapter?.saveProperty !== undefined);
|
|
60
|
+
const editableExtras = $derived(adapter?.saveProperty !== undefined ? (extras ?? []) : []);
|
|
61
|
+
const removable = $derived(removeOp !== undefined && adapter?.applyOp !== undefined);
|
|
43
62
|
|
|
44
63
|
let open = $state(false);
|
|
45
64
|
let draftText = $state('');
|
|
46
65
|
let draftHref = $state('');
|
|
66
|
+
let draftExtras = $state<string[]>([]);
|
|
47
67
|
let saving = $state(false);
|
|
68
|
+
let confirmingRemove = $state(false);
|
|
69
|
+
let removing = $state(false);
|
|
48
70
|
let error = $state<string | null>(null);
|
|
49
71
|
|
|
50
72
|
function show(): void {
|
|
51
73
|
draftText = text.value;
|
|
52
74
|
draftHref = href.value;
|
|
75
|
+
draftExtras = editableExtras.map((extra) => extra.value ?? '');
|
|
53
76
|
error = null;
|
|
54
77
|
open = true;
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
/** Escape/Cancel·la/backdrop: discard the drafts, keep the stored values. */
|
|
58
81
|
function cancel(): void {
|
|
59
|
-
if (saving) return;
|
|
82
|
+
if (saving || removing) return;
|
|
60
83
|
open = false;
|
|
61
84
|
}
|
|
62
85
|
|
|
@@ -78,6 +101,12 @@
|
|
|
78
101
|
await adapter.saveProperty!(href.descriptor, nextHref);
|
|
79
102
|
draftHref = nextHref;
|
|
80
103
|
}
|
|
104
|
+
for (const [index, extra] of editableExtras.entries()) {
|
|
105
|
+
const next = draftExtras[index].trim();
|
|
106
|
+
if (next === (extra.value ?? '')) continue;
|
|
107
|
+
await adapter.saveProperty!(extra.descriptor, next);
|
|
108
|
+
draftExtras[index] = next;
|
|
109
|
+
}
|
|
81
110
|
open = false;
|
|
82
111
|
} catch (thrown) {
|
|
83
112
|
error =
|
|
@@ -86,6 +115,24 @@
|
|
|
86
115
|
saving = false;
|
|
87
116
|
}
|
|
88
117
|
}
|
|
118
|
+
|
|
119
|
+
/** Confirmed removal closes everything; a failure reports in THIS modal. */
|
|
120
|
+
async function remove(): Promise<void> {
|
|
121
|
+
if (!adapter?.applyOp || removeOp === undefined) return;
|
|
122
|
+
removing = true;
|
|
123
|
+
error = null;
|
|
124
|
+
try {
|
|
125
|
+
await adapter.applyOp(removeOp);
|
|
126
|
+
confirmingRemove = false;
|
|
127
|
+
open = false;
|
|
128
|
+
} catch (thrown) {
|
|
129
|
+
confirmingRemove = false;
|
|
130
|
+
error =
|
|
131
|
+
thrown instanceof Error && thrown.message ? thrown.message : messages.edit_saveError();
|
|
132
|
+
} finally {
|
|
133
|
+
removing = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
89
136
|
</script>
|
|
90
137
|
|
|
91
138
|
{#if editing}
|
|
@@ -117,6 +164,12 @@
|
|
|
117
164
|
<input type="text" bind:value={draftHref} disabled={saving} />
|
|
118
165
|
</label>
|
|
119
166
|
{/if}
|
|
167
|
+
{#each editableExtras as extra, index (index)}
|
|
168
|
+
<label>
|
|
169
|
+
<span>{extra.descriptor.label}</span>
|
|
170
|
+
<input type="text" bind:value={draftExtras[index]} disabled={saving} />
|
|
171
|
+
</label>
|
|
172
|
+
{/each}
|
|
120
173
|
</div>
|
|
121
174
|
{#if error}
|
|
122
175
|
<p class="error" role="alert">{error}</p>
|
|
@@ -124,8 +177,26 @@
|
|
|
124
177
|
<div class="actions">
|
|
125
178
|
<Button pending={saving ? 1 : 0} onclick={() => void save()}>{messages.edit_save()}</Button>
|
|
126
179
|
<Button variant="ghost" pending={null} onclick={cancel}>{messages.edit_cancel()}</Button>
|
|
180
|
+
{#if removable}
|
|
181
|
+
<span class="danger">
|
|
182
|
+
<Button variant="ghost" pending={null} onclick={() => (confirmingRemove = true)}>
|
|
183
|
+
{messages.edit_remove()}
|
|
184
|
+
</Button>
|
|
185
|
+
</span>
|
|
186
|
+
{/if}
|
|
127
187
|
</div>
|
|
128
188
|
</Modal>
|
|
189
|
+
{#if removable}
|
|
190
|
+
<ConfirmDialog
|
|
191
|
+
open={confirmingRemove}
|
|
192
|
+
title={messages.edit_remove()}
|
|
193
|
+
message={messages.edit_removeConfirm({ label: label ?? text.value })}
|
|
194
|
+
confirmLabel={messages.edit_remove()}
|
|
195
|
+
pending={removing}
|
|
196
|
+
onconfirm={() => void remove()}
|
|
197
|
+
oncancel={() => (confirmingRemove = false)}
|
|
198
|
+
/>
|
|
199
|
+
{/if}
|
|
129
200
|
{:else}
|
|
130
201
|
{@render control()}
|
|
131
202
|
{/if}
|
|
@@ -184,4 +255,12 @@
|
|
|
184
255
|
gap: var(--space-2);
|
|
185
256
|
margin-top: var(--space-4);
|
|
186
257
|
}
|
|
258
|
+
|
|
259
|
+
/* Elimina sits apart from the commit pair, wearing the error hue — the
|
|
260
|
+
ConfirmDialog token override, reused. */
|
|
261
|
+
.danger {
|
|
262
|
+
margin-left: auto;
|
|
263
|
+
--color-brand: var(--series-8);
|
|
264
|
+
--color-ink: var(--series-8);
|
|
265
|
+
}
|
|
187
266
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type { EditDescriptor, PropertyDescriptor } from '../types.js';
|
|
2
|
+
import type { EditDescriptor, EntityOp, PropertyDescriptor } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* A LINK edited whole: one modal for its text and its destination, instead
|
|
5
5
|
* of an inline label swap plus a separate property panel — a link's two
|
|
@@ -11,8 +11,10 @@ import type { EditDescriptor, PropertyDescriptor } from '../types.js';
|
|
|
11
11
|
* opens the modal. Desa commits ONLY the halves that changed — the text
|
|
12
12
|
* through `adapter.save`, the destination through `adapter.saveProperty` —
|
|
13
13
|
* so a failure on one keeps the other's success and the modal stays open
|
|
14
|
-
* with the reason.
|
|
15
|
-
*
|
|
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.
|
|
16
18
|
*/
|
|
17
19
|
interface Props {
|
|
18
20
|
/** Inline-label half; undefined leaves the control alone. */
|
|
@@ -25,6 +27,16 @@ interface Props {
|
|
|
25
27
|
descriptor: PropertyDescriptor | undefined;
|
|
26
28
|
value: string;
|
|
27
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.
|
|
33
|
+
*/
|
|
34
|
+
extras?: {
|
|
35
|
+
descriptor: PropertyDescriptor;
|
|
36
|
+
value: string | null;
|
|
37
|
+
}[];
|
|
38
|
+
/** With `adapter.applyOp`, the modal gains a confirmed Elimina action. */
|
|
39
|
+
removeOp?: EntityOp;
|
|
28
40
|
/** Accessible name for the modal, e.g. the link's current text. */
|
|
29
41
|
label?: string;
|
|
30
42
|
control: Snippet;
|