@vit-foundation/ui 0.25.0 → 0.26.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 (62) hide show
  1. package/README.md +1 -1
  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/Modal.svelte +32 -11
  9. package/dist/components/ui/Modal.svelte.d.ts +17 -1
  10. package/dist/config/context.d.ts +0 -6
  11. package/dist/config/context.js +1 -1
  12. package/dist/config/index.d.ts +2 -2
  13. package/dist/config/index.js +1 -1
  14. package/dist/content-components.d.ts +6 -3
  15. package/dist/content-components.js +5 -1
  16. package/dist/contract.d.ts +8 -4
  17. package/dist/contract.js +7 -3
  18. package/dist/edit/Editable.svelte +17 -161
  19. package/dist/edit/Editable.svelte.d.ts +2 -50
  20. package/dist/edit/chrome/AddSlot.svelte +11 -78
  21. package/dist/edit/chrome/AddSlot.svelte.d.ts +2 -15
  22. package/dist/edit/chrome/EditFrame.svelte +15 -158
  23. package/dist/edit/chrome/EditFrame.svelte.d.ts +2 -31
  24. package/dist/edit/chrome/EditPanel.svelte +14 -22
  25. package/dist/edit/chrome/EditPanel.svelte.d.ts +2 -13
  26. package/dist/edit/chrome/LinkEdit.svelte +13 -277
  27. package/dist/edit/chrome/LinkEdit.svelte.d.ts +2 -48
  28. package/dist/edit/chrome-props.d.ts +96 -0
  29. package/dist/edit/chrome-props.js +1 -0
  30. package/dist/edit/context.d.ts +38 -5
  31. package/dist/edit/context.js +18 -7
  32. package/dist/edit/index.d.ts +6 -4
  33. package/dist/edit/index.js +4 -3
  34. package/dist/edit/live/AddSlot.svelte +83 -0
  35. package/dist/edit/live/AddSlot.svelte.d.ts +4 -0
  36. package/dist/edit/live/EditFrame.svelte +153 -0
  37. package/dist/edit/live/EditFrame.svelte.d.ts +4 -0
  38. package/dist/edit/live/EditPanel.svelte +25 -0
  39. package/dist/edit/live/EditPanel.svelte.d.ts +4 -0
  40. package/dist/edit/live/Editable.svelte +162 -0
  41. package/dist/edit/live/Editable.svelte.d.ts +4 -0
  42. package/dist/edit/live/LinkEdit.svelte +276 -0
  43. package/dist/edit/live/LinkEdit.svelte.d.ts +4 -0
  44. package/dist/edit/{chrome → live}/PropertyRow.svelte +1 -1
  45. package/dist/edit/{commit.svelte.d.ts → live/commit.svelte.d.ts} +2 -2
  46. package/dist/edit/live/index.d.ts +10 -0
  47. package/dist/edit/live/index.js +14 -0
  48. package/package.json +1 -1
  49. package/dist/components/account/index.d.ts +0 -3
  50. package/dist/components/account/index.js +0 -2
  51. package/dist/components/auth/index.d.ts +0 -7
  52. package/dist/components/auth/index.js +0 -4
  53. package/dist/components/contact/index.d.ts +0 -2
  54. package/dist/components/contact/index.js +0 -1
  55. package/dist/content/index.d.ts +0 -6
  56. package/dist/content/index.js +0 -3
  57. /package/dist/edit/{chrome → live}/ConfirmDialog.svelte +0 -0
  58. /package/dist/edit/{chrome → live}/ConfirmDialog.svelte.d.ts +0 -0
  59. /package/dist/edit/{chrome → live}/EditPopover.svelte +0 -0
  60. /package/dist/edit/{chrome → live}/EditPopover.svelte.d.ts +0 -0
  61. /package/dist/edit/{chrome → live}/PropertyRow.svelte.d.ts +0 -0
  62. /package/dist/edit/{commit.svelte.js → live/commit.svelte.js} +0 -0
package/README.md CHANGED
@@ -29,7 +29,7 @@ Everything exports flat from the root, and again grouped by role:
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>;
@@ -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;
@@ -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';
@@ -17,9 +17,13 @@
17
17
  * is what makes it a promise rather than a description: adding a `.svelte`
18
18
  * anywhere beneath it fails here rather than in a consumer's server build.
19
19
  *
