@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
package/README.md CHANGED
@@ -25,11 +25,11 @@ Everything exports flat from the root, and again grouped by role:
25
25
  | Entry point | What lives there |
26
26
  | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
27
  | [`/primitives`](./docs/components/primitives.md) | Button, GhostButton, Link, CardMedia, CardTitle, CopyIntro, DateText, DecorShapes, FilterChips, Pagination, RichText, SearchInput, ShareRow |
28
- | [`/forms`](./docs/forms.md) | Field, FormFeedback, FormErrorFeedback, FormResultSlot, Honeypot, LocaleField, NewsletterIntentField — plus the remote-form seam: structural types, transport constants, field bounds |
28
+ | [`/forms`](./docs/forms.md) | Field, SelectField, TextField, FormFeedback, FormErrorFeedback, FormResultSlot, Honeypot, LocaleField, NewsletterIntentField — plus the remote-form seam: structural types, transport constants, field bounds |
29
29
  | [`/chrome`](./docs/components/chrome.md) | PageShell, Nav, Footer |
30
30
  | [`/content`](./docs/components/content.md) | WeeklieCard, ProjectCard, Timeline, TimelineMilestone, TeamMemberCard, CollaboratorList, JobList, SortSelect, the nine page modules (HomePage … WeeklyPage) — plus the data shapes, the page-copy vocabulary, helpers and the two list rules (createWeeklyList, createUrlFilters) |
31
31
  | [`/community`](./docs/components/community.md) | AuthPageShell, LoginForm, SignupForm, GoogleAuthForm, AccountPanel, NewsletterSignup, CommentSection, ReactionBar, ContactForm |
