@vit-foundation/ui 0.14.0 → 0.16.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 +10 -10
- package/dist/components/jobs/JobList.svelte +11 -19
- package/dist/components/jobs/JobList.svelte.d.ts +1 -1
- package/dist/components/layout/Footer.svelte +21 -42
- package/dist/components/layout/Footer.svelte.d.ts +1 -1
- package/dist/components/layout/Nav.svelte +29 -46
- package/dist/components/layout/Nav.svelte.d.ts +4 -3
- package/dist/components/layout/site-link-edit.d.ts +5 -4
- package/dist/components/projects/ProjectCard.svelte +2 -2
- package/dist/components/projects/ProjectCard.svelte.d.ts +1 -1
- package/dist/components/team/CollaboratorList.svelte +10 -16
- package/dist/components/timeline/Timeline.svelte +10 -23
- package/dist/components/timeline/TimelineMilestone.svelte +2 -2
- package/dist/components/timeline/TimelineMilestone.svelte.d.ts +1 -1
- package/dist/components/weeklies/WeeklieCard.svelte +2 -2
- package/dist/components/weeklies/WeeklieCard.svelte.d.ts +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/messages.js +5 -1
- package/dist/config/types.d.ts +35 -3
- package/dist/content-components.d.ts +5 -1
- package/dist/content-components.js +4 -1
- package/dist/edit/chrome/EditPanel.svelte +2 -2
- package/dist/edit/chrome/EditPanel.svelte.d.ts +2 -2
- package/dist/edit/chrome/LinkEdit.svelte +85 -6
- package/dist/edit/chrome/LinkEdit.svelte.d.ts +15 -3
- package/dist/edit/chrome/PropertyRow.svelte +40 -9
- package/dist/edit/chrome/PropertyRow.svelte.d.ts +6 -2
- package/dist/edit/collection.svelte.d.ts +49 -0
- package/dist/edit/collection.svelte.js +39 -0
- package/dist/edit/helpers.d.ts +5 -5
- package/dist/edit/helpers.js +2 -2
- package/dist/edit/index.d.ts +4 -1
- package/dist/edit/index.js +1 -0
- package/dist/edit/types.d.ts +24 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/utils/milestones.d.ts +15 -0
- package/dist/utils/milestones.js +17 -0
- package/dist/utils/paths.d.ts +6 -0
- package/dist/utils/paths.js +14 -0
- package/dist/utils/url-filters.svelte.d.ts +58 -0
- package/dist/utils/url-filters.svelte.js +47 -0
- package/dist/utils/weekly-list.svelte.d.ts +82 -0
- package/dist/utils/weekly-list.svelte.js +120 -0
- package/package.json +1 -1
package/dist/config/types.d.ts
CHANGED
|
@@ -142,7 +142,39 @@ export interface UiMessages {
|
|
|
142
142
|
category_education(): string;
|
|
143
143
|
category_collaboration(): string;
|
|
144
144
|
category_press(): string;
|
|
145
|
+
/**
|
|
146
|
+
* Editorial state, as the panel's `flag` rows word it: a card's
|
|
147
|
+
* published/draft pair, an opening's open/closed pair. Catalog keys rather
|
|
148
|
+
* than editor strings because the CMS edits them per locale like any other
|
|
149
|
+
* wording — and required, like every key but the *Href pair, because
|
|
150
|
+
* `messages` is all-or-nothing (see ./messages.js).
|
|
151
|
+
*/
|
|
152
|
+
status_published(): string;
|
|
153
|
+
status_draft(): string;
|
|
154
|
+
status_open(): string;
|
|
155
|
+
status_closed(): string;
|
|
145
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* The catalog keys whose message takes NO parameters — the only ones edit
|
|
159
|
+
* mode may offer as text, inline or in a panel: editing the RENDERED text of
|
|
160
|
+
* a parameterized message (`pagination_status`, `weeklie_number`) would
|
|
161
|
+
* overwrite its template with one interpolation. Derived from the message
|
|
162
|
+
* signatures, so the rule is checked where a key is named; the optional
|
|
163
|
+
* *Href pair qualifies like any other zero-argument message.
|
|
164
|
+
*/
|
|
165
|
+
export type ParameterlessKey = {
|
|
166
|
+
[K in keyof UiMessages]-?: NonNullable<UiMessages[K]> extends () => string ? K : never;
|
|
167
|
+
}[keyof UiMessages];
|
|
168
|
+
/** The complement: keys whose message interpolates something. */
|
|
169
|
+
export type ParameterizedKey = Exclude<keyof UiMessages, ParameterlessKey>;
|
|
170
|
+
/**
|
|
171
|
+
* A key a chrome PROPERTY may name: any string but one of the package's own
|
|
172
|
+
* parameterized keys. Wider than `ParameterlessKey` on purpose — the site's
|
|
173
|
+
* catalog has wording the components never render themselves (a search
|
|
174
|
+
* placeholder is the host's), and that key the host vouches for; the
|
|
175
|
+
* package refuses only what it can check.
|
|
176
|
+
*/
|
|
177
|
+
export type NotParameterized<K extends string> = K extends ParameterizedKey ? never : K;
|
|
146
178
|
/**
|
|
147
179
|
* Everything the components read from their host app, resolved. Apps provide
|
|
148
180
|
* a partial (`UiConfigInput`) through UiProvider; components read this
|
|
@@ -179,10 +211,10 @@ export interface UiConfig {
|
|
|
179
211
|
* Edit descriptor for one of THIS config's message strings, by catalog
|
|
180
212
|
* key — the interface-wording half of edit mode. Optional and undefined by
|
|
181
213
|
* default, so a read-only app (and every story/test) renders the messages
|
|
182
|
-
* as plain text. A CMS-shaped host supplies it alongside an EditAdapter
|
|
183
|
-
*
|
|
214
|
+
* as plain text. A CMS-shaped host supplies it alongside an EditAdapter.
|
|
215
|
+
* The key type is the rule: only a `ParameterlessKey` may edit as text.
|
|
184
216
|
*/
|
|
185
|
-
messageEdit?: (key:
|
|
217
|
+
messageEdit?: (key: ParameterlessKey) => EditDescriptor | undefined;
|
|
186
218
|
}
|
|
187
219
|
/** What an app hands UiProvider: any subset; the rest keeps package defaults. */
|
|
188
220
|
export interface UiConfigInput {
|
|
@@ -17,6 +17,10 @@ export type { MilestoneEditMap } from './components/timeline/TimelineMilestone.s
|
|
|
17
17
|
export { default as WeeklieCard } from './components/weeklies/WeeklieCard.svelte';
|
|
18
18
|
export type { WeeklyEditMap } from './components/weeklies/WeeklieCard.svelte';
|
|
19
19
|
export * from './content/index.js';
|
|
20
|
-
export { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from './utils/milestones.js';
|
|
20
|
+
export { MILESTONE_CATEGORY_COLOR, matchesMilestoneFilter, milestoneCategoryLabel } from './utils/milestones.js';
|
|
21
|
+
export { createUrlFilters } from './utils/url-filters.svelte.js';
|
|
22
|
+
export type { UrlFilters, UrlFiltersConfig } from './utils/url-filters.svelte.js';
|
|
23
|
+
export { createWeeklyList, WEEKLY_LIST_DEFAULTS } from './utils/weekly-list.svelte.js';
|
|
24
|
+
export type { WeeklyList, WeeklyListConfig, WeeklyListFilters, WeeklyListPage, WeeklyListServerData } from './utils/weekly-list.svelte.js';
|
|
21
25
|
export { contactCategoryLabel } from './utils/contact.js';
|
|
22
26
|
export { formatDate, yearOf } from './utils/dates.js';
|
|
@@ -12,6 +12,9 @@ export { default as Timeline } from './components/timeline/Timeline.svelte';
|
|
|
12
12
|
export { default as TimelineMilestone } from './components/timeline/TimelineMilestone.svelte';
|
|
13
13
|
export { default as WeeklieCard } from './components/weeklies/WeeklieCard.svelte';
|
|
14
14
|
export * from './content/index.js';
|
|
15
|
-
export { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from './utils/milestones.js';
|
|
15
|
+
export { MILESTONE_CATEGORY_COLOR, matchesMilestoneFilter, milestoneCategoryLabel } from './utils/milestones.js';
|
|
16
|
+
// The two list rules: URL-mirrored filters, and the weeklies index over them.
|
|
17
|
+
export { createUrlFilters } from './utils/url-filters.svelte.js';
|
|
18
|
+
export { createWeeklyList, WEEKLY_LIST_DEFAULTS } from './utils/weekly-list.svelte.js';
|
|
16
19
|
export { contactCategoryLabel } from './utils/contact.js';
|
|
17
20
|
export { formatDate, yearOf } from './utils/dates.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { PropertyDescriptor } from '../types.js';
|
|
2
|
+
import type { PropertyDescriptor, PropertyValue } from '../types.js';
|
|
3
3
|
import PropertyRow from './PropertyRow.svelte';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* knows nothing about the entity — the descriptors say everything.
|
|
9
9
|
*/
|
|
10
10
|
interface Props {
|
|
11
|
-
rows: { descriptor: PropertyDescriptor; value:
|
|
11
|
+
rows: { descriptor: PropertyDescriptor; value: PropertyValue }[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
let { rows }: Props = $props();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PropertyDescriptor } from '../types.js';
|
|
1
|
+
import type { PropertyDescriptor, PropertyValue } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* The property form an EditFrame's popover holds: one PropertyRow per
|
|
4
4
|
* entry, each with its own independent commit lifecycle. The component
|
|
@@ -7,7 +7,7 @@ import type { PropertyDescriptor } from '../types.js';
|
|
|
7
7
|
interface Props {
|
|
8
8
|
rows: {
|
|
9
9
|
descriptor: PropertyDescriptor;
|
|
10
|
-
value:
|
|
10
|
+
value: PropertyValue;
|
|
11
11
|
}[];
|
|
12
12
|
}
|
|
13
13
|
declare const EditPanel: import("svelte").Component<Props, {}, "">;
|
|
@@ -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;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getUiConfig } from '../../config/context.js';
|
|
3
|
+
import type { ParameterlessKey } from '../../config/types.js';
|
|
3
4
|
import { getEditAdapter } from '../context.js';
|
|
4
|
-
import type { PropertyDescriptor } from '../types.js';
|
|
5
|
+
import type { PropertyDescriptor, PropertyValue } from '../types.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* One property of an EditPanel: label, a control by `descriptor.type`, and
|
|
@@ -11,10 +12,14 @@
|
|
|
11
12
|
*
|
|
12
13
|
* The row assumes an adapter with `saveProperty` exists — EditPanel only
|
|
13
14
|
* renders when the frame's triple gate already established both.
|
|
15
|
+
*
|
|
16
|
+
* The control state is text throughout — a flag's boolean rides as
|
|
17
|
+
* 'true'/'false' in its <select> and becomes a boolean again only at the
|
|
18
|
+
* adapter boundary, so one draft/saved pair serves every type.
|
|
14
19
|
*/
|
|
15
20
|
interface Props {
|
|
16
21
|
descriptor: PropertyDescriptor;
|
|
17
|
-
value:
|
|
22
|
+
value: PropertyValue;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
let { descriptor, value }: Props = $props();
|
|
@@ -22,21 +27,28 @@
|
|
|
22
27
|
const adapter = getEditAdapter();
|
|
23
28
|
const config = getUiConfig();
|
|
24
29
|
|
|
30
|
+
/** The control's reading of a property value. */
|
|
31
|
+
const asText = (next: PropertyValue): string =>
|
|
32
|
+
typeof next === 'boolean' ? String(next) : (next ?? '');
|
|
33
|
+
|
|
34
|
+
/** A flag state's wording, from the same catalog the site renders. */
|
|
35
|
+
const wording = (key: ParameterlessKey): string => config.messages[key]?.() ?? key;
|
|
36
|
+
|
|
25
37
|
let status = $state<'idle' | 'dirty' | 'saving' | 'error'>('idle');
|
|
26
38
|
let announcement = $state('');
|
|
27
39
|
/** Last persisted value, what a failed draft is measured against. */
|
|
28
40
|
// svelte-ignore state_referenced_locally
|
|
29
|
-
let savedValue = $state(value
|
|
41
|
+
let savedValue = $state(asText(value));
|
|
30
42
|
// svelte-ignore state_referenced_locally
|
|
31
|
-
let draft = $state(value
|
|
43
|
+
let draft = $state(asText(value));
|
|
32
44
|
let fileInput: HTMLInputElement | undefined = $state();
|
|
33
45
|
|
|
34
46
|
// Follow the prop only while idle — never repaint a held draft (the
|
|
35
47
|
// Editable rule).
|
|
36
48
|
// svelte-ignore state_referenced_locally
|
|
37
|
-
let lastPropValue = $state(value
|
|
49
|
+
let lastPropValue = $state(asText(value));
|
|
38
50
|
$effect(() => {
|
|
39
|
-
const next = value
|
|
51
|
+
const next = asText(value);
|
|
40
52
|
if (next !== lastPropValue) {
|
|
41
53
|
lastPropValue = next;
|
|
42
54
|
if (status === 'idle') {
|
|
@@ -46,14 +58,14 @@
|
|
|
46
58
|
}
|
|
47
59
|
});
|
|
48
60
|
|
|
49
|
-
async function persist(next:
|
|
61
|
+
async function persist(next: PropertyValue): Promise<void> {
|
|
50
62
|
if (!adapter?.saveProperty) return;
|
|
51
63
|
status = 'saving';
|
|
52
64
|
announcement = config.editMessages.edit_saving();
|
|
53
65
|
try {
|
|
54
66
|
await adapter.saveProperty(descriptor, next);
|
|
55
|
-
savedValue = next
|
|
56
|
-
draft = next
|
|
67
|
+
savedValue = asText(next);
|
|
68
|
+
draft = asText(next);
|
|
57
69
|
status = 'idle';
|
|
58
70
|
announcement = config.editMessages.edit_saved();
|
|
59
71
|
} catch {
|
|
@@ -69,6 +81,10 @@
|
|
|
69
81
|
status = 'idle';
|
|
70
82
|
return;
|
|
71
83
|
}
|
|
84
|
+
if (descriptor.type === 'flag') {
|
|
85
|
+
void persist(trimmed === 'true');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
72
88
|
if (trimmed === '') {
|
|
73
89
|
if (!descriptor.nullable) {
|
|
74
90
|
status = 'error';
|
|
@@ -122,6 +138,21 @@
|
|
|
122
138
|
<option value={option.value}>{option.label}</option>
|
|
123
139
|
{/each}
|
|
124
140
|
</select>
|
|
141
|
+
{:else if descriptor.type === 'flag'}
|
|
142
|
+
<!-- Two states, worded from the catalog: the host names the pair
|
|
143
|
+
(published/draft, open/closed), the adapter receives a boolean. -->
|
|
144
|
+
<select
|
|
145
|
+
{id}
|
|
146
|
+
data-vit-editing={status}
|
|
147
|
+
value={draft}
|
|
148
|
+
onchange={(event) => {
|
|
149
|
+
draft = event.currentTarget.value;
|
|
150
|
+
commit();
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<option value="true">{wording(descriptor.on ?? 'status_published')}</option>
|
|
154
|
+
<option value="false">{wording(descriptor.off ?? 'status_draft')}</option>
|
|
155
|
+
</select>
|
|
125
156
|
{:else if descriptor.type === 'image'}
|
|
126
157
|
{#if savedValue}
|
|
127
158
|
<img class="thumb" src={savedValue} alt="" />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PropertyDescriptor } from '../types.js';
|
|
1
|
+
import type { PropertyDescriptor, PropertyValue } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* One property of an EditPanel: label, a control by `descriptor.type`, and
|
|
4
4
|
* its own commit lifecycle — Editable's state machine, panel-shaped. Each
|
|
@@ -7,10 +7,14 @@ import type { PropertyDescriptor } from '../types.js';
|
|
|
7
7
|
*
|
|
8
8
|
* The row assumes an adapter with `saveProperty` exists — EditPanel only
|
|
9
9
|
* renders when the frame's triple gate already established both.
|
|
10
|
+
*
|
|
11
|
+
* The control state is text throughout — a flag's boolean rides as
|
|
12
|
+
* 'true'/'false' in its <select> and becomes a boolean again only at the
|
|
13
|
+
* adapter boundary, so one draft/saved pair serves every type.
|
|
10
14
|
*/
|
|
11
15
|
interface Props {
|
|
12
16
|
descriptor: PropertyDescriptor;
|
|
13
|
-
value:
|
|
17
|
+
value: PropertyValue;
|
|
14
18
|
}
|
|
15
19
|
declare const PropertyRow: import("svelte").Component<Props, {}, "">;
|
|
16
20
|
type PropertyRow = ReturnType<typeof PropertyRow>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CollectionRef, EntityOp } from './types.js';
|
|
2
|
+
type CreateOp = Extract<EntityOp, {
|
|
3
|
+
kind: 'create';
|
|
4
|
+
}>;
|
|
5
|
+
type RemoveOp = Extract<EntityOp, {
|
|
6
|
+
kind: 'remove';
|
|
7
|
+
}>;
|
|
8
|
+
/** An edit map a list may inject removal into. */
|
|
9
|
+
export interface RemovableMap {
|
|
10
|
+
/** Set by the list from its `collection` — removal of this row. */
|
|
11
|
+
removeOp?: RemoveOp;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The structural half of one editable LIST, as its template reads it.
|
|
15
|
+
* Both members answer `undefined` while the collection is not live, so the
|
|
16
|
+
* template's `{#if}` is the whole gate.
|
|
17
|
+
*/
|
|
18
|
+
export interface CollectionEditing<Row, Map extends RemovableMap> {
|
|
19
|
+
/** The create op for the add slot at the end of the list. */
|
|
20
|
+
readonly add: CreateOp | undefined;
|
|
21
|
+
/** The create op for an add slot anchored before one row (Timeline). */
|
|
22
|
+
addBefore(id: string | number): CreateOp | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The host's map for one row, plus the remove op the LIST injects — lists
|
|
25
|
+
* own identity, so a host never spells a removal. Rows without an `id`
|
|
26
|
+
* keep the host's map untouched: a remove op needs an identity, and
|
|
27
|
+
* read-only hosts never pass one.
|
|
28
|
+
*/
|
|
29
|
+
mapFor(row: Row): Map | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* What every list with a `collection` prop used to repeat — Timeline,
|
|
33
|
+
* CollaboratorList, JobList, Nav and Footer each carried the triple gate
|
|
34
|
+
* (collection named ∧ adapter editing ∧ `applyOp` present) and the same
|
|
35
|
+
* remove-op injection, five times verbatim. One helper, one place to keep
|
|
36
|
+
* the rule: a structural affordance renders only when all three hold.
|
|
37
|
+
*
|
|
38
|
+
* `read` is a thunk over the list's props so both stay reactive; the
|
|
39
|
+
* adapter comes from context, so call this during component init like
|
|
40
|
+
* `getEditAdapter` itself.
|
|
41
|
+
*/
|
|
42
|
+
export declare function collectionEditing<Row extends {
|
|
43
|
+
id?: string | number;
|
|
44
|
+
}, Map extends RemovableMap>(read: () => {
|
|
45
|
+
collection: CollectionRef | undefined;
|
|
46
|
+
/** The host's per-row map (`editFor`, or Nav/Footer's `propertiesFor`). */
|
|
47
|
+
editFor: ((row: Row) => Map | undefined) | undefined;
|
|
48
|
+
}): CollectionEditing<Row, Map>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getEditAdapter } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* What every list with a `collection` prop used to repeat — Timeline,
|
|
4
|
+
* CollaboratorList, JobList, Nav and Footer each carried the triple gate
|
|
5
|
+
* (collection named ∧ adapter editing ∧ `applyOp` present) and the same
|
|
6
|
+
* remove-op injection, five times verbatim. One helper, one place to keep
|
|
7
|
+
* the rule: a structural affordance renders only when all three hold.
|
|
8
|
+
*
|
|
9
|
+
* `read` is a thunk over the list's props so both stay reactive; the
|
|
10
|
+
* adapter comes from context, so call this during component init like
|
|
11
|
+
* `getEditAdapter` itself.
|
|
12
|
+
*/
|
|
13
|
+
export function collectionEditing(read) {
|
|
14
|
+
const adapter = getEditAdapter();
|
|
15
|
+
const live = $derived.by(() => {
|
|
16
|
+
const { collection } = read();
|
|
17
|
+
return collection !== undefined &&
|
|
18
|
+
(adapter?.isEditing ?? false) &&
|
|
19
|
+
adapter?.applyOp !== undefined
|
|
20
|
+
? collection
|
|
21
|
+
: undefined;
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
get add() {
|
|
25
|
+
return live ? { kind: 'create', collection: live } : undefined;
|
|
26
|
+
},
|
|
27
|
+
addBefore(id) {
|
|
28
|
+
return live
|
|
29
|
+
? { kind: 'create', collection: live, anchor: { id, placement: 'before' } }
|
|
30
|
+
: undefined;
|
|
31
|
+
},
|
|
32
|
+
mapFor(row) {
|
|
33
|
+
const map = read().editFor?.(row);
|
|
34
|
+
if (!live || row.id === undefined)
|
|
35
|
+
return map;
|
|
36
|
+
return { ...map, removeOp: { kind: 'remove', collection: live, id: row.id } };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/edit/helpers.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type { Locale } from '../config/types.js';
|
|
1
|
+
import type { Locale, NotParameterized, ParameterlessKey } from '../config/types.js';
|
|
2
2
|
import type { CollectionRef, EditableEntity, EditDescriptor, PropertyDescriptor } from './types.js';
|
|
3
3
|
/** Everything a property descriptor carries beyond its ref. */
|
|
4
4
|
type PropertySpec = Omit<PropertyDescriptor, 'ref'>;
|
|
5
5
|
/**
|
|
6
6
|
* Descriptor for one interface-wording message (a Paraglide catalog key).
|
|
7
|
-
*
|
|
8
|
-
* RENDERED text of a parameterized one would overwrite its template.
|
|
7
|
+
* The key type carries the rule — see `ParameterlessKey` in ../config/types.js.
|
|
9
8
|
*/
|
|
10
|
-
export declare function chromeEdit(key:
|
|
9
|
+
export declare function chromeEdit(key: ParameterlessKey, locale: Locale, options?: {
|
|
11
10
|
format?: EditDescriptor['format'];
|
|
12
11
|
label?: string;
|
|
13
12
|
}): EditDescriptor;
|
|
@@ -38,8 +37,9 @@ export declare function entityProperty(entity: EditableEntity, id: string | numb
|
|
|
38
37
|
/**
|
|
39
38
|
* A panel property over one interface-wording message — how strings that can
|
|
40
39
|
* never hold a caret (an `<option>` label, an input placeholder) still edit.
|
|
40
|
+
* Accepts the site's own keys too; see `NotParameterized` in ../config/types.js.
|
|
41
41
|
*/
|
|
42
|
-
export declare function chromeProperty(key:
|
|
42
|
+
export declare function chromeProperty<K extends string>(key: NotParameterized<K>, spec: PropertySpec): PropertyDescriptor;
|
|
43
43
|
/** Names one entity collection at one render site. */
|
|
44
44
|
export declare function collectionOf(entity: EditableEntity, scope?: string): CollectionRef;
|
|
45
45
|
export {};
|
package/dist/edit/helpers.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Descriptor for one interface-wording message (a Paraglide catalog key).
|
|
3
|
-
*
|
|
4
|
-
* RENDERED text of a parameterized one would overwrite its template.
|
|
3
|
+
* The key type carries the rule — see `ParameterlessKey` in ../config/types.js.
|
|
5
4
|
*/
|
|
6
5
|
export function chromeEdit(key, locale, options) {
|
|
7
6
|
return { ref: { kind: 'chrome', key }, locale, ...options };
|
|
@@ -33,6 +32,7 @@ export function entityProperty(entity, id) {
|
|
|
33
32
|
/**
|
|
34
33
|
* A panel property over one interface-wording message — how strings that can
|
|
35
34
|
* never hold a caret (an `<option>` label, an input placeholder) still edit.
|
|
35
|
+
* Accepts the site's own keys too; see `NotParameterized` in ../config/types.js.
|
|
36
36
|
*/
|
|
37
37
|
export function chromeProperty(key, spec) {
|
|
38
38
|
return { ref: { kind: 'chrome', key }, ...spec };
|
package/dist/edit/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { getEditAdapter, setEditAdapter } from './context.js';
|
|
2
|
+
export { collectionEditing } from './collection.svelte.js';
|
|
3
|
+
export type { CollectionEditing, RemovableMap } from './collection.svelte.js';
|
|
2
4
|
export { default as ActionLabel } from './ActionLabel.svelte';
|
|
3
5
|
export { default as Editable } from './Editable.svelte';
|
|
4
6
|
export { default as AddSlot } from './chrome/AddSlot.svelte';
|
|
@@ -10,4 +12,5 @@ export { default as EditPopover } from './chrome/EditPopover.svelte';
|
|
|
10
12
|
export { default as LinkEdit } from './chrome/LinkEdit.svelte';
|
|
11
13
|
export { chromeEdit, chromeProperty, collectionOf, entityEdit, entityProperty, pageCopyEdit } from './helpers.js';
|
|
12
14
|
export { localize } from './types.js';
|
|
13
|
-
export type { CollectionRef, ContentRef, EditableEntity, EditAdapter, EditDescriptor, EntityOp, LocalizedText, PropertyDescriptor, PropertyOption, PropertyType } from './types.js';
|
|
15
|
+
export type { CollectionRef, ContentRef, EditableEntity, EditAdapter, EditDescriptor, EntityOp, LocalizedText, PropertyDescriptor, PropertyOption, PropertyType, PropertyValue } from './types.js';
|
|
16
|
+
export type { NotParameterized, ParameterlessKey } from '../config/types.js';
|
package/dist/edit/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { getEditAdapter, setEditAdapter } from './context.js';
|
|
2
|
+
export { collectionEditing } from './collection.svelte.js';
|
|
2
3
|
export { default as ActionLabel } from './ActionLabel.svelte';
|
|
3
4
|
export { default as Editable } from './Editable.svelte';
|
|
4
5
|
export { default as AddSlot } from './chrome/AddSlot.svelte';
|