20
- * It adds nothing. Every name below is already exported from a semantic
21
- * subpath and stays there — this is a second door into the same rooms, not a
22
- * new floor, and the barrel remains the surface a component host reads.
20
+ * It adds almost nothing. Every name below is already exported from a
21
+ * semantic subpath and stays there — this is a second door into the same
22
+ * rooms, not a new floor, and the barrel remains the surface a component host
23
+ * reads. `contract-names.test.ts` holds that to be true, name by name and from
24
+ * the same source module; its `CONTRACT_ONLY` list names the three exceptions
25
+ * (the two destination classifiers and the title format), each with its
26
+ * reason, so a fourth cannot arrive unexplained.
23
27
  *
24
28
  * It was cut to fit ONE consumer, and the second one did not fit through it.
25
29
  * vit-brain's four restatements were the motive, so the first version carried
@@ -38,7 +42,7 @@
38
42
  * the guard walks them like the rest.
39
43
  */
40
44
  export { LOCALES, BASE_LOCALE } from './config/types.js';
41
- export type { Locale, NotParameterized, ParameterizedKey, ParameterlessKey, SiteLink, UiMessages } from './config/types.js';
45
+ export type { Locale, NotParameterized, ParameterlessKey, SiteLink, UiMessages } from './config/types.js';
42
46
  export { localize } from './edit/types.js';
43
47
  export type { CollectionRef, ContentRef, EditableEntity, EditDescriptor, EntityOp, LocalizedText, PropertyDescriptor, PropertyOption, PropertyType, PropertyValue } from './edit/types.js';
44
48
  export { REACTIONS } from './content/types.js';
package/dist/contract.js CHANGED
@@ -17,9 +17,13 @@
17
17
  * is what makes it a promise rather than a description: adding a `.svelte`
18
18
  * anywhere beneath it fails here rather than in a consumer's server build.
19
19
  *
20
- * It adds nothing. Every name below is already exported from a semantic
21
- * subpath and stays there — this is a second door into the same rooms, not a
22
- * new floor, and the barrel remains the surface a component host reads.
20
+ * It adds almost nothing. Every name below is already exported from a
21
+ * semantic subpath and stays there — this is a second door into the same
22
+ * rooms, not a new floor, and the barrel remains the surface a component host
23
+ * reads. `contract-names.test.ts` holds that to be true, name by name and from
24
+ * the same source module; its `CONTRACT_ONLY` list names the three exceptions
25
+ * (the two destination classifiers and the title format), each with its
26
+ * reason, so a fourth cannot arrive unexplained.
23
27
  *
24
28
  * It was cut to fit ONE consumer, and the second one did not fit through it.
25
29
  * vit-brain's four restatements were the motive, so the first version carried
@@ -1,14 +1,11 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { getUiConfig } from '../config/context.js';
4
- import { getEditAdapter } from './context.js';
5
- import { commitState } from './commit.svelte.js';
6
- import type { EditDescriptor } from './types.js';
2
+ import type { EditableProps } from './chrome-props.js';
3
+ import { getEditAdapter, getEditChrome } from './context.js';
7
4
 
8
5
  /**
9
- * The inline-edit primitive. It renders no element of its own: the child
10
- * snippet receives the text and a bag of attributes, and spreads both onto
11
- * whatever semantic element the call site owns —
6
+ * The inline-edit primitive's GATE. It renders no element of its own: the
7
+ * child snippet receives the text and a bag of attributes, and spreads
8
+ * both onto whatever semantic element the call site owns —
12
9
  *
13
10
  * <Editable edit={descriptor} value={title}>
14
11
  * {#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
@@ -20,164 +17,23 @@
20
17
  *
21
18
  * Inactive — no `edit` descriptor, no adapter in context (a read-only
22
19
  * app), or the adapter's `isEditing` off — it renders the child with an
23
- * empty attribute bag: no listeners, no wrapper, no cost.
24
- *
25
- * Active, it makes the element a plain-text `contenteditable` textbox.
26
- * The DOM owns the draft while the reader types; this component reads it
27
- * back on commit. Enter commits single-line fields (Cmd/Ctrl+Enter for
28
- * 'multiline'), Escape reverts to the last saved value, blur commits a
29
- * dirty draft. A failed save keeps the draft on screen in the error state
30
- * so nothing typed is lost. State is announced through a visually hidden
31
- * `role="status"` region, the pattern ShareRow set for its copy action.
32
- *
33
- * Styling for the affordance lives in base.css (`[data-vit-editing]`):
34
- * the element is rendered by the *caller's* markup, which scoped styles
35
- * here could never reach.
20
+ * empty attribute bag: no listeners, no wrapper, no cost. Active, it
21
+ * mounts the LIVE half the host installed (`edit/live/Editable.svelte`,
22
+ * through `setEditAdapter`'s chrome), which owns the contenteditable, the
23
+ * commit lifecycle and the announcements. This file imports none of that,
24
+ * so a renderer that imports it ships none of that (`chrome-props.ts`).
36
25
  */
