@hubpav/hwio-ui 0.9.1 → 0.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubpav/hwio-ui",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "HARDWARIO web design system for Astro: DaisyUI 5 themes generated from the brand tokens, the Tailwind entry stylesheet and the HWio* components.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,9 +32,9 @@
32
32
  "test": "node --test \"test/**/*.test.mjs\""
33
33
  },
34
34
  "dependencies": {
35
- "@hubpav/hwio-brand": "0.9.1",
36
- "@hubpav/hwio-web-runtime": "0.9.1",
37
- "@hubpav/hwio-web-worker": "0.9.1",
35
+ "@hubpav/hwio-brand": "0.10.0",
36
+ "@hubpav/hwio-web-runtime": "0.10.0",
37
+ "@hubpav/hwio-web-worker": "0.10.0",
38
38
  "daisyui": "5.7.47"
39
39
  },
40
40
  "peerDependencies": {
@@ -1,16 +1,19 @@
1
1
  ---
2
2
  /**
3
3
  * Sticky site header: logo slot, primary nav (flat links, or mega items whose `mega:<key>` slot opens as a
4
- * full-width panel under the bar), CTAs, language switcher, theme toggle, and a DaisyUI drawer for small
5
- * screens, where a mega item lists its `children` as a collapsible group instead.
4
+ * full-width panel under the bar), CTAs, language switcher, theme toggle, and on small screens a menu sheet,
5
+ * where a mega item lists its `children` as a collapsible group instead.
6
6
  *
7
- * The drawer is opened and closed by real buttons (`data-hwio-drawer-toggle`), so Enter and Space work; the
8
- * nav runtime keeps the checkbox, `aria-expanded` and focus in sync (into the panel on open, back to the
9
- * trigger on close, Escape closes). Below `sm` the bar drops the language dropdown (the drawer carries the
10
- * pills), so logo, theme toggle and the 48 px trigger fit a 320 px viewport without horizontal overflow.
7
+ * The small-screen menu (owner, 2026-11-02: the drawer's close button sat elsewhere than the hamburger and
8
+ * the page jumped sideways and to the top when it opened): one toggle button stays in the bar and turns from
9
+ * the menu icon into the close icon in place; the sheet opens directly under the bar, full width, scrolling
10
+ * inside itself. The nav runtime locks the page without moving it (the scrollbar's width is kept as padding),
11
+ * makes the page behind inert, moves focus into the sheet without scrolling, and closes on the toggle,
12
+ * Escape, a followed link, another popup and a resize to the desktop layout. Below `sm` the bar drops the
13
+ * language dropdown (the sheet carries the pills), so logo, theme toggle and the 48 px toggle fit 320 px.
11
14
  *
12
15
  * The theme toggle and the language dropdown are quiet utilities (.hwio-utility: the nav's weight, size
13
- * and gray, thin icons). The language dropdown, the mega panels and the drawer close each other: one
16
+ * and gray, thin icons). The language dropdown, the mega panels and the menu close each other: one
14
17
  * popup at a time (@hubpav/hwio-web-runtime/popup). A CTA with an icon collapses to the icon alone below
15
18
  * 2xl; an external CTA's arrow shows only beside its full label.
16
19
  *
@@ -27,7 +30,7 @@ import HWioThemeToggle from './HWioThemeToggle.astro';
27
30
  interface NavItem extends HWioLinkItem {
28
31
  /** Key of the `mega:<key>` slot; the item becomes a button that opens the panel (hover, click, keyboard). */
29
32
  mega?: string;
30
- /** Links listed under the item in the drawer (the panel is desktop-only); a description renders as a second, muted line. */
33
+ /** Links listed under the item in the small-screen menu (the panel is desktop-only); a description renders as a second, muted line. */
31
34
  children?: (HWioLinkItem & { description?: string })[];
32
35
  }
