@vit-foundation/ui 0.25.0 → 0.27.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 (70) hide show
  1. package/README.md +2 -2
  2. package/dist/community.d.ts +12 -3
  3. package/dist/community.js +7 -3
  4. package/dist/components/pages/ProjectPage.svelte +3 -9
  5. package/dist/components/pages/ProjectPage.svelte.d.ts +3 -5
  6. package/dist/components/pages/WeeklyPage.svelte +7 -12
  7. package/dist/components/pages/WeeklyPage.svelte.d.ts +7 -9
  8. package/dist/components/ui/Icon.svelte +4 -0
  9. package/dist/components/ui/Icon.svelte.d.ts +1 -0
  10. package/dist/components/ui/Modal.svelte +32 -11
  11. package/dist/components/ui/Modal.svelte.d.ts +17 -1
  12. package/dist/components/ui/SelectField.svelte +207 -0
  13. package/dist/components/ui/SelectField.svelte.d.ts +76 -0
  14. package/dist/config/context.d.ts +0 -6
  15. package/dist/config/context.js +1 -1
  16. package/dist/config/index.d.ts +2 -2
  17. package/dist/config/index.js +1 -1
  18. package/dist/content-components.d.ts +6 -3
  19. package/dist/content-components.js +5 -1
  20. package/dist/contract.d.ts +8 -4
  21. package/dist/contract.js +7 -3
  22. package/dist/edit/Editable.svelte +17 -161
  23. package/dist/edit/Editable.svelte.d.ts +2 -50
  24. package/dist/edit/chrome/AddSlot.svelte +11 -78
  25. package/dist/edit/chrome/AddSlot.svelte.d.ts +2 -15
  26. package/dist/edit/chrome/EditFrame.svelte +15 -158
  27. package/dist/edit/chrome/EditFrame.svelte.d.ts +2 -31
  28. package/dist/edit/chrome/EditPanel.svelte +14 -22
  29. package/dist/edit/chrome/EditPanel.svelte.d.ts +2 -13
  30. package/dist/edit/chrome/LinkEdit.svelte +13 -277
  31. package/dist/edit/chrome/LinkEdit.svelte.d.ts +2 -48
  32. package/dist/edit/chrome-props.d.ts +96 -0
  33. package/dist/edit/chrome-props.js +1 -0
  34. package/dist/edit/context.d.ts +38 -5
  35. package/dist/edit/context.js +18 -7
  36. package/dist/edit/index.d.ts +6 -4
  37. package/dist/edit/index.js +4 -3
  38. package/dist/edit/live/AddSlot.svelte +83 -0
  39. package/dist/edit/live/AddSlot.svelte.d.ts +4 -0
  40. package/dist/edit/live/EditFrame.svelte +153 -0
  41. package/dist/edit/live/EditFrame.svelte.d.ts +4 -0
  42. package/dist/edit/live/EditPanel.svelte +25 -0
  43. package/dist/edit/live/EditPanel.svelte.d.ts +4 -0
  44. package/dist/edit/live/Editable.svelte +162 -0
  45. package/dist/edit/live/Editable.svelte.d.ts +4 -0
  46. package/dist/edit/live/LinkEdit.svelte +276 -0
  47. package/dist/edit/live/LinkEdit.svelte.d.ts +4 -0
  48. package/dist/edit/{chrome → live}/PropertyRow.svelte +1 -1
  49. package/dist/edit/{commit.svelte.d.ts → live/commit.svelte.d.ts} +2 -2
  50. package/dist/edit/live/index.d.ts +10 -0
  51. package/dist/edit/live/index.js +14 -0
  52. package/dist/forms/index.d.ts +1 -0
  53. package/dist/forms/index.js +1 -0
  54. package/dist/styles/base.css +1 -1
  55. package/dist/styles/tokens.css +4 -0
  56. package/package.json +1 -1
  57. package/dist/components/account/index.d.ts +0 -3
  58. package/dist/components/account/index.js +0 -2
  59. package/dist/components/auth/index.d.ts +0 -7
  60. package/dist/components/auth/index.js +0 -4
  61. package/dist/components/contact/index.d.ts +0 -2
  62. package/dist/components/contact/index.js +0 -1
  63. package/dist/content/index.d.ts +0 -6
  64. package/dist/content/index.js +0 -3
  65. /package/dist/edit/{chrome → live}/ConfirmDialog.svelte +0 -0
  66. /package/dist/edit/{chrome → live}/ConfirmDialog.svelte.d.ts +0 -0
  67. /package/dist/edit/{chrome → live}/EditPopover.svelte +0 -0
  68. /package/dist/edit/{chrome → live}/EditPopover.svelte.d.ts +0 -0
  69. /package/dist/edit/{chrome → live}/PropertyRow.svelte.d.ts +0 -0
  70. /package/dist/edit/{commit.svelte.js → live/commit.svelte.js} +0 -0
