@slotted/styles 0.0.0-stage → 0.1.0-beta.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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/package.json +276 -3
  4. package/src/accordion/accordion.css +96 -0
  5. package/src/accordion/accordion.tokens.json +14 -0
  6. package/src/action-bar/action-bar.css +455 -0
  7. package/src/action-bar/action-bar.tokens.json +50 -0
  8. package/src/action-sheet/action-sheet.css +462 -0
  9. package/src/action-sheet/action-sheet.tokens.json +59 -0
  10. package/src/alert/alert.css +764 -0
  11. package/src/alert/alert.tokens.json +77 -0
  12. package/src/app-shell/app-shell.css +1899 -0
  13. package/src/app-shell/app-shell.tokens.json +88 -0
  14. package/src/aspect-ratio/aspect-ratio.css +131 -0
  15. package/src/aspect-ratio/aspect-ratio.tokens.json +1 -0
  16. package/src/attachment/attachment.css +906 -0
  17. package/src/attachment/attachment.tokens.json +85 -0
  18. package/src/avatar/avatar.css +598 -0
  19. package/src/avatar/avatar.tokens.json +64 -0
  20. package/src/avatar-group/avatar-group.css +268 -0
  21. package/src/avatar-group/avatar-group.tokens.json +16 -0
  22. package/src/badge/badge.css +462 -0
  23. package/src/badge/badge.tokens.json +58 -0
  24. package/src/breadcrumb/breadcrumb.css +441 -0
  25. package/src/breadcrumb/breadcrumb.tokens.json +50 -0
  26. package/src/button/button.css +754 -0
  27. package/src/button/button.tokens.json +72 -0
  28. package/src/button-group/button-group.css +455 -0
  29. package/src/button-group/button-group.tokens.json +11 -0
  30. package/src/calendar/calendar.css +846 -0
  31. package/src/calendar/calendar.tokens.json +67 -0
  32. package/src/card/card.css +602 -0
  33. package/src/card/card.tokens.json +68 -0
  34. package/src/carousel/carousel.css +860 -0
  35. package/src/carousel/carousel.tokens.json +62 -0
  36. package/src/cascader/cascader.css +1320 -0
  37. package/src/cascader/cascader.tokens.json +103 -0
  38. package/src/chart/chart.css +1473 -0
  39. package/src/chart/chart.tokens.json +107 -0
  40. package/src/checkbox/checkbox.css +1011 -0
  41. package/src/checkbox/checkbox.tokens.json +102 -0
  42. package/src/checkbox-group/checkbox-group.css +70 -0
  43. package/src/checkbox-group/checkbox-group.tokens.json +6 -0
  44. package/src/code-block/code-block.css +555 -0
  45. package/src/code-block/code-block.tokens.json +69 -0
  46. package/src/collapsible/collapsible.css +546 -0
  47. package/src/collapsible/collapsible.tokens.json +55 -0
  48. package/src/color-picker/color-picker.css +607 -0
  49. package/src/color-picker/color-picker.tokens.json +58 -0
  50. package/src/column-manager/column-manager.css +452 -0
  51. package/src/column-manager/column-manager.tokens.json +43 -0
  52. package/src/combobox/combobox.css +1048 -0
  53. package/src/combobox/combobox.tokens.json +112 -0
  54. package/src/command-palette/command-palette.css +1000 -0
  55. package/src/command-palette/command-palette.tokens.json +75 -0
  56. package/src/container/container.css +198 -0
  57. package/src/container/container.tokens.json +18 -0
  58. package/src/context-menu/context-menu.css +104 -0
  59. package/src/context-menu/context-menu.tokens.json +11 -0
  60. package/src/currency-field/currency-field.css +787 -0
  61. package/src/currency-field/currency-field.tokens.json +80 -0
  62. package/src/dashboard-grid/dashboard-grid.css +748 -0
  63. package/src/dashboard-grid/dashboard-grid.tokens.json +67 -0
  64. package/src/data-grid/data-grid.css +2484 -0
  65. package/src/data-grid/data-grid.tokens.json +100 -0
  66. package/src/data-list/data-list.css +455 -0
  67. package/src/data-list/data-list.tokens.json +35 -0
  68. package/src/date-field/date-field.css +560 -0
  69. package/src/date-field/date-field.tokens.json +80 -0
  70. package/src/date-picker/date-picker.css +420 -0
  71. package/src/date-picker/date-picker.tokens.json +49 -0
  72. package/src/date-range-picker/date-range-picker.css +983 -0
  73. package/src/date-range-picker/date-range-picker.tokens.json +109 -0
  74. package/src/datetime-field/datetime-field.css +850 -0
  75. package/src/datetime-field/datetime-field.tokens.json +91 -0
  76. package/src/datetime-picker/datetime-picker.css +843 -0
  77. package/src/datetime-picker/datetime-picker.tokens.json +78 -0
  78. package/src/datetime-range-picker/datetime-range-picker.css +1375 -0
  79. package/src/datetime-range-picker/datetime-range-picker.tokens.json +135 -0
  80. package/src/description-list/description-list.css +675 -0
  81. package/src/description-list/description-list.tokens.json +66 -0
  82. package/src/detail-panel/detail-panel.css +329 -0
  83. package/src/detail-panel/detail-panel.tokens.json +33 -0
  84. package/src/dialog/dialog.css +968 -0
  85. package/src/dialog/dialog.tokens.json +66 -0
  86. package/src/divider/divider.css +131 -0
  87. package/src/divider/divider.tokens.json +18 -0
  88. package/src/drawer/drawer.css +1303 -0
  89. package/src/drawer/drawer.tokens.json +75 -0
  90. package/src/entity-picker/entity-picker.css +1367 -0
  91. package/src/entity-picker/entity-picker.tokens.json +118 -0
  92. package/src/event-calendar/event-calendar.css +2148 -0
  93. package/src/event-calendar/event-calendar.tokens.json +87 -0
  94. package/src/field/field.css +707 -0
  95. package/src/field/field.tokens.json +58 -0
  96. package/src/field-group/field-group.css +226 -0
  97. package/src/field-group/field-group.tokens.json +33 -0
  98. package/src/filter-bar/filter-bar.css +1306 -0
  99. package/src/filter-bar/filter-bar.tokens.json +78 -0
  100. package/src/form/form.css +218 -0
  101. package/src/form/form.tokens.json +32 -0
  102. package/src/fullscreen/fullscreen.css +376 -0
  103. package/src/fullscreen/fullscreen.tokens.json +50 -0
  104. package/src/grid/grid.css +173 -0
  105. package/src/grid/grid.tokens.json +20 -0
  106. package/src/highlight/highlight.css +135 -0
  107. package/src/highlight/highlight.tokens.json +16 -0
  108. package/src/hover-card/hover-card.css +93 -0
  109. package/src/hover-card/hover-card.tokens.json +17 -0
  110. package/src/icon/icon.css +136 -0
  111. package/src/icon/icon.tokens.json +14 -0
  112. package/src/image/image.css +449 -0
  113. package/src/image/image.tokens.json +27 -0
  114. package/src/import-export-center/import-export-center.css +1175 -0
  115. package/src/import-export-center/import-export-center.tokens.json +84 -0
  116. package/src/inline/inline.css +149 -0
  117. package/src/inline/inline.tokens.json +14 -0
  118. package/src/inline-message/inline-message.css +396 -0
  119. package/src/inline-message/inline-message.tokens.json +19 -0
  120. package/src/kbd/kbd.css +490 -0
  121. package/src/kbd/kbd.tokens.json +40 -0
  122. package/src/link/link.css +237 -0
  123. package/src/link/link.tokens.json +25 -0
  124. package/src/list/list.css +240 -0
  125. package/src/list/list.tokens.json +42 -0
  126. package/src/list-item/list-item.css +937 -0
  127. package/src/list-item/list-item.tokens.json +114 -0
  128. package/src/listbox/listbox.css +674 -0
  129. package/src/listbox/listbox.tokens.json +72 -0
  130. package/src/loading-overlay/loading-overlay.css +730 -0
  131. package/src/loading-overlay/loading-overlay.tokens.json +48 -0
  132. package/src/markdown/markdown.css +168 -0
  133. package/src/markdown/markdown.tokens.json +18 -0
  134. package/src/menu/menu.css +817 -0
  135. package/src/menu/menu.tokens.json +78 -0
  136. package/src/menubar/menubar.css +170 -0
  137. package/src/menubar/menubar.tokens.json +32 -0
  138. package/src/message/message.css +1453 -0
  139. package/src/message/message.tokens.json +85 -0
  140. package/src/message-scroller/message-scroller.css +371 -0
  141. package/src/message-scroller/message-scroller.tokens.json +47 -0
  142. package/src/multi-select/multi-select.css +1427 -0
  143. package/src/multi-select/multi-select.tokens.json +114 -0
  144. package/src/navigation-menu/navigation-menu.css +1029 -0
  145. package/src/navigation-menu/navigation-menu.tokens.json +70 -0
  146. package/src/notification-center/notification-center.css +856 -0
  147. package/src/notification-center/notification-center.tokens.json +84 -0
  148. package/src/number-field/number-field.css +794 -0
  149. package/src/number-field/number-field.tokens.json +80 -0
  150. package/src/otp-input/otp-input.css +525 -0
  151. package/src/otp-input/otp-input.tokens.json +69 -0
  152. package/src/page/page.css +179 -0
  153. package/src/page/page.tokens.json +20 -0
  154. package/src/page-header/page-header.css +1181 -0
  155. package/src/page-header/page-header.tokens.json +71 -0
  156. package/src/pagination/pagination.css +500 -0
  157. package/src/pagination/pagination.tokens.json +56 -0
  158. package/src/popover/popover.css +526 -0
  159. package/src/popover/popover.tokens.json +60 -0
  160. package/src/progress-indicator/progress-indicator.css +1584 -0
  161. package/src/progress-indicator/progress-indicator.tokens.json +50 -0
  162. package/src/prompt-composer/prompt-composer.css +680 -0
  163. package/src/prompt-composer/prompt-composer.tokens.json +99 -0
  164. package/src/prose/prose.css +733 -0
  165. package/src/prose/prose.tokens.json +79 -0
  166. package/src/radio-group/radio-group.css +778 -0
  167. package/src/radio-group/radio-group.tokens.json +103 -0
  168. package/src/rating/rating.css +1071 -0
  169. package/src/rating/rating.tokens.json +88 -0
  170. package/src/result-state/result-state.css +980 -0
  171. package/src/result-state/result-state.tokens.json +63 -0
  172. package/src/runtime-properties.mjs +321 -0
  173. package/src/saved-views/saved-views.css +394 -0
  174. package/src/saved-views/saved-views.tokens.json +41 -0
  175. package/src/scroll-area/scroll-area.css +481 -0
  176. package/src/scroll-area/scroll-area.tokens.json +15 -0
  177. package/src/section/section.css +177 -0
  178. package/src/section/section.tokens.json +39 -0
  179. package/src/segmented-control/segmented-control.css +564 -0
  180. package/src/segmented-control/segmented-control.tokens.json +67 -0
  181. package/src/select/select.css +1593 -0
  182. package/src/select/select.tokens.json +110 -0
  183. package/src/side-navigation/side-navigation.css +1096 -0
  184. package/src/side-navigation/side-navigation.tokens.json +87 -0
  185. package/src/skeleton/skeleton.css +431 -0
  186. package/src/skeleton/skeleton.tokens.json +29 -0
  187. package/src/slider/slider.css +792 -0
  188. package/src/slider/slider.tokens.json +75 -0
  189. package/src/sparkline/sparkline.css +282 -0
  190. package/src/sparkline/sparkline.tokens.json +45 -0
  191. package/src/spinner/spinner.css +224 -0
  192. package/src/spinner/spinner.tokens.json +8 -0
  193. package/src/split-button/split-button.css +253 -0
  194. package/src/split-button/split-button.tokens.json +12 -0
  195. package/src/splitter/splitter.css +336 -0
  196. package/src/splitter/splitter.tokens.json +24 -0
  197. package/src/stack/stack.css +128 -0
  198. package/src/stack/stack.tokens.json +14 -0
  199. package/src/stat-card/stat-card.css +641 -0
  200. package/src/stat-card/stat-card.tokens.json +95 -0
  201. package/src/status-indicator/status-indicator.css +481 -0
  202. package/src/status-indicator/status-indicator.tokens.json +38 -0
  203. package/src/stepper/stepper.css +629 -0
  204. package/src/stepper/stepper.tokens.json +72 -0
  205. package/src/switch/switch.css +703 -0
  206. package/src/switch/switch.tokens.json +75 -0
  207. package/src/table/table.css +1482 -0
  208. package/src/table/table.tokens.json +69 -0
  209. package/src/table-of-contents/table-of-contents.css +533 -0
  210. package/src/table-of-contents/table-of-contents.tokens.json +53 -0
  211. package/src/tabs/tabs.css +998 -0
  212. package/src/tabs/tabs.tokens.json +82 -0
  213. package/src/tag/tag.css +768 -0
  214. package/src/tag/tag.tokens.json +85 -0
  215. package/src/tags-input/tags-input.css +757 -0
  216. package/src/tags-input/tags-input.tokens.json +84 -0
  217. package/src/text-field/text-field.css +900 -0
  218. package/src/text-field/text-field.tokens.json +87 -0
  219. package/src/textarea/textarea.css +365 -0
  220. package/src/textarea/textarea.tokens.json +59 -0
  221. package/src/time-field/time-field.css +719 -0
  222. package/src/time-field/time-field.tokens.json +86 -0
  223. package/src/time-picker/time-picker.css +665 -0
  224. package/src/time-picker/time-picker.tokens.json +66 -0
  225. package/src/time-range-picker/time-range-picker.css +1287 -0
  226. package/src/time-range-picker/time-range-picker.tokens.json +134 -0
  227. package/src/timeline/timeline.css +1596 -0
  228. package/src/timeline/timeline.tokens.json +96 -0
  229. package/src/toast/toast.css +943 -0
  230. package/src/toast/toast.tokens.json +90 -0
  231. package/src/toggle/toggle.css +290 -0
  232. package/src/toggle/toggle.tokens.json +54 -0
  233. package/src/toggle-group/toggle-group.css +463 -0
  234. package/src/toggle-group/toggle-group.tokens.json +54 -0
  235. package/src/toolbar/toolbar.css +434 -0
  236. package/src/toolbar/toolbar.tokens.json +49 -0
  237. package/src/tooltip/tooltip.css +368 -0
  238. package/src/tooltip/tooltip.tokens.json +39 -0
  239. package/src/top-navigation/top-navigation.css +1287 -0
  240. package/src/top-navigation/top-navigation.tokens.json +91 -0
  241. package/src/tour/tour.css +680 -0
  242. package/src/tour/tour.tokens.json +67 -0
  243. package/src/transfer/transfer.css +571 -0
  244. package/src/transfer/transfer.tokens.json +49 -0
  245. package/src/tree/tree.css +1000 -0
  246. package/src/tree/tree.tokens.json +80 -0
  247. package/src/typography/typography.css +211 -0
  248. package/src/typography/typography.tokens.json +31 -0
  249. package/src/upload/upload.css +1706 -0
  250. package/src/upload/upload.tokens.json +123 -0
  251. package/src/visually-hidden/visually-hidden.css +52 -0
  252. package/src/visually-hidden/visually-hidden.tokens.json +1 -0
  253. package/src/wizard/wizard.css +740 -0
  254. package/src/wizard/wizard.tokens.json +63 -0