33
36
  interface Props {
@@ -37,17 +40,17 @@ interface Props {
37
40
  showThemeToggle?: boolean;
38
41
  scrollspy?: boolean;
39
42
  labels: { nav: string; menu: string; close: string; theme?: string; languages?: string };
40
- /** Viewport from which the inline nav and CTAs replace the drawer (default lg; xl for long navs). */
43
+ /** Viewport from which the inline nav and CTAs replace the small-screen menu (default lg; xl for long navs). */
41
44
  desktopFrom?: 'lg' | 'xl';
42
45
  /** 'tight' (with desktopFrom xl only): smaller nav labels and padding from 1280 to 1535 px. Default 'default'. */
43
46
  navDensity?: 'default' | 'tight';
44
47
  id?: string;
45
48
  class?: string;
46
49
  }
47
- const { nav, ctas = [], languages = [], showThemeToggle = true, scrollspy = false, labels, desktopFrom = 'lg', navDensity = 'default', id = 'hwio-nav-drawer', class: className = '' } = Astro.props;
50
+ const { nav, ctas = [], languages = [], showThemeToggle = true, scrollspy = false, labels, desktopFrom = 'lg', navDensity = 'default', id = 'hwio-nav-menu', class: className = '' } = Astro.props;
48
51
  const BREAK = {
49
- lg: { nav: 'hidden flex-1 self-stretch justify-center lg:flex', ctas: 'hidden items-center gap-2 lg:flex', drawer: 'drawer drawer-end w-auto lg:hidden', panels: 'hidden lg:block', densityTight: '' },
50
- xl: { nav: 'hidden flex-1 self-stretch justify-center xl:flex', ctas: 'hidden items-center gap-2 xl:flex', drawer: 'drawer drawer-end w-auto xl:hidden', panels: 'hidden xl:block', densityTight: 'hwio-nav-tight' },
52
+ lg: { nav: 'hidden flex-1 self-stretch justify-center lg:flex', ctas: 'hidden items-center gap-2 lg:flex', menu: 'lg:hidden', from: '64rem', panels: 'hidden lg:block', densityTight: '' },
53
+ xl: { nav: 'hidden flex-1 self-stretch justify-center xl:flex', ctas: 'hidden items-center gap-2 xl:flex', menu: 'xl:hidden', from: '80rem', panels: 'hidden xl:block', densityTight: 'hwio-nav-tight' },
51
54
  } as const;
52
55
  const bp = BREAK[desktopFrom];
53
56
  const CTA_VARIANT = { primary: 'primary', secondary: 'secondary', ghost: 'ghost' } as const;
@@ -90,55 +93,48 @@ const linkAttrs = (item: HWioLinkItem) => (item.external ? { target: '_blank', r
90
93
  </HWioButton>
91
94
  ))}
92
95
  </div>
93
- <div class={bp.drawer}>
94
- {/* The checkbox only drives DaisyUI's CSS; the buttons are the controls, so it leaves the tab order and the a11y tree. */}
95
- <input id={id} type="checkbox" class="drawer-toggle" tabindex="-1" aria-hidden="true" />
96
- <div class="drawer-content">
97
- <button type="button" class="btn btn-ghost btn-square drawer-button" aria-label={labels.menu} aria-controls={`${id}-panel`} aria-expanded="false" data-hwio-drawer-toggle={id}>
98
- <HWioIcon name="menu" size="lg" />
99
- </button>
100
- </div>
101
- <div class="drawer-side z-[60]">
102
- <label for={id} aria-label={labels.close} class="drawer-overlay"></label>
103
- <div id={`${id}-panel`} class="menu min-h-full w-80 max-w-[85vw] gap-2 bg-base-100 p-6 text-base-content">
104
- <div class="flex justify-end">
105
- <button type="button" class="btn btn-ghost btn-square btn-sm" aria-label={labels.close} aria-controls={`${id}-panel`} aria-expanded="false" data-hwio-drawer-toggle={id}><HWioIcon name="close" /></button>
106
- </div>
107
- <ul class="menu w-full text-base">
108
- {nav.map((item) => (
109
- <li>
110
- {item.children?.length ? (
111
- <details>
112
- <summary>{item.label}</summary>
113
- <ul>
114
- {item.children.map((child) => (
115
- <li>
116
- <a href={child.href} {...linkAttrs(child)} data-track={child.track?.id} data-track-location="mobile-menu">
117
- {/* One grid cell: DaisyUI lays menu items out as a column-flow grid, so the two lines nest in one span. */}
118
- <span class="flex flex-col items-start gap-0.5">
119
- <span class="inline-flex items-center gap-1">{child.label}{child.external && <HWioIcon name="external" size="sm" class="opacity-50" />}</span>
120
- {child.description && <span class="hwio-menu-desc">{child.description}</span>}
121
- </span>
122
- </a>
123
- </li>
124
- ))}
125
- </ul>
126
- </details>
127
- ) : (
128
- <a href={item.href} {...linkAttrs(item)} aria-current={item.current ? 'page' : undefined} data-track={item.track?.id} data-track-location="mobile-menu">{item.label}</a>
129
- )}
130
- </li>
131
- ))}
132
- </ul>
133
- <div class="mt-4 flex flex-col gap-2">
134
- {ctas.map((cta) => (
135
- <HWioButton href={cta.href} external={cta.external} variant={CTA_VARIANT[cta.variant ?? 'primary']} track={{ id: cta.track?.id ?? 'header-cta', location: 'mobile-menu' }}>{cta.label}</HWioButton>
136
- ))}
137
- </div>
138
- {languages.length > 1 && <div class="mt-4"><HWioLanguageSwitcher items={languages} label={labels.languages ?? 'Language'} variant="pills" /></div>}
139
- </div>
96
+ <button type="button" class:list={['btn btn-ghost btn-square hwio-menu-toggle', bp.menu]} aria-label={labels.menu} aria-controls={`${id}-panel`} aria-expanded="false" data-hwio-menu-toggle={id} data-hwio-menu-until={bp.from} data-hwio-label-open={labels.menu} data-hwio-label-close={labels.close}>
97
+ <HWioIcon name="menu" size="lg" class="hwio-menu-icon-open" />
98
+ <HWioIcon name="close" size="lg" class="hwio-menu-icon-close" />
99
+ </button>
100
+ </div>
101
+ </div>
102
+ <div id={`${id}-panel`} class:list={['hwio-mobile-menu', bp.menu]} data-hwio-menu-panel={id} aria-label={labels.nav} role="region" hidden>
103
+ <div class="container flex flex-col gap-6 py-4 pb-10">
104
+ <ul class="menu -mx-3 w-auto max-w-xl p-0 text-base">
105
+ {nav.map((item) => (
106
+ <li>
107
+ {item.children?.length ? (
108
+ <details>
109
+ <summary>{item.label}</summary>
110
+ <ul>
111
+ {item.children.map((child) => (
112
+ <li>
113
+ <a href={child.href} {...linkAttrs(child)} data-track={child.track?.id} data-track-location="mobile-menu">
114
+ {/* One grid cell: DaisyUI lays menu items out as a column-flow grid, so the two lines nest in one span. */}
115
+ <span class="flex flex-col items-start gap-0.5">
116
+ <span class="inline-flex items-center gap-1">{child.label}{child.external && <HWioIcon name="external" size="sm" class="opacity-50" />}</span>
117
+ {child.description && <span class="hwio-menu-desc">{child.description}</span>}
118
+ </span>
119
+ </a>
120
+ </li>
121
+ ))}
122
+ </ul>
123
+ </details>
124
+ ) : (
125
+ <a href={item.href} {...linkAttrs(item)} aria-current={item.current ? 'page' : undefined} data-track={item.track?.id} data-track-location="mobile-menu">{item.label}</a>
126
+ )}
127
+ </li>
128
+ ))}
129
+ </ul>
130
+ {ctas.length > 0 && (
131
+ <div class="flex max-w-xl flex-col gap-2 sm:flex-row">
132
+ {ctas.map((cta) => (
133
+ <HWioButton href={cta.href} external={cta.external} variant={CTA_VARIANT[cta.variant ?? 'primary']} track={{ id: cta.track?.id ?? 'header-cta', location: 'mobile-menu' }} class="sm:flex-1">{cta.label}</HWioButton>
134
+ ))}
140
135
  </div>
141
- </div>
136
+ )}
137
+ {languages.length > 1 && <HWioLanguageSwitcher items={languages} label={labels.languages ?? 'Language'} variant="pills" />}
142
138
  </div>
