@scalewing/react 1.46.0 → 1.47.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 (38) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +59 -2
  3. package/dist/app-shell.test-support.d.ts +5 -0
  4. package/dist/app-shell.test-support.js +45 -0
  5. package/dist/components/AppShell.d.ts +12 -0
  6. package/dist/components/AppShell.js +48 -0
  7. package/dist/components/NavCount.d.ts +12 -5
  8. package/dist/components/NavCount.js +9 -4
  9. package/dist/components/SectionNav.d.ts +2 -1
  10. package/dist/components/SectionNav.js +3 -19
  11. package/dist/components/app-shell/AppShellDrawer.d.ts +24 -0
  12. package/dist/components/app-shell/AppShellDrawer.js +17 -0
  13. package/dist/components/app-shell/AppShellLink.d.ts +14 -0
  14. package/dist/components/app-shell/AppShellLink.js +27 -0
  15. package/dist/components/app-shell/AppShellNav.d.ts +15 -0
  16. package/dist/components/app-shell/AppShellNav.js +29 -0
  17. package/dist/components/app-shell/AppShellSidebar.d.ts +18 -0
  18. package/dist/components/app-shell/AppShellSidebar.js +18 -0
  19. package/dist/components/app-shell/types.d.ts +87 -0
  20. package/dist/components/app-shell/types.js +1 -0
  21. package/dist/components/app-shell/validate.d.ts +6 -0
  22. package/dist/components/app-shell/validate.js +52 -0
  23. package/dist/components/nav-items.d.ts +21 -0
  24. package/dist/components/nav-items.js +26 -0
  25. package/dist/css/css-app-shell-drawer.d.ts +2 -0
  26. package/dist/css/css-app-shell-drawer.js +91 -0
  27. package/dist/css/css-app-shell-nav.d.ts +3 -0
  28. package/dist/css/css-app-shell-nav.js +167 -0
  29. package/dist/css/css-app-shell.d.ts +4 -0
  30. package/dist/css/css-app-shell.js +150 -0
  31. package/dist/css/css-components.js +3 -0
  32. package/dist/css/css-count-pill.d.ts +7 -0
  33. package/dist/css/css-count-pill.js +42 -12
  34. package/dist/css/stylesheet.js +2 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/styles.css +328 -7
  38. package/package.json +1 -1
