@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.
- package/CHANGELOG.md +7 -0
- package/README.md +59 -2
- package/dist/app-shell.test-support.d.ts +5 -0
- package/dist/app-shell.test-support.js +45 -0
- package/dist/components/AppShell.d.ts +12 -0
- package/dist/components/AppShell.js +48 -0
- 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/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 +87 -0
- package/dist/components/app-shell/types.js +1 -0
- package/dist/components/app-shell/validate.d.ts +6 -0
- package/dist/components/app-shell/validate.js +52 -0
- package/dist/components/nav-items.d.ts +21 -0
- package/dist/components/nav-items.js +26 -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.d.ts +4 -0
- package/dist/css/css-app-shell.js +150 -0
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-count-pill.d.ts +7 -0
- package/dist/css/css-count-pill.js +42 -12
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/styles.css +328 -7
- 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,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,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
|
|
4
|
-
const pills = [
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
13
|
-
* NavCount share it, so an area's count reads
|
|
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
|
-
*
|
|
37
|
-
* was added since the person last looked),
|
|
38
|
-
* on-colour figures (4.5:1 in every palette, as
|
|
39
|
-
*
|
|
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
|
-
|
|
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 [
|
|
93
|
+
return [
|
|
94
|
+
...pills,
|
|
95
|
+
...tonedPills.flatMap((pill) => pillTones.map((tone) => countToneClass(pill, tone))),
|
|
96
|
+
];
|
|
67
97
|
}
|
package/dist/css/stylesheet.js
CHANGED
|
@@ -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';
|