37
- interface EditableAttrs {
38
- contenteditable?: 'plaintext-only' | 'true';
39
- role?: 'textbox';
40
- 'aria-label'?: string;
41
- 'aria-multiline'?: 'true';
42
- 'data-vit-editing'?: 'idle' | 'dirty' | 'saving' | 'error';
43
- onbeforeinput?: (event: InputEvent) => void;
44
- oninput?: (event: Event) => void;
45
- onblur?: (event: FocusEvent) => void;
46
- onkeydown?: (event: KeyboardEvent) => void;
47
- onpaste?: (event: ClipboardEvent) => void;
48
- }
49
-
50
- interface Props {
51
- /** Identity of the string. Omit it and this is a passthrough. */
52
- edit?: EditDescriptor;
53
- value: string;
54
- children: Snippet<[string, EditableAttrs]>;
55
- }
56
-
57
- let { edit, value, children }: Props = $props();
26
+ let { edit, value, children }: EditableProps = $props();
58
27
 
59
28
  const adapter = getEditAdapter();
60
- const config = getUiConfig();
61
-
62
- /**
63
- * The commit lifecycle — the four states, the announcements, the last
64
- * persisted value and the follow-the-prop rule — is shared with
65
- * `PropertyRow` (see `./commit.svelte.js`). What stays here is where the
66
- * DRAFT lives: in the DOM, because this is a contenteditable.
67
- */
68
- // svelte-ignore state_referenced_locally
69
- const commit_ = commitState(value, config.editMessages);
70
- /** What the child snippet renders. Only rewritten while the DOM is not
71
- * being typed in — Svelte and the reader must not fight over the node. */
72
- // svelte-ignore state_referenced_locally
73
- let renderText = $state(value);
74
-
75
- $effect(() => {
76
- const adopted = commit_.follow(value);
77
- if (adopted !== null) renderText = adopted;
78
- });
29
+ const chrome = getEditChrome();
79
30
 
80
31
  const active = $derived(edit !== undefined && (adapter?.isEditing ?? false));
81
- const multiline = $derived(edit?.format === 'multiline' || edit?.format === 'richtext');
82
-
83
- /** contenteditable text, innerText keeps line breaks; NBSPs become spaces. */
84
- function textOf(element: HTMLElement): string {
85
- return (element.innerText ?? element.textContent ?? '').replace(/\u00a0/g, ' ');
86
- }
87
-
88
- function handleBeforeInput(event: InputEvent): void {
89
- // 'plaintext-only' is not universal (Firefox); strip rich insertions.
90
- if (event.inputType === 'insertFromPaste' || event.inputType === 'insertFromDrop') return;
91
- if (!multiline && event.inputType === 'insertParagraph') event.preventDefault();
92
- }
93
-
94
- function handlePaste(event: ClipboardEvent): void {
95
- // Plain text only, whatever the clipboard holds.
96
- event.preventDefault();
97
- const text = event.clipboardData?.getData('text/plain') ?? '';
98
- const selection = window.getSelection();
99
- if (!selection || selection.rangeCount === 0) return;
100
- const range = selection.getRangeAt(0);
101
- range.deleteContents();
102
- range.insertNode(document.createTextNode(multiline ? text : text.replace(/\s*\n\s*/g, ' ')));
103
- selection.collapseToEnd();
104
- commit_.markDirty();
105
- }
106
-
107
- function handleInput(): void {
108
- commit_.markDirty();
109
- }
110
-
111
- async function commit(element: HTMLElement): Promise<void> {
112
- if (!edit || !adapter) return;
113
- const draft = textOf(element).trim();
114
- if (draft === commit_.saved) {
115
- commit_.settle();
116
- return;
117
- }
118
- // A failed save leaves the draft in the DOM; the reader decides whether
119
- // to retry.
120
- await commit_.commit(draft, () => adapter.save(edit, draft));
121
- }
122
-
123
- function handleBlur(event: FocusEvent): void {
124
- const status = commit_.status;
125
- if (status === 'dirty' || status === 'error') void commit(event.currentTarget as HTMLElement);
126
- }
127
-
128
- function handleKeydown(event: KeyboardEvent): void {
129
- const element = event.currentTarget as HTMLElement;
130
- if (event.key === 'Escape') {
131
- // Svelte's cached text still equals renderText, so a state write
132
- // alone cannot repaint a node the reader has mutated: restore the
133
- // DOM directly, then settle.
134
- element.textContent = commit_.saved;
135
- commit_.revert();
136
- element.blur();
137
- return;
138
- }
139
- const commitKey = multiline
140
- ? event.key === 'Enter' && (event.metaKey || event.ctrlKey)
141
- : event.key === 'Enter';
142
- if (commitKey) {
143
- event.preventDefault();
144
- void commit(element);
145
- }
146
- }
147
-
148
- const attrs = $derived<EditableAttrs>(
149
- active
150
- ? {
151
- contenteditable: 'plaintext-only',
152
- role: 'textbox',
153
- 'aria-label': edit?.label,
154
- 'aria-multiline': multiline ? 'true' : undefined,
155
- 'data-vit-editing': commit_.status,
156
- onbeforeinput: handleBeforeInput,
157
- oninput: handleInput,
158
- onblur: handleBlur,
159
- onkeydown: handleKeydown,
160
- onpaste: handlePaste
161
- }
162
- : {}
163
- );
164
32
  </script>