@@ -0,0 +1,52 @@
1
+ import { isOneOf } from '../../is-one-of.js';
2
+ import { navCountTones } from '../NavCount.js';
3
+ import { assertNavItems } from '../nav-items.js';
4
+ /**
5
+ * An icon that is no glyph: nothing, a boolean or an empty string (which
6
+ * React draws as nothing), or 0 (which it draws as the text "0").
7
+ */
8
+ function drawsNothing(node) {
9
+ return (node === undefined ||
10
+ node === null ||
11
+ typeof node === 'boolean' ||
12
+ node === '' ||
13
+ node === 0);
14
+ }
15
+ function isBlank(words) {
16
+ return typeof words !== 'string' || !words.trim();
17
+ }
18
+ function assertGroups(groups) {
19
+ if (groups.length === 0)
20
+ throw new RangeError('AppShell needs at least one group');
21
+ if (groups.some((group) => isBlank(group.id)))
22
+ throw new RangeError('AppShell groups need nonempty ids');
23
+ if (new Set(groups.map((group) => group.id)).size !== groups.length)
24
+ throw new RangeError('AppShell groups need unique ids');
25
+ if (groups.some((group) => group.label !== undefined && isBlank(group.label)))
26
+ throw new RangeError('AppShell group labels are nonempty when given');
27
+ if (groups.some((group) => group.items.length === 0))
28
+ throw new RangeError('AppShell groups need at least one item');
29
+ }
30
+ /**
31
+ * Fails closed on the shell's words, its groups and their items (ids unique
32
+ * across every group, each with an icon, which is all the rail shows).
33
+ */
34
+ export function assertAppShellProps({ groups, labels, navLabel, }) {
35
+ if (isBlank(navLabel))
36
+ throw new RangeError('AppShell navLabel must be nonempty');
37
+ if (isBlank(labels?.openMenu) || isBlank(labels?.closeMenu))
38
+ throw new RangeError('AppShell labels openMenu and closeMenu are nonempty');
39
+ assertGroups(groups);
40
+ const items = groups.flatMap((group) => group.items);
41
+ assertNavItems('AppShell', items);
42
+ if (items.some((item) => drawsNothing(item.icon)))
43
+ throw new RangeError('AppShell items need an icon, all the rail shows');
44
+ for (const item of items) {
45
+ if (item.countTone === undefined)
46
+ continue;
47
+ if (!isOneOf(navCountTones, item.countTone))
48
+ throw new RangeError(`AppShell item countTone must be one of ${navCountTones.join(', ')}`);
49
+ if (item.count === undefined)
50
+ throw new RangeError('AppShell item countTone comes with a count');
51
+ }
52
+ }
@@ -0,0 +1,21 @@
1
+ /** What a navigation's item is marked by: `aria-current`, or nothing. */
2
+ export type NavItemCurrent = 'page' | 'location' | boolean;
3
+ /** The parts of a navigation item that SectionNav and AppShell validate alike. */
4
+ type CheckedNavItem = {
5
+ id: string;
6
+ label: string;
7
+ count?: number;
8
+ countLabel?: string;
9
+ };
10
+ /**
11
+ * `aria-current` for an item: `page`, `location`, `true` for a bare `true`,
12
+ * and none for `false` or nothing.
13
+ */
14
+ export declare function navItemAriaCurrent(current: NavItemCurrent | undefined): 'page' | 'location' | 'true' | undefined;
15
+ /**
16
+ * Fails closed on a navigation's items, named after the component that
17
+ * draws them: empty or duplicate ids, empty labels, a count that is not a
18
+ * whole number from 0, and a blank `countLabel` or one without a count.
19
+ */
20
+ export declare function assertNavItems(component: string, items: readonly CheckedNavItem[]): void;
21
+ export {};
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `aria-current` for an item: `page`, `location`, `true` for a bare `true`,
3
+ * and none for `false` or nothing.
4
+ */
5
+ export function navItemAriaCurrent(current) {
6
+ if (current === true)
7
+ return 'true';
8
+ return current || undefined;
9
+ }
10
+ /**
11
+ * Fails closed on a navigation's items, named after the component that
12
+ * draws them: empty or duplicate ids, empty labels, a count that is not a
13
+ * whole number from 0, and a blank `countLabel` or one without a count.
14
+ */
15
+ export function assertNavItems(component, items) {
16
+ if (items.some((item) => !item.id.trim() || !item.label.trim()))
17
+ throw new RangeError(`${component} items need nonempty ids and labels`);
18
+ if (new Set(items.map((item) => item.id)).size !== items.length)
19
+ throw new RangeError(`${component} items need unique ids`);
20
+ if (items.some((item) => item.count !== undefined &&
21
+ (!Number.isInteger(item.count) || item.count < 0)))
22
+ throw new RangeError(`${component} item counts are whole numbers from 0`);
23
+ if (items.some((item) => item.countLabel !== undefined &&
24
+ (!item.countLabel.trim() || item.count === undefined)))
25
+ throw new RangeError(`${component} item countLabel is nonempty and comes with a count`);
26
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cssAppShellDrawerClasses(): string;
2
+ export declare function appShellDrawerClassCatalog(): string[];
@@ -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,3 @@
1
+ export declare function cssAppShellNavClasses(): string;
2
+ export declare function cssAppShellRailNavRules(indent: string): string;
3
+ export declare function appShellNavClassCatalog(): string[];
@@ -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,4 @@
1
+ /** From this width up AppShell draws its full sidebar; script reads it too. */
2
+ export declare const appShellWideQuery = "(min-width: 75rem)";
3
+ export declare function cssAppShellClasses(): string;
4
+ export declare function appShellClassCatalog(): string[];
@@ -0,0 +1,150 @@
1
+ import { breakpointQuery } from './breakpoints.js';
2
+ import { appShellDrawerClassCatalog, cssAppShellDrawerClasses, } from './css-app-shell-drawer.js';
3
+ import { appShellNavClassCatalog, cssAppShellNavClasses, cssAppShellRailNavRules, } from './css-app-shell-nav.js';
4
+ import { iconButtonRules } from './css-icon-button.js';
5
+ /*
6
+ * The width from which AppShell's sidebar is full rather than a rail: an
7
+ * internal media query of the component, not a public breakpoint (the
8
+ * `md` and `lg` tokens stay the only ones products use).
9
+ */
10
+ const wideShellWidth = 75;
11
+ /** From this width up AppShell draws its full sidebar; script reads it too. */
12
+ export const appShellWideQuery = `(min-width: ${wideShellWidth}rem)`;
13
+ /* The full sidebar's width, a named geometry of this frame. */
14
+ const sidebarWidth = '15rem';
15
+ /* The rail: the 44 px target with a space-3 gutter each side, and its hairline. */
16
+ const railWidth = 'calc(var(--sw-control-md-min-height) + 2 * var(--sw-space-3) + 1px)';
17
+ const fromLg = breakpointQuery('from', 'lg');
18
+ /*
19
+ * Below `lg`: the page under a sticky top bar (an AppHeader `bar`: the menu
20
+ * button, the title, the end controls), no sidebar. The frame never makes
21
+ * `main` a scroll box: the page scrolls with the document.
22
+ */
23
+ function barRules() {
24
+ return `.sw-app-shell {
25
+ min-height: 100vh;
26
+ min-height: 100dvh;
27
+ }
28
+
29
+ .sw-app-shell-bar {
30
+ align-items: center;
31
+ display: flex;
32
+ gap: var(--sw-space-2);
33
+ }
34
+
35
+ .sw-app-shell-bar-title {
36
+ flex: 1 1 auto;
37
+ min-width: 0;
38
+ }
39
+
40
+ .sw-app-shell-bar-end {
41
+ align-items: center;
42
+ display: flex;
43
+ flex: none;
44
+ gap: var(--sw-space-2);
45
+ }
46
+
47
+ ${iconButtonRules('sw-app-shell-menu')}
48
+
49
+ .sw-app-shell-main {
50
+ display: block;
51
+ min-width: 0;
52
+ }
53
+
54
+ .sw-app-shell-sidebar,
55
+ .sw-app-shell-sidebar .sw-app-shell-rail-slot {
56
+ display: none;
57
+ }`;
58
+ }
59
+ /*
60
+ * From \`lg\`: no bar; the sidebar is a column of the frame's grid, sticky at
61
+ * the top at the viewport's height on the solid surface with a hairline at
62
+ * its inline end. Its header and footer keep their height and only the nav
63
+ * between them scrolls, when it is taller than the window. A rail to 75rem,
64
+ * full from there.
65
+ */
66
+ function sidebarRules() {
67
+ return `@media ${fromLg} {
68
+ .sw-app-shell {
69
+ align-items: start;
70
+ display: grid;
71
+ grid-template-columns: auto minmax(0, 1fr);
72
+ }
73
+
74
+ .sw-app-shell-bar {
75
+ display: none;
76
+ }
77
+
78
+ .sw-app-shell-sidebar {
79
+ background: var(--sw-color-surface);
80
+ border-inline-end: 1px solid var(--sw-color-border);
81
+ box-sizing: border-box;
82
+ display: flex;
83
+ flex-direction: column;
84
+ height: 100vh;
85
+ height: 100dvh;
86
+ position: sticky;
87
+ top: 0;
88
+ width: ${railWidth};
89
+ }
90
+
91
+ @media not all and ${appShellWideQuery} {
92
+ .sw-app-shell-sidebar .sw-app-shell-slot {
93
+ display: none;
94
+ }
95
+
96
+ .sw-app-shell-sidebar .sw-app-shell-rail-slot {
97
+ display: block;
98
+ }
99
+
100
+ ${cssAppShellRailNavRules(' ')}
101
+ }
102
+ }
103
+
104
+ @media ${appShellWideQuery} {
105
+ .sw-app-shell-sidebar {
106
+ width: ${sidebarWidth};
107
+ }
108
+ }`;
109
+ }
110
+ /*
111
+ * AppShell: a workspace frame with a pinned side navigation (see
112
+ * components/AppShell.tsx). The links, groups and the rail's nav are
113
+ * `css-app-shell-nav.ts`; the menu drawer is `css-app-shell-drawer.ts`.
114
+ */
115
+ export function cssAppShellClasses() {
116
+ return `${barRules()}
117
+
118
+ .sw-app-shell-header,
119
+ .sw-app-shell-footer {
120
+ flex: none;
121
+ padding: var(--sw-space-3);
122
+ }
123
+
124
+ .sw-app-shell-footer {
125
+ border-top: 1px solid var(--sw-color-border);
126
+ }
127
+
128
+ ${cssAppShellNavClasses()}
129
+
130
+ ${sidebarRules()}
131
+
132
+ ${cssAppShellDrawerClasses()}`;
133
+ }
134
+ export function appShellClassCatalog() {
135
+ return [
136
+ 'sw-app-shell',
137
+ 'sw-app-shell-bar',
138
+ 'sw-app-shell-menu',
139
+ 'sw-app-shell-bar-title',
140
+ 'sw-app-shell-bar-end',
141
+ 'sw-app-shell-main',
142
+ 'sw-app-shell-sidebar',
143
+ 'sw-app-shell-slot',
144
+ 'sw-app-shell-rail-slot',
145
+ 'sw-app-shell-header',
146
+ 'sw-app-shell-footer',
147
+ ...appShellNavClassCatalog(),
148
+ ...appShellDrawerClassCatalog(),
149
+ ];
150
+ }
@@ -21,6 +21,7 @@ import { cssCheckboxClasses } from './css-checkbox.js';
21
21
  import { cssRadioGroupClasses } from './css-radio-group.js';
22
22
  import { cssChromeClasses } from './css-chrome.js';
23
23
  import { cssSectionNavClasses } from './css-section-nav.js';
24
+ import { cssAppShellClasses } from './css-app-shell.js';
24
25
  import { cssDataClasses } from './css-data.js';
25
26
  import { cssSegmentedClasses } from './css-segmented.js';
26
27
  import { cssTableClasses } from './css-table.js';
@@ -185,6 +186,8 @@ ${cssChromeClasses()}
185
186
 
186
187
  ${cssSectionNavClasses()}
187
188
 
189
+ ${cssAppShellClasses()}
190
+
188
191
  ${cssDialogClasses()}
189
192
 
190
193
  ${cssDialogFrameClasses()}
@@ -1,2 +1,9 @@
1
+ /** The pills that take a tone modifier: NavCount's and AppShell's item count. */
2
+ declare const tonedPills: readonly ["sw-nav-count", "sw-app-shell-count"];
3
+ /** The tones other than the pill's own danger. */
4
+ declare const pillTones: readonly ["accent", "warning"];
5
+ /** A toned pill's modifier class, such as `sw-nav-count-warning`. */
6
+ export declare function countToneClass(pill: (typeof tonedPills)[number], tone: (typeof pillTones)[number]): string;
1
7
  export declare function cssCountPillClasses(): string;
2
8
  export declare function countPillClassCatalog(): string[];
9
+ export {};
@@ -1,16 +1,33 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { toneTintFill, toneTintInk } from './tone-tint.js';
2
3
  const caption = typographyVariants.caption;
3
- /** The counts drawn as the pill: SectionNav's and a Nav word's. */
4
- const pills = ['sw-section-nav-count', 'sw-nav-count'];
5
- /** NavCount's news tone, a modifier of its pill. */
6
- const accentPill = 'sw-nav-count-accent';
4
+ /** The counts drawn as the pill: SectionNav's, a Nav word's and AppShell's. */
5
+ const pills = [
6
+ 'sw-section-nav-count',
7
+ 'sw-nav-count',
8
+ 'sw-app-shell-count',
9
+ ];
10
+ /** The pills that take a tone modifier: NavCount's and AppShell's item count. */
11
+ const tonedPills = ['sw-nav-count', 'sw-app-shell-count'];
12
+ /** The tones other than the pill's own danger. */
13
+ const pillTones = ['accent', 'warning'];
14
+ /** A toned pill's modifier class, such as `sw-nav-count-warning`. */
15
+ export function countToneClass(pill, tone) {
16
+ return `${pill}-${tone}`;
17
+ }
18
+ function tonedSelector(tone) {
19
+ return tonedPills
20
+ .map((pill) => `[data-theme] .${pill}.${countToneClass(pill, tone)}`)
21
+ .join(',\n');
22
+ }
7
23
  /*
8
24
  * A count after a destination's words (a number waiting there, such as
9
25
  * tasks to do) is a filled pill: the danger colour with its on-colour
10
26
  * figures (4.5:1 in every palette, as a danger Button's label), caption
11
27
  * size at weight 700, a space-5 less space-1 (20) high and at least as
12
- * wide. Forced colours draw its edge. SectionNav's item count and Nav's
13
- * NavCount share it, so an area's count reads the same in both.
28
+ * wide. Forced colours draw its edge. SectionNav's item count, Nav's
29
+ * NavCount and AppShell's item count share it, so an area's count reads
30
+ * the same in each.
14
31
  */
15
32
  export function cssCountPillClasses() {
16
33
  const selector = pills.map((name) => `[data-theme] .${name}`).join(',\n');
@@ -33,16 +50,26 @@ export function cssCountPillClasses() {
33
50
  }
34
51
 
35
52
  /*
36
- * NavCount's \`tone="accent"\`: a count that is news, not work waiting (what
37
- * was added since the person last looked), is the accent fill with its
38
- * on-colour figures (4.5:1 in every palette, as a primary Button's label).
39
- * One class more specific than the pill, so it wins in any order.
53
+ * \`tone="accent"\` (NavCount, an AppShell item's \`countTone\`): a count that
54
+ * is news, not work waiting (what was added since the person last looked),
55
+ * is the accent fill with its on-colour figures (4.5:1 in every palette, as
56
+ * a primary Button's label). \`tone="warning"\`: work that needs the person
57
+ * but is not urgent is amber, the warning tint with the warning ink (a
58
+ * warning Notice's, 4.5:1 on the tint in every palette) inside the warning
59
+ * edge a warning Badge draws. One class more specific than the pill, so
60
+ * each wins in any order.
40
61
  */
41
- [data-theme] .sw-nav-count.${accentPill} {
62
+ ${tonedSelector('accent')} {
42
63
  background: var(--sw-color-accent);
43
64
  color: var(--sw-color-onAccent);
44
65
  }
45
66
 
67
+ ${tonedSelector('warning')} {
68
+ background: ${toneTintFill('warning')};
69
+ border: 1px solid var(--sw-color-warning);
70
+ color: ${toneTintInk('warning')};
71
+ }
72
+
46
73
  /*
47
74
  * A Nav link holding a count lays out as SectionNav's link does: the words
48
75
  * and the pill centred on one line a space-2 apart (the space text node
@@ -63,5 +90,8 @@ export function cssCountPillClasses() {
63
90
  }`;
64
91
  }
65
92
  export function countPillClassCatalog() {
66
- return [...pills, accentPill];
93
+ return [
94
+ ...pills,
95
+ ...tonedPills.flatMap((pill) => pillTones.map((tone) => countToneClass(pill, tone))),
96
+ ];
67
97
  }
@@ -50,6 +50,7 @@ import { segmentedClassCatalog } from './css-segmented.js';
50
50
  import { tableClassCatalog } from './css-table.js';
51
51
  import { chromeClassCatalog } from './css-chrome.js';
52
52
  import { sectionNavClassCatalog } from './css-section-nav.js';
53
+ import { appShellClassCatalog } from './css-app-shell.js';
53
54
  import { dialogClassCatalog } from './css-dialog.js';
54
55
  import { dialogBandedClassCatalog } from './css-dialog-banded.js';
55
56
  import { dialogFrameClassCatalog } from './css-dialog-frame.js';
@@ -93,6 +94,7 @@ export function utilityClassCatalog() {
93
94
  ...paginationClassCatalog(),
94
95
  ...chromeClassCatalog(),
95
96
  ...sectionNavClassCatalog(),
97
+ ...appShellClassCatalog(),
96
98
  ...dialogClassCatalog(),
97
99
  ...dialogFrameClassCatalog(),
98
100
  ...dialogBandedClassCatalog(),
package/dist/index.d.ts CHANGED
@@ -52,6 +52,7 @@ export { Split } from './components/Split.js';
52
52
  export type { SplitProps } from './components/Split.js';
53
53
  export { Nav, navAppearances, type NavAppearance, type NavProps, } from './components/Nav.js';
54
54
  export { NavCount, navCountTones, type NavCountProps, type NavCountTone, } from './components/NavCount.js';
55
+ export { AppShell, type AppShellLabels, type AppShellNavGroup, type AppShellNavItem, type AppShellProps, } from './components/AppShell.js';
55
56
  export { SectionNav, sectionNavAppearances, type SectionNavAppearance, type SectionNavItem, type SectionNavProps, } from './components/SectionNav.js';
56
57
  export { SegmentedControl, type SegmentedControlProps, type SegmentedControlVariant, segmentedSizes, type SegmentedSize, type SegmentedItem, } from './components/SegmentedControl.js';
57
58
  export { Combobox, comboboxSizes, type ComboboxAction, type ComboboxBadge, type ComboboxGroup, type ComboboxOption, type ComboboxProps, type ComboboxSize, } from './components/Combobox.js';
package/dist/index.js CHANGED
@@ -49,6 +49,7 @@ export { Inline } from './components/Inline.js';
49
49
  export { Split } from './components/Split.js';
50
50
  export { Nav, navAppearances, } from './components/Nav.js';
51
51
  export { NavCount, navCountTones, } from './components/NavCount.js';
52
+ export { AppShell, } from './components/AppShell.js';
52
53
  export { SectionNav, sectionNavAppearances, } from './components/SectionNav.js';
53
54
  export { SegmentedControl, segmentedSizes, } from './components/SegmentedControl.js';
54
55
  export { Combobox, comboboxSizes, } from './components/Combobox.js';