@getvitops/core 8.3.0-alpha.4 → 8.3.0-alpha.5

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @getvitops/core
2
2
 
3
+ ## 8.3.0-alpha.5
4
+
5
+ ### Minor Changes
6
+
7
+ - 4d94c8b: **Added: `site.chrome` — a configurable masthead and nav behaviour.** Non-breaking.
8
+
9
+ - `site.chrome.masthead` (new `@getvitops/config` schema): `position` (`static|sticky|fixed`), `behavior` (`always|hide-on-scroll`), `surface` (`solid|glass|transparent|transparent-until-scrolled`), `tone` (a `colors.roles` key), `layout` (`full|inset|floating`), `width`, `elevation` (`shadow`, `border`, `onScroll`), `height` and `inset`. `height` and `inset` take fluid lengths (`calc()`, `clamp()`, `min()`, `max()`, with `cqi`/`vw`/… units), checked against a closed grammar because they are written into CSS. `validateConfig` rejects a `tone` or `shadow` the design system does not define (only when the default theme states them directly, not via `extends`).
10
+ - `site.chrome.nav`: `compact` (`drawer|dropdown`), `drawerSide`, `breakpoint`/`mobileBreakpoint`/`megaBreakpoint`, `depth`, `megaDepth`.
11
+ - `<Masthead config={site}>` (`@getvitops/astro`, slots `brand`/default/`actions`) and `resolveMasthead()` / `resolveNavOptions()`. A prop overrides one field per layout. The key is `chrome`/`masthead` on purpose: `<header>` is valid in any section and `banner` already means the announcement bar and the landmark role.
12
+ - `<SiteNav>` gains `compact="dropdown"` (a columnar panel anchored under the toggle), `drawerSide` (previously hardcoded to the end edge in Astro), `megaDepth` and `megaPanel`. `.sitenav--mega` now styles second-level items as column headings and has an optional featured column.
13
+ - `@getvitops/emdash`: `VitopsMegaPanel` fills that column from the widget area `mega-<item url slug>` (or a `mega-area:<name>` class on the menu item). No area means no column.
14
+ - `@getvitops/create` (emdash template): the layout renders through `<Masthead>`; `site.json` ships a sticky glass masthead with a shadow once scrolled. The hand-written `.navbar { block-size: 64px }` is gone (use `masthead.height`).
15
+ - CSS (`patterns/masthead.css`, `patterns/sitenav.css`): **all formats**, including tailwind, where the patterns are inlined into `@layer components`. Hide-on-scroll and the `*-on-scroll` states are `scroll-state()` container queries that only subtract from a settled (solid, elevated, visible) bar, so a browser without them (Firefox today) shows that bar rather than a transparent one. A sticky or fixed masthead sets `--header-offset` / `--top-offset` so anchors land below it.
16
+ - `@getvitops/generator`: new `TIERS.masthead`; `TIERS.sitenav` documents the new props.
17
+
3
18
  ## 8.3.0-alpha.4
4
19
 
5
20
  ### Minor Changes
package/css/index.css CHANGED
@@ -40,6 +40,7 @@
40
40
  /* navbar before sitenav: sitenav's wide state composes it. */
41
41
  @import './patterns/navbar.css';
42
42
  @import './patterns/sitenav.css';
43
+ @import './patterns/masthead.css';
43
44
  /* After sitenav (the nav family stays contiguous) and after drawer.css, whose
44
45
  panel surface navshell's desktop block has to undo. */
