@vit-foundation/ui 0.5.0 → 0.9.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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getUiConfig } from '../../config/context.js';
3
+ import ActionLabel from '../../edit/ActionLabel.svelte';
3
4
  import Editable from '../../edit/Editable.svelte';
4
5
  import { withNewsletterIntent } from '../../forms/transport.js';
5
6
  import type { NewsletterToggleFormInstance } from './AccountPanel.svelte';
@@ -61,9 +62,16 @@
61
62
  <form {...f}>
62
63
  <input {...f.fields.action.as('hidden', 'subscribe')} />
63
64
  <LocaleField field={f.fields.locale} />
64
- <Button type="submit" pending={f.pending}>
65
- {msg.account_newsletterSubscribe()}
66
- </Button>
65
+ <ActionLabel
66
+ edit={config.messageEdit?.('account_newsletterSubscribe')}
67
+ value={msg.account_newsletterSubscribe()}
68
+ >
69
+ {#snippet control()}
70
+ <Button type="submit" pending={f.pending}>
71
+ {msg.account_newsletterSubscribe()}
72
+ </Button>
73
+ {/snippet}
74
+ </ActionLabel>
67
75
  </form>
68
76
  {#if f.result}
69
77
  <div class="feedback-slot">
@@ -19,6 +19,7 @@
19
19
 
20
20
  <script lang="ts">
21
21
  import { getUiConfig } from '../../config/context.js';
22
+ import ActionLabel from '../../edit/ActionLabel.svelte';
22
23
  import { CONTACT_MESSAGE, CONTACT_NAME, EMAIL } from '../../forms/constraints.js';
23
24
  import { CONTACT_CATEGORIES } from '../../content/types.js';
24
25
  import { contactCategoryLabel } from '../../utils/contact.js';
@@ -57,6 +58,7 @@
57
58
  <Field
58
59
  id="contact-category"
59
60
  label={msg.contact_categoryLabel()}
61
+ labelEdit={config.messageEdit?.('contact_categoryLabel')}
60
62
  field={f.fields.category}
61
63
  constraint={null}
62
64
  >
@@ -72,6 +74,7 @@
72
74
  <Field
73
75
  id="contact-name"
74
76
  label={msg.contact_nameLabel()}
77
+ labelEdit={config.messageEdit?.('contact_nameLabel')}
75
78
  field={f.fields.name}
76
79
  constraint={CONTACT_NAME}
77
80
  >
@@ -89,6 +92,7 @@
89
92
  <Field
90
93
  id="contact-email"
91
94
  label={msg.contact_emailLabel()}
95
+ labelEdit={config.messageEdit?.('contact_emailLabel')}
92
96
  field={f.fields.email}
93
97
  constraint={EMAIL}
94
98
  >
@@ -106,6 +110,7 @@
106
110
  <Field
107
111
  id="contact-message"
108
112
  label={msg.contact_messageLabel()}
113
+ labelEdit={config.messageEdit?.('contact_messageLabel')}
109
114
  field={f.fields.message}
110
115
  constraint={CONTACT_MESSAGE}
111
116
  >
@@ -120,7 +125,11 @@
120
125
  <Honeypot form={f.fields} id="contact-website" />
121
126
 
122
127
  <div class="actions">
123
- <Button type="submit" pending={f.pending}>{msg.contact_submit()}</Button>
128
+ <ActionLabel edit={config.messageEdit?.('contact_submit')} value={msg.contact_submit()}>
129
+ {#snippet control()}
130
+ <Button type="submit" pending={f.pending}>{msg.contact_submit()}</Button>
131
+ {/snippet}
132
+ </ActionLabel>
124
133
  </div>
125
134
 
126
135
  {#if f.result && !f.result.ok}
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { getUiConfig } from '../../config/context.js';
3
3
  import type { SiteLink } from '../../config/types.js';
4
+ import ActionLabel from '../../edit/ActionLabel.svelte';
4
5
  import Editable from '../../edit/Editable.svelte';
6
+ import type { EditDescriptor } from '../../edit/types.js';
5
7
  import Link from '../ui/Link.svelte';
6
8
 
7
9
  /**
@@ -11,9 +13,11 @@
11
13
  */
12
14
  interface Props {
13
15
  links: SiteLink[];
16
+ /** Edit descriptors for the link labels — see Nav. */
17
+ editFor?: (link: SiteLink) => EditDescriptor | undefined;
14
18
  }
15
19
 
16
- let { links }: Props = $props();
20
+ let { links, editFor = undefined }: Props = $props();
17
21
 
18
22
  const config = getUiConfig();
19
23
 
@@ -27,7 +31,11 @@
27
31
  </span>
28
32
  <ul>
29
33
  {#each links as link (link.href)}
30
- <li><Link href={link.href}>{link.label}</Link></li>
34
+ <li>
35
+ <ActionLabel edit={editFor?.(link)} value={link.label}>
36
+ {#snippet control()}<Link href={link.href}>{link.label}</Link>{/snippet}
37
+ </ActionLabel>
38
+ </li>
31
39
  {/each}
32
40
  </ul>
33
41
  <p class="rights">
@@ -1,4 +1,5 @@
1
1
  import type { SiteLink } from '../../config/types.js';
2
+ import type { EditDescriptor } from '../../edit/types.js';
2
3
  /**
3
4
  * The links are a prop rather than a package constant: the host app owns
4
5
  * its routes, and Nav and Footer render the same list — the foundation
@@ -6,6 +7,8 @@ import type { SiteLink } from '../../config/types.js';
6
7
  */
7
8
  interface Props {
8
9
  links: SiteLink[];
10
+ /** Edit descriptors for the link labels — see Nav. */
11
+ editFor?: (link: SiteLink) => EditDescriptor | undefined;
9
12
  }
10
13
  declare const Footer: import("svelte").Component<Props, {}, "">;
11
14
  type Footer = ReturnType<typeof Footer>;
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { getUiConfig } from '../../config/context.js';
3
3
  import type { SiteLink } from '../../config/types.js';
4
+ import ActionLabel from '../../edit/ActionLabel.svelte';
5
+ import type { EditDescriptor } from '../../edit/types.js';
4
6
  import { isPathUnder } from '../../utils/paths.js';
5
7
  import GhostButton from '../ui/GhostButton.svelte';
6
8
  import Link from '../ui/Link.svelte';
@@ -16,9 +18,16 @@
16
18
  account?: { displayName: string } | null;
17
19
  /** The URL highlighting reads from. Only tests and stories pass one. */
18
20
  url?: URL;
21
+ /**
22
+ * Edit descriptors for the link LABELS — a function because the host
23
+ * owns both the routes and the message keys behind them. While the
24
+ * adapter is editing, a link with a descriptor renders as editable
25
+ * text instead of an anchor (see ActionLabel).
26
+ */
27
+ editFor?: (link: SiteLink) => EditDescriptor | undefined;
19
28
  }
20
29
 
21
- let { links, account = null, url = undefined }: Props = $props();
30
+ let { links, account = null, url = undefined, editFor = undefined }: Props = $props();
22
31
 
23
32
  const config = getUiConfig();
24
33
  const msg = $derived(config.messages);
@@ -77,9 +86,13 @@
77
86
  <ul class="links">
78
87
  {#each links as link (link.href)}
79
88
  <li>
80
- <Link href={link.href} aria-current={isCurrent(link.href) ? 'page' : undefined}>
81
- {link.label}
82
- </Link>
89
+ <ActionLabel edit={editFor?.(link)} value={link.label}>
90
+ {#snippet control()}
91
+ <Link href={link.href} aria-current={isCurrent(link.href) ? 'page' : undefined}>
92
+ {link.label}
93
+ </Link>
94
+ {/snippet}
95
+ </ActionLabel>
83
96
  </li>
84
97
  {/each}
85
98
  </ul>
@@ -1,4 +1,5 @@
1
1
  import type { SiteLink } from '../../config/types.js';
2
+ import type { EditDescriptor } from '../../edit/types.js';
2
3
  interface Props {
3
4
  /**
4
5
  * Primary navigation entries. A prop rather than a package constant:
@@ -12,6 +13,13 @@ interface Props {
12
13
  } | null;
13
14
  /** The URL highlighting reads from. Only tests and stories pass one. */
14
15
  url?: URL;
16
+ /**
17
+ * Edit descriptors for the link LABELS — a function because the host
18
+ * owns both the routes and the message keys behind them. While the
19
+ * adapter is editing, a link with a descriptor renders as editable
20
+ * text instead of an anchor (see ActionLabel).
21
+ */
22
+ editFor?: (link: SiteLink) => EditDescriptor | undefined;
15
23
  }
16
24
  declare const Nav: import("svelte").Component<Props, {}, "">;
17
25
  type Nav = ReturnType<typeof Nav>;
@@ -10,6 +10,7 @@
10
10
 
11
11
  <script lang="ts">
12
12
  import { getUiConfig } from '../../config/context.js';
13
+ import ActionLabel from '../../edit/ActionLabel.svelte';
13
14
  import type { ProjectCardData } from '../../content/types.js';
14
15
  import { getEditAdapter } from '../../edit/context.js';
15
16
  import Editable from '../../edit/Editable.svelte';
@@ -57,9 +58,16 @@
57
58
  {#snippet children(text, attrs)}<p {...attrs}>{text}</p>{/snippet}
58
59
  </Editable>
59
60
  {#if project.hasStory}
60
- <Link href={`/what-we-do/${project.slug}`} class="more">
61
- {config.messages.common_readMore()}
62
- </Link>
61
+ <ActionLabel
62
+ edit={config.messageEdit?.('common_readMore')}
63
+ value={config.messages.common_readMore()}
64
+ >
65
+ {#snippet control()}
66
+ <Link href={`/what-we-do/${project.slug}`} class="more">
67
+ {config.messages.common_readMore()}
68
+ </Link>
69
+ {/snippet}
70
+ </ActionLabel>
63
71
  {/if}
64
72
  </div>
65
73
  </article>
@@ -10,6 +10,7 @@
10
10
 
11
11
  <script lang="ts">
12
12
  import { getUiConfig } from '../../config/context.js';
13
+ import ActionLabel from '../../edit/ActionLabel.svelte';
13
14
  import type { MilestoneData } from '../../content/types.js';
14
15
  import Editable from '../../edit/Editable.svelte';
15
16
  import { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from '../../utils/milestones.js';
@@ -47,7 +48,12 @@
47
48
 
48
49
  <article style={`--milestone-color: ${color}`}>
49
50
  <span class="dot" aria-hidden="true"></span>
50
- <p class="category">{milestoneCategoryLabel(milestone.category, config.messages)}</p>
51
+ <Editable
52
+ edit={config.messageEdit?.(`category_${milestone.category}`)}
53
+ value={milestoneCategoryLabel(milestone.category, config.messages)}
54
+ >
55
+ {#snippet children(text, attrs)}<p class="category" {...attrs}>{text}</p>{/snippet}
56
+ </Editable>
51
57
  <DateText value={milestone.occurredOn} />
52
58
  <Editable edit={edit?.title} value={milestone.title}>
53
59
  {#snippet children(text, attrs)}<h3 {...attrs}>{text}</h3>{/snippet}
@@ -62,11 +68,20 @@
62
68
  {/if}
63
69
  {#if milestone.linkUrl}
64
70
  <p class="more">
65
- {#if isExternal}
66
- <a href={milestone.linkUrl} rel="external noopener">{config.messages.common_readMore()}</a>
67
- {:else}
68
- <Link href={milestone.linkUrl}>{config.messages.common_readMore()}</Link>
69
- {/if}
71
+ <ActionLabel
72
+ edit={config.messageEdit?.('common_readMore')}
73
+ value={config.messages.common_readMore()}
74
+ >
75
+ {#snippet control()}
76
+ {#if isExternal}
77
+ <a href={milestone.linkUrl} rel="external noopener"
78
+ >{config.messages.common_readMore()}</a
79
+ >
80
+ {:else if milestone.linkUrl}
81
+ <Link href={milestone.linkUrl}>{config.messages.common_readMore()}</Link>
82
+ {/if}
83
+ {/snippet}
84
+ </ActionLabel>
70
85
  </p>
71
86
  {/if}
72
87
  </article>
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { FieldConstraint } from '../../content/types.js';
4
+ import ActionLabel from '../../edit/ActionLabel.svelte';
5
+ import type { EditDescriptor } from '../../edit/types.js';
4
6
 
5
7
  /** Attributes the control must spread (`{...attrs}`) to stay wired. */
6
8
  interface ControlAttrs {
@@ -46,18 +48,37 @@
46
48
  constraint: FieldConstraint | null;
47
49
  /** Render the label visually hidden (surrounding context already labels it). */
48
50
  hideLabel?: boolean;
51
+ /**
52
+ * Edit descriptor for the LABEL's wording. While editing, only the
53
+ * label swaps for editable text — an editable span is not a <label>,
54
+ * so clicking it no longer focuses the control, and the control itself
55
+ * stays rendered and live. Hidden labels are never offered.
56
+ */
57
+ labelEdit?: EditDescriptor;
49
58
  /** The input/textarea/select, spreading the given attrs. */
50
59
  children: Snippet<[ControlAttrs]>;
51
60
  }
52
61
 
53
- let { id, label, field, constraint, hideLabel = false, children }: Props = $props();
62
+ let {
63
+ id,
64
+ label,
65
+ field,
66
+ constraint,
67
+ hideLabel = false,
68
+ labelEdit = undefined,
69
+ children
70
+ }: Props = $props();
54
71
 
55
72
  const errorMessage = $derived(field.issues()?.[0]?.message);
56
73
  const showError = $derived(errorMessage !== undefined);
57
74
  </script>
58
75
 
59
76
  <div class="field">
60
- <label class:visually-hidden={hideLabel} for={id}>{label}</label>
77
+ <ActionLabel edit={hideLabel ? undefined : labelEdit} value={label}>
78
+ {#snippet control()}
79
+ <label class:visually-hidden={hideLabel} for={id}>{label}</label>
80
+ {/snippet}
81
+ </ActionLabel>
61
82
  {@render children({
62
83
  id,
63
84
  'aria-describedby': showError ? `${id}-error` : undefined,
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { FieldConstraint } from '../../content/types.js';
3
+ import type { EditDescriptor } from '../../edit/types.js';
3
4
  /** Attributes the control must spread (`{...attrs}`) to stay wired. */
4
5
  interface ControlAttrs {
5
6
  id: string;
@@ -47,6 +48,13 @@ interface Props {
47
48
  constraint: FieldConstraint | null;
48
49
  /** Render the label visually hidden (surrounding context already labels it). */
49
50
  hideLabel?: boolean;
51
+ /**
52
+ * Edit descriptor for the LABEL's wording. While editing, only the
53
+ * label swaps for editable text — an editable span is not a <label>,
54
+ * so clicking it no longer focuses the control, and the control itself
55
+ * stays rendered and live. Hidden labels are never offered.
56
+ */
57
+ labelEdit?: EditDescriptor;
50
58
  /** The input/textarea/select, spreading the given attrs. */
51
59
  children: Snippet<[ControlAttrs]>;
52
60
  }
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import ActionLabel from '../../edit/ActionLabel.svelte';
3
+ import type { EditDescriptor } from '../../edit/types.js';
2
4
  import GhostButton from './GhostButton.svelte';
3
5
 
4
6
  interface Chip {
@@ -13,9 +15,16 @@
13
15
  label: string;
14
16
  /** Called with the toggled value, or null when the active chip is unselected. */
15
17
  onchange: (value: string | null) => void;
18
+ /**
19
+ * Edit descriptors for the chip LABELS — a function because the host
20
+ * owns what a chip's wording is (a theme's localized name, a category
21
+ * message). While editing, a chip with a descriptor renders as
22
+ * editable text instead of a button (see ActionLabel).
23
+ */
24
+ editFor?: (chip: Chip) => EditDescriptor | undefined;
16
25
  }
17
26
 
18
- let { chips, selected, label, onchange }: Props = $props();
27
+ let { chips, selected, label, onchange, editFor = undefined }: Props = $props();
19
28
 
20
29
  function toggle(value: string): void {
21
30
  onchange(selected === value ? null : value);
@@ -24,13 +33,17 @@
24
33
 
25
34
  <div class="chips" role="group" aria-label={label}>
26
35
  {#each chips as chip (chip.value)}
27
- <GhostButton
28
- variant="chip"
29
- aria-pressed={selected === chip.value}
30
- onclick={() => toggle(chip.value)}
31
- >
32
- {chip.label}
33
- </GhostButton>
36
+ <ActionLabel edit={editFor?.(chip)} value={chip.label}>
37
+ {#snippet control()}
38
+ <GhostButton
39
+ variant="chip"
40
+ aria-pressed={selected === chip.value}
41
+ onclick={() => toggle(chip.value)}
42
+ >
43
+ {chip.label}
44
+ </GhostButton>
45
+ {/snippet}
46
+ </ActionLabel>
34
47
  {/each}
35
48
  </div>
36
49
 
@@ -1,3 +1,4 @@
1
+ import type { EditDescriptor } from '../../edit/types.js';
1
2
  interface Chip {
2
3
  value: string;
3
4
  label: string;
@@ -9,6 +10,13 @@ interface Props {
9
10
  label: string;
10
11
  /** Called with the toggled value, or null when the active chip is unselected. */
11
12
  onchange: (value: string | null) => void;
13
+ /**
14
+ * Edit descriptors for the chip LABELS — a function because the host
15
+ * owns what a chip's wording is (a theme's localized name, a category
16
+ * message). While editing, a chip with a descriptor renders as
17
+ * editable text instead of a button (see ActionLabel).
18
+ */
19
+ editFor?: (chip: Chip) => EditDescriptor | undefined;
12
20
  }
13
21
  declare const FilterChips: import("svelte").Component<Props, {}, "">;
14
22
  type FilterChips = ReturnType<typeof FilterChips>;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getUiConfig } from '../../config/context.js';
3
+ import ActionLabel from '../../edit/ActionLabel.svelte';
3
4
  import Link from './Link.svelte';
4
5
 
5
6
  /**
@@ -37,21 +38,32 @@
37
38
 
38
39
  {#if pages > 1 || isOutOfRange}
39
40
  <nav class="pagination" aria-label={msg.pagination_label()}>
40
- {#if hasPrevious}
41
- <Link href={href(previousPage)} rel="prev">{msg.pagination_previous()}</Link>
42
- {:else}
43
- <span class="disabled">{msg.pagination_previous()}</span>
44
- {/if}
41
+ <ActionLabel
42
+ edit={config.messageEdit?.('pagination_previous')}
43
+ value={msg.pagination_previous()}
44
+ >
45
+ {#snippet control()}
46
+ {#if hasPrevious}
47
+ <Link href={href(previousPage)} rel="prev">{msg.pagination_previous()}</Link>
48
+ {:else}
49
+ <span class="disabled">{msg.pagination_previous()}</span>
50
+ {/if}
51
+ {/snippet}
52
+ </ActionLabel>
45
53
 
46
54
  <p class="status" aria-live="polite">
47
55
  {msg.pagination_status({ page, pages })}
48
56
  </p>
49
57
 
50
- {#if hasNext}
51
- <Link href={href(page + 1)} rel="next">{msg.pagination_next()}</Link>
52
- {:else}
53
- <span class="disabled">{msg.pagination_next()}</span>
54
- {/if}
58
+ <ActionLabel edit={config.messageEdit?.('pagination_next')} value={msg.pagination_next()}>
59
+ {#snippet control()}
60
+ {#if hasNext}
61
+ <Link href={href(page + 1)} rel="next">{msg.pagination_next()}</Link>
62
+ {:else}
63
+ <span class="disabled">{msg.pagination_next()}</span>
64
+ {/if}
65
+ {/snippet}
66
+ </ActionLabel>
55
67
  </nav>
56
68
  {/if}
57
69
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  <script lang="ts">
12
12
  import { getUiConfig } from '../../config/context.js';
13
+ import ActionLabel from '../../edit/ActionLabel.svelte';
13
14
  import Editable from '../../edit/Editable.svelte';
14
15
  import { COMMENT_BODY } from '../../forms/constraints.js';
15
16
  import type { CommentData, CommentThreadData } from '../../content/types.js';
@@ -118,9 +119,16 @@
118
119
  <input {...reply.fields.weeklySlug.as('hidden', weeklySlug)} />
119
120
  <input {...reply.fields.parentId.as('hidden', String(thread.id))} />
120
121
  <div class="actions">
121
- <Button type="submit" pending={reply.pending}>
122
- {msg.comments_replySubmit()}
123
- </Button>
122
+ <ActionLabel
123
+ edit={config.messageEdit?.('comments_replySubmit')}
124
+ value={msg.comments_replySubmit()}
125
+ >
126
+ {#snippet control()}
127
+ <Button type="submit" pending={reply.pending}>
128
+ {msg.comments_replySubmit()}
129
+ </Button>
130
+ {/snippet}
131
+ </ActionLabel>
124
132
  </div>
125
133
  <FormErrorFeedback result={reply.result} messages={errorMessages} />
126
134
  </form>
@@ -148,7 +156,11 @@
148
156
  <input {...main.fields.weeklySlug.as('hidden', weeklySlug)} />
149
157
 
150
158
  <div class="actions">
151
- <Button type="submit" pending={main.pending}>{msg.comments_submit()}</Button>
159
+ <ActionLabel edit={config.messageEdit?.('comments_submit')} value={msg.comments_submit()}>
160
+ {#snippet control()}
161
+ <Button type="submit" pending={main.pending}>{msg.comments_submit()}</Button>
162
+ {/snippet}
163
+ </ActionLabel>
152
164
  </div>
153
165
 
154
166
  <FormErrorFeedback result={main.result} messages={errorMessages} />
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { getUiConfig } from '../../config/context.js';
3
3
  import type { SortDirection } from '../../content/types.js';
4
+ import ActionLabel from '../../edit/ActionLabel.svelte';
4
5
 
5
6
  interface Props {
6
7
  value: SortDirection;
@@ -18,13 +19,17 @@
18
19
  }
19
20
  </script>
20
21
 
21
- <label>
22
- <span>{msg.weeklies_sortLabel()}</span>
23
- <select class="control" {value} onchange={handleChange}>
24
- <option value="desc">{msg.weeklies_sortDesc()}</option>
25
- <option value="asc">{msg.weeklies_sortAsc()}</option>
26
- </select>
27
- </label>
22
+ <ActionLabel edit={config.messageEdit?.('weeklies_sortLabel')} value={msg.weeklies_sortLabel()}>
23
+ {#snippet control()}
24
+ <label>
25
+ <span>{msg.weeklies_sortLabel()}</span>
26
+ <select class="control" {value} onchange={handleChange}>
27
+ <option value="desc">{msg.weeklies_sortDesc()}</option>
28
+ <option value="asc">{msg.weeklies_sortAsc()}</option>
29
+ </select>
30
+ </label>
31
+ {/snippet}
32
+ </ActionLabel>
28
33
 
29
34
  <style>
30
35
  label {
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { getEditAdapter } from './context.js';
4
+ import Editable from './Editable.svelte';
5
+ import type { EditDescriptor } from './types.js';
6
+
7
+ /**
8
+ * The label of an INTERACTIVE control — a button, a nav link — made
9
+ * editable the way WeeklieCard's title is: while the adapter is editing
10
+ * and a descriptor is supplied, the control is REPLACED by its label as
11
+ * editable text, because a caret inside a live control would activate it
12
+ * (a link navigates mid-draft, a label focuses its input). The control
13
+ * comes back the moment editing turns off; with no descriptor or no
14
+ * adapter it renders untouched, byte-identical to a read-only build.
15
+ */
16
+ interface Props {
17
+ /** Undefined leaves the control alone — the inert default. */
18
+ edit: EditDescriptor | undefined;
19
+ /** The label text the control renders. */
20
+ value: string;
21
+ /** The real control, rendered whenever the label is not being edited. */
22
+ control: Snippet;
23
+ }
24
+
25
+ let { edit, value, control }: Props = $props();
26
+
27
+ const adapter = getEditAdapter();
28
+ const editing = $derived(edit !== undefined && (adapter?.isEditing ?? false));
29
+ </script>
30
+
31
+ {#if editing}
32
+ <Editable {edit} {value}>
33
+ {#snippet children(text, attrs)}<span class="action-label" {...attrs}>{text}</span>{/snippet}
34
+ </Editable>
35
+ {:else}
36
+ {@render control()}
37
+ {/if}
38
+
39
+ <style>
40
+ /* Roughly the control's footprint, so the swap doesn't collapse the row. */
41
+ .action-label {
42
+ display: inline-block;
43
+ padding: 0.2em 0.4em;
44
+ }
45
+ </style>
@@ -0,0 +1,22 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { EditDescriptor } from './types.js';
3
+ /**
4
+ * The label of an INTERACTIVE control — a button, a nav link — made
5
+ * editable the way WeeklieCard's title is: while the adapter is editing
6
+ * and a descriptor is supplied, the control is REPLACED by its label as
7
+ * editable text, because a caret inside a live control would activate it
8
+ * (a link navigates mid-draft, a label focuses its input). The control
9
+ * comes back the moment editing turns off; with no descriptor or no
10
+ * adapter it renders untouched, byte-identical to a read-only build.
11
+ */
12
+ interface Props {
13
+ /** Undefined leaves the control alone — the inert default. */
14
+ edit: EditDescriptor | undefined;
15
+ /** The label text the control renders. */
16
+ value: string;
17
+ /** The real control, rendered whenever the label is not being edited. */
18
+ control: Snippet;
19
+ }
20
+ declare const ActionLabel: import("svelte").Component<Props, {}, "">;
21
+ type ActionLabel = ReturnType<typeof ActionLabel>;
22
+ export default ActionLabel;
@@ -1,4 +1,5 @@
1
1
  export { getEditAdapter, setEditAdapter } from './context.js';
2
+ export { default as ActionLabel } from './ActionLabel.svelte';
2
3
  export { default as Editable } from './Editable.svelte';
3
4
  export { chromeEdit, entityEdit, pageCopyEdit } from './helpers.js';
4
5
  export { localize } from './types.js';
@@ -1,4 +1,5 @@
1
1
  export { getEditAdapter, setEditAdapter } from './context.js';
2
+ export { default as ActionLabel } from './ActionLabel.svelte';
2
3
  export { default as Editable } from './Editable.svelte';
3
4
  export { chromeEdit, entityEdit, pageCopyEdit } from './helpers.js';
4
5
  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';
18
+ export type EditableEntity = 'weeklies' | 'projects' | 'milestones' | 'team_members' | 'job_openings' | 'themes';
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vit-foundation/ui",
3
- "version": "0.5.0",
3
+ "version": "0.9.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",