143
139
  </div>
144
140
  {megaItems.length > 0 && (
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  /**
3
- * Language switcher: `pills` (a joined row of language codes, for the drawer) or `dropdown` (a
3
+ * Language switcher: `pills` (a joined row of language codes, for the small-screen menu) or `dropdown` (a
4
4
  * `<details>` menu). The dropdown closes on a click outside, on Escape (focus back on its summary),
5
- * when focus leaves it and when another popup opens (a mega panel, the drawer, another dropdown);
5
+ * when focus leaves it and when another popup opens (a mega panel, the small-screen menu, another dropdown);
6
6
  * opening it closes an open mega panel (@hubpav/hwio-web-runtime/dropdown).
7
7
  */
8
8
  import HWioIcon from './HWioIcon.astro';
@@ -459,7 +459,7 @@
459
459
  [data-theme$='-dark'] .hwio-backdrop-light { display: none; }
460
460
  [data-theme$='-dark'] .hwio-backdrop-dark { display: block; }
461
461
 
462
- /* Menu panels (HWioMegaLinks) and the drawer's second lines (owner, 2026-10-30): a caps group title
462
+ /* Menu panels (HWioMegaLinks) and the small-screen menu's second lines (owner, 2026-10-30): a caps group title
463
463
  in Gray, link labels that never wrap, descriptions small (13 px), Regular, Gray and balanced. */
464
464
  .hwio-mega-title {
465
465
  color: var(--hwio-text-secondary, color-mix(in oklab, var(--color-base-content) 72%, transparent));
@@ -528,7 +528,7 @@
528
528
  /* Text links: themes whose primary is too light for text on the page ground (er3o teal) set
529
529
  --hwio-link to their text-safe hue; the default keeps DaisyUI's primary. */
530
530
  .btn-link { color: var(--hwio-link, var(--color-primary)); }
531
- /* DaisyUI marks a focused menu item (header nav, drawer, the language dropdown's `.menu.dropdown-content`)
531
+ /* DaisyUI marks a focused menu item (header nav, small-screen menu, the language dropdown's `.menu.dropdown-content`)
532
532
  with a 10 % tint only and sets outline-style: none, on every `details > summary` unconditionally. That
533
533
  fails WCAG 2.4.7; restore the system ring from the base layer. Same selector shape as DaisyUI's rule. */
534
534
  .menu :where(li > :not(ul, menu, details, .menu-title), li > details > summary):focus-visible {
@@ -536,13 +536,74 @@
536
536
  outline-offset: 2px;
537
537
  }
538
538
  /* DaisyUI 5.7.30+ paints a menu link carrying aria-current like .menu-active (the neutral fill). The
539
- header marks the current page with its own label colour and bar (.hwio-nav-current) and the drawer
539
+ header marks the current page with its own label colour and bar (.hwio-nav-current) and the small-screen menu
540
540
  lists it as a plain row, so those links keep the ground; .menu-active items (the language dropdown)
541
541
  keep DaisyUI's fill. currentColor as `color` inherits, so the nav's own colour utilities still win. */
542
542
  .menu :where(li) > [aria-current]:not([aria-current='false'], [aria-current=''], .menu-active, .btn) {
543
543
  --menu-active-bg: transparent;
544
544
  --menu-active-fg: currentColor;
545
545
  }
546
+ /* HWioHeader small-screen menu (owner, 2026-11-02). One toggle in the bar: the menu and the close icon share
547
+ one grid cell and cross-fade, so the close sits exactly where the hamburger was. The sheet opens under the
548
+ bar (the header is sticky at the top, so top: 100% is the bar's bottom edge), full width, and scrolls
549
+ inside itself; it fades in and settles 8 px, without motion when the visitor asks for less. While it is
550
+ open the nav runtime sets html.hwio-scroll-lock: overflow hidden, with the scrollbar's width kept as
551
+ padding, so nothing on the page moves. The icons stack by position, never by display, so the toggle's
552
+ lg:hidden / xl:hidden keeps working. */
553
+ .hwio-menu-toggle {
554
+ position: relative;
555
+ }
556
+ .hwio-menu-toggle > svg {
557
+ position: absolute;
558
+ inset: 0;
559
+ margin: auto;
560
+ transition: opacity 160ms ease-out, rotate 200ms ease-out, scale 200ms ease-out;
561
+ }
562
+ .hwio-menu-toggle[aria-expanded='true'] > .hwio-menu-icon-open,
563
+ .hwio-menu-toggle:not([aria-expanded='true']) > .hwio-menu-icon-close {
564
+ opacity: 0;
565
+ rotate: 45deg;
566
+ scale: 0.7;
567
+ }
568
+ .hwio-mobile-menu {
569
+ position: absolute;
570
+ inset-inline: 0;
571
+ top: 100%;
572
+ height: calc(100dvh - 4rem - 1px);
573
+ overflow-y: auto;
574
+ overscroll-behavior: contain;
575
+ border-top: 1px solid var(--color-base-300);
576
+ background-color: var(--color-base-100);
577
+ opacity: 0;
578
+ translate: 0 -0.5rem;
579
+ transition: opacity 180ms ease-out, translate 180ms ease-out;
580
+ }
581
+ @media (width >= 64rem) {
582
+ .hwio-mobile-menu { height: calc(100dvh - 72px - 1px); }
583
+ }
584
+ .hwio-mobile-menu[data-state='open'] {
585
+ opacity: 1;
586
+ translate: 0 0;
587
+ }
588
+ /* With the sheet open the bar is solid: its translucent ground would show the page's heading behind the logo. */
589
+ header:has(> .hwio-mobile-menu:not([hidden])) {
590
+ background-color: var(--color-base-100);
591
+ }
592
+ html.hwio-scroll-lock {
593
+ overflow: hidden;
594
+ padding-inline-end: var(--hwio-scrollbar, 0px);
595
+ }
596
+ /* The base body's overflow-x: hidden would make the body its own scroll container once the root stops
597
+ propagating overflow: the sticky header would then stick to the body's top, off screen, and the page
598
+ would jump. While locked the root clips, so the body goes back to visible. */
599
+ html.hwio-scroll-lock body {
600
+ overflow: visible;
601
+ }
602
+ @media (prefers-reduced-motion: reduce) {
603
+ .hwio-menu-toggle > svg,
604
+ .hwio-mobile-menu { transition: none; }
605
+ .hwio-mobile-menu { translate: none; }
606
+ }
546
607
  /* HWioHeader navDensity="tight" (desktopFrom="xl"): from 1280 to 1535 px the container is 1280 px wide
547
608
  while the full nav and CTAs show, and long SK/PL/CS labels overflowed it. Labels drop from 15 to 14 px
548
609
  and item padding from 12 to 8 px in that range only; the current-page bar follows the padding.