45
46
  @import './patterns/navshell.css';
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Masthead — the site's top bar: brand, navigation, actions. Pure CSS (tier 1); `<Masthead>`
3
+ * (`@getvitops/astro`) only emits the classes below from `site.chrome.masthead`.
4
+ *
5
+ * <header class="masthead masthead--sticky masthead--glass masthead--shadow-on-scroll">
6
+ * <div class="masthead__row navbar centered">brand · nav · actions</div>
7
+ * </header>
8
+ *
9
+ * Named "masthead", not "header"/"banner": `<header>` is valid in any section, and `.banner` is
10
+ * the announcement bar. The element is a plain `<header>` with no explicit `role`.
11
+ *
12
+ * ── SAFE BASE, SUBTRACTIVE ENHANCEMENTS ──
13
+ * The un-modified rule is the settled state: visible, solid (or glass), elevated, in flow.
14
+ * Scroll behaviour is layered on with `scroll-state()` container queries that only ever
15
+ * SUBTRACT from it — `*-on-scroll` clears the surface/shadow/border while the page is at the
16
+ * top; `hide-on-scroll` slides the bar away. Where the query is unsupported (Firefox today)
17
+ * nothing subtracts, so the bar stays visible, solid and elevated: a query can change how the
18
+ * bar looks, never leave it unreadable or take the nav away. (Writing these the other way
19
+ * round — base clear, query reveals — leaves an unsupporting browser with a transparent bar
20
+ * over body text forever.)
21
+ *
22
+ * Hooks (all optional custom properties): `--masthead-height`, `--masthead-bg`, `--masthead-fg`,
23
+ * `--masthead-blur`, `--masthead-glass-opacity`, `--masthead-inset`, `--masthead-shadow`,
24
+ * `--masthead-border`. `site.chrome.masthead` sets `--masthead-height` and `--masthead-inset`
25
+ * on `:root` (see `mastheadRootCss()`), so a clamp()/cqi length there reaches `--header-offset`.
26
+ */
27
+
28
+ /* Zero-specificity colour defaults, so a `.tone` (or any class) on the element wins. */
29
+ :where(.masthead) {
30
+ --_masthead-base: var(--tone-bg, var(--color-bg-surface));
31
+
32
+ background-color: var(--masthead-bg, var(--_masthead-base));
33
+ color: var(--masthead-fg, var(--tone-fg, var(--color-text-surface)));
34
+ }
35
+
36
+ .masthead {
37
+ --_masthead-h: var(--masthead-height, 4rem);
38
+ --_masthead-inset: 0px;
39
+
40
+ position: relative;
41
+ z-index: var(--z-tier-sticky);
42
+ display: block;
43
+ box-sizing: border-box;
44
+ transition:
45
+ translate 0.25s ease,
46
+ background-color 0.25s ease,
47
+ box-shadow 0.25s ease,
48
+ backdrop-filter 0.25s ease;
49
+ }
50
+
51
+ .masthead__row {
52
+ display: block;
53
+ }
54
+
55
+ /* The row's flex child carries the height; `.centered` (when used) is the grid around it. */
56
+ .masthead__row > .navbar {
57
+ min-block-size: var(--_masthead-h);
58
+ }
59
+
60
+ /* `width: full` has no track to supply a gutter. */
61
+ .masthead__row:not(.centered) > .navbar {
62
+ padding-inline: var(--masthead-gutter, 1rem);
63
+ }
64
+
65
+ /* ── Position ──────────────────────────────────────────────────────────────── */
66
+ .masthead--sticky {
67
+ position: sticky;
68
+ inset-block-start: var(--_masthead-inset);
69
+ }
70
+
71
+ /* Out of flow: overlays the first section. Pair with `--transparent*`. */
72
+ .masthead--fixed {
73
+ position: fixed;
74
+ inset-block-start: var(--_masthead-inset);
75
+ inset-inline: 0;
76
+ }
77
+
78
+ /* Anchors and `scroll-padding` land below a pinned masthead. Sticky keeps its space in
79
+ flow, fixed does not, but both cover the top of the viewport. */
80
+ :root:has(.masthead--sticky, .masthead--fixed) {
81
+ --header-offset: calc(var(--masthead-height, 4rem) + var(--masthead-inset, 0px));
82
+ --top-offset: var(--header-offset);
83
+ }
84
+
85
+ /* ── Surface ───────────────────────────────────────────────────────────────── */
86
+ .masthead--glass {
87
+ background-color: color-mix(
88
+ in oklch,
89
+ var(--masthead-bg, var(--_masthead-base)) var(--masthead-glass-opacity, 72%),
90
+ transparent
91
+ );
92
+ backdrop-filter: blur(var(--masthead-blur, 12px));
93
+ -webkit-backdrop-filter: blur(var(--masthead-blur, 12px));
94
+ }
95
+
96
+ /* A browser without backdrop-filter would show page content through a tint. Go solid. */
97
+ @supports not (backdrop-filter: blur(1px)) {
98
+ .masthead--glass {
99
+ background-color: var(--masthead-bg, var(--_masthead-base));
100
+ }
101
+ }
102
+
103
+ .masthead--transparent {
104
+ background-color: transparent;
105
+ }
106
+
107
+ /* ── Layout ────────────────────────────────────────────────────────────────── */
108
+ .masthead--inset,
109
+ .masthead--floating {
110
+ --_masthead-inset: var(--masthead-inset, 0.75rem);
111
+
112
+ margin-inline: var(--_masthead-inset);
113
+ margin-block-start: var(--_masthead-inset);
114
+ }
115
+
116
+ /* `fixed` has no flow to margin against: pin the box in by its insets instead. */
117
+ .masthead--fixed.masthead--inset,
118
+ .masthead--fixed.masthead--floating {
119
+ margin: 0;
120
+ inset-inline: var(--_masthead-inset);
121
+ }
122
+
123
+ .masthead--inset {
124
+ border-radius: var(--br-control, 0.375rem);
125
+ }
126
+
127
+ .masthead--floating {
128
+ border-radius: var(--br-panel, 1rem);
129
+ overflow: clip;
130
+ }
131
+
132
+ /* ── Elevation ─────────────────────────────────────────────────────────────── */
133
+ .masthead--shadow {
134
+ box-shadow: var(--masthead-shadow, var(--shadow-sm));
135
+ }
136
+
137
+ .masthead--border {
138
+ border-block-end: var(
139
+ --masthead-border,
140
+ 1px solid var(--tone-border, var(--color-border-surface))
141
+ );
142
+ }
143
+
144
+ /* A full outline once the masthead is detached from the viewport edge. */
145
+ .masthead--border:is(.masthead--inset, .masthead--floating) {
146
+ border: var(--masthead-border, 1px solid var(--tone-border, var(--color-border-surface)));
147
+ }
148
+
149
+ /* ── Scroll behaviour ───────────────────────────────────────────────────────
150
+ The page is the scroll-state container. Only when a masthead asks for one, so a
151
+ site that uses none pays nothing. */
152
+ :root:has(
153
+ .masthead--hide-on-scroll,
154
+ .masthead--transparent-until-scrolled,
155
+ .masthead--shadow-on-scroll,
156
+ .masthead--border-on-scroll
157
+ ) {
158
+ container: masthead-page / scroll-state;
159
+ }
160
+
161
+ /* At the top of the page, the *-on-scroll modifiers subtract their settled look. `not
162
+ scroll-state(scrollable: top)` is "nothing above the viewport", i.e. scrollY = 0. */
163
+ @container masthead-page not scroll-state(scrollable: top) {
164
+ .masthead--transparent-until-scrolled {
165
+ background-color: transparent;
166
+ /* A glass bar would otherwise blur the hero behind an otherwise clear bar. */
167
+ backdrop-filter: none;
168
+ -webkit-backdrop-filter: none;
169
+ }
170
+ .masthead--shadow-on-scroll {
171
+ box-shadow: none;
172
+ }
173
+ .masthead--border-on-scroll {
174
+ border-color: transparent;
175
+ }
176
+ }
177
+
178
+ /* Slide away while the last scroll was downward; back when it reverses. Never while the
179
+ visitor is inside it: focus, an open popover or an open disclosure keeps it on screen. */
180
+ @container masthead-page scroll-state(scrolled: block-end) {
181
+ .masthead--hide-on-scroll:not(:focus-within, :has(:popover-open, details[open])) {
182
+ translate: 0 calc(-100% - var(--_masthead-inset) - 1px);
183
+ }
184
+ }
185
+
186
+ @media (prefers-reduced-motion: reduce) {
187
+ .masthead {
188
+ transition: none;
189
+ }
190
+ }
191
+
192
+ /* Shadows and tints disappear in forced-colors; keep a real edge. */
193
+ @media (forced-colors: active) {
194
+ .masthead {
195
+ border-block-end: 1px solid CanvasText;
196
+ }
197
+ .masthead--inset,
198
+ .masthead--floating {
199
+ border: 1px solid CanvasText;
200
+ }
201
+ }
@@ -179,6 +179,77 @@
179
179
  }
