@happyvertical/smrt-ui 0.51.39 → 0.52.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 (212) hide show
  1. package/AGENTS.md +14 -2
  2. package/README.md +257 -5
  3. package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
  4. package/dist/actions/swipe-dismiss.d.ts +42 -0
  5. package/dist/actions/swipe-dismiss.d.ts.map +1 -0
  6. package/dist/actions/swipe-dismiss.js +69 -0
  7. package/dist/components/calendar/Calendar.svelte +5 -0
  8. package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
  9. package/dist/components/calendar/CalendarView.svelte +1250 -0
  10. package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
  11. package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
  12. package/dist/components/calendar/DayView.svelte +5 -0
  13. package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
  14. package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
  15. package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
  16. package/dist/components/calendar/calendar-model.d.ts +178 -0
  17. package/dist/components/calendar/calendar-model.d.ts.map +1 -0
  18. package/dist/components/calendar/calendar-model.js +362 -0
  19. package/dist/components/calendar/index.d.ts +12 -1
  20. package/dist/components/calendar/index.d.ts.map +1 -1
  21. package/dist/components/calendar/index.js +11 -1
  22. package/dist/components/data/CollectionList.svelte +23 -5
  23. package/dist/components/data/CollectionList.svelte.d.ts +8 -2
  24. package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
  25. package/dist/components/data/DataTable.svelte +118 -40
  26. package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
  27. package/dist/components/data/DataTableController.d.ts +4 -0
  28. package/dist/components/data/DataTableController.d.ts.map +1 -1
  29. package/dist/components/data/DataTableController.js +11 -4
  30. package/dist/components/data/ListSortSelect.svelte +121 -0
  31. package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
  32. package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
  33. package/dist/components/data/SortableHeader.svelte +169 -0
  34. package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
  35. package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
  36. package/dist/components/data/__tests__/CollectionList.test.js +37 -0
  37. package/dist/components/data/__tests__/DataTable.test.js +76 -0
  38. package/dist/components/data/__tests__/DataTableController.test.js +35 -0
  39. package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
  40. package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
  41. package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
  42. package/dist/components/data/__tests__/list-sort.test.js +91 -0
  43. package/dist/components/data/__tests__/surface-navigation.test.js +121 -0
  44. package/dist/components/data/data-surface.d.ts +1 -0
  45. package/dist/components/data/data-surface.d.ts.map +1 -1
  46. package/dist/components/data/data-surface.js +1 -0
  47. package/dist/components/data/index.d.ts +6 -0
  48. package/dist/components/data/index.d.ts.map +1 -1
  49. package/dist/components/data/index.js +6 -0
  50. package/dist/components/data/link-surface.d.ts +79 -0
  51. package/dist/components/data/link-surface.d.ts.map +1 -0
  52. package/dist/components/data/link-surface.js +155 -0
  53. package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
  54. package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
  55. package/dist/components/data/list-sort-state.svelte.js +47 -0
  56. package/dist/components/data/list-sort.d.ts +94 -0
  57. package/dist/components/data/list-sort.d.ts.map +1 -0
  58. package/dist/components/data/list-sort.js +182 -0
  59. package/dist/components/data/step-surface.d.ts +73 -0
  60. package/dist/components/data/step-surface.d.ts.map +1 -0
  61. package/dist/components/data/step-surface.js +177 -0
  62. package/dist/components/data/surface-navigation.d.ts +49 -0
  63. package/dist/components/data/surface-navigation.d.ts.map +1 -0
  64. package/dist/components/data/surface-navigation.js +100 -0
  65. package/dist/components/data/types.d.ts +19 -0
  66. package/dist/components/data/types.d.ts.map +1 -1
  67. package/dist/components/feedback/PhoneSheet.svelte +224 -0
  68. package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
  69. package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
  70. package/dist/components/feedback/WorkingStrip.svelte +426 -0
  71. package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
  72. package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
  73. package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
  74. package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
  75. package/dist/components/feedback/index.d.ts +4 -0
  76. package/dist/components/feedback/index.d.ts.map +1 -1
  77. package/dist/components/feedback/index.js +5 -0
  78. package/dist/components/feedback/working-status.d.ts +34 -0
  79. package/dist/components/feedback/working-status.d.ts.map +1 -0
  80. package/dist/components/feedback/working-status.js +5 -0
  81. package/dist/components/forms/Checkbox.svelte +26 -2
  82. package/dist/components/forms/Combobox.svelte +68 -12
  83. package/dist/components/forms/Combobox.svelte.d.ts +6 -0
  84. package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
  85. package/dist/components/forms/DictationButton.svelte +128 -0
  86. package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
  87. package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
  88. package/dist/components/forms/DictationStatus.svelte +164 -0
  89. package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
  90. package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
  91. package/dist/components/forms/Fieldset.svelte +5 -1
  92. package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
  93. package/dist/components/forms/Fieldset.svelte.d.ts.map +1 -1
  94. package/dist/components/forms/FormActionBar.svelte +84 -0
  95. package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
  96. package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
  97. package/dist/components/forms/Input.svelte +9 -0
  98. package/dist/components/forms/Radio.svelte +15 -4
  99. package/dist/components/forms/SearchInput.svelte +207 -0
  100. package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
  101. package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
  102. package/dist/components/forms/Select.svelte +9 -0
  103. package/dist/components/forms/StagedControlReview.svelte +21 -12
  104. package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
  105. package/dist/components/forms/Switch.svelte +11 -5
  106. package/dist/components/forms/TagsInput.svelte +8 -1
  107. package/dist/components/forms/Textarea.svelte +8 -0
  108. package/dist/components/forms/__tests__/Combobox.test.js +87 -0
  109. package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
  110. package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
  111. package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
  112. package/dist/components/forms/__tests__/FormActionBar.test.js +22 -0
  113. package/dist/components/forms/__tests__/SearchInput.test.js +92 -0
  114. package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
  115. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +19 -0
  116. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
  117. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
  118. package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
  119. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
  120. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
  121. package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
  122. package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
  123. package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
  124. package/dist/components/forms/__tests__/dictation.test.js +486 -0
  125. package/dist/components/forms/__tests__/long-press.test.js +175 -0
  126. package/dist/components/forms/__tests__/place-picker.fixture.svelte +33 -0
  127. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
  128. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -0
  129. package/dist/components/forms/audio-capture.d.ts +69 -0
  130. package/dist/components/forms/audio-capture.d.ts.map +1 -0
  131. package/dist/components/forms/audio-capture.js +193 -0
  132. package/dist/components/forms/control-interaction.d.ts +6 -0
  133. package/dist/components/forms/control-interaction.d.ts.map +1 -1
  134. package/dist/components/forms/control-interaction.js +12 -1
  135. package/dist/components/forms/control-proposals.d.ts +55 -0
  136. package/dist/components/forms/control-proposals.d.ts.map +1 -0
  137. package/dist/components/forms/control-proposals.js +188 -0
  138. package/dist/components/forms/dictation-transcribe.d.ts +36 -0
  139. package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
  140. package/dist/components/forms/dictation-transcribe.js +65 -0
  141. package/dist/components/forms/dictation.svelte.d.ts +200 -0
  142. package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
  143. package/dist/components/forms/dictation.svelte.js +686 -0
  144. package/dist/components/forms/index.d.ts +14 -1
  145. package/dist/components/forms/index.d.ts.map +1 -1
  146. package/dist/components/forms/index.js +14 -1
  147. package/dist/components/forms/insert-text.d.ts +12 -0
  148. package/dist/components/forms/insert-text.d.ts.map +1 -0
  149. package/dist/components/forms/insert-text.js +38 -0
  150. package/dist/components/forms/long-press.d.ts +88 -0
  151. package/dist/components/forms/long-press.d.ts.map +1 -0
  152. package/dist/components/forms/long-press.js +213 -0
  153. package/dist/components/forms/ready-beep.d.ts +24 -0
  154. package/dist/components/forms/ready-beep.d.ts.map +1 -0
  155. package/dist/components/forms/ready-beep.js +82 -0
  156. package/dist/components/layout/PageHeader.svelte +151 -17
  157. package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
  158. package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
  159. package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
  160. package/dist/components/layout/index.d.ts +1 -0
  161. package/dist/components/layout/index.d.ts.map +1 -1
  162. package/dist/components/layout/index.js +1 -0
  163. package/dist/components/layout/page-header-context.d.ts +25 -0
  164. package/dist/components/layout/page-header-context.d.ts.map +1 -0
  165. package/dist/components/layout/page-header-context.js +16 -0
  166. package/dist/components/nav/Tabs.svelte +241 -0
  167. package/dist/components/nav/Tabs.svelte.d.ts +15 -0
  168. package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
  169. package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
  170. package/dist/components/nav/index.d.ts +1 -0
  171. package/dist/components/nav/index.d.ts.map +1 -1
  172. package/dist/components/nav/index.js +1 -0
  173. package/dist/components/nav/tabs-overflow.d.ts +12 -0
  174. package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
  175. package/dist/components/nav/tabs-overflow.js +17 -0
  176. package/dist/components/nav/types.d.ts +8 -0
  177. package/dist/components/nav/types.d.ts.map +1 -1
  178. package/dist/components/ui/Button.svelte +7 -0
  179. package/dist/components/ui/Card.svelte +14 -5
  180. package/dist/components/ui/Dropdown.svelte +55 -1
  181. package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
  182. package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
  183. package/dist/components/ui/IconToggle.svelte +139 -0
  184. package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
  185. package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
  186. package/dist/components/ui/IconToggleGroup.svelte +56 -0
  187. package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
  188. package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
  189. package/dist/components/ui/Pagination.svelte +1 -1
  190. package/dist/components/ui/Popover.svelte +17 -1
  191. package/dist/components/ui/Popover.svelte.d.ts +8 -0
  192. package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
  193. package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
  194. package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
  195. package/dist/components/ui/__tests__/Popover.test.js +44 -0
  196. package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
  197. package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
  198. package/dist/components/ui/index.d.ts +2 -0
  199. package/dist/components/ui/index.d.ts.map +1 -1
  200. package/dist/components/ui/index.js +2 -0
  201. package/dist/i18n/strings.ui.d.ts +41 -0
  202. package/dist/i18n/strings.ui.d.ts.map +1 -1
  203. package/dist/i18n/strings.ui.js +46 -0
  204. package/dist/svelte/__tests__/playground.test.js +1 -0
  205. package/dist/svelte/playground/CalendarPreview.svelte +140 -0
  206. package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
  207. package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
  208. package/dist/svelte/playground.d.ts.map +1 -1
  209. package/dist/svelte/playground.js +10 -0
  210. package/dist/types-generic.d.ts +2 -0
  211. package/dist/types-generic.d.ts.map +1 -1
  212. package/package.json +3 -3
