@bitbaum/sitekit 0.5.1 → 0.7.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/README.md CHANGED
@@ -73,6 +73,23 @@ design tokens. Existing document sites keep their utility-class appearance.
73
73
  links={[{ href: '/roadmap', label: 'Roadmap' }]} extra="Beta" />
74
74
  ```
75
75
 
76
+ For a grouped application menu, pass `navGroups` alongside `navItems`. Each
77
+ group has a `label` and `items` containing `path`, `label` and an optional
78
+ `description`. Import `SiteNavGroup` from `@bitbaum/sitekit/react` for typed
79
+ configuration. Nonempty groups replace the flat application navigation with
80
+ one native disclosure and a responsive panel; omitted or empty groups retain
81
+ the flat API. Group labels, destinations and descriptions belong to the host.
82
+
83
+ ```tsx
84
+ navGroups={[{ label: 'Create', items: [
85
+ { path: 'write', label: 'Write', description: 'Start an article.' }
86
+ ] }]}
87
+ ```
88
+
89
+ Run `pnpm exec playwright install --with-deps chromium` before `pnpm run verify`.
90
+ An existing Chromium can be selected with `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`.
91
+ The browser regression uses the public renderer and stylesheet at four widths.
92
+
76
93
  The package owns the responsive menu, its native no-JavaScript fallback,
77
94
  Escape/outside/route dismissal, skip link, active states, touch targets and
78
95
  footer layout. Products supply labels, navigation data, identity and account
@@ -166,3 +183,14 @@ can configure `--sk-header-height`, `--sk-mobile-header-height`,
166
183
  `SiteTabBar` renders a fixed bottom navigation for small screens (hidden from
167
184
  48rem up, where the masthead nav takes over). Pass 3–5 `{ path, label, icon }`
168
185
  items and the current path; it renders its own spacer so content is not hidden.
186
+
187
+ Use `SiteIcon` for the icons — `{ icon: <SiteIcon name="write" /> }` — not a
188
+ unicode glyph: the set is drawn on `currentColor`, so icon and label take one
189
+ colour and the current tab is one rule. Names: `SITE_ICON_NAMES`. Pass
190
+ `tabBar` to `SiteMasthead` on the same page so the masthead drops its menu
191
+ button on phones instead of repeating the tab bar's destinations:
192
+
193
+ ```tsx
194
+ <SiteMasthead appearance="application" tabBar navItems={nav} … />
195
+ <SiteTabBar items={nav.map((n) => ({ ...n, icon: <SiteIcon name={n.icon} /> }))} … />
196
+ ```
@@ -1,9 +1,12 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { SiteChrome as SiteChromeSpec, SiteNavItem } from '../schema.js';
3
3
  import { type LinkLike } from './link.js';
4
+ import { type SiteNavGroup } from './SiteNav.js';
4
5
  interface Props {
5
6
  chrome: SiteChromeSpec;
6
7
  navItems: SiteNavItem[];
8
+ /** Descriptive application navigation; product-owned group configuration. */
9
+ navGroups?: SiteNavGroup[];
7
10
  currentPath: string;
8
11
  /** Framework link component (e.g. next/link). Defaults to a plain anchor. */
9
12
  Link?: LinkLike;
@@ -15,8 +18,15 @@ interface Props {
15
18
  skipLabel?: string;
16
19
  navLabel?: string;
17
20
  menuLabel?: string;
21
+ /**
22
+ * The page also renders a `SiteTabBar` with these destinations. On phones
23
+ * the tab bar is then the navigation, and the masthead's menu button is
24
+ * dropped below the tab bar breakpoint instead of repeating it — one way to
25
+ * each place, not two. The account actions stay in the masthead.
26
+ */
27
+ tabBar?: boolean;
18
28
  }
19
- export declare function SiteMasthead({ chrome, navItems, currentPath, Link, appearance, brand, actions, mainId, skipLabel, navLabel, menuLabel, }: Props): import("react").JSX.Element;
29
+ export declare function SiteMasthead({ chrome, navItems, navGroups, currentPath, Link, appearance, brand, actions, mainId, skipLabel, navLabel, menuLabel, tabBar, }: Props): import("react").JSX.Element;
20
30
  export declare function SiteFooter({ chrome, appearance, links, groups, Link, label, extra, }: {
21
31
  chrome: SiteChromeSpec;
22
32
  appearance?: 'application';
@@ -13,9 +13,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  import { href } from '../model.js';
14
14
  import { DefaultLink } from './link.js';
15
15
  import { SiteNav } from './SiteNav.js';
16
- export function SiteMasthead({ chrome, navItems, currentPath, Link = DefaultLink, appearance, brand, actions, mainId, skipLabel = 'Skip to content', navLabel = 'Main navigation', menuLabel = 'Menu', }) {
16
+ export function SiteMasthead({ chrome, navItems, navGroups, currentPath, Link = DefaultLink, appearance, brand, actions, mainId, skipLabel = 'Skip to content', navLabel = 'Main navigation', menuLabel = 'Menu', tabBar = false, }) {
17
17
  if (appearance === 'application')
18
- return (_jsxs(_Fragment, { children: [mainId && (_jsx("a", { className: "sk-skip", href: '#' + mainId, children: skipLabel })), _jsx("header", { className: "sk-masthead", children: _jsxs("div", { className: "sk-shell-frame sk-masthead-row", children: [_jsx(Link, { href: "/", className: "sk-brand", children: brand ?? chrome.name }), _jsx(SiteNav, { items: navItems, currentPath: currentPath, Link: Link, appearance: "application", label: navLabel, menuLabel: menuLabel, menuActions: actions }), actions && _jsx("div", { className: "sk-account-actions", children: actions })] }) })] }));
18
+ return (_jsxs(_Fragment, { children: [mainId && (_jsx("a", { className: "sk-skip", href: '#' + mainId, children: skipLabel })), _jsx("header", { className: tabBar ? 'sk-masthead sk-masthead--tabbar' : 'sk-masthead', children: _jsxs("div", { className: "sk-shell-frame sk-masthead-row", children: [_jsx(Link, { href: "/", className: "sk-brand", children: brand ?? chrome.name }), _jsx(SiteNav, { items: navItems, groups: navGroups, currentPath: currentPath, Link: Link, appearance: "application", label: navLabel, menuLabel: menuLabel, menuActions: actions }), actions && _jsx("div", { className: "sk-account-actions", children: actions })] }) })] }));
19
19
  return (_jsx("header", { className: "sticky top-0 z-30 border-b border-subtle bg-surface-page/85 backdrop-blur", children: _jsx("div", { className: "mx-auto max-w-shell px-4 sm:px-6 lg:px-8", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-x-6 py-2", children: [_jsx(Link, { href: href(), className: "inline-flex min-h-11 shrink-0 items-center rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", children: _jsx("span", { className: "font-heading text-lg font-semibold tracking-display text-fg-primary", children: chrome.name }) }), _jsx(SiteNav, { items: navItems, currentPath: currentPath, Link: Link })] }) }) }));
20
20
  }
21
21
  export function SiteFooter({ chrome, appearance, links = [], groups, Link = DefaultLink, label = 'Footer navigation', extra, }) {
@@ -9,6 +9,12 @@
9
9
  import type { ReactNode } from 'react';
10
10
  import type { SiteNavItem } from '../schema.js';
11
11
  import { type LinkLike } from './link.js';
12
+ export interface SiteNavGroup {
13
+ label: string;
14
+ items: Array<SiteNavItem & {
15
+ description?: string;
16
+ }>;
17
+ }
12
18
  interface Props {
13
19
  /**
14
20
  * Deliberately NOT `SitePage[]`. Everything the nav receives ends up in the
@@ -16,6 +22,8 @@ interface Props {
16
22
  */
17
23
  items: SiteNavItem[];
18
24
  currentPath: string;
25
+ /** Opt-in grouped application menu. Flat items remain unchanged when omitted. */
26
+ groups?: SiteNavGroup[];
19
27
  /** Framework link component (e.g. next/link). Defaults to a plain anchor. */
20
28
  Link?: LinkLike;
21
29
  appearance?: 'application';
@@ -28,5 +36,5 @@ interface Props {
28
36
  */
29
37
  menuActions?: ReactNode;
30
38
  }
31
- export declare function SiteNav({ items, currentPath, Link, appearance, label, menuLabel, menuActions, }: Props): import("react").JSX.Element | null;
39
+ export declare function SiteNav({ items, groups, currentPath, Link, appearance, label, menuLabel, menuActions, }: Props): import("react").JSX.Element | null;
32
40
  export {};
@@ -3,8 +3,12 @@ import { href } from '../model.js';
3
3
  import { DefaultLink } from './link.js';
4
4
  import { MenuDismiss } from './MenuDismiss.js';
5
5
  import { NavAutoScroll } from './NavAutoScroll.js';
6
- export function SiteNav({ items, currentPath, Link = DefaultLink, appearance, label = 'Sections', menuLabel = 'Menu', menuActions, }) {
6
+ export function SiteNav({ items, groups, currentPath, Link = DefaultLink, appearance, label = 'Sections', menuLabel = 'Menu', menuActions, }) {
7
7
  if (appearance === 'application') {
8
+ const populatedGroups = groups?.filter((group) => group.items.length > 0);
9
+ if (populatedGroups?.length) {
10
+ return (_jsxs(_Fragment, { children: [_jsxs("details", { className: "sk-grouped-menu", "data-sitekit-menu": true, children: [_jsxs("summary", { children: [menuLabel, _jsx("span", { "aria-hidden": "true", children: "\u2630" })] }), _jsxs("nav", { className: "sk-grouped-panel", "aria-label": label, children: [_jsx("div", { className: "sk-nav-groups", children: populatedGroups.map((group) => (_jsxs("section", { className: "sk-nav-group", "aria-label": group.label, children: [_jsx("h2", { children: group.label }), group.items.map((item) => (_jsxs(Link, { href: href(item.path), "aria-current": item.path === currentPath ? 'page' : undefined, children: [_jsx("span", { className: "sk-nav-link-label", children: item.label }), item.description && (_jsx("span", { className: "sk-nav-description", children: item.description }))] }, item.path)))] }, group.label))) }), menuActions && _jsx("div", { className: "sk-grouped-actions", children: menuActions })] })] }), _jsx(MenuDismiss, { currentPath: currentPath })] }));
11
+ }
8
12
  if (items.length === 0)
9
13
  return null;
10
14
  const links = () => items.map((item) => (_jsx(Link, { href: href(item.path), "aria-current": item.path === currentPath ? 'page' : undefined, children: item.label }, item.path)));
@@ -0,0 +1,19 @@
1
+ /**
2
+ * One small icon set for the shell: the tab bar and the account actions.
3
+ *
4
+ * Products used to pass unicode glyphs (◎ ✎ ⌕ ⇄ ★) as tab icons, which
5
+ * render differently on every phone and never look like an interface. These
6
+ * are 24-unit stroke outlines on `currentColor`, so a tab's icon and label
7
+ * take the same colour and the active state is one rule. Decorative only:
8
+ * every one is `aria-hidden`, the label beside it is what is read out.
9
+ */
10
+ import type { SVGProps } from 'react';
11
+ export type SiteIconName = 'home' | 'write' | 'search' | 'compass' | 'star' | 'user' | 'chat' | 'bell' | 'settings' | 'list' | 'globe' | 'book' | 'heart' | 'plus' | 'menu' | 'calendar' | 'wallet' | 'chart';
12
+ export interface SiteIconProps extends Omit<SVGProps<SVGSVGElement>, 'name'> {
13
+ name: SiteIconName;
14
+ /** Rendered size in CSS pixels; the tab bar sets its own. */
15
+ size?: number | string;
16
+ }
17
+ /** A shell icon on `currentColor`. Decorative: hidden from assistive tech. */
18
+ export declare function SiteIcon({ name, size, className, ...rest }: SiteIconProps): import("react").JSX.Element;
19
+ export declare const SITE_ICON_NAMES: SiteIconName[];
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const PATHS = {
3
+ home: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M3 11.5 12 4l9 7.5" }), _jsx("path", { d: "M5.5 10.5V20h13v-9.5" }), _jsx("path", { d: "M10 20v-5h4v5" })] })),
4
+ write: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M4 20h4l10.5-10.5a2.1 2.1 0 0 0-3-3L5 17z" }), _jsx("path", { d: "M13.5 8.5 16.5 11.5" })] })),
5
+ search: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "10.5", cy: "10.5", r: "6.5" }), _jsx("path", { d: "M20 20l-4.5-4.5" })] })),
6
+ compass: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m15.5 8.5-2 5-5 2 2-5z" })] })),
7
+ star: _jsx("path", { d: "m12 3.5 2.6 5.4 5.9.8-4.3 4.1 1.1 5.9L12 16.9l-5.3 2.8 1.1-5.9-4.3-4.1 5.9-.8z" }),
8
+ user: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "8.5", r: "4" }), _jsx("path", { d: "M4.5 20.5c.8-4 4-6 7.5-6s6.7 2 7.5 6" })] })),
9
+ chat: _jsx("path", { d: "M4 5.5h16v10H9.5L5.5 19v-3.5H4z" }),
10
+ bell: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M6 16.5V11a6 6 0 0 1 12 0v5.5l1.5 2h-15z" }), _jsx("path", { d: "M10 20.5a2 2 0 0 0 4 0" })] })),
11
+ settings: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M12 3v2.5M12 18.5V21M3 12h2.5M18.5 12H21M5.6 5.6l1.8 1.8M16.6 16.6l1.8 1.8M5.6 18.4l1.8-1.8M16.6 7.4l1.8-1.8" })] })),
12
+ list: _jsx("path", { d: "M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01" }),
13
+ globe: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M3 12h18M12 3c3 3.2 3 14.8 0 18M12 3c-3 3.2-3 14.8 0 18" })] })),
14
+ book: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M4 5.5A2.5 2.5 0 0 1 6.5 3H20v15H6.5A2.5 2.5 0 0 0 4 20.5z" }), _jsx("path", { d: "M4 18A2.5 2.5 0 0 1 6.5 15.5H20" })] })),
15
+ heart: (_jsx("path", { d: "M12 20s-7.5-4.6-7.5-10A4.2 4.2 0 0 1 12 7.7 4.2 4.2 0 0 1 19.5 10c0 5.4-7.5 10-7.5 10z" })),
16
+ plus: _jsx("path", { d: "M12 5v14M5 12h14" }),
17
+ menu: _jsx("path", { d: "M4 7h16M4 12h16M4 17h16" }),
18
+ calendar: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "4", y: "5", width: "16", height: "15", rx: "2" }), _jsx("path", { d: "M4 10h16M8 3v4M16 3v4" })] })),
19
+ wallet: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M3 7.5h16a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }), _jsx("path", { d: "M3 7.5V6a2 2 0 0 1 2-2h11" }), _jsx("circle", { cx: "16.5", cy: "14", r: "1.2" })] })),
20
+ chart: _jsx("path", { d: "M4 20V4M4 20h16M8 16v-5M12 16V8M16 16v-3M20 16V6" }),
21
+ };
22
+ /** A shell icon on `currentColor`. Decorative: hidden from assistive tech. */
23
+ export function SiteIcon({ name, size, className, ...rest }) {
24
+ return (_jsx("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", className: className ? `sk-icon ${className}` : 'sk-icon', "data-icon": name, ...rest, children: PATHS[name] }));
25
+ }
26
+ export const SITE_ICON_NAMES = Object.keys(PATHS);
@@ -8,8 +8,9 @@
8
8
  */
9
9
  export { SiteSections } from './SiteSections.js';
10
10
  export { SiteMasthead, SiteFooter } from './SiteChrome.js';
11
- export { SiteNav } from './SiteNav.js';
11
+ export { SiteNav, type SiteNavGroup } from './SiteNav.js';
12
12
  export { SiteTabBar, type SiteTabItem } from './SiteTabBar.js';
13
+ export { SiteIcon, SITE_ICON_NAMES, type SiteIconName, type SiteIconProps } from './icons.js';
13
14
  export { NavAutoScroll } from './NavAutoScroll.js';
14
15
  export { DefaultLink, type LinkLike, type LinkProps } from './link.js';
15
16
  export { SectionHeading, Blurb, SectionBody } from './sections/Primitives.js';
@@ -10,6 +10,7 @@ export { SiteSections } from './SiteSections.js';
10
10
  export { SiteMasthead, SiteFooter } from './SiteChrome.js';
11
11
  export { SiteNav } from './SiteNav.js';
12
12
  export { SiteTabBar } from './SiteTabBar.js';
13
+ export { SiteIcon, SITE_ICON_NAMES } from './icons.js';
13
14
  export { NavAutoScroll } from './NavAutoScroll.js';
14
15
  export { DefaultLink } from './link.js';
15
16
  export { SectionHeading, Blurb, SectionBody } from './sections/Primitives.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/sitekit",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "A website as data: a closed union of section shapes validated at runtime, one set of React renderers, and per-field provenance so a generated site can prove it fabricated nothing.",
5
5
  "license": "MIT",
6
6
  "author": "Mao Nakamoto",
@@ -67,8 +67,10 @@
67
67
  "@eslint/js": "^10.0.1",
68
68
  "@types/node": "^26.4.1",
69
69
  "@types/react": "^19.0.0",
70
+ "esbuild": "^0.28.2",
70
71
  "eslint": "^10.9.1",
71
72
  "globals": "^17.12.0",
73
+ "playwright": "^1.64.1",
72
74
  "prettier": "3.9.10",
73
75
  "react": "^19.0.0",
74
76
  "react-dom": "^19.0.0",
package/styles.css CHANGED
@@ -339,37 +339,51 @@
339
339
  gap: 1.5rem;
340
340
  }
341
341
  }
342
- /* Mobile bottom tab bar (SiteTabBar). Hidden from --sk-tabbar-breakpoint up. */
342
+ /* Mobile bottom tab bar (SiteTabBar). Hidden from --sk-tabbar-breakpoint up.
343
+ One row of equal cells; the current one carries a short rule above its icon
344
+ and the full text colour, so the active state reads without a fill. */
343
345
  .sk-tabbar {
344
346
  position: fixed;
345
347
  inset: auto 0 0 0;
346
348
  z-index: 45;
347
- display: flex;
348
- justify-content: space-around;
349
- gap: 2px;
350
- padding: 6px 6px calc(6px + env(safe-area-inset-bottom));
349
+ display: grid;
350
+ grid-auto-flow: column;
351
+ grid-auto-columns: minmax(0, 1fr);
352
+ padding: 0 4px calc(0px + env(safe-area-inset-bottom));
351
353
  background: var(--sk-bg, Canvas);
352
354
  border-top: 1px solid var(--sk-line, #d6d6d6);
353
355
  font-family: var(--sk-font, system-ui, sans-serif);
354
356
  }
355
357
  .sk-tab {
356
- flex: 1;
358
+ position: relative;
357
359
  min-width: 0;
360
+ min-height: 56px;
358
361
  display: flex;
359
362
  flex-direction: column;
360
363
  align-items: center;
361
- gap: 2px;
362
- padding: 6px 2px;
363
- border-radius: 10px;
364
+ justify-content: center;
365
+ gap: 3px;
366
+ padding: 8px 4px 6px;
364
367
  color: var(--sk-muted, #555);
365
368
  font-size: 11px;
366
369
  font-weight: 600;
370
+ letter-spacing: 0.01em;
367
371
  text-decoration: none;
372
+ -webkit-tap-highlight-color: transparent;
368
373
  }
369
374
  .sk-tab-icon {
370
- font-size: 20px;
375
+ display: inline-flex;
376
+ width: 24px;
377
+ height: 24px;
378
+ align-items: center;
379
+ justify-content: center;
380
+ font-size: 22px;
371
381
  line-height: 1;
372
382
  }
383
+ .sk-tab-icon > svg {
384
+ width: 24px;
385
+ height: 24px;
386
+ }
373
387
  .sk-tab-label {
374
388
  white-space: nowrap;
375
389
  overflow: hidden;
@@ -378,14 +392,36 @@
378
392
  }
379
393
  .sk-tab[aria-current='page'] {
380
394
  color: var(--sk-fg, CanvasText);
381
- background: var(--sk-tab-active, color-mix(in srgb, currentColor 8%, transparent));
395
+ }
396
+ .sk-tab[aria-current='page']::before {
397
+ content: '';
398
+ position: absolute;
399
+ top: -1px;
400
+ left: 50%;
401
+ width: 28px;
402
+ height: 3px;
403
+ transform: translateX(-50%);
404
+ border-radius: 0 0 3px 3px;
405
+ background: var(--sk-tab-active, currentColor);
382
406
  }
383
407
  .sk-tab:focus-visible {
384
408
  outline: 2px solid var(--sk-focus, Highlight);
385
- outline-offset: 2px;
409
+ outline-offset: -4px;
410
+ border-radius: 10px;
386
411
  }
387
412
  .sk-tabbar-spacer {
388
- height: var(--sk-tabbar-height, 68px);
413
+ height: calc(var(--sk-tabbar-height, 56px) + env(safe-area-inset-bottom));
414
+ }
415
+ /* Below the tab bar breakpoint the tab bar IS the navigation: a masthead that
416
+ says so (`tabBar`) drops its menu button rather than offering the same five
417
+ places twice. */
418
+ @media (max-width: 47.999rem) {
419
+ .sk-masthead--tabbar .sk-mobile-menu {
420
+ display: none;
421
+ }
422
+ .sk-masthead--tabbar .sk-masthead-row:has(.sk-mobile-menu) > .sk-account-actions {
423
+ display: flex;
424
+ }
389
425
  }
390
426
  @media (min-width: 48rem) {
391
427
  .sk-tabbar,
@@ -393,3 +429,111 @@
393
429
  display: none;
394
430
  }
395
431
  }
432
+ .sk-icon {
433
+ display: inline-block;
434
+ vertical-align: middle;
435
+ flex-shrink: 0;
436
+ }
437
+ /* One native disclosure and one link tree for grouped application navigation. */
438
+ .sk-masthead-row:has(.sk-grouped-menu) {
439
+ position: relative;
440
+ }
441
+ .sk-grouped-menu {
442
+ margin-inline-start: auto;
443
+ }
444
+ .sk-grouped-menu summary {
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ gap: 0.5rem;
449
+ min-height: 44px;
450
+ padding: 0.65rem 0.85rem;
451
+ list-style: none;
452
+ cursor: pointer;
453
+ border: 1px solid var(--sk-line, #d6d6d6);
454
+ border-radius: var(--sk-radius, 0.5rem);
455
+ font-size: 0.875rem;
456
+ font-weight: 600;
457
+ }
458
+ .sk-grouped-menu summary::-webkit-details-marker {
459
+ display: none;
460
+ }
461
+ .sk-grouped-panel {
462
+ position: absolute;
463
+ inset-block-start: 100%;
464
+ inset-inline-end: var(--sk-gutter, 1rem);
465
+ width: min(54rem, calc(100% - 2 * var(--sk-gutter, 1rem)));
466
+ box-sizing: border-box;
467
+ max-height: 75dvh;
468
+ overflow-y: auto;
469
+ padding: 1rem;
470
+ background: var(--sk-bg, Canvas);
471
+ border: 1px solid var(--sk-line, #d6d6d6);
472
+ border-radius: var(--sk-radius, 0.5rem);
473
+ box-shadow: 0 18px 40px #00000014;
474
+ }
475
+ .sk-nav-groups {
476
+ display: grid;
477
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
478
+ gap: 1rem;
479
+ }
480
+ .sk-nav-group {
481
+ min-width: 0;
482
+ }
483
+ .sk-nav-group h2 {
484
+ margin: 0 0 0.5rem;
485
+ font-size: 0.875rem;
486
+ color: var(--sk-muted, #555);
487
+ }
488
+ .sk-grouped-actions > a,
489
+ .sk-nav-group a {
490
+ display: flex;
491
+ flex-direction: column;
492
+ justify-content: center;
493
+ box-sizing: border-box;
494
+ min-height: 44px;
495
+ padding: 0.75rem;
496
+ border-radius: var(--sk-radius, 0.5rem);
497
+ color: var(--sk-fg, CanvasText);
498
+ text-decoration: none;
499
+ overflow-wrap: anywhere;
500
+ }
501
+ .sk-nav-group a:hover,
502
+ .sk-nav-group a[aria-current='page'] {
503
+ background: var(--sk-raised, #f0f0f0);
504
+ }
505
+ .sk-nav-link-label {
506
+ font-weight: 600;
507
+ }
508
+ .sk-nav-description {
509
+ margin-top: 0.25rem;
510
+ font-size: 0.875rem;
511
+ line-height: 1.5;
512
+ color: var(--sk-muted, #555);
513
+ }
514
+ .sk-grouped-actions {
515
+ display: none;
516
+ }
517
+ @media (max-width: 600px) {
518
+ .sk-nav-groups {
519
+ grid-template-columns: minmax(0, 1fr);
520
+ }
521
+ }
522
+ @media (max-width: 380px) {
523
+ /* Give host account popovers the full narrow viewport, including padding. */
524
+ .sk-grouped-panel {
525
+ inset-inline: 0;
526
+ width: 100%;
527
+ }
528
+ .sk-masthead-row:has(.sk-grouped-menu) > .sk-account-actions {
529
+ display: none;
530
+ }
531
+ .sk-grouped-actions {
532
+ display: flex;
533
+ flex-direction: column;
534
+ gap: 0.5rem;
535
+ border-top: 1px solid var(--sk-line, #d6d6d6);
536
+ padding-top: 0.75rem;
537
+ margin-top: 0.75rem;
538
+ }
539
+ }