@@ -0,0 +1,1181 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * The shell of a screen's top: a title area that never leaves, a header
4
+ * area that scrolls away under it and comes back, a pill of controls on the
5
+ * chrome's edge, and a slot for tabs. SAP Fiori's Dynamic Page, rebuilt on
6
+ * one sticky root.
7
+ *
8
+ * ┌──────────────────────────────────────────────┐
9
+ * │ title-area (sticky at 0 inside the root) │
10
+ * │ nav ▸ back · breadcrumbs │
11
+ * │ title-row ▸ visual · title · actions · nav │
12
+ * ├──────────────────────────────────────────────┤
13
+ * │ header-area (scrolls under the title area) │
14
+ * │ figure · content ▸ description … │
15
+ * ├─────────────────── controls ─────────────────┤ ← zero-height row, the rule, the pill
16
+ * │ tabs │
17
+ * └──────────────────────────────────────────────┘
18
+ *
19
+ * ── How it collapses ─────────────────────────────────────────────────────
20
+ *
21
+ * The root is `position: sticky` with a negative offset the size of the
22
+ * header area, measured by the binding into `--slotted-page-header-collapse`.
23
+ * Scrolling, the root rises until its top is H pixels above the scrollport
24
+ * and stops there; the title area, sticky at 0 inside the root, stays at the
25
+ * top; the header area passes under it, hidden by the title area's opaque
26
+ * ground and higher stacking; the controls and the tabs, last in the root,
27
+ * are what remains under the title. Pinned, or revealed in place after a
28
+ * scroll, the offset is 0 and the whole root stays.
29
+ *
30
+ * Sirius reached the same picture with `display: contents` on the host, and
31
+ * lost the host's box with it. This root keeps a background, a shadow, a
32
+ * container query and the geometry a ref returns.
33
+ *
34
+ * Every part is reached from the part that owns it with a child combinator,
35
+ * because the content the consumer projects is other families — a
36
+ * description-list has a `description`, a card has `actions` and a `header`
37
+ * — and a descendant rule would restyle theirs.
38
+ */
39
+ [data-ui='page-header'] {
40
+ --_surface: var(--slotted-page-header-background, var(--slotted-surface, Canvas));
41
+ /* What the root and the title area paint; `plain` takes it away at rest. */
42
+ --_ground: var(--_surface);
43
+ /* How far below the scroller's edge the chrome holds; `floating` sets it. */
44
+ --_sticky-inset: 0px;
45
+ --_rule: var(--slotted-page-header-border, var(--slotted-border-subtle, ButtonBorder));
46
+ --_text: var(--slotted-text, CanvasText);
47
+ --_muted: var(--slotted-text-muted, GrayText);
48
+ --_gap: var(--slotted-page-header-gap, var(--slotted-space-4, 0.5rem));
49
+ --_padding-block: var(--slotted-page-header-padding-block, var(--slotted-space-5, 0.75rem));
50
+ /**
51
+ * The title is set on the type scale, not the control scale: it is the
52
+ * largest line on the screen, and the control scale tops out at fourteen
53
+ * pixels. The snapped title takes the step below its expanded size, so a
54
+ * stuck bar is a bar and not a poster.
55
+ */
56
+ --_heading-size: var(
57
+ --slotted-page-header-heading-size-md,
58
+ var(--slotted-font-size-2xl, 1.25rem)
59
+ );
60
+ --_heading-line-height: var(--slotted-line-height-2xl, 1.4);
61
+ --_snapped-heading-size: var(
62
+ --slotted-page-header-snapped-heading-size-md,
63
+ var(--slotted-font-size-lg, 1rem)
64
+ );
65
+ --_snapped-heading-line-height: var(--slotted-line-height-lg, 1.5);
66
+ --_subheading-size: var(--slotted-font-size-md, 0.875rem);
67
+ --_description-measure: var(--slotted-page-header-description-measure, 70ch);
68
+ /**
69
+ * The pill's buttons are the small control step, so the pill is one row
70
+ * of the density it sits in; the glyph is the small control glyph.
71
+ */
72
+ --_control-size: var(
73
+ --slotted-page-header-control-size,
74
+ var(--slotted-control-height-sm, 30px)
75
+ );
76
+ --_icon-size: var(--slotted-control-icon-size-sm, 14px);
77
+ --_rule-size: var(--slotted-border-width-thin, 1px);
78
+ --_duration: var(--slotted-duration-medium);
79
+ --_easing: var(--slotted-easing-standard, cubic-bezier(0.2, 0, 0, 1));
80
+
81
+ background-color: var(--_ground);
82
+ color: var(--_text);
83
+ container-name: page-header;
84
+ container-type: inline-size;
85
+ font-family: var(--slotted-font-family-sans, ui-sans-serif, system-ui, sans-serif);
86
+ inline-size: 100%;
87
+ inset-block-start: calc(var(--_sticky-inset) - var(--slotted-page-header-collapse, 0px));
88
+ min-inline-size: 0;
89
+ /**
90
+ * Scroll anchoring would move the scroll position to keep the content
91
+ * under the reader still when the header area changes height, which is
92
+ * exactly the jump Sirius absorbed with a timer of two hundred milliseconds after each reveal.
93
+ */
94
+ overflow-anchor: none;
95
+ position: sticky;
96
+ /**
97
+ * The offset moves only when a reveal begins or ends, so the transition
98
+ * is the slide of the header area under the title and never a fight with
99
+ * the scroll, during which the offset is constant.
100
+ */
101
+ transition: inset-block-start var(--_duration) var(--_easing);
102
+ /**
103
+ * Above every other sticky thing on the page — a section's header, a
104
+ * table's column header, a pinned corner — so what a short section pushes
105
+ * up at the end of the scroll goes under the chrome and never over it.
106
+ */
107
+ z-index: calc(var(--slotted-z-index-sticky, 100) + 3);
108
+ /**
109
+ * The chrome's edge is one hairline, the whole width of the header, in
110
+ * every state: with tabs under it, with an area, with a title alone. It
111
+ * is the root's own border, not a rule inside the container, so it runs
112
+ * to the header's edges and not to the measure's. `overflow-x: clip` is
113
+ * for the rule the pill straddles, drawn wider than the container below;
114
+ * the block axis stays visible for the pill's lower half.
115
+ */
116
+ border-block-end: var(--_rule-size) solid var(--_rule);
117
+ overflow-x: clip;
118
+ }
119
+
120
+ [data-ui='page-header'][data-size='sm'] {
121
+ --_heading-size: var(
122
+ --slotted-page-header-heading-size-sm,
123
+ var(--slotted-font-size-xl, 1.125rem)
124
+ );
125
+ --_heading-line-height: var(--slotted-line-height-xl, 1.45);
126
+ --_snapped-heading-size: var(
127
+ --slotted-page-header-snapped-heading-size-sm,
128
+ var(--slotted-font-size-md, 0.875rem)
129
+ );
130
+ --_snapped-heading-line-height: var(--slotted-line-height-md, 1.5);
131
+ }
132
+
133
+ [data-ui='page-header'][data-size='lg'] {
134
+ --_heading-size: var(
135
+ --slotted-page-header-heading-size-lg,
136
+ var(--slotted-font-size-3xl, 1.5rem)
137
+ );
138
+ --_heading-line-height: var(--slotted-line-height-3xl, 1.3);
139
+ --_snapped-heading-size: var(
140
+ --slotted-page-header-snapped-heading-size-lg,
141
+ var(--slotted-font-size-xl, 1.125rem)
142
+ );
143
+ --_snapped-heading-line-height: var(--slotted-line-height-xl, 1.45);
144
+ }
145
+
146
+ /**
147
+ * Pinned, revealed in place, or never collapsing by scroll: the whole root
148
+ * stays. The pin's own pressed state is read here rather than restated on
149
+ * the root, so the vocabulary gains no `pinned` — a pin is a toggle button
150
+ * and `pressed` is its word. Under `collapse="manual"` the offset is what
151
+ * the scroll would use, so it is zero for good.
152
+ */
153
+ [data-ui='page-header'][data-revealed],
154
+ [data-ui='page-header'][data-collapse='manual'],
155
+ [data-ui='page-header']:has([data-part='pin'][data-pressed]) {
156
+ inset-block-start: var(--_sticky-inset);
157
+ }
158
+
159
+ /* ── Variant ─────────────────────────────────────────────────────────── */
160
+
161
+ /**
162
+ * `band`, the default, is the chrome as a strip of the surface with one
163
+ * hairline under it. `plain` paints no ground of its own at rest and takes
164
+ * the one it stands on; once content passes under the chrome — the binding
165
+ * writes `data-stuck` — the surface comes back, because a header with no
166
+ * ground would show the page through its title. The hairline stays in
167
+ * both: it is where the chrome ends.
168
+ */
169
+ [data-ui='page-header'][data-variant='plain']:not([data-stuck]) {
170
+ --_ground: transparent;
171
+ }
172
+
173
+ /**
174
+ * `floating` is a box of its own for a header that stands inside padding:
175
+ * the hairline goes all the way round, the corners are a card's step,
176
+ * and stuck it slides to an inset below the scroller's edge with a shadow,
177
+ * as a thing lying on the content and not a strip across it. The inset is
178
+ * the stuck state's alone: a header flush with its scroller's edge at rest
179
+ * sat an inset down over the content under it. The title area paints its
180
+ * own ground, so its top corners follow the root's, and it slides with it.
181
+ */
182
+ [data-ui='page-header'][data-variant='floating'] {
183
+ --_radius: var(--slotted-page-header-radius, var(--slotted-radius-lg, 8px));
184
+
185
+ border: var(--_rule-size) solid var(--_rule);
186
+ border-radius: var(--_radius);
187
+ box-sizing: border-box;
188
+ }
189
+ [data-ui='page-header'][data-variant='floating'] > * > [data-part='title-area'] {
190
+ border-start-end-radius: calc(var(--_radius) - var(--_rule-size));
191
+ border-start-start-radius: calc(var(--_radius) - var(--_rule-size));
192
+ transition: inset-block-start var(--_duration) var(--_easing);
193
+ }
194
+ [data-ui='page-header'][data-variant='floating'][data-stuck] {
195
+ --_sticky-inset: var(--slotted-page-header-inset, var(--slotted-space-4, 0.5rem));
196
+
197
+ box-shadow: var(--slotted-page-header-shadow, var(--slotted-shadow-sm));
198
+ }
199
+
200
+ /**
201
+ * Compact, below 600 pixels: nothing sticks. The root and the title area
202
+ * are in the flow and scroll away with the page, because on a phone a
203
+ * record's header and tabs held 40 per cent of the screen for the whole
204
+ * scroll. `relative` and not `static`, so the root keeps the stacking the
205
+ * pill straddling its edge relies on, at an offset of nought. The binding
206
+ * writes the attribute from the root's own width: a container query cannot
207
+ * select the container it measures.
208
+ */
209
+ [data-ui='page-header'][data-compact] {
210
+ inset-block-start: 0;
211
+ position: relative;
212
+ }
213
+ [data-ui='page-header'][data-compact] > * > [data-part='title-area'] {
214
+ inset-block-start: 0;
215
+ position: relative;
216
+ }
217
+
218
+ /* ── The title area ──────────────────────────────────────────────────── */
219
+
220
+ /**
221
+ * The one hop through the container: `> *` is the container the family
222
+ * renders to align its rows to the page's measure, and it is not named,
223
+ * because a family does not select another family's identity.
224
+ */
225
+ [data-ui='page-header'] > * > [data-part='title-area'] {
226
+ background-color: var(--_ground);
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: var(--slotted-space-3, 0.375rem);
230
+ inset-block-start: var(--_sticky-inset);
231
+ overflow-anchor: none;
232
+ padding-block: var(--_padding-block) var(--slotted-space-4, 0.5rem);
233
+ position: sticky;
234
+ z-index: 2;
235
+ }
236
+
237
+ /**
238
+ * The pill straddles the chrome's edge, so whatever ends at that edge
239
+ * keeps half a pill clear of it: the title area, because stuck the edge
240
+ * is its own, and the header area's columns, because at rest it is theirs.
241
+ * The title area reserves it in every state rather than only when stuck,
242
+ * because a padding that changed at the snap would move the tabs by that
243
+ * much at the moment the header area left.
244
+ */
245
+ [data-ui='page-header']:has([data-part='controls'] > *) > * > [data-part='title-area'] {
246
+ padding-block-end: calc(var(--slotted-space-4, 0.5rem) + var(--_control-size) / 2);
247
+ }
248
+
249
+ [data-ui='page-header'] > * > [data-part='title-area'] > [data-part='nav'] {
250
+ align-items: center;
251
+ color: var(--_muted);
252
+ display: flex;
253
+ font-size: var(--slotted-font-size-sm, 0.8125rem);
254
+ gap: var(--slotted-space-5, 0.75rem);
255
+ line-height: var(--slotted-line-height-sm, 1.45);
256
+ min-inline-size: 0;
257
+ }
258
+ [data-ui='page-header'] > * > [data-part='title-area'] > [data-part='nav']:empty {
259
+ display: none;
260
+ }
261
+ /**
262
+ * One way back at a time. Beside breadcrumbs the arrow says what their
263
+ * first crumb already says, so it yields to them; below 600 pixels the
264
+ * breadcrumbs go and the arrow is what remains. A consumer writes both
265
+ * once and the width decides.
266
+ */
267
+ [data-ui='page-header']
268
+ > *
269
+ > [data-part='title-area']
270
+ > [data-part='nav']
271
+ > [data-part='back']:has(~ [data-part='breadcrumbs']:not(:empty)) {
272
+ display: none;
273
+ }
274
+
275
+ /**
276
+ * The way back. A link or a button the consumer renders; the arrow is the
277
+ * family's, the `arrow-left` Glyph element the binding puts before the
278
+ * label, and it points along the reading direction, so its `directional`
279
+ * box mirrors it in a right-to-left document.
280
+ */
281
+ [data-ui='page-header'] > * > [data-part='title-area'] > [data-part='nav'] > [data-part='back'] {
282
+ --slotted-icon-size: var(--_icon-size);
283
+
284
+ align-items: center;
285
+ background: none;
286
+ border: 0;
287
+ border-radius: var(--slotted-control-radius, var(--slotted-radius-sm, 4px));
288
+ color: var(--_muted);
289
+ cursor: pointer;
290
+ display: inline-flex;
291
+ flex: none;
292
+ font: inherit;
293
+ font-weight: var(--slotted-font-weight-medium, 500);
294
+ gap: var(--slotted-control-gap-sm, 6px);
295
+ margin: 0;
296
+ min-block-size: var(--slotted-target-min, 24px);
297
+ padding: 0;
298
+ text-decoration: none;
299
+ transition: color var(--slotted-duration-short) var(--_easing);
300
+ }
301
+ /* On a finger the way back is a full target among 44-pixel controls: it
302
+ reaches the comfortable size by padding and gives it back as a negative
303
+ margin, so the nav row keeps its height and nothing under it moves. */
304
+ @media (pointer: coarse) {
305
+ [data-ui='page-header']
306
+ > *
307
+ > [data-part='title-area']
308
+ > [data-part='nav']
309
+ > [data-part='back'] {
310
+ --_reach: max(
311
+ 0px,
312
+ calc((var(--slotted-target-comfortable, 44px) - var(--slotted-target-min, 24px)) / 2)
313
+ );
314
+
315
+ margin-block: calc(var(--_reach) * -1);
316
+ padding-block: var(--_reach);
317
+ }
318
+ }
319
+ [data-ui='page-header']
320
+ > *
321
+ > [data-part='title-area']
322
+ > [data-part='nav']
323
+ > [data-part='back']:not([data-disabled]):not(:disabled):hover {
324
+ color: var(--_text);
325
+ }
326
+ [data-ui='page-header']
327
+ > *
328
+ > [data-part='title-area']
329
+ > [data-part='nav']
330
+ > [data-part='back']:focus-visible {
331
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
332
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
333
+ }
334
+
335
+ [data-ui='page-header']
336
+ > *
337
+ > [data-part='title-area']
338
+ > [data-part='nav']
339
+ > [data-part='breadcrumbs'] {
340
+ min-inline-size: 0;
341
+ }
342
+
343
+ /**
344
+ * The row: the visual, the title column, and the actions and the record
345
+ * navigation pushed to the end. Everything after the title wraps under it
346
+ * when the width runs out, in the order it has in the DOM.
347
+ */
348
+ [data-ui='page-header'] > * > [data-part='title-area'] > [data-part='title-row'] {
349
+ align-items: flex-start;
350
+ display: flex;
351
+ flex-wrap: wrap;
352
+ gap: var(--_gap) var(--slotted-space-6, 1rem);
353
+ min-inline-size: 0;
354
+ }
355
+
356
+ [data-ui='page-header']
357
+ > *
358
+ > [data-part='title-area']
359
+ > [data-part='title-row']
360
+ > [data-part='visual'] {
361
+ align-items: center;
362
+ /* On the title's first line, as Primer's LeadingVisual: centred on the
363
+ whole column it sank beside the second line of a wrapped title. */
364
+ align-self: flex-start;
365
+ display: inline-flex;
366
+ flex: none;
367
+ }
368
+
369
+ /**
370
+ * The title column is a wrapping row so the marks sit on the heading's own
371
+ * line, on its baseline — which is what "a mark beside a line of text is
372
+ * aligned to the text" asks for — and the subheading, forced to a full
373
+ * row, takes the line under it.
374
+ */
375
+ [data-ui='page-header']
376
+ > *
377
+ > [data-part='title-area']
378
+ > [data-part='title-row']
379
+ > [data-part='title'] {
380
+ align-items: center;
381
+ column-gap: var(--slotted-space-4, 0.5rem);
382
+ display: flex;
383
+ /**
384
+ * Twelve rems and not twenty: the basis is what decides whether the
385
+ * visual keeps the title beside it at 420 pixels, and the column grows to
386
+ * the line anyway.
387
+ */
388
+ flex: 1 1 20rem;
389
+ flex-wrap: wrap;
390
+ /* At least a control tall, so a one-line heading stands on the axis of
391
+ the button beside it — sixteen pixels off it on a touch screen, where
392
+ the row aligns to the start for the sake of a heading that wraps. */
393
+ min-block-size: var(--slotted-control-height-md, 34px);
394
+ min-inline-size: 0;
395
+ row-gap: var(--slotted-space-1, 0.125rem);
396
+ }
397
+
398
+ /**
399
+ * Expanded, the heading wraps to two lines and then clamps; the binding
400
+ * repeats the full text in `title`, so nothing leaves the DOM. Snapped it is
401
+ * one line, a step smaller, and the clamp is the ellipsis a bar can afford
402
+ * — drawn as a block with `nowrap`, because a `-webkit-box` reports a
403
+ * width to the flex algorithm that is not the width it then paints, and the
404
+ * marks beside it ran past the container in the first capture.
405
+ */
406
+ [data-ui='page-header']
407
+ > *
408
+ > [data-part='title-area']
409
+ > [data-part='title-row']
410
+ > [data-part='title']
411
+ > [data-part='heading'] {
412
+ -webkit-box-orient: vertical;
413
+ color: var(--_text);
414
+ display: -webkit-box;
415
+ font-family: var(
416
+ --slotted-font-family-heading,
417
+ var(--slotted-font-family-sans, ui-sans-serif, system-ui, sans-serif)
418
+ );
419
+ font-size: var(--_heading-size);
420
+ font-weight: var(--slotted-font-weight-semibold, 600);
421
+ letter-spacing: -0.01em;
422
+ -webkit-line-clamp: 2;
423
+ line-height: var(--_heading-line-height);
424
+ margin: 0;
425
+ min-inline-size: 0;
426
+ overflow: hidden;
427
+ overflow-wrap: anywhere;
428
+ text-wrap: pretty;
429
+ /* No transition on the size: text re-rasterised on every frame of a
430
+ font-size change blurs and settles with a jolt, so the step is a cut. */
431
+ }
432
+ [data-ui='page-header']:not([data-expanded])
433
+ > *
434
+ > [data-part='title-area']
435
+ > [data-part='title-row']
436
+ > [data-part='title']
437
+ > [data-part='heading'] {
438
+ display: block;
439
+ flex: 0 1 auto;
440
+ font-size: var(--_snapped-heading-size);
441
+ line-height: var(--_snapped-heading-line-height);
442
+ text-overflow: ellipsis;
443
+ white-space: nowrap;
444
+ }
445
+
446
+ [data-ui='page-header']
447
+ > *
448
+ > [data-part='title-area']
449
+ > [data-part='title-row']
450
+ > [data-part='title']
451
+ > [data-part='marks'] {
452
+ align-items: center;
453
+ display: inline-flex;
454
+ flex-wrap: wrap;
455
+ gap: var(--slotted-space-2, 0.25rem);
456
+ min-inline-size: 0;
457
+ }
458
+ [data-ui='page-header']
459
+ > *
460
+ > [data-part='title-area']
461
+ > [data-part='title-row']
462
+ > [data-part='title']
463
+ > [data-part='marks']:empty {
464
+ display: none;
465
+ }
466
+
467
+ [data-ui='page-header']
468
+ > *
469
+ > [data-part='title-area']
470
+ > [data-part='title-row']
471
+ > [data-part='title']
472
+ > [data-part='subheading'] {
473
+ -webkit-box-orient: vertical;
474
+ color: var(--_muted);
475
+ display: -webkit-box;
476
+ flex-basis: 100%;
477
+ font-size: var(--_subheading-size);
478
+ /* Two lines, and the whole string in `title`: a header is a fixed cost
479
+ on every screen, so nothing in it grows without a bound. */
480
+ -webkit-line-clamp: 2;
481
+ line-height: var(--slotted-line-height-md, 1.5);
482
+ margin: 0;
483
+ min-inline-size: 0;
484
+ overflow: hidden;
485
+ text-wrap: pretty;
486
+ }
487
+ /* Snapped, the bar has one line to give it. */
488
+ [data-ui='page-header']:not([data-expanded])
489
+ > *
490
+ > [data-part='title-area']
491
+ > [data-part='title-row']
492
+ > [data-part='title']
493
+ > [data-part='subheading'] {
494
+ display: block;
495
+ overflow: hidden;
496
+ text-overflow: ellipsis;
497
+ white-space: nowrap;
498
+ }
499
+
500
+ [data-ui='page-header']
501
+ > *
502
+ > [data-part='title-area']
503
+ > [data-part='title-row']
504
+ > [data-part='actions'] {
505
+ align-items: center;
506
+ display: flex;
507
+ flex: 0 1 auto;
508
+ /* Loose buttons wrap rather than breaking their own words; a toolbar in
509
+ here folds what does not fit into its menu before that happens. */
510
+ flex-wrap: wrap;
511
+ gap: var(--slotted-control-gap-md, 8px);
512
+ min-inline-size: 0;
513
+ }
514
+
515
+ /**
516
+ * A toolbar pads its controls for the reach of their focus rings, which
517
+ * made it a control and two insets tall: beside a title that reserves one
518
+ * control, a record whose actions land with its answer grew its title row,
519
+ * and everything under it moved. The row gives the inset back, so the bar
520
+ * stands a control tall and its rings still draw inside its own padding.
521
+ * The pad is the toolbar's own — its inset, or the ring's reach for a bar
522
+ * that scrolls — written from the same public tokens; a framed bar keeps
523
+ * its box.
524
+ */
525
+ [data-ui='page-header']
526
+ > *
527
+ > [data-part='title-area']
528
+ > [data-part='title-row']
529
+ > [data-part='actions']
530
+ > [data-ui='toolbar']:not([data-variant='outline']) {
531
+ margin-block: calc(-1 * var(--slotted-toolbar-inset, var(--slotted-space-2, 0.25rem)));
532
+ }
533
+ [data-ui='page-header']
534
+ > *
535
+ > [data-part='title-area']
536
+ > [data-part='title-row']
537
+ > [data-part='actions']
538
+ > [data-ui='toolbar']:not([data-variant='outline']):is(
539
+ [data-overflow='menu'],
540
+ [data-overflow='scroll']
541
+ ) {
542
+ margin-block: calc(
543
+ -1 *
544
+ var(
545
+ --slotted-toolbar-inset,
546
+ max(
547
+ var(--slotted-space-2, 0.25rem),
548
+ calc(var(--slotted-focus-ring-offset, 2px) + var(--slotted-focus-ring-width, 2px))
549
+ )
550
+ )
551
+ );
552
+ }
553
+
554
+ /**
555
+ * A toolbar that folds into its menu keeps the actions on the title row at
556
+ * every width: the row stops wrapping, the title keeps its own width, and
557
+ * the cluster takes everything the title leaves, so the toolbar measures
558
+ * that room and folds the rest. Fiori's Dynamic Page. Loose actions have no
559
+ * menu to fold into and wrap.
560
+ *
561
+ * The cluster's width is the row's remainder and never its own content. A
562
+ * cluster sized by its content was the trigger alone once everything had
563
+ * folded, and it did not grow when the row did: the toolbar kept its width,
564
+ * its observer never fired, and the actions stayed folded after the window
565
+ * widened again, while a fresh load at the same width showed them all.
566
+ */
567
+ [data-ui='page-header']
568
+ > *
569
+ > [data-part='title-area']
570
+ > [data-part='title-row']:has(
571
+ > [data-part='actions'] > [data-ui='toolbar'][data-overflow='menu']
572
+ ) {
573
+ flex-wrap: nowrap;
574
+ }
575
+ /* The title starts from its own width, not from the basis a wrapping row gives it, at every
576
+ width, and does not grow past it: the room it does not use is the cluster's. It shrinks only
577
+ once the cluster is down to its menu's trigger, because nothing else in the row can.
578
+ Its own width is its widest line. A wrapping flex row measures its items end to end, as if on
579
+ one line: a heading, its marks and the subheading under them came to 769 pixels for two lines
580
+ of 420, and the toolbar beside them folded every command into its menu next to empty room. As
581
+ a grid — the heading and its marks on the first line, the subheading across both columns
582
+ under them — the title is as wide as its widest line. */
583
+ [data-ui='page-header']
584
+ > *
585
+ > [data-part='title-area']
586
+ > [data-part='title-row']:has(
587
+ > [data-part='actions'] > [data-ui='toolbar'][data-overflow='menu']
588
+ )
589
+ > [data-part='title'] {
590
+ align-items: center;
591
+ display: grid;
592
+ flex: 0 1 auto;
593
+ grid-template-columns: minmax(0, auto) auto;
594
+ justify-content: start;
595
+ }
596
+ [data-ui='page-header']
597
+ > *
598
+ > [data-part='title-area']
599
+ > [data-part='title-row']:has(
600
+ > [data-part='actions'] > [data-ui='toolbar'][data-overflow='menu']
601
+ )
602
+ > [data-part='title']
603
+ > [data-part='heading'] {
604
+ grid-column: 1;
605
+ }
606
+ [data-ui='page-header']
607
+ > *
608
+ > [data-part='title-area']
609
+ > [data-part='title-row']:has(
610
+ > [data-part='actions'] > [data-ui='toolbar'][data-overflow='menu']
611
+ )
612
+ > [data-part='title']
613
+ > [data-part='subheading'] {
614
+ grid-column: 1 / -1;
615
+ }
616
+ /* The cluster starts from nothing and grows into what the title leaves, so its width follows
617
+ the row's in both directions. It never gives way below its menu's trigger, a medium control,
618
+ and the toolbar's inset on either side: under it the trigger stood past the screen's edge on
619
+ a phone. */
620
+ [data-ui='page-header']
621
+ > *
622
+ > [data-part='title-area']
623
+ > [data-part='title-row']
624
+ > [data-part='actions']:has(> [data-ui='toolbar'][data-overflow='menu']) {
625
+ flex: 1 1 0%;
626
+ min-inline-size: calc(
627
+ var(--slotted-control-height-md, 34px) + 2 *
628
+ var(
629
+ --slotted-toolbar-inset,
630
+ max(
631
+ var(--slotted-space-2, 0.25rem),
632
+ calc(var(--slotted-focus-ring-offset, 2px) + var(--slotted-focus-ring-width, 2px))
633
+ )
634
+ )
635
+ );
636
+ }
637
+ /* The toolbar fills the cluster, so the room it measures is the cluster's, and its controls
638
+ stand at the end of the row as they did when the cluster was their own width. `safe`, because
639
+ a bar of pinned controls wider than the room scrolls, and overflow at the start of a scroll
640
+ container cannot be scrolled to. */
641
+ [data-ui='page-header']
642
+ > *
643
+ > [data-part='title-area']
644
+ > [data-part='title-row']
645
+ > [data-part='actions']
646
+ > [data-ui='toolbar'][data-overflow='menu'] {
647
+ flex: 1 1 0%;
648
+ justify-content: safe flex-end;
649
+ }
650
+
651
+ /**
652
+ * Previous and next record, after the actions and set apart from them by a
653
+ * rule, which is where sap.f puts navigationActions.
654
+ */
655
+ [data-ui='page-header']
656
+ > *
657
+ > [data-part='title-area']
658
+ > [data-part='title-row']
659
+ > [data-part='navigation'] {
660
+ align-items: center;
661
+ border-inline-start: var(--_rule-size) solid var(--_rule);
662
+ display: flex;
663
+ flex: none;
664
+ gap: var(--slotted-control-gap-sm, 6px);
665
+ /* The far end of whichever row it lands on: alone on a wrapped row it
666
+ stayed at the start, under the title, in the dense capture. */
667
+ margin-inline-start: auto;
668
+ /* The toolbar beside it pads its controls by the same step; without it
669
+ the arrows sat four pixels above the actions' line. */
670
+ padding-block: var(--slotted-space-2, 0.25rem);
671
+ padding-inline-start: var(--slotted-space-4, 0.5rem);
672
+ }
673
+ /**
674
+ * Previous and next are directional: in a right-to-left document the
675
+ * previous record is to the right, and the consumer's chevrons are drawn
676
+ * for the other direction. The glyphs mirror; the order is already the
677
+ * row's.
678
+ */
679
+ [data-ui='page-header']
680
+ > *
681
+ > [data-part='title-area']
682
+ > [data-part='title-row']
683
+ > [data-part='navigation']:dir(rtl)
684
+ :is(svg, [data-ui='icon']) {
685
+ scale: -1 1;
686
+ }
687
+
688
+ /* ── The header area ─────────────────────────────────────────────────── */
689
+
690
+ /**
691
+ * A grid with one row, so that collapsing by the control is a track going
692
+ * to 0fr — the content's own height at every frame, nothing measured — as
693
+ * collapsible does. Figure and content share the row.
694
+ */
695
+ [data-ui='page-header'] > * > [data-part='header-area'] {
696
+ display: grid;
697
+ grid-template-columns: auto minmax(0, 1fr);
698
+ grid-template-rows: 1fr;
699
+ min-inline-size: 0;
700
+ overflow-anchor: none;
701
+ transition:
702
+ grid-template-rows var(--_duration) var(--_easing),
703
+ opacity var(--_duration) var(--_easing);
704
+ }
705
+ [data-ui='page-header'] > * > [data-part='header-area']:not(:has([data-part='figure'])) {
706
+ grid-template-columns: minmax(0, 1fr);
707
+ }
708
+ [data-ui='page-header'] > * > [data-part='header-area'] > * {
709
+ min-block-size: 0;
710
+ }
711
+
712
+ /**
713
+ * Collapsed by the control: expanded is false and the scroll has not
714
+ * passed the area. Scrolled past, expanded is false too, and the sticky
715
+ * offset is what hides the area — a 0fr row there would shorten the root
716
+ * under the reader and jump the scroll by the area's height.
717
+ */
718
+ [data-ui='page-header']:not([data-expanded]):not([data-scrolled])
719
+ > *
720
+ > [data-part='header-area'] {
721
+ grid-template-rows: 0fr;
722
+ opacity: 0;
723
+ }
724
+ [data-ui='page-header']:not([data-expanded]):not([data-scrolled])
725
+ > *
726
+ > [data-part='header-area']
727
+ > * {
728
+ overflow: clip;
729
+ }
730
+
731
+ [data-ui='page-header'] > * > [data-part='header-area'] > [data-part='figure'] {
732
+ display: flex;
733
+ padding-block-end: var(--_padding-block);
734
+ padding-inline-end: var(--slotted-space-6, 1rem);
735
+ }
736
+
737
+ [data-ui='page-header'] > * > [data-part='header-area'] > [data-part='content'] {
738
+ display: flex;
739
+ flex-direction: column;
740
+ gap: var(--_gap);
741
+ min-inline-size: 0;
742
+ padding-block-end: var(--_padding-block);
743
+ }
744
+
745
+ [data-ui='page-header']
746
+ > *
747
+ > [data-part='header-area']
748
+ > [data-part='content']
749
+ > [data-part='description'] {
750
+ -webkit-box-orient: vertical;
751
+ color: var(--_muted);
752
+ display: -webkit-box;
753
+ font-size: var(--slotted-font-size-md, 0.875rem);
754
+ -webkit-line-clamp: 3;
755
+ line-height: var(--slotted-line-height-md, 1.5);
756
+ margin: 0;
757
+ max-inline-size: var(--_description-measure);
758
+ overflow: hidden;
759
+ text-wrap: pretty;
760
+ }
761
+
762
+ /* The half pill the header area's columns keep clear of the edge; see the
763
+ title area's reservation above. After the columns' own rules, so it wins. */
764
+ [data-ui='page-header']:has([data-part='controls'] > *)
765
+ > *
766
+ > [data-part='header-area']
767
+ > [data-part='figure'],
768
+ [data-ui='page-header']:has([data-part='controls'] > *)
769
+ > *
770
+ > [data-part='header-area']
771
+ > [data-part='content'] {
772
+ padding-block-end: calc(var(--_padding-block) + var(--_control-size) / 2);
773
+ }
774
+
775
+ /**
776
+ * Collapsed by the control the track is 0fr, and the half pill the columns
777
+ * reserved would keep the area 27 pixels tall: the reservation goes with
778
+ * the content, and the pill straddles the title area's own reservation.
779
+ */
780
+ [data-ui='page-header']:not([data-expanded]):not([data-scrolled])
781
+ > *
782
+ > [data-part='header-area']
783
+ > * {
784
+ padding-block: 0;
785
+ }
786
+
787
+ /* ── The controls, on the chrome's edge ──────────────────────────────── */
788
+
789
+ /**
790
+ * A zero-height row that carries the rule under the chrome and the pill
791
+ * that straddles it. It sits after the header area and before the tabs, so
792
+ * it is on the chrome's bottom edge in every state without moving in the
793
+ * DOM: at rest under the header area, stuck under the title area, because
794
+ * the header area is then under the title. Sirius moved the pill between
795
+ * two slots with `ngTemplateOutlet`.
796
+ *
797
+ * The row is `relative` and the pill `absolute`, so the tabs under it lay
798
+ * out as if it were not there; the pill overlaps their top edge.
799
+ */
800
+ [data-ui='page-header'] > * > [data-part='controls'] {
801
+ block-size: 0;
802
+ position: relative;
803
+ /* Above the title area, and above the wait drawn on the same edge. */
804
+ z-index: 4;
805
+ }
806
+ /**
807
+ * With tabs under the chrome the edge the pill straddles is above them, and
808
+ * it is drawn the whole width of the header — wider than the container and
809
+ * clipped by the root — so it is the same line as the root's own edge.
810
+ * Without tabs the root's edge is where the pill sits, and no second rule
811
+ * is drawn.
812
+ */
813
+ [data-ui='page-header']:has(> * > [data-part='tabs']:not(:empty))
814
+ > *
815
+ > [data-part='controls']::before {
816
+ border-block-start: var(--_rule-size) solid var(--_rule);
817
+ content: '';
818
+ inset-block-start: 0;
819
+ inset-inline: -50vw;
820
+ position: absolute;
821
+ }
822
+ [data-ui='page-header'] > * > [data-part='controls'] > * {
823
+ /* The pill is a control on the hairline: it keeps the surface in every variant. */
824
+ background-color: var(--_surface);
825
+ border: var(--_rule-size) solid var(--_rule);
826
+ border-radius: var(--slotted-radius-full, 9999px);
827
+ display: inline-flex;
828
+ gap: var(--slotted-space-1, 0.125rem);
829
+ inline-size: max-content;
830
+ inset-block-start: 0;
831
+ inset-inline: 0;
832
+ margin-inline: auto;
833
+ padding: var(--slotted-space-1, 0.125rem);
834
+ position: absolute;
835
+ translate: 0 -50%;
836
+ }
837
+
838
+ /**
839
+ * Two inner controls, drawn by the family and not mounted from `button`
840
+ * (R2): a button carries a height, a padding, a frame and a shadow that do
841
+ * not fit inside a pill on a hairline. The ground moves under the pointer
842
+ * and nothing else does; press goes one step further; both hold back for a
843
+ * disabled control.
844
+ */
845
+ [data-ui='page-header'] > * > [data-part='controls'] > * > [data-part='toggle'],
846
+ [data-ui='page-header'] > * > [data-part='controls'] > * > [data-part='pin'] {
847
+ /* One step above the nav row's arrow: a mark on a hairline pill reads
848
+ thin at the small step, and the reviewer asked for weight. The marks
849
+ are the Glyph elements the bindings render — `chevron-down` in the
850
+ toggle, `pin` or `pin-off` in the pin by its state. */
851
+ --slotted-icon-size: var(--slotted-control-icon-size-md, 16px);
852
+
853
+ align-items: center;
854
+ appearance: none;
855
+ background-color: transparent;
856
+ block-size: var(--_control-size);
857
+ border: 0;
858
+ border-radius: var(--slotted-radius-full, 9999px);
859
+ color: var(--_muted);
860
+ cursor: pointer;
861
+ display: inline-flex;
862
+ inline-size: var(--_control-size);
863
+ justify-content: center;
864
+ margin: 0;
865
+ min-block-size: var(--slotted-target-min, 24px);
866
+ min-inline-size: var(--slotted-target-min, 24px);
867
+ padding: 0;
868
+ transition:
869
+ background-color var(--slotted-duration-short) var(--_easing),
870
+ color var(--slotted-duration-short) var(--_easing);
871
+ }
872
+ [data-ui='page-header']
873
+ > *
874
+ > [data-part='controls']
875
+ > *
876
+ > [data-part='toggle']
877
+ > [data-ui='icon'] {
878
+ transition: rotate var(--slotted-duration-short) var(--_easing);
879
+ }
880
+ [data-ui='page-header'][data-expanded]
881
+ > *
882
+ > [data-part='controls']
883
+ > *
884
+ > [data-part='toggle']
885
+ > [data-ui='icon'] {
886
+ rotate: 180deg;
887
+ }
888
+ /**
889
+ * Too much of the screen to hold: expanded, the header would take more
890
+ * than six tenths of the scrollport, or it is narrower than 600 pixels. On
891
+ * a phone a pinned header took nearly the whole height and left nothing to
892
+ * read, so the pin is not offered — the binding writes the fact and lets a
893
+ * held header go — and below 600 pixels the sheet hides it on its own.
894
+ */
895
+ [data-ui='page-header'][data-tall] > * > [data-part='controls'] > * > [data-part='pin'] {
896
+ display: none;
897
+ }
898
+ [data-ui='page-header'] > * > [data-part='controls'] > * > [data-part='pin'][data-pressed] {
899
+ background-color: var(--slotted-tone-accent-surface, transparent);
900
+ color: var(--slotted-tone-accent-text, LinkText);
901
+ }
902
+ [data-ui='page-header']
903
+ > *
904
+ > [data-part='controls']
905
+ > *
906
+ > [data-part='pin'][data-pressed]:not(:disabled):hover {
907
+ background-color: var(--slotted-tone-accent-subtle-hover, ButtonFace);
908
+ color: var(--slotted-tone-accent-text, LinkText);
909
+ }
910
+
911
+ [data-ui='page-header']
912
+ > *
913
+ > [data-part='controls']
914
+ > *
915
+ > [data-part='toggle']:not(:disabled):hover,
916
+ [data-ui='page-header']
917
+ > *
918
+ > [data-part='controls']
919
+ > *
920
+ > [data-part='pin']:not(:disabled):hover {
921
+ background-color: var(--slotted-tone-secondary-subtle-hover, ButtonFace);
922
+ color: var(--_text);
923
+ }
924
+ [data-ui='page-header']
925
+ > *
926
+ > [data-part='controls']
927
+ > *
928
+ > [data-part='toggle']:not(:disabled):active,
929
+ [data-ui='page-header']
930
+ > *
931
+ > [data-part='controls']
932
+ > *
933
+ > [data-part='pin']:not(:disabled):active {
934
+ background-color: var(--slotted-tone-secondary-subtle-active, ButtonFace);
935
+ }
936
+ [data-ui='page-header'] > * > [data-part='controls'] > * > [data-part='toggle']:focus-visible,
937
+ [data-ui='page-header'] > * > [data-part='controls'] > * > [data-part='pin']:focus-visible {
938
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
939
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
940
+ }
941
+ [data-ui='page-header'] > * > [data-part='controls'] > * > [data-part='toggle']:disabled,
942
+ [data-ui='page-header'] > * > [data-part='controls'] > * > [data-part='pin']:disabled {
943
+ color: var(--slotted-disabled-foreground, GrayText);
944
+ cursor: default;
945
+ }
946
+
947
+ /* ── The tabs ────────────────────────────────────────────────────────── */
948
+
949
+ /**
950
+ * The slot a tabs family sits in, and the last thing in the root, which is
951
+ * why it needs no sticky of its own. The pill above it needs the room its
952
+ * lower half takes, so the slot starts half a control down.
953
+ */
954
+ [data-ui='page-header'] > * > [data-part='tabs'] {
955
+ /* The root's bottom edge is the tabs' track: one line, the whole width. */
956
+ --slotted-tabs-track-color: transparent;
957
+
958
+ min-inline-size: 0;
959
+ padding-block-start: calc(var(--_control-size) / 2 + var(--slotted-space-2, 0.25rem));
960
+ }
961
+ [data-ui='page-header'] > * > [data-part='tabs']:empty {
962
+ display: none;
963
+ }
964
+ /* Without a pill the tabs sit on the rule. */
965
+ [data-ui='page-header'] > * > [data-part='controls']:empty + [data-part='tabs'] {
966
+ padding-block-start: 0;
967
+ }
968
+
969
+ /* ── One state only ──────────────────────────────────────────────────── */
970
+
971
+ /**
972
+ * A child of the title area that exists in one state. Fiori has seven slots
973
+ * for this; one attribute does the same, and `display: none` keeps one
974
+ * heading in the accessibility tree at a time.
975
+ *
976
+ * The attribute is written twice, beside `data-part`, on purpose: the
977
+ * deepest part rule sets `display` with six attribute selectors, and this
978
+ * one has to outweigh it for any part at any depth, so it carries six as
979
+ * well and comes later. It is honoured on parts, which is where a consumer
980
+ * writes it.
981
+ */
982
+ [data-ui='page-header'][data-expanded]
983
+ > *
984
+ > [data-part='title-area']
985
+ [data-part][data-when='snapped'][data-when],
986
+ [data-ui='page-header']:not([data-expanded])
987
+ > *
988
+ > [data-part='title-area']
989
+ [data-part][data-when='expanded'][data-when] {
990
+ display: none;
991
+ }
992
+
993
+ /* ── Loading ─────────────────────────────────────────────────────────── */
994
+
995
+ /**
996
+ * The placeholder is skeleton's — a family that waits paints none of its
997
+ * own — and this sheet says only how wide each one is. A line is one `lh`
998
+ * of the text it replaces, which is what keeps the height still when the
999
+ * title arrives.
1000
+ */
1001
+ [data-ui='page-header'][data-loading]
1002
+ > *
1003
+ > [data-part='title-area']
1004
+ > [data-part='title-row']
1005
+ > [data-part='title']
1006
+ > [data-part='heading'] {
1007
+ /* The width a title has, and no growth: grown, the heading pushed the
1008
+ marks to the far end and they jumped back beside it when the title
1009
+ arrived. The bar is what the words will be. */
1010
+ --slotted-skeleton-inline-size: 24ch;
1011
+ flex: 0 1 auto;
1012
+ }
1013
+ [data-ui='page-header'][data-loading]
1014
+ > *
1015
+ > [data-part='title-area']
1016
+ > [data-part='title-row']
1017
+ > [data-part='title']
1018
+ > [data-part='subheading'] {
1019
+ --slotted-skeleton-inline-size: 32ch;
1020
+ }
1021
+ [data-ui='page-header'][data-loading]
1022
+ > *
1023
+ > [data-part='header-area']
1024
+ > [data-part='content']
1025
+ > [data-part='description'] {
1026
+ --slotted-skeleton-inline-size: 40ch;
1027
+ }
1028
+
1029
+ /* ── The wait on the chrome's edge ───────────────────────────────────── */
1030
+
1031
+ /**
1032
+ * A refresh of what is already on screen: the content stays, and the one
1033
+ * thing drawn is a progress bar over the line where the chrome ends. The
1034
+ * frame is a row of the container one hairline tall and given back by its
1035
+ * margin, so it costs the flow nothing, and it runs from `full` to `full` —
1036
+ * the lines the container's own breakout part spans — so the bar is the
1037
+ * header's width, as the line is, and not the measure's.
1038
+ *
1039
+ * Written after the controls and before the tabs, as the controls are, it
1040
+ * stands on the edge the pill straddles; on the root's own edge the bar
1041
+ * would run under the tabs and read as the selected tab's indicator drawn
1042
+ * across the whole row. With no tabs it is the root's own hairline. The bar
1043
+ * stands on the frame's block-end edge and its thickness rises above it,
1044
+ * which is why the frame does not clip.
1045
+ *
1046
+ * A snapped header keeps its edge on screen, and the frame keeps its place
1047
+ * beside the controls, so the bar stays with it. Above the title area,
1048
+ * whose ground would cover the bar's upper pixel once the area has slid
1049
+ * under the title; below the controls, whose pill straddles the same line.
1050
+ */
1051
+ [data-ui='page-header'] > * > [data-part='progress'] {
1052
+ block-size: var(--_rule-size);
1053
+ grid-column: full;
1054
+ margin-block-end: calc(-1 * var(--_rule-size));
1055
+ pointer-events: none;
1056
+ position: relative;
1057
+ z-index: 3;
1058
+ }
1059
+
1060
+ /**
1061
+ * One wait at a time. The title's skeleton is the first load, and a
1062
+ * refresh of nothing is not a refresh; a region around the header that
1063
+ * waits as a whole — a shell between two pages — answers for everything
1064
+ * inside it. The second rule reads an ARIA state rather than another
1065
+ * family's identity, so any region a consumer marks busy takes it.
1066
+ */
1067
+ [data-ui='page-header'][data-loading] > * > [data-part='progress'],
1068
+ [aria-busy='true'] [data-ui='page-header'] > * > [data-part='progress'] {
1069
+ display: none;
1070
+ }
1071
+
1072
+ /* ── Narrow ──────────────────────────────────────────────────────────── */
1073
+
1074
+ /**
1075
+ * Below 600 pixels: the breadcrumbs go and the way back stays; the title
1076
+ * steps down; the actions and the navigation take the row under the title,
1077
+ * navigation first so the pair of arrows keeps to the edge. The rule that
1078
+ * moves the navigation into the actions row is sap.f's at 1280, read
1079
+ * against the component's own width rather than the viewport.
1080
+ */
1081
+ @container page-header (width < 600px) {
1082
+ [data-ui='page-header'] {
1083
+ --_heading-size: var(
1084
+ --slotted-page-header-heading-size-sm,
1085
+ var(--slotted-font-size-xl, 1.125rem)
1086
+ );
1087
+ --_heading-line-height: var(--slotted-line-height-xl, 1.45);
1088
+ --_snapped-heading-size: var(
1089
+ --slotted-page-header-snapped-heading-size-sm,
1090
+ var(--slotted-font-size-md, 0.875rem)
1091
+ );
1092
+ --_snapped-heading-line-height: var(--slotted-line-height-md, 1.5);
1093
+ }
1094
+ [data-ui='page-header']
1095
+ > *
1096
+ > [data-part='title-area']
1097
+ > [data-part='nav']
1098
+ > [data-part='back']
1099
+ ~ [data-part='breadcrumbs'] {
1100
+ display: none;
1101
+ }
1102
+ [data-ui='page-header']
1103
+ > *
1104
+ > [data-part='title-area']
1105
+ > [data-part='nav']
1106
+ > [data-part='back']:has(~ [data-part='breadcrumbs']:not(:empty)) {
1107
+ display: inline-flex;
1108
+ }
1109
+ [data-ui='page-header'] > * > [data-part='controls'] > * > [data-part='pin'] {
1110
+ display: none;
1111
+ }
1112
+ [data-ui='page-header']
1113
+ > *
1114
+ > [data-part='title-area']
1115
+ > [data-part='title-row']
1116
+ > [data-part='title'] {
1117
+ flex: 1 1 0;
1118
+ min-inline-size: 60%;
1119
+ }
1120
+ /**
1121
+ * The cluster grows so that, wrapped onto its own row, it takes the row.
1122
+ * Its content stands at the end whichever row it lands on: beside a short
1123
+ * title that leaves it room, the grown box put one button at its own
1124
+ * start, mid-row, off the edge every other width aligns it to.
1125
+ */
1126
+ [data-ui='page-header']
1127
+ > *
1128
+ > [data-part='title-area']
1129
+ > [data-part='title-row']
1130
+ > [data-part='actions'] {
1131
+ flex: 1 1 auto;
1132
+ justify-content: flex-end;
1133
+ }
1134
+ /* The marks take a line of their own under the heading: beside it their break depends on
1135
+ what they say, which the waiting header cannot know, and the header grew when it landed. */
1136
+ [data-ui='page-header']
1137
+ > *
1138
+ > [data-part='title-area']
1139
+ > [data-part='title-row']
1140
+ > [data-part='title']
1141
+ > [data-part='marks'] {
1142
+ flex-basis: 100%;
1143
+ }
1144
+ [data-ui='page-header']
1145
+ > *
1146
+ > [data-part='title-area']
1147
+ > [data-part='title-row']:has(
1148
+ > [data-part='actions'] > [data-ui='toolbar'][data-overflow='menu']
1149
+ )
1150
+ > [data-part='title']
1151
+ > [data-part='marks'] {
1152
+ grid-column: 1 / -1;
1153
+ }
1154
+ /* One line, as snapped: the phone has no second one to give it. */
1155
+ [data-ui='page-header']
1156
+ > *
1157
+ > [data-part='title-area']
1158
+ > [data-part='title-row']
1159
+ > [data-part='title']
1160
+ > [data-part='subheading'] {
1161
+ display: block;
1162
+ text-overflow: ellipsis;
1163
+ white-space: nowrap;
1164
+ }
1165
+ [data-ui='page-header']
1166
+ > *
1167
+ > [data-part='title-area']
1168
+ > [data-part='title-row']
1169
+ > [data-part='navigation'] {
1170
+ border-inline-start: 0;
1171
+ padding-inline-start: 0;
1172
+ }
1173
+ [data-ui='page-header'] > * > [data-part='header-area'] {
1174
+ grid-template-columns: minmax(0, 1fr);
1175
+ }
1176
+ [data-ui='page-header'] > * > [data-part='header-area'] > [data-part='figure'] {
1177
+ padding-block-end: var(--_gap);
1178
+ padding-inline-end: 0;
1179
+ }
1180
+ }
1181
+ }