32
- | [`/edit`](./docs/edit-mode.md) | Editable, setEditAdapter/getEditAdapter, descriptors and helpers, collectionEditing, LocalizedText |
32
+ | [`/edit`](./docs/edit-mode.md) | Editable, setEditAdapter(adapter, EDIT_CHROME)/getEditAdapter, descriptors and helpers, collectionEditing, LocalizedText |
33
33
  | [`/config`](./docs/getting-started.md#wiring-an-app-uiprovider) | UiProvider, UiConfig, the locale set, the default Catalan messages |
34
34
  | `/contract` | The component-free half: LOCALES, BASE_LOCALE, localize, REACTIONS, PAGE_COPY_KEYS and the edit-descriptor types — the one subpath a host may import from SERVER code |
35
35
  | `/testing` | createRemoteFormMock — the remote-form stand-in for stories and tests |
@@ -3,9 +3,18 @@
3
3
  * and contact. Every one takes its remote form(s) as required props — the
4
4
  * host app owns schemas and preflight; see ./forms for the seam's types.
5
5
  */
6
- export * from './components/account/index.js';
7
- export * from './components/auth/index.js';
8
- export * from './components/contact/index.js';
6
+ export { default as AccountPanel } from './components/account/AccountPanel.svelte';
7
+ export type { DeleteAccountFormInstance, LogoutFormInstance, NewsletterToggleFormInstance, UpdateNameFormInstance } from './components/account/AccountPanel.svelte';
8
+ export { default as NewsletterSignup } from './components/account/NewsletterSignup.svelte';
9
+ export { default as AuthPageShell } from './components/auth/AuthPageShell.svelte';
10
+ export { default as GoogleAuthForm } from './components/auth/GoogleAuthForm.svelte';
11
+ export type { GoogleLoginFormInstance } from './components/auth/GoogleAuthForm.svelte';
12
+ export { default as LoginForm } from './components/auth/LoginForm.svelte';
13
+ export type { LoginFormInstance, MagicLinkFormInstance } from './components/auth/LoginForm.svelte';
14
+ export { default as SignupForm } from './components/auth/SignupForm.svelte';
15
+ export type { SignupFormInstance } from './components/auth/SignupForm.svelte';
16
+ export { default as ContactForm } from './components/contact/ContactForm.svelte';
17
+ export type { ContactFormInstance } from './components/contact/ContactForm.svelte';
9
18
  export { default as CommentSection } from './components/weeklies/CommentSection.svelte';
10
19
  export type { CommentFormInstance } from './components/weeklies/CommentSection.svelte';
11
20
  export { default as ReactionBar } from './components/weeklies/ReactionBar.svelte';
package/dist/community.js CHANGED
@@ -3,8 +3,12 @@
3
3
  * and contact. Every one takes its remote form(s) as required props — the
4
4
  * host app owns schemas and preflight; see ./forms for the seam's types.
5
5
  */
6
- export * from './components/account/index.js';
7
- export * from './components/auth/index.js';
8
- export * from './components/contact/index.js';
6
+ export { default as AccountPanel } from './components/account/AccountPanel.svelte';
7
+ export { default as NewsletterSignup } from './components/account/NewsletterSignup.svelte';
8
+ export { default as AuthPageShell } from './components/auth/AuthPageShell.svelte';
9
+ export { default as GoogleAuthForm } from './components/auth/GoogleAuthForm.svelte';
10
+ export { default as LoginForm } from './components/auth/LoginForm.svelte';
11
+ export { default as SignupForm } from './components/auth/SignupForm.svelte';
12
+ export { default as ContactForm } from './components/contact/ContactForm.svelte';
9
13
  export { default as CommentSection } from './components/weeklies/CommentSection.svelte';
10
14
  export { default as ReactionBar } from './components/weeklies/ReactionBar.svelte';
@@ -1,13 +1,6 @@
1
- <script module lang="ts">
2
- import type { ArticleEdit } from '../../content/types.js';
3
-
4
- /** What a CMS may open on a project's own page: title, excerpt, and the body as rich text. */
5
- export type ProjectPageEdit = ArticleEdit;
6
- </script>
7
-
8
1
  <script lang="ts">
9
2
  import { getUiConfig } from '../../config/context.js';
10
- import type { ProjectArticleData } from '../../content/types.js';
3
+ import type { ArticleEdit, ProjectArticleData } from '../../content/types.js';
11
4
  import ActionLabel from '../../edit/ActionLabel.svelte';
12
5
  import Editable from '../../edit/Editable.svelte';
13
6
  import PageShell from '../layout/PageShell.svelte';
@@ -24,7 +17,8 @@
24
17
  */
25
18
  interface Props {
26
19
  project: ProjectArticleData;
27
- edit?: ProjectPageEdit;
20
+ /** What a CMS may open on a project's own page: title, excerpt, and the body as rich text. */
21
+ edit?: ArticleEdit;
28
22
  }
29
23
 
30
24
  let { project, edit }: Props = $props();
@@ -1,7 +1,4 @@
1
- import type { ArticleEdit } from '../../content/types.js';
2
- /** What a CMS may open on a project's own page: title, excerpt, and the body as rich text. */
3
- export type ProjectPageEdit = ArticleEdit;
4
- import type { ProjectArticleData } from '../../content/types.js';
1
+ import type { ArticleEdit, ProjectArticleData } from '../../content/types.js';
5
2
  /**
6
3
  * A project's story: date, title, excerpt, hero image, the rich-text body
7
4
  * with a share row, and — when the project points somewhere — a preview
@@ -10,7 +7,8 @@ import type { ProjectArticleData } from '../../content/types.js';
10
7
  */
11
8
  interface Props {
12
9
  project: ProjectArticleData;
13
- edit?: ProjectPageEdit;
10
+ /** What a CMS may open on a project's own page: title, excerpt, and the body as rich text. */
11
+ edit?: ArticleEdit;
14
12
  }
15
13
  declare const ProjectPage: import("svelte").Component<Props, {}, "">;
16
14
  type ProjectPage = ReturnType<typeof ProjectPage>;
@@ -1,17 +1,7 @@
1
- <script module lang="ts">
2
- import type { ArticleEdit } from '../../content/types.js';
3
-
4
- /**
5
- * What a CMS may open on a weekly's own page: title, excerpt, and the body
6
- * as rich text. The «Dades i fonts» and «Segueix explorant» headings and
7
- * the back link edit through `config.messageEdit` over their own keys.
8
- */
9
- export type WeeklyPageEdit = ArticleEdit;
10
- </script>
11
-
12
1
  <script lang="ts">
13
2
  import { getUiConfig } from '../../config/context.js';
14
3
  import type {
4
+ ArticleEdit,
15
5
  CommentThreadData,
16
6
  ReactionSummary,
17
7
  WeeklyArticleData,
@@ -48,7 +38,12 @@
48
38
  /** One preflighted instance per reply box; see CommentSection. */
49
39
  replyFormFor: (threadId: string) => CommentFormInstance;
50
40
  reactionForms: ReactionBarForms;
51
- edit?: WeeklyPageEdit;
41
+ /**
42
+ * What a CMS may open on a weekly's own page: title, excerpt, and the body
43
+ * as rich text. The «Dades i fonts» and «Segueix explorant» headings and
44
+ * the back link edit through `config.messageEdit` over their own keys.
45
+ */
46
+ edit?: ArticleEdit;
52
47
  }
53
48
 
54
49
  let {
@@ -1,11 +1,4 @@
1
- import type { ArticleEdit } from '../../content/types.js';
2
- /**
3
- * What a CMS may open on a weekly's own page: title, excerpt, and the body
4
- * as rich text. The «Dades i fonts» and «Segueix explorant» headings and
5
- * the back link edit through `config.messageEdit` over their own keys.
6
- */
7
- export type WeeklyPageEdit = ArticleEdit;
8
- import type { CommentThreadData, ReactionSummary, WeeklyArticleData, WeeklyCardData } from '../../content/types.js';
1
+ import type { ArticleEdit, CommentThreadData, ReactionSummary, WeeklyArticleData, WeeklyCardData } from '../../content/types.js';
9
2
  import { type CommentFormInstance } from '../weeklies/CommentSection.svelte';
10
3
  import { type ReactionBarForms } from '../weeklies/ReactionBar.svelte';
11
4
  /**
@@ -28,7 +21,12 @@ interface Props {
28
21
  /** One preflighted instance per reply box; see CommentSection. */
29
22
  replyFormFor: (threadId: string) => CommentFormInstance;
30
23
  reactionForms: ReactionBarForms;
31
- edit?: WeeklyPageEdit;
24
+ /**
25
+ * What a CMS may open on a weekly's own page: title, excerpt, and the body
26
+ * as rich text. The «Dades i fonts» and «Segueix explorant» headings and
27
+ * the back link edit through `config.messageEdit` over their own keys.
28
+ */
29
+ edit?: ArticleEdit;
32
30
  }
33
31
  declare const WeeklyPage: import("svelte").Component<Props, {}, "">;
34
32
  type WeeklyPage = ReturnType<typeof WeeklyPage>;
@@ -10,6 +10,10 @@
10
10
  'M5 7h14M10 7V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2m3 0v12a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V7m3 4v6m4-6v6',
11
11
  duplicate: 'M9 9h10v12H9zM5 15V3h10',
12
12
  plus: 'M12 5v14M5 12h14',
13
+ /* A month sheet: body, its two hanging tabs, and the rule under the
14
+ weekday row. One subpath list, like `trash` and `logout`. */
15
+ calendar:
16
+ 'M5 6h14a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zM4 10.5h16M8.5 3.5V6M15.5 3.5V6',
13
17
  'chevron-right': 'M9 5l7 7-7 7',
14
18
  'chevron-left': 'M15 5l-7 7 7 7',
15
19
  close: 'M6 6l12 12M18 6L6 18',
@@ -8,6 +8,7 @@ declare const PATHS: {
8
8
  readonly trash: "M5 7h14M10 7V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2m3 0v12a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V7m3 4v6m4-6v6";
9
9
  readonly duplicate: "M9 9h10v12H9zM5 15V3h10";
10
10
  readonly plus: "M12 5v14M5 12h14";
11
+ readonly calendar: "M5 6h14a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zM4 10.5h16M8.5 3.5V6M15.5 3.5V6";
11
12
  readonly 'chevron-right': "M9 5l7 7-7 7";
12
13
  readonly 'chevron-left': "M15 5l-7 7 7 7";
13
14
  readonly close: "M6 6l12 12M18 6L6 18";
@@ -1,7 +1,24 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
2
+ import { onMount, type Snippet } from 'svelte';
3
3
  import IconButton from './IconButton.svelte';
4
4
 
5
+ /**
6
+ * A modal over the native <dialog>: showModal() traps focus and layers the
7
+ * backdrop, the cancel event covers Escape, a backdrop click closes.
8
+ *
9
+ * The element is NOT part of the server-rendered document. A <dialog> is
10
+ * flow content, so wherever a caller mounts one inside phrasing content —
11
+ * a LinkEdit inside a <p>, which NewsletterSignup does — the HTML parser
12
+ * closes the paragraph early, Svelte reports `node_invalid_placement_ssr`
13
+ * on the way out and a hydration mismatch on the way back in, on every
14
+ * page that renders the caller. No dialog is open at first paint (opening
15
+ * one takes a click), so the server has nothing to say about it; the
16
+ * element is created after mount, by script, which the content model does
17
+ * not police. Placement is this module's to own — a caller cannot know
18
+ * what its parent is — and owning it here fixes every caller at once,
19
+ * ConfirmDialog included.
20
+ */
21
+
5
22
  interface Props {
6
23
  open: boolean;
7
24
  title: string;
@@ -16,9 +33,11 @@
16
33
  let { open, title, onclose, closeLabel = 'Tanca', children }: Props = $props();
17
34
 
18
35
  let dialog: HTMLDialogElement | undefined = $state();
36
+ let mounted = $state(false);
37
+ onMount(() => {
38
+ mounted = true;
39
+ });
19
40
 
20
- // Native <dialog>: showModal() traps focus and layers the backdrop; the
21
- // cancel event covers Escape.
22
41
  $effect(() => {
23
42
  if (!dialog) return;
24
43
  if (open && !dialog.open) dialog.showModal();
@@ -32,15 +51,17 @@
32
51
  }
33
52
  </script>
34
53
 
35
- <dialog bind:this={dialog} oncancel={onclose} {onclick}>
36
- <div class="panel">
37
- <div class="top">
38
- <h2>{title}</h2>
39
- <IconButton icon="close" label={closeLabel} onclick={onclose} />
54
+ {#if mounted}
55
+ <dialog bind:this={dialog} oncancel={onclose} {onclick}>
56
+ <div class="panel">
57
+ <div class="top">
58
+ <h2>{title}</h2>
59
+ <IconButton icon="close" label={closeLabel} onclick={onclose} />
60
+ </div>
61
+ {@render children()}
40
62
  </div>
41
- {@render children()}
42
- </div>
43
- </dialog>
63
+ </dialog>
64
+ {/if}
44
65
 
45
66
  <style>
46
67
  dialog {
@@ -1,4 +1,20 @@
1
- import type { Snippet } from 'svelte';
1
+ import { type Snippet } from 'svelte';
2
+ /**
3
+ * A modal over the native <dialog>: showModal() traps focus and layers the
4
+ * backdrop, the cancel event covers Escape, a backdrop click closes.
5
+ *
6
+ * The element is NOT part of the server-rendered document. A <dialog> is
7
+ * flow content, so wherever a caller mounts one inside phrasing content —
8
+ * a LinkEdit inside a <p>, which NewsletterSignup does — the HTML parser
9
+ * closes the paragraph early, Svelte reports `node_invalid_placement_ssr`
10
+ * on the way out and a hydration mismatch on the way back in, on every
11
+ * page that renders the caller. No dialog is open at first paint (opening
12
+ * one takes a click), so the server has nothing to say about it; the
13
+ * element is created after mount, by script, which the content model does
14
+ * not police. Placement is this module's to own — a caller cannot know
15
+ * what its parent is — and owning it here fixes every caller at once,
16
+ * ConfirmDialog included.
17
+ */
2
18
  interface Props {
3
19
  open: boolean;
4
20
  title: string;
@@ -0,0 +1,207 @@
1
+ <script lang="ts" module>
2
+ /** One option row. `value` is what the control carries: a string, always. */
3
+ export interface SelectOption {
4
+ value: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ }
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import type { Snippet } from 'svelte';
12
+ import type { HTMLSelectAttributes } from 'svelte/elements';
13
+
14
+ /**
15
+ * A labelled `<select>` — `TextField`'s sibling for a choice out of a list.
16
+ *
17
+ * The package had none, so eleven call sites across the two hosts each grew
18
+ * their own. That produced four spellings of ONE navy pill, a second box
19
+ * skin whose corner radius was hard-coded past `--radius` because the admin
20
+ * theme sets that token to `999px`, and two incompatible ways of reporting
21
+ * the same refusal: the user editor wires `aria-invalid` and
22
+ * `aria-describedby`, while the record field one module over announced its
23
+ * rejection with a bare `role="alert()"` and left the control unmarked. A
24
+ * control that cannot be rendered unwired is the point here, the way
25
+ * `Field`'s snippet is.
26
+ *
27
+ * The value is a STRING and stays one. Both hosts had already converged on
28
+ * that independently — a `<select>` binds a string, and a year, a row id or
29
+ * a boolean flag becomes itself again in the one adapter that reads it — so
30
+ * a value type here would fork the component per caller.
31
+ */
32
+ interface Props {
33
+ label: string;
34
+ id: string;
35
+ /**
36
+ * How it presents itself. Three, which is what the call sites were:
37
+ *
38
+ * - `field` — labelled box, the default.
39
+ * - `pill` — filled navy, for a toolbar beside a heading.
40
+ * - `bare` — box, label to the screen reader only, for inline chrome
41
+ * that already reads as labelled by what surrounds it.
42
+ */
43
+ variant?: 'field' | 'pill' | 'bare';
44
+ /** Label beside the control rather than above it — a toolbar row. */
45
+ inline?: boolean;
46
+ value?: string;
47
+ required?: boolean;
48
+ disabled?: boolean;
49
+ /**
50
+ * The leading option. `disabled` makes it a placeholder the reader must
51
+ * choose past; without it the empty value is selectable and MEANS
52
+ * something — "No manager", "every table".
53
+ */
54
+ placeholder?: { label: string; disabled?: boolean };
55
+ /** Options whose labels are data. Use `children` when a label is computed. */
56
+ options?: readonly SelectOption[];
57
+ children?: Snippet;
58
+ error?: string | null;
59
+ /** Standing help. Described alongside an error rather than replaced by it. */
60
+ hint?: string | null;
61
+ /**
62
+ * Beside the binding, not instead of it: the callers that need one are
63
+ * coercing the string (a year to a number) or acting on the choice (a
64
+ * template applied, a draft committed).
65
+ */
66
+ onchange?: (value: string) => void;
67
+ /**
68
+ * A remote field's `.as('select')` attachment, spread instead of bound —
69
+ * the same branch `TextField` takes, so a remote form does not route
70
+ * around this module and re-declare the skin.
71
+ */
72
+ attributes?: HTMLSelectAttributes;
73
+ /** Native form submit: the browser posts under this name. */
74
+ name?: string;
75
+ }
76
+
77
+ let {
78
+ label,
79
+ id,
80
+ variant = 'field',
81
+ inline = false,
82
+ value = $bindable(''),
83
+ required = false,
84
+ disabled = false,
85
+ placeholder,
86
+ options,
87
+ children,
88
+ error = null,
89
+ hint = null,
90
+ onchange,
91
+ attributes,
92
+ name
93
+ }: Props = $props();
94
+
95
+ /** Both ids when both are showing: an error does not hide standing help. */
96
+ const describedBy = $derived(
97
+ [error ? `${id}-error` : null, hint ? `${id}-hint` : null].filter(Boolean).join(' ') ||
98
+ undefined
99
+ );
100
+
101
+ const shared = $derived({
102
+ id,
103
+ name,
104
+ required,
105
+ disabled,
106
+ 'aria-invalid': error ? true : undefined,
107
+ 'aria-describedby': describedBy
108
+ });
109
+ </script>
110
+
111
+ {#snippet body()}
112
+ {#if placeholder}
113
+ <option value="" disabled={placeholder.disabled}>{placeholder.label}</option>
114
+ {/if}
115
+ {#each options ?? [] as option (option.value)}
116
+ <option value={option.value} disabled={option.disabled}>{option.label}</option>
117
+ {/each}
118
+ {@render children?.()}
119
+ {/snippet}
120
+
121
+ <div class="select-field" class:inline class:pill={variant === 'pill'}>
122
+ <label for={id} class:visually-hidden={variant === 'bare'}>{label}</label>
123
+ {#if attributes}
124
+ <!-- The form owns the value and its name. -->
125
+ <select {...shared} {...attributes}>{@render body()}</select>
126
+ {:else}
127
+ <select {...shared} bind:value onchange={(event) => onchange?.(event.currentTarget.value)}>
128
+ {@render body()}
129
+ </select>
130
+ {/if}
131
+ {#if hint}
132
+ <p class="hint" id="{id}-hint">{hint}</p>
133
+ {/if}
134
+ {#if error}
135
+ <p class="error" id="{id}-error">{error}</p>
136
+ {/if}
137
+ </div>
138
+
139
+ <style>
140
+ .select-field {
141
+ display: flex;
142
+ flex-direction: column;
143
+ gap: var(--space-1);
144
+ }
145
+
146
+ .select-field.inline {
147
+ flex-direction: row;
148
+ align-items: center;
149
+ gap: var(--space-2);
150
+ }
151
+
152
+ label {
153
+ font-size: var(--text-sm);
154
+ font-weight: 500;
155
+ }
156
+
157
+ select {
158
+ border: 1px solid var(--color-axis);
159
+ /* Not `--radius`: the admin theme sets that to 999px for its pill
160
+ buttons, which is why both of that host's boxed selects hard-coded a
161
+ corner past it. `--radius-control` is the input corner in both. */
162
+ border-radius: var(--radius-control);
163
+ background: var(--color-surface);
164
+ padding: 0.35rem 0.6rem;
165
+ font-family: var(--font-sans);
166
+ font-size: var(--text-base);
167
+ color: var(--color-ink);
168
+ }
169
+
170
+ select:focus-visible {
171
+ outline: none;
172
+ border-color: var(--color-navy);
173
+ }
174
+
175
+ select:disabled {
176
+ opacity: 0.5;
177
+ }
178
+
179
+ /* The filled toolbar control: a heading's neighbour, not a form field. It
180
+ takes its content's width — a form field stretches, a pill does not. */
181
+ .pill select {
182
+ align-self: flex-start;
183
+ border-color: var(--color-navy);
184
+ border-radius: 999px;
185
+ background: var(--color-navy);
186
+ color: var(--color-surface);
187
+ padding: 0.3rem 0.8rem;
188
+ font-size: var(--text-sm);
189
+ }
190
+
191
+ .pill select:focus-visible {
192
+ outline: 2px solid var(--color-navy);
193
+ outline-offset: 2px;
194
+ }
195
+
196
+ .hint {
197
+ color: var(--color-ink-muted);
198
+ font-size: 0.8rem;
199
+ margin: 0;
200
+ }
201
+
202
+ .error {
203
+ color: var(--series-8);
204
+ font-size: 0.8rem;
205
+ margin: 0;
206
+ }
207
+ </style>
@@ -0,0 +1,76 @@
1
+ /** One option row. `value` is what the control carries: a string, always. */
2
+ export interface SelectOption {
3
+ value: string;
4
+ label: string;
5
+ disabled?: boolean;
6
+ }
7
+ import type { Snippet } from 'svelte';
8
+ import type { HTMLSelectAttributes } from 'svelte/elements';
9
+ /**
10
+ * A labelled `<select>` — `TextField`'s sibling for a choice out of a list.
11
+ *
12
+ * The package had none, so eleven call sites across the two hosts each grew
13
+ * their own. That produced four spellings of ONE navy pill, a second box
14
+ * skin whose corner radius was hard-coded past `--radius` because the admin
15
+ * theme sets that token to `999px`, and two incompatible ways of reporting
16
+ * the same refusal: the user editor wires `aria-invalid` and
17
+ * `aria-describedby`, while the record field one module over announced its
18
+ * rejection with a bare `role="alert()"` and left the control unmarked. A
19
+ * control that cannot be rendered unwired is the point here, the way
20
+ * `Field`'s snippet is.
21
+ *
22
+ * The value is a STRING and stays one. Both hosts had already converged on
23
+ * that independently — a `<select>` binds a string, and a year, a row id or
24
+ * a boolean flag becomes itself again in the one adapter that reads it — so
25
+ * a value type here would fork the component per caller.
26
+ */
27
+ interface Props {
28
+ label: string;
29
+ id: string;
30
+ /**
31
+ * How it presents itself. Three, which is what the call sites were:
32
+ *
33
+ * - `field` — labelled box, the default.
34
+ * - `pill` — filled navy, for a toolbar beside a heading.
35
+ * - `bare` — box, label to the screen reader only, for inline chrome
36
+ * that already reads as labelled by what surrounds it.
37
+ */
38
+ variant?: 'field' | 'pill' | 'bare';
39
+ /** Label beside the control rather than above it — a toolbar row. */
40
+ inline?: boolean;
41
+ value?: string;
42
+ required?: boolean;
43
+ disabled?: boolean;
44
+ /**
45
+ * The leading option. `disabled` makes it a placeholder the reader must
46
+ * choose past; without it the empty value is selectable and MEANS
47
+ * something — "No manager", "every table".
48
+ */
49
+ placeholder?: {
50
+ label: string;
51
+ disabled?: boolean;
52
+ };
53
+ /** Options whose labels are data. Use `children` when a label is computed. */
54
+ options?: readonly SelectOption[];
55
+ children?: Snippet;
56
+ error?: string | null;
57
+ /** Standing help. Described alongside an error rather than replaced by it. */
58
+ hint?: string | null;
59
+ /**
60
+ * Beside the binding, not instead of it: the callers that need one are
61
+ * coercing the string (a year to a number) or acting on the choice (a
62
+ * template applied, a draft committed).
63
+ */
64
+ onchange?: (value: string) => void;
65
+ /**
66
+ * A remote field's `.as('select')` attachment, spread instead of bound —
67
+ * the same branch `TextField` takes, so a remote form does not route
68
+ * around this module and re-declare the skin.
69
+ */
70
+ attributes?: HTMLSelectAttributes;
71
+ /** Native form submit: the browser posts under this name. */
72
+ name?: string;
73
+ }
74
+ declare const SelectField: import("svelte").Component<Props, {}, "value">;
75
+ type SelectField = ReturnType<typeof SelectField>;
76
+ export default SelectField;
@@ -1,10 +1,4 @@
1
1
  import { type UiConfig, type UiConfigInput } from './types.js';
2
- /**
3
- * The provider-less config: identity hrefs, Catalan, no router. Complete on
4
- * purpose — every component must render without a provider, which is what
5
- * keeps stories and component tests free of app wiring.
6
- */
7
- export declare const DEFAULT_UI_CONFIG: UiConfig;
8
2
  /**
9
3
  * Installs the app's config for the subtree. Takes a *thunk* so a provider
10
4
  * whose input is a reactive prop stays live: the resolved object delegates
@@ -8,7 +8,7 @@ const KEY = Symbol.for('@vit-foundation/ui:config');
8
8
  * purpose — every component must render without a provider, which is what
9
9
  * keeps stories and component tests free of app wiring.
10
10
  */
11
- export const DEFAULT_UI_CONFIG = {
11
+ const DEFAULT_UI_CONFIG = {
12
12
  href: (path) => path,
13
13
  locale: () => BASE_LOCALE,
14
14
  locales: LOCALES,
@@ -1,7 +1,7 @@
1
- export { DEFAULT_UI_CONFIG, getUiConfig, setUiConfig } from './context.js';
1
+ export { getUiConfig, setUiConfig } from './context.js';
2
2
  export { defaultEditMessages } from './edit-messages.js';
3
3
  export type { EditMessages } from './edit-messages.js';
4
4
  export { defaultMessages } from './messages.js';
5
5
  export { default as UiProvider } from './UiProvider.svelte';
6
6
  export { BASE_LOCALE, LOCALES } from './types.js';
7
- export type { Locale, NotParameterized, ParameterizedKey, ParameterlessKey, SiteLink, UiConfig, UiConfigInput, UiMessages } from './types.js';
7
+ export type { Locale, NotParameterized, ParameterlessKey, SiteLink, UiConfig, UiConfigInput, UiMessages } from './types.js';
@@ -1,4 +1,4 @@
1
- export { DEFAULT_UI_CONFIG, getUiConfig, setUiConfig } from './context.js';
1
+ export { getUiConfig, setUiConfig } from './context.js';
2
2
  export { defaultEditMessages } from './edit-messages.js';
3
3
  export { defaultMessages } from './messages.js';
4
4
  export { default as UiProvider } from './UiProvider.svelte';
@@ -32,10 +32,13 @@ export type { LegalPageEdit } from './components/pages/LegalPage.svelte';
32
32
  export { default as WeekliesPage } from './components/pages/WeekliesPage.svelte';
33
33
  export type { WeekliesPageEdit } from './components/pages/WeekliesPage.svelte';
34
34
  export { default as ProjectPage } from './components/pages/ProjectPage.svelte';
35
- export type { ProjectPageEdit } from './components/pages/ProjectPage.svelte';
36
35
  export { default as WeeklyPage } from './components/pages/WeeklyPage.svelte';
37
- export type { WeeklyPageEdit } from './components/pages/WeeklyPage.svelte';
38
- export * from './content/index.js';
36
+ export { renderBody } from './content/richtext.js';
37
+ export type { RichTextBlock } from './content/richtext.js';
38
+ export { GET_INVOLVED_REASON_KEYS, PAGE_COPY_KEYS } from './content/pages.js';
39
+ export type { CopyEditFor, CopyKey, PageCopy, PageId } from './content/pages.js';
40
+ export { COMMENT_STATUSES, CONTACT_CATEGORIES, MILESTONE_CATEGORIES, PROJECT_KINDS, REACTIONS } from './content/types.js';
41
+ 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 './content/types.js';
39
42
  export { MILESTONE_CATEGORY_COLOR, matchesMilestoneFilter, milestoneCategoryLabel } from './utils/milestones.js';
40
43
  export { createUrlFilters } from './utils/url-filters.svelte.js';
41
44
  export type { UrlFilters, UrlFiltersConfig } from './utils/url-filters.svelte.js';
@@ -23,7 +23,11 @@ export { default as LegalPage } from './components/pages/LegalPage.svelte';
23
23
  export { default as WeekliesPage } from './components/pages/WeekliesPage.svelte';
24
24
  export { default as ProjectPage } from './components/pages/ProjectPage.svelte';
25
25
  export { default as WeeklyPage } from './components/pages/WeeklyPage.svelte';
26
- export * from './content/index.js';
26
+ // The content vocabularies and data shapes, beside the renderers that consume
27
+ // them. `./contract` names the component-free ones a second time, by design.
28
+ export { renderBody } from './content/richtext.js';
29
+ export { GET_INVOLVED_REASON_KEYS, PAGE_COPY_KEYS } from './content/pages.js';
30
+ export { COMMENT_STATUSES, CONTACT_CATEGORIES, MILESTONE_CATEGORIES, PROJECT_KINDS, REACTIONS } from './content/types.js';
27
31
  export { MILESTONE_CATEGORY_COLOR, matchesMilestoneFilter, milestoneCategoryLabel } from './utils/milestones.js';
28
32
  // The two list rules: URL-mirrored filters, and the weeklies index over them.
29
33
  export { createUrlFilters } from './utils/url-filters.svelte.js';