@scalewing/react 1.46.0 → 1.48.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/CHANGELOG.md +74 -0
- package/README.md +228 -4
- package/dist/app-shell.test-support.d.ts +5 -0
- package/dist/app-shell.test-support.js +45 -0
- package/dist/command-palette.test-support.d.ts +21 -0
- package/dist/command-palette.test-support.js +55 -0
- package/dist/components/AppHeader.d.ts +3 -0
- package/dist/components/AppShell.d.ts +13 -0
- package/dist/components/AppShell.js +65 -0
- package/dist/components/Band.d.ts +30 -0
- package/dist/components/Band.js +23 -0
- package/dist/components/ButtonLink.d.ts +33 -0
- package/dist/components/ButtonLink.js +27 -0
- package/dist/components/CommandPalette.d.ts +12 -0
- package/dist/components/CommandPalette.js +143 -0
- package/dist/components/CommandPaletteTrigger.d.ts +9 -0
- package/dist/components/CommandPaletteTrigger.js +16 -0
- package/dist/components/ContextSwitcher.d.ts +12 -0
- package/dist/components/ContextSwitcher.js +62 -0
- package/dist/components/FixtureGroup.d.ts +9 -0
- package/dist/components/FixtureGroup.js +16 -0
- package/dist/components/FixtureRow.d.ts +10 -0
- package/dist/components/FixtureRow.js +46 -0
- package/dist/components/GlyphTile.d.ts +9 -3
- package/dist/components/GlyphTile.js +15 -4
- package/dist/components/NavCount.d.ts +12 -5
- package/dist/components/NavCount.js +9 -4
- package/dist/components/SectionNav.d.ts +2 -1
- package/dist/components/SectionNav.js +3 -19
- package/dist/components/SiteFooter.d.ts +12 -0
- package/dist/components/SiteFooter.js +23 -0
- package/dist/components/SiteHeader.d.ts +14 -0
- package/dist/components/SiteHeader.js +24 -0
- package/dist/components/Skeleton.d.ts +22 -0
- package/dist/components/Skeleton.js +30 -0
- package/dist/components/app-shell/AppShellBar.d.ts +23 -0
- package/dist/components/app-shell/AppShellBar.js +25 -0
- package/dist/components/app-shell/AppShellDrawer.d.ts +24 -0
- package/dist/components/app-shell/AppShellDrawer.js +17 -0
- package/dist/components/app-shell/AppShellLink.d.ts +14 -0
- package/dist/components/app-shell/AppShellLink.js +27 -0
- package/dist/components/app-shell/AppShellNav.d.ts +15 -0
- package/dist/components/app-shell/AppShellNav.js +29 -0
- package/dist/components/app-shell/AppShellSidebar.d.ts +18 -0
- package/dist/components/app-shell/AppShellSidebar.js +18 -0
- package/dist/components/app-shell/types.d.ts +122 -0
- package/dist/components/app-shell/types.js +1 -0
- package/dist/components/app-shell/validate.d.ts +7 -0
- package/dist/components/app-shell/validate.js +70 -0
- package/dist/components/command-palette/PaletteList.d.ts +20 -0
- package/dist/components/command-palette/PaletteList.js +21 -0
- package/dist/components/command-palette/shortcut.d.ts +19 -0
- package/dist/components/command-palette/shortcut.js +75 -0
- package/dist/components/command-palette/types.d.ts +72 -0
- package/dist/components/command-palette/types.js +2 -0
- package/dist/components/command-palette/validate.d.ts +10 -0
- package/dist/components/command-palette/validate.js +50 -0
- package/dist/components/context-switcher/SwitcherOption.d.ts +17 -0
- package/dist/components/context-switcher/SwitcherOption.js +20 -0
- package/dist/components/context-switcher/SwitcherPopup.d.ts +31 -0
- package/dist/components/context-switcher/SwitcherPopup.js +111 -0
- package/dist/components/context-switcher/options.d.ts +33 -0
- package/dist/components/context-switcher/options.js +60 -0
- package/dist/components/context-switcher/types.d.ts +96 -0
- package/dist/components/context-switcher/types.js +1 -0
- package/dist/components/context-switcher/use-switcher-list.d.ts +29 -0
- package/dist/components/context-switcher/use-switcher-list.js +93 -0
- package/dist/components/context-switcher/validate.d.ts +8 -0
- package/dist/components/context-switcher/validate.js +94 -0
- package/dist/components/fixture/fixture-list-context.d.ts +4 -0
- package/dist/components/fixture/fixture-list-context.js +9 -0
- package/dist/components/fixture/types.d.ts +50 -0
- package/dist/components/fixture/types.js +1 -0
- package/dist/components/fixture/validate.d.ts +8 -0
- package/dist/components/fixture/validate.js +36 -0
- package/dist/components/nav-items.d.ts +21 -0
- package/dist/components/nav-items.js +26 -0
- package/dist/components/site-footer/types.d.ts +31 -0
- package/dist/components/site-footer/types.js +1 -0
- package/dist/components/site-footer/validate.d.ts +7 -0
- package/dist/components/site-footer/validate.js +22 -0
- package/dist/components/site-header/SiteHeaderLinks.d.ts +7 -0
- package/dist/components/site-header/SiteHeaderLinks.js +7 -0
- package/dist/components/site-header/types.d.ts +38 -0
- package/dist/components/site-header/types.js +1 -0
- package/dist/components/site-header/use-menu-dismiss.d.ts +8 -0
- package/dist/components/site-header/use-menu-dismiss.js +62 -0
- package/dist/components/site-header/validate.d.ts +7 -0
- package/dist/components/site-header/validate.js +17 -0
- package/dist/components/use-element-height.d.ts +10 -0
- package/dist/components/use-element-height.js +35 -0
- package/dist/context-switcher.test-support.d.ts +9 -0
- package/dist/context-switcher.test-support.js +41 -0
- package/dist/css/css-app-shell-drawer.d.ts +2 -0
- package/dist/css/css-app-shell-drawer.js +91 -0
- package/dist/css/css-app-shell-nav.d.ts +3 -0
- package/dist/css/css-app-shell-nav.js +167 -0
- package/dist/css/css-app-shell-page-bar.d.ts +3 -0
- package/dist/css/css-app-shell-page-bar.js +81 -0
- package/dist/css/css-app-shell.d.ts +4 -0
- package/dist/css/css-app-shell.js +155 -0
- package/dist/css/css-band.d.ts +10 -0
- package/dist/css/css-band.js +101 -0
- package/dist/css/css-button-link.d.ts +3 -0
- package/dist/css/css-button-link.js +48 -0
- package/dist/css/css-command-palette-key.d.ts +1 -0
- package/dist/css/css-command-palette-key.js +33 -0
- package/dist/css/css-command-palette-trigger.d.ts +2 -0
- package/dist/css/css-command-palette-trigger.js +88 -0
- package/dist/css/css-command-palette.d.ts +2 -0
- package/dist/css/css-command-palette.js +209 -0
- package/dist/css/css-components.js +27 -0
- package/dist/css/css-context-switcher-trigger.d.ts +15 -0
- package/dist/css/css-context-switcher-trigger.js +175 -0
- package/dist/css/css-context-switcher.d.ts +4 -0
- package/dist/css/css-context-switcher.js +219 -0
- package/dist/css/css-count-pill.d.ts +7 -0
- package/dist/css/css-count-pill.js +50 -12
- package/dist/css/css-fixture.d.ts +5 -0
- package/dist/css/css-fixture.js +260 -0
- package/dist/css/css-glyph-tile.d.ts +2 -0
- package/dist/css/css-glyph-tile.js +29 -1
- package/dist/css/css-site-footer.d.ts +2 -0
- package/dist/css/css-site-footer.js +152 -0
- package/dist/css/css-site-header.d.ts +2 -0
- package/dist/css/css-site-header.js +262 -0
- package/dist/css/css-skeleton.d.ts +10 -0
- package/dist/css/css-skeleton.js +110 -0
- package/dist/css/stacking.js +7 -4
- package/dist/css/stylesheet.js +18 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/styles.css +2122 -512
- package/package.json +2 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { breakpointQuery } from '../../css/breakpoints.js';
|
|
3
|
+
/** From here the bar shows its row of destinations and hides the menu. */
|
|
4
|
+
const wideQuery = breakpointQuery('from', 'lg');
|
|
5
|
+
/**
|
|
6
|
+
* What script adds to the menu, a native `details` that opens and closes
|
|
7
|
+
* without it: Escape closes it and puts focus back on its button; choosing
|
|
8
|
+
* a link in it, focus leaving it, a press outside it, or the window
|
|
9
|
+
* widening to the row of destinations closes it. Nothing traps focus.
|
|
10
|
+
*/
|
|
11
|
+
export function useMenuDismiss(menuRef) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const menu = menuRef.current;
|
|
14
|
+
if (!menu)
|
|
15
|
+
return;
|
|
16
|
+
const close = () => {
|
|
17
|
+
menu.open = false;
|
|
18
|
+
};
|
|
19
|
+
const onKeyDown = (event) => {
|
|
20
|
+
if (event.key !== 'Escape' || !menu.open)
|
|
21
|
+
return;
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
close();
|
|
24
|
+
menu.querySelector('summary')?.focus();
|
|
25
|
+
};
|
|
26
|
+
const onClick = (event) => {
|
|
27
|
+
if (event.target instanceof Element && event.target.closest('a'))
|
|
28
|
+
close();
|
|
29
|
+
};
|
|
30
|
+
const onFocusOut = (event) => {
|
|
31
|
+
const next = event.relatedTarget;
|
|
32
|
+
if (next instanceof Node && menu.contains(next))
|
|
33
|
+
return;
|
|
34
|
+
if (next !== null)
|
|
35
|
+
close();
|
|
36
|
+
};
|
|
37
|
+
const onPointerDown = (event) => {
|
|
38
|
+
if (event.target instanceof Node && menu.contains(event.target))
|
|
39
|
+
return;
|
|
40
|
+
close();
|
|
41
|
+
};
|
|
42
|
+
const wide = typeof window.matchMedia === 'function'
|
|
43
|
+
? window.matchMedia(wideQuery)
|
|
44
|
+
: undefined;
|
|
45
|
+
const onWiden = () => {
|
|
46
|
+
if (wide?.matches)
|
|
47
|
+
close();
|
|
48
|
+
};
|
|
49
|
+
menu.addEventListener('keydown', onKeyDown);
|
|
50
|
+
menu.addEventListener('click', onClick);
|
|
51
|
+
menu.addEventListener('focusout', onFocusOut);
|
|
52
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
53
|
+
wide?.addEventListener('change', onWiden);
|
|
54
|
+
return () => {
|
|
55
|
+
menu.removeEventListener('keydown', onKeyDown);
|
|
56
|
+
menu.removeEventListener('click', onClick);
|
|
57
|
+
menu.removeEventListener('focusout', onFocusOut);
|
|
58
|
+
document.removeEventListener('pointerdown', onPointerDown);
|
|
59
|
+
wide?.removeEventListener('change', onWiden);
|
|
60
|
+
};
|
|
61
|
+
}, [menuRef]);
|
|
62
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SiteHeaderProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fails closed on the header's words and destinations: no items, blank or
|
|
4
|
+
* repeated ids, blank labels or hrefs, and a blank navigation name or menu
|
|
5
|
+
* button name.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertSiteHeaderProps({ items, labels, navLabel, }: Pick<SiteHeaderProps, 'items' | 'labels' | 'navLabel'>): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { assertNavItems } from '../nav-items.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fails closed on the header's words and destinations: no items, blank or
|
|
4
|
+
* repeated ids, blank labels or hrefs, and a blank navigation name or menu
|
|
5
|
+
* button name.
|
|
6
|
+
*/
|
|
7
|
+
export function assertSiteHeaderProps({ items, labels, navLabel, }) {
|
|
8
|
+
if (items.length === 0)
|
|
9
|
+
throw new RangeError('SiteHeader needs at least one item');
|
|
10
|
+
assertNavItems('SiteHeader', items);
|
|
11
|
+
if (items.some((item) => !item.href.trim()))
|
|
12
|
+
throw new RangeError('SiteHeader items need nonempty hrefs');
|
|
13
|
+
if (!navLabel.trim())
|
|
14
|
+
throw new RangeError('SiteHeader needs a nonempty navLabel');
|
|
15
|
+
if (!labels.openMenu.trim() || !labels.closeMenu.trim())
|
|
16
|
+
throw new RangeError('SiteHeader labels need nonempty openMenu and closeMenu');
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Follows the height of an element's border box, in CSS pixels: on
|
|
4
|
+
* mount, before the first paint, and whenever its box changes size. It is
|
|
5
|
+
* `null` before that, on the server, while `enabled` is false and while the
|
|
6
|
+
* element draws no box (`display: none`), so a caller can fall back to a
|
|
7
|
+
* value of its own. A browser with no ResizeObserver measures on window
|
|
8
|
+
* resizes instead.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useElementHeight(ref: RefObject<HTMLElement | null>, enabled?: boolean): number | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLayoutEffect, useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Follows the height of an element's border box, in CSS pixels: on
|
|
5
|
+
* mount, before the first paint, and whenever its box changes size. It is
|
|
6
|
+
* `null` before that, on the server, while `enabled` is false and while the
|
|
7
|
+
* element draws no box (`display: none`), so a caller can fall back to a
|
|
8
|
+
* value of its own. A browser with no ResizeObserver measures on window
|
|
9
|
+
* resizes instead.
|
|
10
|
+
*/
|
|
11
|
+
export function useElementHeight(ref, enabled = true) {
|
|
12
|
+
const [height, setHeight] = useState(null);
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
const box = ref.current;
|
|
15
|
+
if (!box || !enabled) {
|
|
16
|
+
setHeight(null);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
function measure() {
|
|
20
|
+
if (!box)
|
|
21
|
+
return;
|
|
22
|
+
const next = box.getBoundingClientRect().height;
|
|
23
|
+
setHeight(next > 0 ? next : null);
|
|
24
|
+
}
|
|
25
|
+
measure();
|
|
26
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
27
|
+
window.addEventListener('resize', measure);
|
|
28
|
+
return () => window.removeEventListener('resize', measure);
|
|
29
|
+
}
|
|
30
|
+
const observer = new ResizeObserver(measure);
|
|
31
|
+
observer.observe(box);
|
|
32
|
+
return () => observer.disconnect();
|
|
33
|
+
}, [ref, enabled]);
|
|
34
|
+
return height;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RenderResult } from '@testing-library/react';
|
|
2
|
+
import { type Mock } from 'vitest';
|
|
3
|
+
import { type ContextSwitcherGroup, type ContextSwitcherProps } from './components/ContextSwitcher.js';
|
|
4
|
+
/** Field stations: a bare group, then a labelled one with badges and counts. */
|
|
5
|
+
export declare const stations: ContextSwitcherGroup[];
|
|
6
|
+
/** The switcher with its required props, and any prop overridden. */
|
|
7
|
+
export declare function renderSwitcher(props?: Partial<ContextSwitcherProps>): RenderResult & {
|
|
8
|
+
onSelect: Mock;
|
|
9
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi } from 'vitest';
|
|
4
|
+
import { ContextSwitcher, } from './components/ContextSwitcher.js';
|
|
5
|
+
/** Field stations: a bare group, then a labelled one with badges and counts. */
|
|
6
|
+
export const stations = [
|
|
7
|
+
{
|
|
8
|
+
id: 'home',
|
|
9
|
+
options: [
|
|
10
|
+
{
|
|
11
|
+
id: 'all',
|
|
12
|
+
label: 'All stations',
|
|
13
|
+
icon: _jsx("svg", { "data-glyph": "all" }),
|
|
14
|
+
count: 7,
|
|
15
|
+
countLabel: '7 need you',
|
|
16
|
+
countTone: 'warning',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'mine',
|
|
22
|
+
label: 'Stations you run',
|
|
23
|
+
options: [
|
|
24
|
+
{
|
|
25
|
+
id: 'heron',
|
|
26
|
+
label: 'Heron Marsh',
|
|
27
|
+
detail: 'Warden',
|
|
28
|
+
badge: { label: 'Live', tone: 'success' },
|
|
29
|
+
},
|
|
30
|
+
{ id: 'pine', label: 'Pine Ridge', detail: 'Ranger' },
|
|
31
|
+
{ id: 'bittern', label: 'Bittern Fen', count: 2 },
|
|
32
|
+
{ id: 'bay', label: 'Bayside Dunes', detail: 'Árbol team' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
/** The switcher with its required props, and any prop overridden. */
|
|
37
|
+
export function renderSwitcher(props = {}) {
|
|
38
|
+
const onSelect = vi.fn();
|
|
39
|
+
const view = render(_jsx(ContextSwitcher, { currentId: "heron", groups: stations, label: "Station", onSelect: onSelect, trigger: "Heron Marsh", ...props }));
|
|
40
|
+
return { ...view, onSelect };
|
|
41
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { iconButtonRules } from './css-icon-button.js';
|
|
2
|
+
/* The drawer's width, a named geometry of this frame; a phone keeps a strip of backdrop. */
|
|
3
|
+
const drawerWidth = '20rem';
|
|
4
|
+
const drawer = '.sw-app-shell-drawer';
|
|
5
|
+
/*
|
|
6
|
+
* AppShell's menu below `lg`: a modal `dialog` on the top layer, docked to
|
|
7
|
+
* the inline start (the right in right-to-left) at the viewport's full
|
|
8
|
+
* height on the solid surface, a hairline at its inline end, clearing the
|
|
9
|
+
* safe areas, a space-8 strip of backdrop left in view on a phone. It
|
|
10
|
+
* slides in from its edge unless reduced motion is asked for. Its column
|
|
11
|
+
* is the top row (the close button where the menu button was, then the
|
|
12
|
+
* header), the nav, which alone scrolls, and the footer. The backdrop is
|
|
13
|
+
* Dialog's.
|
|
14
|
+
*/
|
|
15
|
+
export function cssAppShellDrawerClasses() {
|
|
16
|
+
return `${drawer} {
|
|
17
|
+
background: var(--sw-color-surface);
|
|
18
|
+
border: 0;
|
|
19
|
+
border-inline-end: 1px solid var(--sw-color-border);
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
color: var(--sw-color-text);
|
|
22
|
+
height: 100%;
|
|
23
|
+
inset-block: 0;
|
|
24
|
+
inset-inline: 0 auto;
|
|
25
|
+
margin: 0;
|
|
26
|
+
max-height: none;
|
|
27
|
+
max-width: calc(100% - var(--sw-space-8));
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
padding: env(safe-area-inset-top, 0px) 0 env(safe-area-inset-bottom, 0px);
|
|
30
|
+
padding-inline-start: env(safe-area-inset-left, 0px);
|
|
31
|
+
width: ${drawerWidth};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
${drawer}:dir(rtl) {
|
|
35
|
+
padding-inline-start: env(safe-area-inset-right, 0px);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
${drawer}[open] {
|
|
39
|
+
animation: sw-app-shell-drawer-in var(--sw-motion-default) var(--sw-motion-easing);
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
${drawer}[open]:dir(rtl) {
|
|
45
|
+
animation-name: sw-app-shell-drawer-in-rtl;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@keyframes sw-app-shell-drawer-in {
|
|
49
|
+
from { translate: -100% 0; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@keyframes sw-app-shell-drawer-in-rtl {
|
|
53
|
+
from { translate: 100% 0; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
${drawer}::backdrop {
|
|
57
|
+
background: color-mix(in srgb, var(--sw-color-text) 28%, transparent);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sw-app-shell-drawer-top {
|
|
61
|
+
align-items: center;
|
|
62
|
+
display: flex;
|
|
63
|
+
flex: none;
|
|
64
|
+
gap: var(--sw-space-2);
|
|
65
|
+
padding: var(--sw-space-3) var(--sw-space-3) 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sw-app-shell-drawer-top .sw-app-shell-header {
|
|
69
|
+
flex: 1 1 auto;
|
|
70
|
+
min-width: 0;
|
|
71
|
+
padding: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
${iconButtonRules('sw-app-shell-close')}
|
|
75
|
+
|
|
76
|
+
@media (prefers-reduced-motion: reduce) {
|
|
77
|
+
${drawer}[open],
|
|
78
|
+
${drawer}[open]:dir(rtl) { animation: none; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media (forced-colors: active) {
|
|
82
|
+
${drawer} { border-inline-end-color: CanvasText; }
|
|
83
|
+
}`;
|
|
84
|
+
}
|
|
85
|
+
export function appShellDrawerClassCatalog() {
|
|
86
|
+
return [
|
|
87
|
+
'sw-app-shell-drawer',
|
|
88
|
+
'sw-app-shell-drawer-top',
|
|
89
|
+
'sw-app-shell-close',
|
|
90
|
+
];
|
|
91
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { visuallyHiddenDeclarations } from './css-utilities.js';
|
|
3
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
4
|
+
const caption = typographyVariants.caption;
|
|
5
|
+
const label = typographyVariants.label;
|
|
6
|
+
/*
|
|
7
|
+
* The shell's destinations, in the full sidebar and the drawer alike: label
|
|
8
|
+
* size in the text colour at weight 500, the glyph before the label and the
|
|
9
|
+
* count pill (the shared rule, `css-count-pill.ts`) at the row's end. The
|
|
10
|
+
* current one takes the quiet muted fill in the text colour at the label
|
|
11
|
+
* weight, as SectionNav \`filled\` marks it; hover a lighter fill. Each is
|
|
12
|
+
* the sm control and the 44 px target on a coarse pointer. A 2 px
|
|
13
|
+
* transparent border keeps room for forced colours, where the current
|
|
14
|
+
* link's turns Highlight. A group's label is a muted caption at weight 600
|
|
15
|
+
* (sentence case, never uppercase). The link rules start at the canvas
|
|
16
|
+
* (\`[data-theme]\`) to outrank its accent links.
|
|
17
|
+
*/
|
|
18
|
+
export function cssAppShellNavClasses() {
|
|
19
|
+
return `.sw-app-shell-nav {
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex: 1 1 auto;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: var(--sw-space-4);
|
|
25
|
+
min-height: 0;
|
|
26
|
+
overflow-y: auto;
|
|
27
|
+
overscroll-behavior: contain;
|
|
28
|
+
padding: var(--sw-space-2) var(--sw-space-3);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.sw-app-shell-group {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: var(--sw-space-1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sw-app-shell-group-label {
|
|
38
|
+
color: var(--sw-color-muted);
|
|
39
|
+
font-family: var(--sw-font-sans);
|
|
40
|
+
font-size: ${caption.fontSize}px;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
letter-spacing: ${caption.letterSpacing}px;
|
|
43
|
+
line-height: ${caption.lineHeight}px;
|
|
44
|
+
margin: 0;
|
|
45
|
+
padding-inline: var(--sw-space-2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.sw-app-shell-list {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: var(--sw-space-1);
|
|
52
|
+
list-style: none;
|
|
53
|
+
margin: 0;
|
|
54
|
+
padding: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.sw-app-shell-item > .sw-tooltip-anchor {
|
|
58
|
+
display: flex;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[data-theme] .sw-app-shell-link {
|
|
62
|
+
align-items: center;
|
|
63
|
+
border: 2px solid transparent;
|
|
64
|
+
border-radius: var(--sw-radius-sm);
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
color: var(--sw-color-text);
|
|
67
|
+
display: flex;
|
|
68
|
+
flex: 1 1 auto;
|
|
69
|
+
font-family: var(--sw-font-sans);
|
|
70
|
+
font-size: ${label.fontSize}px;
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
gap: var(--sw-space-3);
|
|
73
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
74
|
+
line-height: ${label.lineHeight}px;
|
|
75
|
+
min-height: var(--sw-control-sm-min-height);
|
|
76
|
+
padding: var(--sw-space-1) var(--sw-space-2);
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[data-theme] .sw-app-shell-link:hover {
|
|
81
|
+
background: color-mix(in srgb, var(--sw-color-muted) 12%, transparent);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[data-theme] .sw-app-shell-link:focus-visible {
|
|
85
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
86
|
+
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[data-theme] .sw-app-shell-link[aria-current] {
|
|
90
|
+
background: color-mix(in srgb, var(--sw-color-muted) 18%, transparent);
|
|
91
|
+
font-weight: ${label.fontWeight};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.sw-app-shell-icon {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
flex: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.sw-app-shell-label {
|
|
100
|
+
flex: 1 1 auto;
|
|
101
|
+
min-width: 0;
|
|
102
|
+
overflow-wrap: anywhere;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.sw-app-shell-count {
|
|
106
|
+
flex: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@media ${coarsePointerQuery} {
|
|
110
|
+
[data-theme] .sw-app-shell-link { min-height: ${touchTarget}; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@media (forced-colors: active) {
|
|
114
|
+
[data-theme] .sw-app-shell-link { color: LinkText; }
|
|
115
|
+
[data-theme] .sw-app-shell-link[aria-current] { border-color: Highlight; color: CanvasText; }
|
|
116
|
+
}`;
|
|
117
|
+
}
|
|
118
|
+
/*
|
|
119
|
+
* The rail (from \`lg\` to 75rem, inside the sidebar's media queries): each
|
|
120
|
+
* link the 44 px target holding its glyph alone, the label visually hidden
|
|
121
|
+
* but still its name, the count pill at the link's top end corner. A
|
|
122
|
+
* group's label is hidden too (it still names its list) and every group
|
|
123
|
+
* after the first starts under a hairline.
|
|
124
|
+
*/
|
|
125
|
+
export function cssAppShellRailNavRules(indent) {
|
|
126
|
+
const rules = `.sw-app-shell-sidebar .sw-app-shell-label,
|
|
127
|
+
.sw-app-shell-sidebar .sw-app-shell-group-label { ${visuallyHiddenDeclarations} }
|
|
128
|
+
|
|
129
|
+
.sw-app-shell-sidebar .sw-app-shell-nav {
|
|
130
|
+
gap: var(--sw-space-2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.sw-app-shell-sidebar .sw-app-shell-group + .sw-app-shell-group {
|
|
134
|
+
border-top: 1px solid var(--sw-color-border);
|
|
135
|
+
padding-top: var(--sw-space-2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[data-theme] .sw-app-shell-sidebar .sw-app-shell-link {
|
|
139
|
+
justify-content: center;
|
|
140
|
+
min-height: ${touchTarget};
|
|
141
|
+
padding: 0;
|
|
142
|
+
position: relative;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.sw-app-shell-sidebar .sw-app-shell-count {
|
|
146
|
+
inset-block-start: 0;
|
|
147
|
+
inset-inline-end: 0;
|
|
148
|
+
position: absolute;
|
|
149
|
+
}`;
|
|
150
|
+
return rules
|
|
151
|
+
.split('\n')
|
|
152
|
+
.map((line) => (line ? `${indent}${line}` : line))
|
|
153
|
+
.join('\n');
|
|
154
|
+
}
|
|
155
|
+
export function appShellNavClassCatalog() {
|
|
156
|
+
return [
|
|
157
|
+
'sw-app-shell-nav',
|
|
158
|
+
'sw-app-shell-group',
|
|
159
|
+
'sw-app-shell-group-label',
|
|
160
|
+
'sw-app-shell-list',
|
|
161
|
+
'sw-app-shell-item',
|
|
162
|
+
'sw-app-shell-link',
|
|
163
|
+
'sw-app-shell-icon',
|
|
164
|
+
'sw-app-shell-label',
|
|
165
|
+
// sw-app-shell-count is the shared pill's (css-count-pill.ts).
|
|
166
|
+
];
|
|
167
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { breakpointQuery } from './breakpoints.js';
|
|
2
|
+
import { zIndex } from './stacking.js';
|
|
3
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
4
|
+
/*
|
|
5
|
+
* The compact bar's measured height, set on `.sw-app-shell` by the
|
|
6
|
+
* component while it draws a page bar, so the page bar sticks right under
|
|
7
|
+
* the bar whatever its end controls make it.
|
|
8
|
+
*/
|
|
9
|
+
export const appShellBarHeightVariable = '--sw-app-shell-bar-height';
|
|
10
|
+
/*
|
|
11
|
+
* The bar's usual height before it is measured (and without script): the
|
|
12
|
+
* sm menu button, or the touch target on a coarse pointer, inside the
|
|
13
|
+
* bar's space-2 padding, and its hairline.
|
|
14
|
+
*/
|
|
15
|
+
function barHeightFallback(control) {
|
|
16
|
+
return `calc(${control} + 2 * var(--sw-space-2) + 1px)`;
|
|
17
|
+
}
|
|
18
|
+
const fromLg = breakpointQuery('from', 'lg');
|
|
19
|
+
/*
|
|
20
|
+
* `pageBar`: a band at the top of `main` on every layout, the solid
|
|
21
|
+
* surface with a hairline under it and the page's inline padding (space-4,
|
|
22
|
+
* space-6 from `lg`), its children a wrapping row with space between them.
|
|
23
|
+
* Sticky on the top chrome's layer: under the compact bar below `lg`, at
|
|
24
|
+
* the window's top from `lg`, where the bar is gone. Reduce Transparency
|
|
25
|
+
* needs nothing (it is already solid); forced colours keep the hairline.
|
|
26
|
+
*/
|
|
27
|
+
function pageBarRules() {
|
|
28
|
+
return `.sw-app-shell-page-bar {
|
|
29
|
+
align-items: center;
|
|
30
|
+
background: var(--sw-color-surface);
|
|
31
|
+
border-bottom: 1px solid var(--sw-color-border);
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-wrap: wrap;
|
|
35
|
+
gap: var(--sw-space-2) var(--sw-space-3);
|
|
36
|
+
justify-content: space-between;
|
|
37
|
+
padding: var(--sw-space-2) var(--sw-space-4);
|
|
38
|
+
position: sticky;
|
|
39
|
+
top: var(${appShellBarHeightVariable}, ${barHeightFallback('var(--sw-control-sm-min-height)')});
|
|
40
|
+
${zIndex('topChrome')}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@media ${coarsePointerQuery} {
|
|
44
|
+
.sw-app-shell-page-bar {
|
|
45
|
+
top: var(${appShellBarHeightVariable}, ${barHeightFallback(touchTarget)});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@media ${fromLg} {
|
|
50
|
+
.sw-app-shell-page-bar {
|
|
51
|
+
padding-inline: var(--sw-space-6);
|
|
52
|
+
top: 0;
|
|
53
|
+
}
|
|
54
|
+
}`;
|
|
55
|
+
}
|
|
56
|
+
/*
|
|
57
|
+
* `menuCount`: the shared count pill (`sw-app-shell-count`, its tones
|
|
58
|
+
* included) on the menu button's top end corner, over the glyph's edge as
|
|
59
|
+
* on a rail glyph, mirrored in right-to-left.
|
|
60
|
+
*/
|
|
61
|
+
function menuCountRules() {
|
|
62
|
+
return `.sw-app-shell-menu {
|
|
63
|
+
position: relative;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.sw-app-shell-menu .sw-app-shell-count {
|
|
67
|
+
inset-block-start: calc(-1 * var(--sw-space-1));
|
|
68
|
+
inset-inline-end: calc(-1 * var(--sw-space-1));
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
position: absolute;
|
|
71
|
+
}`;
|
|
72
|
+
}
|
|
73
|
+
/* AppShell's page bar and the menu button's count (see components/AppShell.tsx). */
|
|
74
|
+
export function cssAppShellPageBarClasses() {
|
|
75
|
+
return `${pageBarRules()}
|
|
76
|
+
|
|
77
|
+
${menuCountRules()}`;
|
|
78
|
+
}
|
|
79
|
+
export function appShellPageBarClassCatalog() {
|
|
80
|
+
return ['sw-app-shell-page-bar'];
|
|
81
|
+
}
|