@@ -1,41 +1,107 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * PageHeader - Standard page header with optional back navigation
4
- * refactored for Material 3
3
+ * PageHeader - one header per page: an ancestors-only breadcrumb row, the
4
+ * page title (`<h1>`), and the page's own actions, right-aligned.
5
5
  *
6
- * Provides consistent page header layout with title, optional back link,
7
- * and slot for action buttons.
6
+ * Page-header contract (shared with AdminShell in smrt-svelte/workspace):
7
+ * - `crumbs` are the page's ancestors only, never the page itself; none on
8
+ * section homes. Inside a shell that sets `setPageHeaderContext()`, pages
9
+ * pass only `parents` (ancestors the nav doesn't know, e.g. the article
10
+ * above its video) and the shell adds the rest; the shell also hears the
11
+ * title (for its phone top bar). smrt-svelte's `shellPageTrailFor()`
12
+ * computes such a trail from a nav.
13
+ * - The crumb row carries `data-shell-breadcrumbs` and the title
14
+ * `data-shell-page-title`: on phones AdminShell hides the crumbs and
15
+ * visually hides the title, because its phone top bar shows the back arrow
16
+ * and the title instead.
17
+ * - No in-page back link: the crumbs (and the phone top bar) are the way
18
+ * back. `backHref` is kept only for pages outside such a shell.
19
+ * - An editable title (`titleField`, e.g. an article's headline) renders
20
+ * inside the `<h1>`, so the heading is still the page's title in the
21
+ * outline and is named by the field's value, while the field keeps its own
22
+ * label. Such a heading has no `data-shell-page-title`: it stays visible on
23
+ * phones, because it is the input. Pass the live value as `title` so the
24
+ * shell (the phone top bar) follows along as the person types.
8
25
  */
9
26
  import type { Snippet } from 'svelte';
10
27
  import { ripple } from '../../actions/ripple.js';
11
28
  import { Icon } from '../display/index.js';
29
+ import {
30
+ getPageHeaderContext,
31
+ type PageHeaderCrumb,
32
+ } from './page-header-context.js';
12
33
 
13
34
  export interface Props {
14
35
  /** Page title */
15
36
  title: string;
16
37
  /** Optional subtitle/description */
17
38
  subtitle?: string;
18
- /** Back navigation URL */
39
+ /**
40
+ * All ancestors of this page, outermost first (never the page itself).
41
+ * Overrides the shell's trail; usually pass `parents` instead.
42
+ */
43
+ crumbs?: readonly PageHeaderCrumb[];
44
+ /** Ancestors past the shell's nav, outermost first. */
45
+ parents?: readonly PageHeaderCrumb[];
46
+ /** Shorter title for the shell (e.g. its phone top bar); default `title`. */
47
+ shortTitle?: string;
48
+ /** Accessible name of the breadcrumb nav. */
49
+ crumbsLabel?: string;
50
+ /**
51
+ * Back navigation URL.
52
+ * @deprecated Use `crumbs`; inside AdminShell the phone top bar goes back.
53
+ */
19
54
  backHref?: string;
20
55
  /** Back link label */
21
56
  backLabel?: string;
22
- /** Slot for action buttons */
57
+ /**
58
+ * An editable title rendered inside the `<h1>` in place of `title`'s text
59
+ * (a labelled text field styled as the heading). `title` still names the
60
+ * page for the shell; keep it in sync with the field's value.
61
+ */
62
+ titleField?: Snippet;
63
+ /** Slot for action buttons (right-aligned beside the title) */
23
64
  actions?: Snippet;
24
- /** Slot for additional content below title */
65
+ /** Small line under the title (status, dates, counts) */
66
+ meta?: Snippet;
67
+ /** Slot for additional content below the title row (e.g. page tabs) */
25
68
  children?: Snippet;
26
69
  }
27
70
 
28
71
  const {
29
72
  title,
30
73
  subtitle,
74
+ crumbs,
75
+ parents = [],
76
+ shortTitle,
77
+ crumbsLabel = 'Breadcrumb',
31
78
  backHref,
32
79
  backLabel = 'Back',
80
+ titleField,
33
81
  actions,
82
+ meta,
34
83
  children,
35
84
  }: Props = $props();
85
+
86
+ const shell = getPageHeaderContext();
87
+ const trail = $derived<readonly PageHeaderCrumb[]>(
88
+ crumbs ?? (shell ? shell.crumbs(parents) : parents),
89
+ );
90
+
91
+ $effect(() => shell?.report?.({ title: shortTitle ?? title, parents }));
36
92
  </script>
37
93
 
38
- <header class="page-header">
94
+ <header class="page-header" data-page-header>
95
+ {#if trail.length > 0}
96
+ <nav class="page-crumbs" aria-label={crumbsLabel} data-shell-breadcrumbs>
97
+ <ol>
98
+ {#each trail as crumb, index (`${index}:${crumb.href}`)}
99
+ <li><a href={crumb.href}>{crumb.label}</a></li>
100
+ {/each}
101
+ </ol>
102
+ </nav>
103
+ {/if}
104
+
39
105
  <div class="header-main">
40
106
  <div class="header-content">
41
107
  {#if backHref}
@@ -44,10 +110,19 @@ const {
44
110
  <span>{backLabel}</span>
45
111
  </a>
46
112
  {/if}
47
- <h1 class="page-title">{title}</h1>
113
+ {#if titleField}
114
+ <h1 class="page-title page-title-field" data-page-title-field>
115
+ {@render titleField()}
116
+ </h1>
117
+ {:else}
118
+ <h1 class="page-title" data-shell-page-title>{title}</h1>
119
+ {/if}
48
120
  {#if subtitle}
49
121
  <p class="page-subtitle">{subtitle}</p>
50
122
  {/if}
123
+ {#if meta}
124
+ <div class="page-meta">{@render meta()}</div>
125
+ {/if}
51
126
  </div>
52
127
 
53
128
  {#if actions}
@@ -58,7 +133,7 @@ const {
58
133
  </div>
59
134
 
60
135
  {#if children}
61
- <div class="header-extra">
136
+ <div class="header-extra" data-page-header-extra>
62
137
  {@render children()}
63
138
  </div>
64
139
  {/if}
@@ -66,15 +141,60 @@ const {
66
141
 
67
142
  <style>
68
143
  .page-header {
69
- margin-bottom: 2rem;
70
- padding-top: 1rem;
144
+ margin-bottom: 1.5rem;
145
+ }
146
+
147
+ .page-crumbs {
148
+ margin-bottom: var(--smrt-spacing-1, 4px);
149
+ color: var(--smrt-color-on-surface-variant);
150
+ font: var(--smrt-typography-body-small-font);
151
+ font-size: 0.84rem;
152
+ }
153
+
154
+ .page-crumbs ol {
155
+ display: flex;
156
+ flex-wrap: wrap;
157
+ align-items: center;
158
+ gap: 0.35rem;
159
+ margin: 0;
160
+ padding: 0;
161
+ list-style: none;
162
+ }
163
+
164
+ .page-crumbs li {
165
+ display: flex;
166
+ align-items: center;
167
+ min-width: 0;
168
+ }
169
+
170
+ .page-crumbs li + li::before {
171
+ content: '/';
172
+ margin-inline-end: 0.35rem;
173
+ color: var(--smrt-color-outline);
174
+ }
175
+
176
+ .page-crumbs a {
177
+ color: inherit;
178
+ text-decoration: none;
179
+ overflow-wrap: anywhere;
180
+ }
181
+
182
+ .page-crumbs a:hover {
183
+ color: var(--smrt-color-primary);
184
+ text-decoration: underline;
185
+ }
186
+
187
+ .page-crumbs a:focus-visible {
188
+ outline: 2px solid var(--smrt-color-primary);
189
+ outline-offset: 2px;
190
+ border-radius: var(--smrt-radius-sm, 4px);
71
191
  }
72
192
 
73
193
  .header-main {
74
194
  display: flex;
75
195
  justify-content: space-between;
76
196
  align-items: flex-start;
77
- gap: 1.5rem;
197
+ gap: 0.75rem 1.5rem;
78
198
  flex-wrap: wrap;
79
199
  }
80
200
 
@@ -83,7 +203,7 @@ const {
83
203
  flex-direction: column;
84
204
  gap: var(--smrt-spacing-1, 4px);
85
205
  flex: 1;
86
- min-width: 200px;
206
+ min-width: min(100%, 200px);
87
207
  }
88
208
 
89
209
  .back-link {
@@ -109,13 +229,26 @@ const {
109
229
  color: var(--smrt-color-on-surface);
110
230
  margin: 0;
111
231
  letter-spacing: -0.5px;
232
+ overflow-wrap: anywhere;
233
+ }
234
+
235
+ .page-title-field {
236
+ min-width: 0;
112
237
  }
113
238
 
114
239
  .page-subtitle {
115
240
  font: var(--smrt-typography-body-medium-font);
116
241
  color: var(--smrt-color-on-surface-variant);
117
242
  margin: 0;
118
- margin-top: var(--smrt-spacing-1, 4px);
243
+ }
244
+
245
+ .page-meta {
246
+ display: flex;
247
+ flex-wrap: wrap;
248
+ align-items: center;
249
+ gap: 0.5rem;
250
+ font: var(--smrt-typography-body-small-font);
251
+ color: var(--smrt-color-on-surface-variant);
119
252
  }
120
253
 
121
254
  .header-actions {
@@ -123,9 +256,10 @@ const {
123
256
  gap: 0.75rem;
124
257
  align-items: center;
125
258
  flex-wrap: wrap;
259
+ margin-inline-start: auto;
126
260
  }
127
261
 
128
262
  .header-extra {
129
- margin-top: 1.5rem;
263
+ margin-top: 1rem;
130
264
  }
131
- </style>
265
+ </style>
@@ -1,23 +1,63 @@
1
1
  /**
2
- * PageHeader - Standard page header with optional back navigation
3
- * refactored for Material 3
2
+ * PageHeader - one header per page: an ancestors-only breadcrumb row, the
3
+ * page title (`<h1>`), and the page's own actions, right-aligned.
4
4
  *
5
- * Provides consistent page header layout with title, optional back link,
6
- * and slot for action buttons.
5
+ * Page-header contract (shared with AdminShell in smrt-svelte/workspace):
6
+ * - `crumbs` are the page's ancestors only, never the page itself; none on
7
+ * section homes. Inside a shell that sets `setPageHeaderContext()`, pages
8
+ * pass only `parents` (ancestors the nav doesn't know, e.g. the article
9
+ * above its video) and the shell adds the rest; the shell also hears the
10
+ * title (for its phone top bar). smrt-svelte's `shellPageTrailFor()`
11
+ * computes such a trail from a nav.
12
+ * - The crumb row carries `data-shell-breadcrumbs` and the title
13
+ * `data-shell-page-title`: on phones AdminShell hides the crumbs and
14
+ * visually hides the title, because its phone top bar shows the back arrow
15
+ * and the title instead.
16
+ * - No in-page back link: the crumbs (and the phone top bar) are the way
17
+ * back. `backHref` is kept only for pages outside such a shell.
18
+ * - An editable title (`titleField`, e.g. an article's headline) renders
19
+ * inside the `<h1>`, so the heading is still the page's title in the
20
+ * outline and is named by the field's value, while the field keeps its own
21
+ * label. Such a heading has no `data-shell-page-title`: it stays visible on
22
+ * phones, because it is the input. Pass the live value as `title` so the
23
+ * shell (the phone top bar) follows along as the person types.
7
24
  */
8
25
  import type { Snippet } from 'svelte';
26
+ import { type PageHeaderCrumb } from './page-header-context.js';
9
27
  export interface Props {
10
28
  /** Page title */
11
29
  title: string;
12
30
  /** Optional subtitle/description */
13
31
  subtitle?: string;
14
- /** Back navigation URL */
32
+ /**
33
+ * All ancestors of this page, outermost first (never the page itself).
34
+ * Overrides the shell's trail; usually pass `parents` instead.
35
+ */
36
+ crumbs?: readonly PageHeaderCrumb[];
37
+ /** Ancestors past the shell's nav, outermost first. */
38
+ parents?: readonly PageHeaderCrumb[];
39
+ /** Shorter title for the shell (e.g. its phone top bar); default `title`. */
40
+ shortTitle?: string;
41
+ /** Accessible name of the breadcrumb nav. */
42
+ crumbsLabel?: string;
43
+ /**
44
+ * Back navigation URL.
45
+ * @deprecated Use `crumbs`; inside AdminShell the phone top bar goes back.
46
+ */
15
47
  backHref?: string;
16
48
  /** Back link label */
17
49
  backLabel?: string;
18
- /** Slot for action buttons */
50
+ /**
51
+ * An editable title rendered inside the `<h1>` in place of `title`'s text
52
+ * (a labelled text field styled as the heading). `title` still names the
53
+ * page for the shell; keep it in sync with the field's value.
54
+ */
55
+ titleField?: Snippet;
56
+ /** Slot for action buttons (right-aligned beside the title) */
19
57
  actions?: Snippet;
20
- /** Slot for additional content below title */
58
+ /** Small line under the title (status, dates, counts) */
59
+ meta?: Snippet;
60
+ /** Slot for additional content below the title row (e.g. page tabs) */
21
61
  children?: Snippet;
22
62
  }
23
63
  declare const PageHeader: import("svelte").Component<Props, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"PageHeader.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/layout/PageHeader.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKtC,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sBAAsB;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAkDD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"PageHeader.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/layout/PageHeader.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAGlC,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACpC,uDAAuD;IACvD,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACrC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sBAAsB;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAoFD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -5,7 +5,7 @@
5
5
  * title, optional subtitle, optional back link, and actions/children snippets.
6
6
  */
7
7
  import { render, screen } from '@testing-library/svelte';
8
- import { createRawSnippet } from 'svelte';
8
+ import { createRawSnippet, flushSync } from 'svelte';
9
9
  import { describe, expect, it } from 'vitest';
10
10
  import { expectNoA11yViolations } from '../../../test-support/a11y';
11
11
  import PageHeader from '../PageHeader.svelte';
@@ -66,6 +66,92 @@ describe('PageHeader', () => {
66
66
  render(PageHeader, { props: { title: 'Detail' } });
67
67
  expect(screen.queryByRole('link')).not.toBeInTheDocument();
68
68
  });
69
+ it('renders ancestors-only crumbs above the title', () => {
70
+ render(PageHeader, {
71
+ props: {
72
+ title: 'Fair opens Friday',
73
+ crumbs: [
74
+ { label: 'Alpha Times', href: '/sites/alpha' },
75
+ { label: 'Content', href: '/sites/alpha/articles' },
76
+ ],
77
+ },
78
+ });
79
+ const nav = screen.getByRole('navigation', { name: 'Breadcrumb' });
80
+ expect(nav).toHaveAttribute('data-shell-breadcrumbs');
81
+ const links = nav.querySelectorAll('a');
82
+ expect([...links].map((a) => a.textContent)).toEqual([
83
+ 'Alpha Times',
84
+ 'Content',
85
+ ]);
86
+ // The current page is the heading, not a crumb.
87
+ expect(nav).not.toHaveTextContent('Fair opens Friday');
88
+ expect(screen.getByRole('heading', { level: 1, name: 'Fair opens Friday' })).toHaveAttribute('data-shell-page-title');
89
+ });
90
+ it('renders no crumb row without ancestors', () => {
91
+ render(PageHeader, { props: { title: 'Content', crumbs: [] } });
92
+ expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
93
+ });
94
+ it('renders the meta snippet under the title', () => {
95
+ render(PageHeader, {
96
+ props: { title: 'Detail', meta: snippet('<span>Draft</span>') },
97
+ });
98
+ expect(screen.getByText('Draft')).toBeInTheDocument();
99
+ });
100
+ it('takes its trail from the shell and reports its title', () => {
101
+ const reports = [];
102
+ const context = new Map([
103
+ [
104
+ Symbol.for('smrt-ui.page-header'),
105
+ {
106
+ crumbs: (parents) => [
107
+ { label: 'Alpha Times', href: '/sites/alpha' },
108
+ ...parents,
109
+ ],
110
+ report: (page) => {
111
+ reports.push(page);
112
+ return undefined;
113
+ },
114
+ },
115
+ ],
116
+ ]);
117
+ render(PageHeader, {
118
+ props: {
119
+ title: 'Create video',
120
+ parents: [{ label: 'Videos', href: '/sites/alpha/articles/a1/videos' }],
121
+ },
122
+ context,
123
+ });
124
+ const nav = screen.getByRole('navigation', { name: 'Breadcrumb' });
125
+ expect([...nav.querySelectorAll('a')].map((a) => a.textContent)).toEqual([
126
+ 'Alpha Times',
127
+ 'Videos',
128
+ ]);
129
+ flushSync();
130
+ expect(reports).toEqual([
131
+ {
132
+ title: 'Create video',
133
+ parents: [{ label: 'Videos', href: '/sites/alpha/articles/a1/videos' }],
134
+ },
135
+ ]);
136
+ });
137
+ it('renders an editable title inside the h1 and keeps it visible on phones', async () => {
138
+ const { container } = render(PageHeader, {
139
+ props: {
140
+ title: 'Council passes budget',
141
+ titleField: snippet('<input name="title" aria-label="Title" value="Council passes budget" />'),
142
+ },
143
+ });
144
+ const heading = screen.getByRole('heading', { level: 1 });
145
+ const field = screen.getByRole('textbox', { name: 'Title' });
146
+ expect(heading).toContainElement(field);
147
+ // The heading is named by the field's value; the field keeps its label.
148
+ expect(heading).toHaveAccessibleName('Council passes budget');
149
+ // No data-shell-page-title: the shell must not hide the input on phones.
150
+ expect(heading).not.toHaveAttribute('data-shell-page-title');
151
+ expect(heading).toHaveAttribute('data-page-title-field');
152
+ expect(container.querySelectorAll('h1')).toHaveLength(1);
153
+ await expectNoA11yViolations(container);
154
+ });
69
155
  it('is axe-clean with subtitle, back link, and actions', async () => {
70
156
  const { container } = render(PageHeader, {
71
157
  props: {
@@ -8,5 +8,6 @@ export { default as Grid } from './Grid.svelte';
8
8
  export { default as Header } from './Header.svelte';
9
9
  export { default as Masthead } from './Masthead.svelte';
10
10
  export { default as PageHeader } from './PageHeader.svelte';
11
+ export { getPageHeaderContext, type PageHeaderContext, type PageHeaderCrumb, type PageHeaderReport, setPageHeaderContext, } from './page-header-context.js';
11
12
  export { default as SummaryCard } from './SummaryCard.svelte';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/layout/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/layout/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EACL,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,oBAAoB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
@@ -8,4 +8,5 @@ export { default as Grid } from './Grid.svelte';
8
8
  export { default as Header } from './Header.svelte';
9
9
  export { default as Masthead } from './Masthead.svelte';
10
10
  export { default as PageHeader } from './PageHeader.svelte';
11
+ export { getPageHeaderContext, setPageHeaderContext, } from './page-header-context.js';
11
12
  export { default as SummaryCard } from './SummaryCard.svelte';
@@ -0,0 +1,25 @@
1
+ /** One ancestor of a page. */
2
+ export interface PageHeaderCrumb {
3
+ label: string;
4
+ href: string;
5
+ }
6
+ /** What a page reports to its shell. */
7
+ export interface PageHeaderReport {
8
+ title: string;
9
+ parents: readonly PageHeaderCrumb[];
10
+ }
11
+ export interface PageHeaderContext {
12
+ /**
13
+ * The page's ancestors (never the page itself), given the ancestors it
14
+ * knows beyond the shell's nav. Called inside a reactive derivation, so
15
+ * the shell may read reactive state (the current path, its nav).
16
+ */
17
+ crumbs(parents: readonly PageHeaderCrumb[]): PageHeaderCrumb[];
18
+ /** Hear the page's title and parents; return a cleanup for when it goes. */
19
+ report?(page: PageHeaderReport): (() => void) | undefined;
20
+ }
21
+ /** Called by a shell layout; applies to every `PageHeader` below it. */
22
+ export declare function setPageHeaderContext(context: PageHeaderContext): void;
23
+ /** The enclosing shell's page-header context, or null. */
24
+ export declare function getPageHeaderContext(): PageHeaderContext | null;
25
+ //# sourceMappingURL=page-header-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-header-context.d.ts","sourceRoot":"","sources":["../../../src/components/layout/page-header-context.ts"],"names":[],"mappings":"AAQA,8BAA8B;AAC9B,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wCAAwC;AACxC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,eAAe,EAAE,CAAC;IAC/D,4EAA4E;IAC5E,MAAM,CAAC,CAAC,IAAI,EAAE,gBAAgB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CAC3D;AAID,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAErE;AAED,0DAA0D;AAC1D,wBAAgB,oBAAoB,IAAI,iBAAiB,GAAG,IAAI,CAE/D"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How a shell (an app's workspace layout) takes part in every `PageHeader`
3
+ * below it, without the pages or the packages that render them knowing the
4
+ * shell: it turns a page's own `parents` into the full ancestor trail (from
5
+ * its nav) and hears each page's title, e.g. for a phone top bar.
6
+ */
7
+ import { getContext, setContext } from 'svelte';
8
+ const PAGE_HEADER_CONTEXT = Symbol.for('smrt-ui.page-header');
9
+ /** Called by a shell layout; applies to every `PageHeader` below it. */
10
+ export function setPageHeaderContext(context) {
11
+ setContext(PAGE_HEADER_CONTEXT, context);
12
+ }
13
+ /** The enclosing shell's page-header context, or null. */
14
+ export function getPageHeaderContext() {
15
+ return getContext(PAGE_HEADER_CONTEXT) ?? null;
16
+ }