@scalewing/react 1.45.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 +16 -0
- package/README.md +105 -3
- 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/Button.d.ts +14 -1
- package/dist/components/Button.js +5 -2
- package/dist/components/FileField.d.ts +18 -2
- package/dist/components/FileField.js +26 -22
- package/dist/components/ListGroup.d.ts +8 -3
- package/dist/components/ListGroup.js +13 -5
- 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/Select.d.ts +8 -5
- package/dist/components/Select.js +7 -4
- 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/file-field/ChosenFile.d.ts +45 -0
- package/dist/components/file-field/ChosenFile.js +48 -0
- package/dist/components/file-field/file-field-checks.d.ts +5 -2
- package/dist/components/file-field/file-field-checks.js +7 -2
- package/dist/components/nav-items.d.ts +21 -0
- package/dist/components/nav-items.js +26 -0
- package/dist/components/select/select-naming.d.ts +34 -0
- package/dist/components/select/select-naming.js +22 -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-button-width.d.ts +11 -0
- package/dist/css/css-button-width.js +36 -0
- package/dist/css/css-button.js +3 -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/css-file-field-compact.d.ts +2 -0
- package/dist/css/css-file-field-compact.js +106 -0
- package/dist/css/css-file-field.js +4 -0
- package/dist/css/css-list-tiles.d.ts +6 -0
- package/dist/css/css-list-tiles.js +50 -0
- package/dist/css/css-list.js +5 -1
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/list-options.d.ts +12 -5
- package/dist/list-options.js +19 -5
- package/dist/styles.css +443 -7
- package/package.json +1 -1
|
@@ -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,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a Select is named: by its own `label` (drawn, or visually hidden),
|
|
3
|
+
* or by words the page already draws, through `aria-labelledby`.
|
|
4
|
+
*/
|
|
5
|
+
export type SelectNaming = {
|
|
6
|
+
/** The select's own label, drawn above it as Field's. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Keeps the label for assistive technology only, for toolbar chrome. */
|
|
9
|
+
labelVisuallyHidden?: boolean;
|
|
10
|
+
'aria-labelledby'?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
/**
|
|
13
|
+
* The id of words the page draws for this choice, such as a heading
|
|
14
|
+
* or a label beside it: the select draws no label of its own, so
|
|
15
|
+
* the name is drawn, and read, once.
|
|
16
|
+
*/
|
|
17
|
+
'aria-labelledby': string;
|
|
18
|
+
label?: undefined;
|
|
19
|
+
labelVisuallyHidden?: undefined;
|
|
20
|
+
/** No mark: the page draws the words, and any mark, itself. */
|
|
21
|
+
requiredMark?: undefined;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Fails closed on a Select with no name, or with both: a blank `label`
|
|
25
|
+
* without `aria-labelledby`, a blank `aria-labelledby`, the two together
|
|
26
|
+
* (one would silently hide the other), or `requiredMark` with
|
|
27
|
+
* `aria-labelledby` (there is no label to mark).
|
|
28
|
+
*/
|
|
29
|
+
export declare function assertSelectNaming(naming: {
|
|
30
|
+
label: unknown;
|
|
31
|
+
labelVisuallyHidden: unknown;
|
|
32
|
+
labelledBy: unknown;
|
|
33
|
+
requiredMark: unknown;
|
|
34
|
+
}): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function isBlank(text) {
|
|
2
|
+
return typeof text !== 'string' || text.trim() === '';
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Fails closed on a Select with no name, or with both: a blank `label`
|
|
6
|
+
* without `aria-labelledby`, a blank `aria-labelledby`, the two together
|
|
7
|
+
* (one would silently hide the other), or `requiredMark` with
|
|
8
|
+
* `aria-labelledby` (there is no label to mark).
|
|
9
|
+
*/
|
|
10
|
+
export function assertSelectNaming(naming) {
|
|
11
|
+
if (naming.labelledBy === undefined) {
|
|
12
|
+
if (isBlank(naming.label))
|
|
13
|
+
throw new RangeError('Select needs a label or aria-labelledby');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (isBlank(naming.labelledBy))
|
|
17
|
+
throw new RangeError('Select aria-labelledby must be a non-empty id');
|
|
18
|
+
if (naming.label !== undefined || naming.labelVisuallyHidden !== undefined)
|
|
19
|
+
throw new RangeError('Select takes label (and labelVisuallyHidden) or aria-labelledby, not both');
|
|
20
|
+
if (naming.requiredMark !== undefined)
|
|
21
|
+
throw new RangeError('Select requiredMark needs label: with aria-labelledby the page draws the mark');
|
|
22
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a Button is: `content` (the default) is as wide as its words;
|
|
3
|
+
* `full` fills the container's inline size, and in an Inline row takes
|
|
4
|
+
* the space its siblings leave, such as an Edit beside a ••• menu.
|
|
5
|
+
*/
|
|
6
|
+
export declare const buttonWidths: readonly ["content", "full"];
|
|
7
|
+
export type ButtonWidth = (typeof buttonWidths)[number];
|
|
8
|
+
/** The class a width adds; `content` adds none (`cx` drops `undefined`). */
|
|
9
|
+
export declare function buttonWidthClass(width: ButtonWidth): string | undefined;
|
|
10
|
+
export declare function cssButtonWidthClasses(): string;
|
|
11
|
+
export declare function buttonWidthClassCatalog(): string[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a Button is: `content` (the default) is as wide as its words;
|
|
3
|
+
* `full` fills the container's inline size, and in an Inline row takes
|
|
4
|
+
* the space its siblings leave, such as an Edit beside a ••• menu.
|
|
5
|
+
*/
|
|
6
|
+
export const buttonWidths = ['content', 'full'];
|
|
7
|
+
const full = 'sw-button-full';
|
|
8
|
+
/** The class a width adds; `content` adds none (`cx` drops `undefined`). */
|
|
9
|
+
export function buttonWidthClass(width) {
|
|
10
|
+
return width === 'full' ? full : undefined;
|
|
11
|
+
}
|
|
12
|
+
/*
|
|
13
|
+
* A full button fills a Stack, a Grid cell or any column. As a direct
|
|
14
|
+
* child of an Inline row it shares the row with its siblings, which keep
|
|
15
|
+
* their own width (a ••• or a second button), as a full Select does; in a
|
|
16
|
+
* wrapping row it grows from its words' width instead, so it moves to a
|
|
17
|
+
* line of its own rather than shrinking past them. Its words stay centred.
|
|
18
|
+
*/
|
|
19
|
+
export function cssButtonWidthClasses() {
|
|
20
|
+
return `.${full} {
|
|
21
|
+
width: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sw-inline > .${full} {
|
|
25
|
+
flex: 1 1 0;
|
|
26
|
+
min-width: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sw-inline.sw-wrap > .${full} {
|
|
30
|
+
flex: 1 1 auto;
|
|
31
|
+
width: auto;
|
|
32
|
+
}`;
|
|
33
|
+
}
|
|
34
|
+
export function buttonWidthClassCatalog() {
|
|
35
|
+
return [full];
|
|
36
|
+
}
|
package/dist/css/css-button.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buttonSizes, buttonVariants, controlScale, typographyVariants, } from '@scalewing/tokens';
|
|
2
2
|
import { breakpointQuery } from './breakpoints.js';
|
|
3
3
|
import { buttonGroupAttachedClassCatalog, cssButtonGroupAttachedClasses, } from './css-button-group-attached.js';
|
|
4
|
+
import { buttonWidthClassCatalog, cssButtonWidthClasses, } from './css-button-width.js';
|
|
4
5
|
import { quietFill } from './quiet-fill.js';
|
|
5
6
|
const label = typographyVariants.label;
|
|
6
7
|
/**
|
|
@@ -135,6 +136,7 @@ ${disabledNeutralRules()}
|
|
|
135
136
|
${pressedRules()}
|
|
136
137
|
${ariaDisabledForcedRules()}
|
|
137
138
|
${sizeRules()}
|
|
139
|
+
${cssButtonWidthClasses()}
|
|
138
140
|
${groupRules()}
|
|
139
141
|
|
|
140
142
|
${cssButtonGroupAttachedClasses()}`;
|
|
@@ -162,6 +164,7 @@ export function buttonClassCatalog() {
|
|
|
162
164
|
...webButtonVariants.map((variant) => `sw-button-${variant}`),
|
|
163
165
|
'sw-button-disabled-neutral',
|
|
164
166
|
...buttonSizes.map((size) => `sw-button-${size}`),
|
|
167
|
+
...buttonWidthClassCatalog(),
|
|
165
168
|
'sw-button-group',
|
|
166
169
|
...buttonGroupJustifies.map((name) => `sw-button-group-${name}`),
|
|
167
170
|
...buttonGroupAttachedClassCatalog(),
|
|
@@ -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 {};
|