@vit-foundation/ui 0.10.1 → 0.12.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/contact/ContactForm.svelte +44 -14
- package/dist/components/jobs/JobList.svelte +73 -12
- package/dist/components/jobs/JobList.svelte.d.ts +19 -2
- package/dist/components/projects/ProjectCard.svelte +78 -33
- package/dist/components/projects/ProjectCard.svelte.d.ts +11 -2
- package/dist/components/team/CollaboratorList.svelte +87 -9
- package/dist/components/team/CollaboratorList.svelte.d.ts +26 -0
- package/dist/components/team/TeamMemberCard.svelte +41 -19
- package/dist/components/team/TeamMemberCard.svelte.d.ts +8 -4
- package/dist/components/timeline/Timeline.svelte +47 -2
- package/dist/components/timeline/Timeline.svelte.d.ts +8 -0
- package/dist/components/timeline/TimelineMilestone.svelte +94 -38
- package/dist/components/timeline/TimelineMilestone.svelte.d.ts +16 -2
- package/dist/components/ui/RichText.svelte +12 -6
- package/dist/components/ui/SearchInput.svelte +31 -10
- package/dist/components/ui/SearchInput.svelte.d.ts +7 -0
- package/dist/components/weeklies/SortSelect.svelte +41 -11
- package/dist/components/weeklies/SortSelect.svelte.d.ts +8 -0
- package/dist/components/weeklies/WeeklieCard.svelte +44 -19
- package/dist/components/weeklies/WeeklieCard.svelte.d.ts +9 -2
- package/dist/config/context.js +6 -1
- package/dist/config/edit-messages.d.ts +47 -0
- package/dist/config/edit-messages.js +24 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +1 -0
- package/dist/config/types.d.ts +8 -0
- package/dist/content/types.d.ts +4 -0
- package/dist/edit/Editable.svelte +5 -3
- package/dist/edit/chrome/AddSlot.svelte +89 -0
- package/dist/edit/chrome/AddSlot.svelte.d.ts +17 -0
- package/dist/edit/chrome/ConfirmDialog.svelte +65 -0
- package/dist/edit/chrome/ConfirmDialog.svelte.d.ts +18 -0
- package/dist/edit/chrome/EditFrame.svelte +171 -0
- package/dist/edit/chrome/EditFrame.svelte.d.ts +33 -0
- package/dist/edit/chrome/EditPanel.svelte +29 -0
- package/dist/edit/chrome/EditPanel.svelte.d.ts +15 -0
- package/dist/edit/chrome/EditPopover.svelte +57 -0
- package/dist/edit/chrome/EditPopover.svelte.d.ts +17 -0
- package/dist/edit/chrome/PropertyRow.svelte +241 -0
- package/dist/edit/chrome/PropertyRow.svelte.d.ts +17 -0
- package/dist/edit/helpers.d.ts +19 -1
- package/dist/edit/helpers.js +21 -0
- package/dist/edit/index.d.ts +8 -2
- package/dist/edit/index.js +6 -1
- package/dist/edit/types.d.ts +82 -2
- package/package.json +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getUiConfig } from '../../config/context.js';
|
|
3
|
+
import { getEditAdapter } from '../context.js';
|
|
4
|
+
import type { PropertyDescriptor } from '../types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One property of an EditPanel: label, a control by `descriptor.type`, and
|
|
8
|
+
* its own commit lifecycle — Editable's state machine, panel-shaped. Each
|
|
9
|
+
* row commits on change/blur independently; a failed save keeps the draft
|
|
10
|
+
* in the control so nothing typed is lost.
|
|
11
|
+
*
|
|
12
|
+
* The row assumes an adapter with `saveProperty` exists — EditPanel only
|
|
13
|
+
* renders when the frame's triple gate already established both.
|
|
14
|
+
*/
|
|
15
|
+
interface Props {
|
|
16
|
+
descriptor: PropertyDescriptor;
|
|
17
|
+
value: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { descriptor, value }: Props = $props();
|
|
21
|
+
|
|
22
|
+
const adapter = getEditAdapter();
|
|
23
|
+
const config = getUiConfig();
|
|
24
|
+
|
|
25
|
+
let status = $state<'idle' | 'dirty' | 'saving' | 'error'>('idle');
|
|
26
|
+
let announcement = $state('');
|
|
27
|
+
/** Last persisted value, what a failed draft is measured against. */
|
|
28
|
+
// svelte-ignore state_referenced_locally
|
|
29
|
+
let savedValue = $state(value ?? '');
|
|
30
|
+
// svelte-ignore state_referenced_locally
|
|
31
|
+
let draft = $state(value ?? '');
|
|
32
|
+
let fileInput: HTMLInputElement | undefined = $state();
|
|
33
|
+
|
|
34
|
+
// Follow the prop only while idle — never repaint a held draft (the
|
|
35
|
+
// Editable rule).
|
|
36
|
+
// svelte-ignore state_referenced_locally
|
|
37
|
+
let lastPropValue = $state(value ?? '');
|
|
38
|
+
$effect(() => {
|
|
39
|
+
const next = value ?? '';
|
|
40
|
+
if (next !== lastPropValue) {
|
|
41
|
+
lastPropValue = next;
|
|
42
|
+
if (status === 'idle') {
|
|
43
|
+
savedValue = next;
|
|
44
|
+
draft = next;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
async function persist(next: string | null): Promise<void> {
|
|
50
|
+
if (!adapter?.saveProperty) return;
|
|
51
|
+
status = 'saving';
|
|
52
|
+
announcement = config.editMessages.edit_saving();
|
|
53
|
+
try {
|
|
54
|
+
await adapter.saveProperty(descriptor, next);
|
|
55
|
+
savedValue = next ?? '';
|
|
56
|
+
draft = next ?? '';
|
|
57
|
+
status = 'idle';
|
|
58
|
+
announcement = config.editMessages.edit_saved();
|
|
59
|
+
} catch {
|
|
60
|
+
// Draft stays in the control; the editor decides whether to retry.
|
|
61
|
+
status = 'error';
|
|
62
|
+
announcement = config.editMessages.edit_saveError();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function commit(): void {
|
|
67
|
+
const trimmed = draft.trim();
|
|
68
|
+
if (trimmed === savedValue) {
|
|
69
|
+
status = 'idle';
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (trimmed === '') {
|
|
73
|
+
if (!descriptor.nullable) {
|
|
74
|
+
status = 'error';
|
|
75
|
+
announcement = config.editMessages.edit_emptyRequired();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
void persist(null);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
void persist(trimmed);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function markDirty(): void {
|
|
85
|
+
if (status !== 'saving') status = 'dirty';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function upload(event: Event): Promise<void> {
|
|
89
|
+
const file = (event.currentTarget as HTMLInputElement).files?.[0];
|
|
90
|
+
if (!file || !adapter?.uploadImage) return;
|
|
91
|
+
status = 'saving';
|
|
92
|
+
announcement = config.editMessages.edit_saving();
|
|
93
|
+
try {
|
|
94
|
+
const path = await adapter.uploadImage(descriptor, file);
|
|
95
|
+
await persist(path);
|
|
96
|
+
} catch {
|
|
97
|
+
status = 'error';
|
|
98
|
+
announcement = config.editMessages.edit_saveError();
|
|
99
|
+
} finally {
|
|
100
|
+
if (fileInput) fileInput.value = '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// One id per instance so the label reaches its control.
|
|
105
|
+
const id = $props.id();
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<div class="row">
|
|
109
|
+
<label for={id}>{descriptor.label}</label>
|
|
110
|
+
|
|
111
|
+
{#if descriptor.type === 'select'}
|
|
112
|
+
<select
|
|
113
|
+
{id}
|
|
114
|
+
data-vit-editing={status}
|
|
115
|
+
value={draft}
|
|
116
|
+
onchange={(event) => {
|
|
117
|
+
draft = event.currentTarget.value;
|
|
118
|
+
commit();
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{#each descriptor.options ?? [] as option (option.value)}
|
|
122
|
+
<option value={option.value}>{option.label}</option>
|
|
123
|
+
{/each}
|
|
124
|
+
</select>
|
|
125
|
+
{:else if descriptor.type === 'image'}
|
|
126
|
+
{#if savedValue}
|
|
127
|
+
<img class="thumb" src={savedValue} alt="" />
|
|
128
|
+
{/if}
|
|
129
|
+
<input
|
|
130
|
+
{id}
|
|
131
|
+
type="text"
|
|
132
|
+
data-vit-editing={status}
|
|
133
|
+
placeholder={descriptor.placeholder}
|
|
134
|
+
bind:value={draft}
|
|
135
|
+
oninput={markDirty}
|
|
136
|
+
onchange={commit}
|
|
137
|
+
/>
|
|
138
|
+
{#if adapter?.uploadImage}
|
|
139
|
+
<input
|
|
140
|
+
bind:this={fileInput}
|
|
141
|
+
class="file"
|
|
142
|
+
type="file"
|
|
143
|
+
accept="image/*"
|
|
144
|
+
onchange={(event) => void upload(event)}
|
|
145
|
+
aria-label={config.editMessages.edit_uploadImage()}
|
|
146
|
+
/>
|
|
147
|
+
<button type="button" class="aux" onclick={() => fileInput?.click()}>
|
|
148
|
+
{config.editMessages.edit_uploadImage()}
|
|
149
|
+
</button>
|
|
150
|
+
{/if}
|
|
151
|
+
{:else}
|
|
152
|
+
<input
|
|
153
|
+
{id}
|
|
154
|
+
type={descriptor.type === 'date' ? 'date' : descriptor.type === 'url' ? 'url' : 'text'}
|
|
155
|
+
data-vit-editing={status}
|
|
156
|
+
placeholder={descriptor.placeholder}
|
|
157
|
+
bind:value={draft}
|
|
158
|
+
oninput={markDirty}
|
|
159
|
+
onchange={commit}
|
|
160
|
+
/>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
{#if descriptor.nullable && savedValue !== ''}
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
class="aux"
|
|
167
|
+
onclick={() => {
|
|
168
|
+
draft = '';
|
|
169
|
+
void persist(null);
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{config.editMessages.edit_clearValue()}
|
|
173
|
+
</button>
|
|
174
|
+
{/if}
|
|
175
|
+
|
|
176
|
+
<span class="status" role="status">{announcement}</span>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<style>
|
|
180
|
+
.row {
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
gap: var(--space-1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
label {
|
|
187
|
+
font-size: var(--text-sm);
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
input:not(.file),
|
|
192
|
+
select {
|
|
193
|
+
font: inherit;
|
|
194
|
+
font-size: var(--text-sm);
|
|
195
|
+
padding: var(--space-1) var(--space-2);
|
|
196
|
+
border: 1px solid var(--color-hairline);
|
|
197
|
+
border-radius: var(--radius);
|
|
198
|
+
background: var(--color-surface);
|
|
199
|
+
color: var(--color-ink);
|
|
200
|
+
width: 100%;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.thumb {
|
|
204
|
+
max-width: 100%;
|
|
205
|
+
max-height: 6rem;
|
|
206
|
+
object-fit: cover;
|
|
207
|
+
border-radius: var(--radius);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* The real picker; the styled button drives it. */
|
|
211
|
+
.file {
|
|
212
|
+
position: absolute;
|
|
213
|
+
width: 1px;
|
|
214
|
+
height: 1px;
|
|
215
|
+
opacity: 0;
|
|
216
|
+
pointer-events: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.aux {
|
|
220
|
+
align-self: flex-start;
|
|
221
|
+
border: none;
|
|
222
|
+
background: none;
|
|
223
|
+
padding: 0;
|
|
224
|
+
font-size: var(--text-sm);
|
|
225
|
+
color: var(--color-brand);
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
text-decoration: underline;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.status {
|
|
231
|
+
position: absolute;
|
|
232
|
+
width: 1px;
|
|
233
|
+
height: 1px;
|
|
234
|
+
padding: 0;
|
|
235
|
+
margin: -1px;
|
|
236
|
+
overflow: hidden;
|
|
237
|
+
clip-path: inset(50%);
|
|
238
|
+
white-space: nowrap;
|
|
239
|
+
border: 0;
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PropertyDescriptor } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* One property of an EditPanel: label, a control by `descriptor.type`, and
|
|
4
|
+
* its own commit lifecycle — Editable's state machine, panel-shaped. Each
|
|
5
|
+
* row commits on change/blur independently; a failed save keeps the draft
|
|
6
|
+
* in the control so nothing typed is lost.
|
|
7
|
+
*
|
|
8
|
+
* The row assumes an adapter with `saveProperty` exists — EditPanel only
|
|
9
|
+
* renders when the frame's triple gate already established both.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
descriptor: PropertyDescriptor;
|
|
13
|
+
value: string | null;
|
|
14
|
+
}
|
|
15
|
+
declare const PropertyRow: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type PropertyRow = ReturnType<typeof PropertyRow>;
|
|
17
|
+
export default PropertyRow;
|
package/dist/edit/helpers.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Locale } from '../config/types.js';
|
|
2
|
-
import type { EditableEntity, EditDescriptor } from './types.js';
|
|
2
|
+
import type { CollectionRef, EditableEntity, EditDescriptor, PropertyDescriptor } from './types.js';
|
|
3
|
+
/** Everything a property descriptor carries beyond its ref. */
|
|
4
|
+
type PropertySpec = Omit<PropertyDescriptor, 'ref'>;
|
|
3
5
|
/**
|
|
4
6
|
* Descriptor for one interface-wording message (a Paraglide catalog key).
|
|
5
7
|
* Only parameterless messages are sensibly editable in place — editing the
|
|
@@ -25,3 +27,19 @@ export declare function entityEdit(entity: EditableEntity, id: string | number,
|
|
|
25
27
|
format?: EditDescriptor['format'];
|
|
26
28
|
label?: string;
|
|
27
29
|
}) => EditDescriptor;
|
|
30
|
+
/**
|
|
31
|
+
* Property-descriptor factory for one entity's scalar fields — the panel
|
|
32
|
+
* sibling of `entityEdit`, naming the row once:
|
|
33
|
+
*
|
|
34
|
+
* const property = entityProperty('milestones', milestone.id);
|
|
35
|
+
* occurredOn: property('occurred_on', { type: 'date', label: 'Data' })
|
|
36
|
+
*/
|
|
37
|
+
export declare function entityProperty(entity: EditableEntity, id: string | number): (field: string, spec: PropertySpec) => PropertyDescriptor;
|
|
38
|
+
/**
|
|
39
|
+
* A panel property over one interface-wording message — how strings that can
|
|
40
|
+
* never hold a caret (an `<option>` label, an input placeholder) still edit.
|
|
41
|
+
*/
|
|
42
|
+
export declare function chromeProperty(key: string, spec: PropertySpec): PropertyDescriptor;
|
|
43
|
+
/** Names one entity collection at one render site. */
|
|
44
|
+
export declare function collectionOf(entity: EditableEntity, scope?: string): CollectionRef;
|
|
45
|
+
export {};
|
package/dist/edit/helpers.js
CHANGED
|
@@ -20,3 +20,24 @@ export function pageCopyEdit(page, sectionKey, locale, options) {
|
|
|
20
20
|
export function entityEdit(entity, id, locale) {
|
|
21
21
|
return (field, options) => ({ ref: { kind: 'entity', entity, id, field }, locale, ...options });
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Property-descriptor factory for one entity's scalar fields — the panel
|
|
25
|
+
* sibling of `entityEdit`, naming the row once:
|
|
26
|
+
*
|
|
27
|
+
* const property = entityProperty('milestones', milestone.id);
|
|
28
|
+
* occurredOn: property('occurred_on', { type: 'date', label: 'Data' })
|
|
29
|
+
*/
|
|
30
|
+
export function entityProperty(entity, id) {
|
|
31
|
+
return (field, spec) => ({ ref: { kind: 'entity', entity, id, field }, ...spec });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A panel property over one interface-wording message — how strings that can
|
|
35
|
+
* never hold a caret (an `<option>` label, an input placeholder) still edit.
|
|
36
|
+
*/
|
|
37
|
+
export function chromeProperty(key, spec) {
|
|
38
|
+
return { ref: { kind: 'chrome', key }, ...spec };
|
|
39
|
+
}
|
|
40
|
+
/** Names one entity collection at one render site. */
|
|
41
|
+
export function collectionOf(entity, scope) {
|
|
42
|
+
return scope === undefined ? { entity } : { entity, scope };
|
|
43
|
+
}
|
package/dist/edit/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export { getEditAdapter, setEditAdapter } from './context.js';
|
|
2
2
|
export { default as ActionLabel } from './ActionLabel.svelte';
|
|
3
3
|
export { default as Editable } from './Editable.svelte';
|
|
4
|
-
export {
|
|
4
|
+
export { default as AddSlot } from './chrome/AddSlot.svelte';
|
|
5
|
+
export { default as ConfirmDialog } from './chrome/ConfirmDialog.svelte';
|
|
6
|
+
export { default as EditFrame } from './chrome/EditFrame.svelte';
|
|
7
|
+
export type { EditFrameSpec } from './chrome/EditFrame.svelte';
|
|
8
|
+
export { default as EditPanel } from './chrome/EditPanel.svelte';
|
|
9
|
+
export { default as EditPopover } from './chrome/EditPopover.svelte';
|
|
10
|
+
export { chromeEdit, chromeProperty, collectionOf, entityEdit, entityProperty, pageCopyEdit } from './helpers.js';
|
|
5
11
|
export { localize } from './types.js';
|
|
6
|
-
export type { ContentRef, EditableEntity, EditAdapter, EditDescriptor, LocalizedText } from './types.js';
|
|
12
|
+
export type { CollectionRef, ContentRef, EditableEntity, EditAdapter, EditDescriptor, EntityOp, LocalizedText, PropertyDescriptor, PropertyOption, PropertyType } from './types.js';
|
package/dist/edit/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export { getEditAdapter, setEditAdapter } from './context.js';
|
|
2
2
|
export { default as ActionLabel } from './ActionLabel.svelte';
|
|
3
3
|
export { default as Editable } from './Editable.svelte';
|
|
4
|
-
export {
|
|
4
|
+
export { default as AddSlot } from './chrome/AddSlot.svelte';
|
|
5
|
+
export { default as ConfirmDialog } from './chrome/ConfirmDialog.svelte';
|
|
6
|
+
export { default as EditFrame } from './chrome/EditFrame.svelte';
|
|
7
|
+
export { default as EditPanel } from './chrome/EditPanel.svelte';
|
|
8
|
+
export { default as EditPopover } from './chrome/EditPopover.svelte';
|
|
9
|
+
export { chromeEdit, chromeProperty, collectionOf, entityEdit, entityProperty, pageCopyEdit } from './helpers.js';
|
|
5
10
|
export { localize } from './types.js';
|
package/dist/edit/types.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function localize(value: LocalizedText, locale: Locale): string;
|
|
|
15
15
|
* The entities whose fields the components can mark editable. Names match
|
|
16
16
|
* the content tables so an adapter can route a save without a mapping layer.
|
|
17
17
|
*/
|
|
18
|
-
export type EditableEntity = 'weeklies' | 'projects' | 'milestones' | 'team_members' | 'job_openings' | 'themes';
|
|
18
|
+
export type EditableEntity = 'weeklies' | 'projects' | 'milestones' | 'team_members' | 'job_openings' | 'themes' | 'collaborators';
|
|
19
19
|
/**
|
|
20
20
|
* The identity of one editable string in the content model: a page copy
|
|
21
21
|
* block (`page_content` row), one localized field of a domain entity, or one
|
|
@@ -51,9 +51,75 @@ export interface EditDescriptor {
|
|
|
51
51
|
/** Accessible name for the edit control, e.g. "Títol del weekly #12". */
|
|
52
52
|
label?: string;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The value kinds a property panel can edit. Everything in this content model
|
|
56
|
+
* serializes to a string on the wire — an ISO date, a URL, an enum member, a
|
|
57
|
+
* storage path — so there is no typed value union: the ADAPTER parses, and
|
|
58
|
+
* the host's schemas stay the sole validators.
|
|
59
|
+
*/
|
|
60
|
+
export type PropertyType = 'text' | 'url' | 'date' | 'select' | 'image';
|
|
61
|
+
/** One choice of a `select` property. */
|
|
62
|
+
export interface PropertyOption {
|
|
63
|
+
value: string;
|
|
64
|
+
label: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Identity + shape of one panel-edited property. Unlike `EditDescriptor`,
|
|
68
|
+
* `locale` is optional: dates, urls, enum members and image paths are not
|
|
69
|
+
* localized, and its absence says so to the adapter.
|
|
70
|
+
*/
|
|
71
|
+
export interface PropertyDescriptor {
|
|
72
|
+
ref: ContentRef;
|
|
73
|
+
/** Present only for localized text properties. */
|
|
74
|
+
locale?: Locale;
|
|
75
|
+
type: PropertyType;
|
|
76
|
+
/** Panel row label — required, the panel renders it. */
|
|
77
|
+
label: string;
|
|
78
|
+
/** Required when `type` is 'select'. */
|
|
79
|
+
options?: readonly PropertyOption[];
|
|
80
|
+
placeholder?: string;
|
|
81
|
+
/** Clearing the field saves null (linkUrl, externalUrl). */
|
|
82
|
+
nullable?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Names one entity collection at one render site. `scope` disambiguates when
|
|
86
|
+
* the same entity renders in several lists (a board grid vs. a full roster).
|
|
87
|
+
*/
|
|
88
|
+
export interface CollectionRef {
|
|
89
|
+
entity: EditableEntity;
|
|
90
|
+
scope?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A structural collection operation. Deliberately WITHOUT an `update` verb:
|
|
94
|
+
* field values always travel through `save`/`saveProperty` — one save path
|
|
95
|
+
* per value — and ops stay purely about existence and order.
|
|
96
|
+
*/
|
|
97
|
+
export type EntityOp = {
|
|
98
|
+
kind: 'create';
|
|
99
|
+
collection: CollectionRef;
|
|
100
|
+
anchor?: {
|
|
101
|
+
id: string | number;
|
|
102
|
+
placement: 'before' | 'after';
|
|
103
|
+
};
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'remove';
|
|
106
|
+
collection: CollectionRef;
|
|
107
|
+
id: string | number;
|
|
108
|
+
} | {
|
|
109
|
+
kind: 'reorder';
|
|
110
|
+
collection: CollectionRef;
|
|
111
|
+
id: string | number;
|
|
112
|
+
anchor: {
|
|
113
|
+
id: string | number;
|
|
114
|
+
placement: 'before' | 'after';
|
|
115
|
+
};
|
|
116
|
+
};
|
|
54
117
|
/**
|
|
55
118
|
* What a consuming app plugs in to make the components writable. The package
|
|
56
|
-
* never talks to a database: `save` is the whole persistence contract
|
|
119
|
+
* never talks to a database: `save` is the whole persistence contract for
|
|
120
|
+
* inline text, and the three OPTIONAL members below are the property-panel
|
|
121
|
+
* and structural halves — leave them off and the components render no panel
|
|
122
|
+
* or collection affordance at all, byte-identical to a read-only build.
|
|
57
123
|
*
|
|
58
124
|
* Implementations must:
|
|
59
125
|
* - back `isEditing` with reactive state (`$state`) — it gates every edit
|
|
@@ -68,4 +134,18 @@ export interface EditDescriptor {
|
|
|
68
134
|
export interface EditAdapter {
|
|
69
135
|
readonly isEditing: boolean;
|
|
70
136
|
save(descriptor: EditDescriptor, value: string): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Panel property save. The value stays a string on the wire (ISO date,
|
|
139
|
+
* url, enum member, image path); `null` clears a `nullable` property.
|
|
140
|
+
*/
|
|
141
|
+
saveProperty?(descriptor: PropertyDescriptor, value: string | null): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Structural collection ops. `create` may resolve the new row's id so the
|
|
144
|
+
* UI can point at it after the host's refresh.
|
|
145
|
+
*/
|
|
146
|
+
applyOp?(op: EntityOp): Promise<{
|
|
147
|
+
id?: string | number;
|
|
148
|
+
} | void>;
|
|
149
|
+
/** Image upload; resolves to the stored path an `image` property saves. */
|
|
150
|
+
uploadImage?(descriptor: PropertyDescriptor, file: File): Promise<string>;
|
|
71
151
|
}
|