@@ -0,0 +1,276 @@
1
+ <script lang="ts">
2
+ import Button from '../../components/ui/Button.svelte';
3
+ import Icon from '../../components/ui/Icon.svelte';
4
+ import Modal from '../../components/ui/Modal.svelte';
5
+ import { getUiConfig } from '../../config/context.js';
6
+ import type { ParameterlessKey } from '../../config/types.js';
7
+ import { getEditAdapter } from '../context.js';
8
+ import { propertyValue } from './commit.svelte.js';
9
+ import type { LinkEditProps } from '../chrome-props.js';
10
+ import ConfirmDialog from './ConfirmDialog.svelte';
11
+
12
+ /**
13
+ * A LINK edited whole: one modal for its text and its destination, instead
14
+ * of an inline label swap plus a separate property panel — a link's two
15
+ * halves belong to one gesture.
16
+ *
17
+ * While the adapter is editing and a text descriptor is supplied, the
18
+ * control is REPLACED by a button wearing the label (the ActionLabel rule:
19
+ * a caret or a click inside a live link would navigate mid-edit) that
20
+ * opens the modal. Desa commits ONLY the halves that changed — the text
21
+ * through `adapter.save`, the destination through `adapter.saveProperty` —
22
+ * so a failure on one keeps the other's success and the modal stays open
23
+ * with the reason. Extra rows (a menu link's Ordre) and a confirmed
24
+ * Elimina ride in the same modal when the host supplies them. Without an
25
+ * adapter or a descriptor the control renders untouched, byte-identical to
26
+ * a read-only build.
27
+ */
28
+ let {
29
+ text,
30
+ href,
31
+ extras = undefined,
32
+ removeOp = undefined,
33
+ label = undefined,
34
+ control
35
+ }: LinkEditProps = $props();
36
+
37
+ const adapter = getEditAdapter();
38
+ const config = getUiConfig();
39
+ const messages = $derived(config.editMessages);
40
+
41
+ const editing = $derived(text.edit !== undefined && (adapter?.isEditing ?? false));
42
+ // Every field beyond the text needs both a place and a verb to save with.
43
+ const editsHref = $derived(href.descriptor !== undefined && adapter?.saveProperty !== undefined);
44
+ const editableExtras = $derived(
45
+ adapter?.saveProperty !== undefined
46
+ ? (extras ?? []).filter((extra) => extra.descriptor.type !== 'image')
47
+ : []
48
+ );
49
+
50
+ /** A flag state's wording, from the same catalog the site renders. */
51
+ const wording = (key: ParameterlessKey): string => config.messages[key]?.() ?? key;
52
+ const removable = $derived(removeOp !== undefined && adapter?.applyOp !== undefined);
53
+
54
+ let open = $state(false);
55
+ let draftText = $state('');
56
+ let draftHref = $state('');
57
+ let draftExtras = $state<string[]>([]);
58
+ let saving = $state(false);
59
+ let confirmingRemove = $state(false);
60
+ let removing = $state(false);
61
+ let error = $state<string | null>(null);
62
+
63
+ function show(): void {
64
+ draftText = text.value;
65
+ draftHref = href.value;
66
+ draftExtras = editableExtras.map((extra) => extra.value ?? '');
67
+ error = null;
68
+ open = true;
69
+ }
70
+
71
+ /** Escape/Cancel·la/backdrop: discard the drafts, keep the stored values. */
72
+ function cancel(): void {
73
+ if (saving || removing) return;
74
+ open = false;
75
+ }
76
+
77
+ async function save(): Promise<void> {
78
+ if (!adapter || text.edit === undefined) return;
79
+ const nextText = draftText.trim();
80
+ const nextHref = draftHref.trim();
81
+ saving = true;
82
+ error = null;
83
+ try {
84
+ // Sequential on purpose: if the text lands and the address fails, the
85
+ // reseeded draft no longer differs, so a retry commits only what is
86
+ // still pending.
87
+ if (nextText !== text.value) {
88
+ await adapter.save(text.edit, nextText);
89
+ draftText = nextText;
90
+ }
91
+ if (editsHref && href.descriptor && nextHref !== href.value) {
92
+ await adapter.saveProperty!(href.descriptor, nextHref);
93
+ draftHref = nextHref;
94
+ }
95
+ for (const [index, extra] of editableExtras.entries()) {
96
+ const next = draftExtras[index].trim();
97
+ if (next === (extra.value ?? '')) continue;
98
+ // A flag extra hands the adapter a BOOLEAN, like a panel row does.
99
+ await adapter.saveProperty!(extra.descriptor, propertyValue(extra.descriptor, next));
100
+ draftExtras[index] = next;
101
+ }
102
+ open = false;
103
+ } catch (thrown) {
104
+ error =
105
+ thrown instanceof Error && thrown.message ? thrown.message : messages.edit_saveError();
106
+ } finally {
107
+ saving = false;
108
+ }
109
+ }
110
+
111
+ /** Confirmed removal closes everything; a failure reports in THIS modal. */
112
+ async function remove(): Promise<void> {
113
+ if (!adapter?.applyOp || removeOp === undefined) return;
114
+ removing = true;
115
+ error = null;
116
+ try {
117
+ await adapter.applyOp(removeOp);
118
+ confirmingRemove = false;
119
+ open = false;
120
+ } catch (thrown) {
121
+ confirmingRemove = false;
122
+ error =
123
+ thrown instanceof Error && thrown.message ? thrown.message : messages.edit_saveError();
124
+ } finally {
125
+ removing = false;
126
+ }
127
+ }
128
+ </script>
129
+
130
+ {#if editing}
131
+ <button
132
+ type="button"
133
+ class="link-swap"
134
+ data-vit-editing="idle"
135
+ aria-label={messages.edit_editLink({ label: label ?? text.value })}
136
+ onclick={show}
137
+ >
138
+ {text.value}
139
+ <Icon name="pencil" size={13} />
140
+ </button>
141
+ <Modal
142
+ {open}
143
+ title={messages.edit_editLink({ label: label ?? text.value })}
144
+ onclose={cancel}
145
+ closeLabel={messages.edit_close()}
146
+ >
147
+ <div class="fields">
148
+ <label>
149
+ <span>{messages.edit_linkText()}</span>
150
+ <input type="text" bind:value={draftText} disabled={saving} />
151
+ </label>
152
+ {#if editsHref}
153
+ <label>
154
+ <span>{messages.edit_linkUrl()}</span>
155
+ <!-- type=text, not url: site destinations are relative paths. -->
156
+ <input type="text" bind:value={draftHref} disabled={saving} />
157
+ </label>
158
+ {/if}
159
+ {#each editableExtras as extra, index (index)}
160
+ <label>
161
+ <span>{extra.descriptor.label}</span>
162
+ {#if extra.descriptor.type === 'select'}
163
+ <select bind:value={draftExtras[index]} disabled={saving}>
164
+ {#each extra.descriptor.options ?? [] as option (option.value)}
165
+ <option value={option.value}>{option.label}</option>
166
+ {/each}
167
+ </select>
168
+ {:else if extra.descriptor.type === 'flag'}
169
+ <select bind:value={draftExtras[index]} disabled={saving}>
170
+ <option value="true">{wording(extra.descriptor.on ?? 'status_published')}</option>
171
+ <option value="false">{wording(extra.descriptor.off ?? 'status_draft')}</option>
172
+ </select>
173
+ {:else}
174
+ <input
175
+ type={extra.descriptor.type === 'date' ? 'date' : 'text'}
176
+ placeholder={extra.descriptor.placeholder}
177
+ bind:value={draftExtras[index]}
178
+ disabled={saving}
179
+ />
180
+ {/if}
181
+ </label>
182
+ {/each}
183
+ </div>
184
+ {#if error}
185
+ <p class="error" role="alert">{error}</p>
186
+ {/if}
187
+ <div class="actions">
188
+ <Button pending={saving ? 1 : 0} onclick={() => void save()}>{messages.edit_save()}</Button>
189
+ <Button variant="ghost" pending={null} onclick={cancel}>{messages.edit_cancel()}</Button>
190
+ {#if removable}
191
+ <span class="danger">
192
+ <Button variant="ghost" pending={null} onclick={() => (confirmingRemove = true)}>
193
+ {messages.edit_remove()}
194
+ </Button>
195
+ </span>
196
+ {/if}
197
+ </div>
198
+ </Modal>
199
+ {#if removable}
200
+ <ConfirmDialog
201
+ open={confirmingRemove}
202
+ title={messages.edit_remove()}
203
+ message={messages.edit_removeConfirm({ label: label ?? text.value })}
204
+ confirmLabel={messages.edit_remove()}
205
+ pending={removing}
206
+ onconfirm={() => void remove()}
207
+ oncancel={() => (confirmingRemove = false)}
208
+ />
209
+ {/if}
210
+ {:else}
211
+ {@render control()}
212
+ {/if}
213
+
214
+ <style>
215
+ /* The ActionLabel footprint plus a pencil hint: the same dashed affordance
216
+ (base.css paints [data-vit-editing]), but a click OPENS rather than
217
+ focuses — this swap is a button, never a caret. */
218
+ .link-swap {
219
+ display: inline-flex;
220
+ align-items: center;
221
+ gap: var(--space-1);
222
+ padding: 0.2em 0.4em;
223
+ border: none;
224
+ background: transparent;
225
+ font: inherit;
226
+ color: inherit;
227
+ cursor: pointer;
228
+ }
229
+
230
+ .fields {
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: var(--space-3);
234
+ }
235
+
236
+ label {
237
+ display: flex;
238
+ flex-direction: column;
239
+ gap: var(--space-1);
240
+ font-size: var(--text-sm);
241
+ font-weight: 600;
242
+ }
243
+
244
+ /* PropertyRow's control look — the modal is the panel, relocated. */
245
+ input {
246
+ font: inherit;
247
+ font-size: var(--text-sm);
248
+ font-weight: 400;
249
+ color: var(--color-ink);
250
+ padding: var(--space-1) var(--space-2);
251
+ border: 1px solid var(--color-hairline);
252
+ border-radius: var(--radius);
253
+ background: var(--color-surface);
254
+ width: 100%;
255
+ }
256
+
257
+ .error {
258
+ margin: var(--space-3) 0 0;
259
+ color: var(--series-8);
260
+ font-size: var(--text-sm);
261
+ }
262
+
263
+ .actions {
264
+ display: flex;
265
+ gap: var(--space-2);
266
+ margin-top: var(--space-4);
267
+ }
268
+
269
+ /* Elimina sits apart from the commit pair, wearing the error hue — the
270
+ ConfirmDialog token override, reused. */
271
+ .danger {
272
+ margin-left: auto;
273
+ --color-brand: var(--series-8);
274
+ --color-ink: var(--series-8);
275
+ }
276
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { LinkEditProps } from '../chrome-props.js';
2
+ declare const LinkEdit: import("svelte").Component<LinkEditProps, {}, "">;
3
+ type LinkEdit = ReturnType<typeof LinkEdit>;
4
+ export default LinkEdit;
@@ -2,7 +2,7 @@
2
2
  import { getUiConfig } from '../../config/context.js';
3
3
  import type { ParameterlessKey } from '../../config/types.js';
4
4
  import { getEditAdapter } from '../context.js';
5
- import { commitState, propertyValue } from '../commit.svelte.js';
5
+ import { commitState, propertyValue } from './commit.svelte.js';
6
6
  import type { PropertyDescriptor, PropertyValue } from '../types.js';
7
7
 
8
8
  /**
@@ -1,5 +1,5 @@
1
- import type { EditMessages } from '../config/edit-messages.js';
2
- import type { PropertyDescriptor, PropertyValue } from './types.js';
1
+ import type { EditMessages } from '../../config/edit-messages.js';
2
+ import type { PropertyDescriptor, PropertyValue } from '../types.js';
3
3
  /**
4
4
  * The commit lifecycle of ONE editable value — the state machine
5
5
  * `Editable` and `PropertyRow` both ran, in one place.
@@ -0,0 +1,10 @@
1
+ import type { EditChrome } from '../context.js';
2
+ /**
3
+ * The package's edit chrome, as the table a host hands `setEditAdapter`.
4
+ *
5
+ * This is the ONE module outside `edit/live/` allowed to import the live
6
+ * components (see `live-isolation.test.ts`). A host that edits imports it —
7
+ * brain, at its mirror layout — and a host that only renders never names it,
8
+ * which is what keeps the live half out of that host's bundle.
9
+ */
10
+ export declare const EDIT_CHROME: EditChrome;
@@ -0,0 +1,14 @@
1
+ import AddSlot from './AddSlot.svelte';
2
+ import Editable from './Editable.svelte';
3
+ import EditFrame from './EditFrame.svelte';
4
+ import EditPanel from './EditPanel.svelte';
5
+ import LinkEdit from './LinkEdit.svelte';
6
+ /**
7
+ * The package's edit chrome, as the table a host hands `setEditAdapter`.
8
+ *
9
+ * This is the ONE module outside `edit/live/` allowed to import the live
10
+ * components (see `live-isolation.test.ts`). A host that edits imports it —
11
+ * brain, at its mirror layout — and a host that only renders never names it,
12
+ * which is what keeps the live half out of that host's bundle.
13
+ */
14
+ export const EDIT_CHROME = { Editable, EditFrame, EditPanel, AddSlot, LinkEdit };
@@ -5,6 +5,7 @@ export { default as FormResultSlot } from '../components/ui/FormResultSlot.svelt
5
5
  export { default as Honeypot } from '../components/ui/Honeypot.svelte';
6
6
  export { default as LocaleField } from '../components/ui/LocaleField.svelte';
7
7
  export { default as NewsletterIntentField } from '../components/ui/NewsletterIntentField.svelte';
8
+ export { default as SelectField, type SelectOption } from '../components/ui/SelectField.svelte';
8
9
  export { default as TextField } from '../components/ui/TextField.svelte';
9
10
  export { COMMENT_BODY, CONTACT_MESSAGE, CONTACT_NAME, DISPLAY_NAME, EMAIL, LOGIN_PASSWORD, PASSWORD } from './constraints.js';
10
11
  export { hasNewsletterIntent, HONEYPOT_FIELD, isNewsletterIntent, NEWSLETTER_INTENT_PARAM, NEWSLETTER_INTENT_VALUE, withNewsletterIntent } from './transport.js';
@@ -5,6 +5,7 @@ export { default as FormResultSlot } from '../components/ui/FormResultSlot.svelt
5
5
  export { default as Honeypot } from '../components/ui/Honeypot.svelte';
6
6
  export { default as LocaleField } from '../components/ui/LocaleField.svelte';
7
7
  export { default as NewsletterIntentField } from '../components/ui/NewsletterIntentField.svelte';
8
+ export { default as SelectField } from '../components/ui/SelectField.svelte';
8
9
  export { default as TextField } from '../components/ui/TextField.svelte';
9
10
  export { COMMENT_BODY, CONTACT_MESSAGE, CONTACT_NAME, DISPLAY_NAME, EMAIL, LOGIN_PASSWORD, PASSWORD } from './constraints.js';
10
11
  export { hasNewsletterIntent, HONEYPOT_FIELD, isNewsletterIntent, NEWSLETTER_INTENT_PARAM, NEWSLETTER_INTENT_VALUE, withNewsletterIntent } from './transport.js';
@@ -40,7 +40,7 @@
40
40
  * Sizing (padding, widths) stays with the component that adds the class. */
41
41
  .control {
42
42
  border: 1px solid var(--color-axis);
43
- border-radius: var(--radius);
43
+ border-radius: var(--radius-control);
44
44
  background: var(--color-surface);
45
45
  }
46
46
 
@@ -50,6 +50,10 @@
50
50
 
51
51
  /* Shape & layout */
52
52
  --radius: 8px;
53
+ /* The corner of an input, a select, a textarea — the `.control` skin.
54
+ Separate from `--radius` because a theme that makes its buttons pills
55
+ (the admin app sets `--radius: 999px`) still wants a boxed field. */
56
+ --radius-control: 8px;
53
57
  --radius-lg: 16px;
54
58
  --content-max: 1200px;
55
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vit-foundation/ui",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -1,3 +0,0 @@
1
- export { default as AccountPanel } from './AccountPanel.svelte';
2
- export type { DeleteAccountFormInstance, LogoutFormInstance, NewsletterToggleFormInstance, UpdateNameFormInstance } from './AccountPanel.svelte';
3
- export { default as NewsletterSignup } from './NewsletterSignup.svelte';
@@ -1,2 +0,0 @@
1
- export { default as AccountPanel } from './AccountPanel.svelte';
2
- export { default as NewsletterSignup } from './NewsletterSignup.svelte';
@@ -1,7 +0,0 @@
1
- export { default as AuthPageShell } from './AuthPageShell.svelte';
2
- export { default as GoogleAuthForm } from './GoogleAuthForm.svelte';
3
- export type { GoogleLoginFormInstance } from './GoogleAuthForm.svelte';
4
- export { default as LoginForm } from './LoginForm.svelte';
5
- export type { LoginFormInstance, MagicLinkFormInstance } from './LoginForm.svelte';
6
- export { default as SignupForm } from './SignupForm.svelte';
7
- export type { SignupFormInstance } from './SignupForm.svelte';
@@ -1,4 +0,0 @@
1
- export { default as AuthPageShell } from './AuthPageShell.svelte';
2
- export { default as GoogleAuthForm } from './GoogleAuthForm.svelte';
3
- export { default as LoginForm } from './LoginForm.svelte';
4
- export { default as SignupForm } from './SignupForm.svelte';
@@ -1,2 +0,0 @@
1
- export { default as ContactForm } from './ContactForm.svelte';
2
- export type { ContactFormInstance } from './ContactForm.svelte';
@@ -1 +0,0 @@
1
- export { default as ContactForm } from './ContactForm.svelte';
@@ -1,6 +0,0 @@
1
- export { renderBody } from './richtext.js';
2
- export type { RichTextBlock } from './richtext.js';
3
- export { GET_INVOLVED_REASON_KEYS, PAGE_COPY_KEYS } from './pages.js';
4
- export type { CopyEditFor, CopyKey, PageCopy, PageId } from './pages.js';
5
- export { COMMENT_STATUSES, CONTACT_CATEGORIES, MILESTONE_CATEGORIES, PROJECT_KINDS, REACTIONS } from './types.js';
6
- export type { ArticleEdit, CollaboratorData, CommentData, CommentThreadData, CommentStatus, ContactCategory, FieldConstraint, FormFailReason, JobOpeningData, MilestoneCategory, MilestoneData, ProjectArticleData, ProjectCardData, ProjectKind, Reaction, ReactionSummary, ReactionTarget, SortDirection, TeamMemberData, ThemeData, WeeklyArticleData, WeeklyCardData, WeeklySourceData } from './types.js';
@@ -1,3 +0,0 @@
1
- export { renderBody } from './richtext.js';
2
- export { GET_INVOLVED_REASON_KEYS, PAGE_COPY_KEYS } from './pages.js';
3
- export { COMMENT_STATUSES, CONTACT_CATEGORIES, MILESTONE_CATEGORIES, PROJECT_KINDS, REACTIONS } from './types.js';
File without changes
File without changes