165
33
 
166
- {@render children(renderText, attrs)}
167
- {#if active}
168
- <span class="status" role="status">{commit_.announcement}</span>
34
+ {#if active && chrome}
35
+ {@const Live = chrome.Editable}
36
+ <Live {edit} {value} {children} />
37
+ {:else}
38
+ {@render children(value, {})}
169
39
  {/if}
170
-
171
- <style>
172
- .status {
173
- position: absolute;
174
- width: 1px;
175
- height: 1px;
176
- padding: 0;
177
- margin: -1px;
178
- overflow: hidden;
179
- clip-path: inset(50%);
180
- white-space: nowrap;
181
- border: 0;
182
- }
183
- </style>
@@ -1,52 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- import type { EditDescriptor } from './types.js';
3
- /**
4
- * The inline-edit primitive. It renders no element of its own: the child
5
- * snippet receives the text and a bag of attributes, and spreads both onto
6
- * whatever semantic element the call site owns —
7
- *
8
- * <Editable edit={descriptor} value={title}>
9
- * {#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
10
- * </Editable>
11
- *
12
- * — the same contract Field uses for its control attrs, and for the same
13
- * reason: the element (h1, p, span) is part of the document outline and
14
- * belongs to the caller, not behind a prop.
15
- *
16
- * Inactive — no `edit` descriptor, no adapter in context (a read-only
17
- * app), or the adapter's `isEditing` off — it renders the child with an
18
- * empty attribute bag: no listeners, no wrapper, no cost.
19
- *
20
- * Active, it makes the element a plain-text `contenteditable` textbox.
21
- * The DOM owns the draft while the reader types; this component reads it
22
- * back on commit. Enter commits single-line fields (Cmd/Ctrl+Enter for
23
- * 'multiline'), Escape reverts to the last saved value, blur commits a
24
- * dirty draft. A failed save keeps the draft on screen in the error state
25
- * so nothing typed is lost. State is announced through a visually hidden
26
- * `role="status"` region, the pattern ShareRow set for its copy action.
27
- *
28
- * Styling for the affordance lives in base.css (`[data-vit-editing]`):
29
- * the element is rendered by the *caller's* markup, which scoped styles
30
- * here could never reach.
31
- */
32
- interface EditableAttrs {
33
- contenteditable?: 'plaintext-only' | 'true';
34
- role?: 'textbox';
35
- 'aria-label'?: string;
36
- 'aria-multiline'?: 'true';
37
- 'data-vit-editing'?: 'idle' | 'dirty' | 'saving' | 'error';
38
- onbeforeinput?: (event: InputEvent) => void;
39
- oninput?: (event: Event) => void;
40
- onblur?: (event: FocusEvent) => void;
41
- onkeydown?: (event: KeyboardEvent) => void;
42
- onpaste?: (event: ClipboardEvent) => void;
43
- }
44
- interface Props {
45
- /** Identity of the string. Omit it and this is a passthrough. */
46
- edit?: EditDescriptor;
47
- value: string;
48
- children: Snippet<[string, EditableAttrs]>;
49
- }
50
- declare const Editable: import("svelte").Component<Props, {}, "">;
1
+ import type { EditableProps } from './chrome-props.js';
2
+ declare const Editable: import("svelte").Component<EditableProps, {}, "">;
51
3
  type Editable = ReturnType<typeof Editable>;
52
4
  export default Editable;