180
180
  }
181
181
 
182
+ /* ── Compact dropdown — NARROW ONLY ─────────────────────────────────────────
183
+ `compact: 'dropdown'` swaps the edge drawer for a panel anchored under the toggle,
184
+ laid out as columns (each top-level item a column, its sub-links beneath). Same
185
+ markup; `navClass` emits `sitenav--panel-dropdown` INSTEAD of a `drawer-*` class,
186
+ so nothing here competes with the drawer's equal-specificity edge rules. Scoped to
187
+ the narrow state for the reason the whole file is (see "TWO BRANCHES"). */
188
+ .sitenav--panel-dropdown {
189
+ anchor-scope: --sitenav-toggle;
190
+ }
191
+ .sitenav--panel-dropdown > .sitenav__toggle {
192
+ anchor-name: --sitenav-toggle;
193
+ }
194
+
195
+ @container style(--_sitenav-wide: 0) {
196
+ .sitenav--panel-dropdown .sitenav__panel {
197
+ position-anchor: --sitenav-toggle;
198
+ position-area: block-end span-inline-start;
199
+ inset: auto;
200
+ margin: var(--sitenav-dropdown-gap, 0.5rem) 0;
201
+ inline-size: min(calc(100dvw - 1.5rem), var(--sitenav-dropdown-size, 36rem));
202
+ max-block-size: calc(100dvh - 6rem);
203
+ padding: 1.25rem;
204
+ border: var(--b-panel);
205
+ border-radius: var(--br-panel);
206
+ box-shadow: var(--ds-panel);
207
+ translate: 0 -0.5rem;
208
+ }
209
+ .sitenav--panel-dropdown .sitenav__panel:popover-open {
210
+ translate: 0 0;
211
+ }
212
+ @starting-style {
213
+ .sitenav--panel-dropdown .sitenav__panel:popover-open {
214
+ opacity: 0;
215
+ translate: 0 -0.5rem;
216
+ }
217
+ }
218
+ /* The dropdown is anchored, not modal: no close button, no dimmed page. */
219
+ .sitenav--panel-dropdown .sitenav__close {
220
+ display: none;
221
+ }
222
+ .sitenav--panel-dropdown .sitenav__panel {
223
+ padding-block-start: 1.25rem;
224
+ }
225
+ .sitenav--panel-dropdown .sitenav__panel::backdrop {
226
+ background: transparent;
227
+ }
228
+ .sitenav--panel-dropdown .sitenav__panel:popover-open::backdrop {
229
+ background: transparent;
230
+ }
231
+ .sitenav--panel-dropdown .sitenav__list {
232
+ display: grid;
233
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
234
+ gap: 1rem 1.5rem;
235
+ align-items: start;
236
+ }
237
+ /* Columns, not accordions: every sub-list is open, so the caret has nothing to do. */
238
+ .sitenav--panel-dropdown .sitenav__item--branch {
239
+ display: block;
240
+ }
241
+ .sitenav--panel-dropdown .sitenav__item--branch > .sitenav__submenu {
242
+ overflow: visible;
243
+ padding-inline-start: 0;
244
+ }
245
+ .sitenav--panel-dropdown .sitenav__disclosure {
246
+ display: none;
247
+ }
248
+ .sitenav--panel-dropdown .sitenav__list > .sitenav__item--branch > .sitenav__link {
249
+ font-weight: 600;
250
+ }
251
+ }
252
+
182
253
  /* ── Lists + links ─────────────────────────────────────────────────────────── */
