@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.
Files changed (46) hide show
  1. package/dist/components/contact/ContactForm.svelte +44 -14
  2. package/dist/components/jobs/JobList.svelte +73 -12
  3. package/dist/components/jobs/JobList.svelte.d.ts +19 -2
  4. package/dist/components/projects/ProjectCard.svelte +78 -33
  5. package/dist/components/projects/ProjectCard.svelte.d.ts +11 -2
  6. package/dist/components/team/CollaboratorList.svelte +87 -9
  7. package/dist/components/team/CollaboratorList.svelte.d.ts +26 -0
  8. package/dist/components/team/TeamMemberCard.svelte +41 -19
  9. package/dist/components/team/TeamMemberCard.svelte.d.ts +8 -4
  10. package/dist/components/timeline/Timeline.svelte +47 -2
  11. package/dist/components/timeline/Timeline.svelte.d.ts +8 -0
  12. package/dist/components/timeline/TimelineMilestone.svelte +94 -38
  13. package/dist/components/timeline/TimelineMilestone.svelte.d.ts +16 -2
  14. package/dist/components/ui/RichText.svelte +12 -6
  15. package/dist/components/ui/SearchInput.svelte +31 -10
  16. package/dist/components/ui/SearchInput.svelte.d.ts +7 -0
  17. package/dist/components/weeklies/SortSelect.svelte +41 -11
  18. package/dist/components/weeklies/SortSelect.svelte.d.ts +8 -0
  19. package/dist/components/weeklies/WeeklieCard.svelte +44 -19
  20. package/dist/components/weeklies/WeeklieCard.svelte.d.ts +9 -2
  21. package/dist/config/context.js +6 -1
  22. package/dist/config/edit-messages.d.ts +47 -0
  23. package/dist/config/edit-messages.js +24 -0
  24. package/dist/config/index.d.ts +2 -0
  25. package/dist/config/index.js +1 -0
  26. package/dist/config/types.d.ts +8 -0
  27. package/dist/content/types.d.ts +4 -0
  28. package/dist/edit/Editable.svelte +5 -3
  29. package/dist/edit/chrome/AddSlot.svelte +89 -0
  30. package/dist/edit/chrome/AddSlot.svelte.d.ts +17 -0
  31. package/dist/edit/chrome/ConfirmDialog.svelte +65 -0
  32. package/dist/edit/chrome/ConfirmDialog.svelte.d.ts +18 -0
  33. package/dist/edit/chrome/EditFrame.svelte +171 -0
  34. package/dist/edit/chrome/EditFrame.svelte.d.ts +33 -0
  35. package/dist/edit/chrome/EditPanel.svelte +29 -0
  36. package/dist/edit/chrome/EditPanel.svelte.d.ts +15 -0
  37. package/dist/edit/chrome/EditPopover.svelte +57 -0
  38. package/dist/edit/chrome/EditPopover.svelte.d.ts +17 -0
  39. package/dist/edit/chrome/PropertyRow.svelte +241 -0
  40. package/dist/edit/chrome/PropertyRow.svelte.d.ts +17 -0
  41. package/dist/edit/helpers.d.ts +19 -1
  42. package/dist/edit/helpers.js +21 -0
  43. package/dist/edit/index.d.ts +8 -2
  44. package/dist/edit/index.js +6 -1
  45. package/dist/edit/types.d.ts +82 -2
  46. 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;
@@ -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 {};
@@ -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
+ }
@@ -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 { chromeEdit, entityEdit, pageCopyEdit } from './helpers.js';
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';
@@ -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 { chromeEdit, entityEdit, pageCopyEdit } from './helpers.js';
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';
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vit-foundation/ui",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",