183
254
  .sitenav__list,
184
255
  .sitenav__submenu {
@@ -534,6 +605,38 @@
534
605
  .sitenav--mega .sitenav__submenu .sitenav__disclosure {
535
606
  display: none;
536
607
  }
608
+ /* Second level reads as a column heading; its description (`titleAttr`) stays. */
609
+ .sitenav--mega
610
+ .sitenav__list
611
+ > .sitenav__item--branch
612
+ > .sitenav__submenu
613
+ > .sitenav__item--branch
614
+ > .sitenav__link {
615
+ font-weight: 600;
616
+ padding-inline: 0;
617
+ }
618
+ .sitenav--mega .sitenav__submenu .sitenav__item--branch > .sitenav__submenu {
619
+ padding-inline: 0;
620
+ }
621
+ /* Optional featured column (EmDash widget area via `megaPanel`). */
622
+ .sitenav--mega .sitenav__mega-extra {
623
+ flex: 1 1 14rem;
624
+ margin-inline-start: auto;
625
+ max-inline-size: 22rem;
626
+ padding-inline-start: 1.5rem;
627
+ border-inline-start: 1px solid var(--color-border-surface, currentColor);
628
+ }
629
+ .sitenav--mega .sitenav__mega-extra:empty {
630
+ display: none;
631
+ }
632
+ }
633
+
634
+ /* Extra column is a wide-mega feature: in a drawer (a mega menu that is the only tier) it
635
+ would be an unlabelled block between accordions. Empty (no widget area) shows nothing. */
636
+ @container style(--_sitenav-wide: 0) {
637
+ .sitenav__mega-extra {
638
+ display: none;
639
+ }
537
640
  }
538
641
 
539
642
  /* ── In-page menu (`vitops_nav` block) ─────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvitops/core",
3
- "version": "8.3.0-alpha.4",
3
+ "version": "8.3.0-alpha.5",
4
4
  "description": "The Vitops design-system framework — CSS partials, Lit web components, and browser polyfills.",
5
5
  "homepage": "https://docs.vitops.ca",
6
6
  "bugs": "https://github.com/getvitops/design-system/issues",
@@ -29,7 +29,7 @@
29
29
  "access": "public"
30
30
  },
31
31
  "devDependencies": {
32
- "@getvitops/utils": "8.3.0-alpha.4",
32
+ "@getvitops/utils": "8.3.0-alpha.5",
33
33
  "@lit-labs/signals": "^0.3.0",
34
34
  "@oddbird/css-anchor-positioning": "^0.9.0",
35
35
  "@oddbird/popover-polyfill": "^0.7.0",