@danjelp/ngx-app-shell 1.0.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/LICENSE +21 -0
- package/README.md +559 -0
- package/fesm2022/danjelp-ngx-app-shell.mjs +1944 -0
- package/fesm2022/danjelp-ngx-app-shell.mjs.map +1 -0
- package/package.json +55 -0
- package/src/lib/styles/_tokens.scss +340 -0
- package/styles/_tokens.scss +3 -0
- package/types/danjelp-ngx-app-shell.d.ts +1210 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
@use 'sass:list';
|
|
2
|
+
@use 'sass:map';
|
|
3
|
+
|
|
4
|
+
/// Single source of truth for the shell's design tokens.
|
|
5
|
+
///
|
|
6
|
+
/// Three layers, each overriding the one below:
|
|
7
|
+
///
|
|
8
|
+
/// 1. global tokens `--shell-surface`, `--shell-accent` … ($shell-tokens)
|
|
9
|
+
/// 2. component tokens `--shell-topbar-bg`, `--shell-drawer-duration` …
|
|
10
|
+
/// ($shell-component-tokens, fall back to layer 1)
|
|
11
|
+
/// 3. presets `appearance` / `motion` config, bound as host
|
|
12
|
+
/// attributes; they only change layer-2 *defaults*, so
|
|
13
|
+
/// any component token still wins.
|
|
14
|
+
///
|
|
15
|
+
/// Every value is used as the inline fallback inside the components, so the
|
|
16
|
+
/// shell renders correctly with no global stylesheet at all.
|
|
17
|
+
$shell-tokens: (
|
|
18
|
+
// ── Structure ───────────────────────────────────────────────────────────
|
|
19
|
+
'topbar-height': 64px,
|
|
20
|
+
'topbar-padding-inline': 12px,
|
|
21
|
+
'topbar-gap': 8px,
|
|
22
|
+
'sidebar-width': 264px,
|
|
23
|
+
'sidebar-collapsed-width': 68px,
|
|
24
|
+
'sidebar-overlay-width': min(88vw, 320px),
|
|
25
|
+
'sidebar-padding': 12px,
|
|
26
|
+
'sidebar-gap': 2px,
|
|
27
|
+
'footer-min-height': 48px,
|
|
28
|
+
'main-padding': 24px,
|
|
29
|
+
'main-max-width': none,
|
|
30
|
+
|
|
31
|
+
// ── Surfaces & text ────────────────────────────────────────────────────
|
|
32
|
+
'color-scheme': light,
|
|
33
|
+
'surface': #ffffff,
|
|
34
|
+
'surface-raised': #f7f7f8,
|
|
35
|
+
'sidebar-surface': #fafafb,
|
|
36
|
+
'topbar-surface': #ffffff,
|
|
37
|
+
'fg': #1b1c1f,
|
|
38
|
+
'fg-muted': #6b6f76,
|
|
39
|
+
'border': #e6e6e9,
|
|
40
|
+
'hover-surface': #f0f0f3,
|
|
41
|
+
'active-surface': #e9e9ee,
|
|
42
|
+
'accent': #4f46e5,
|
|
43
|
+
'accent-fg': #ffffff,
|
|
44
|
+
'accent-surface': #eef0fe,
|
|
45
|
+
'focus-ring': #4f46e5,
|
|
46
|
+
'scrim': rgb(15 17 21 / 45%),
|
|
47
|
+
'shadow-panel': 0 12px 32px -12px rgb(15 17 21 / 28%),
|
|
48
|
+
|
|
49
|
+
// ── Shape, rhythm, motion ──────────────────────────────────────────────
|
|
50
|
+
'radius': 10px,
|
|
51
|
+
'radius-sm': 6px,
|
|
52
|
+
'nav-item-height': 36px,
|
|
53
|
+
'nav-item-gap': 10px,
|
|
54
|
+
'nav-icon-size': 18px,
|
|
55
|
+
'font-family': inherit,
|
|
56
|
+
'font-size': 14px,
|
|
57
|
+
'font-size-sm': 12.5px,
|
|
58
|
+
'transition-duration': 180ms,
|
|
59
|
+
'transition-easing': cubic-bezier(0.2, 0, 0, 1),
|
|
60
|
+
|
|
61
|
+
// ── Stacking ───────────────────────────────────────────────────────────
|
|
62
|
+
'z-main': 0,
|
|
63
|
+
'z-topbar': 20,
|
|
64
|
+
'z-scrim': 30,
|
|
65
|
+
'z-sidebar': 40,
|
|
66
|
+
'z-flyout': 50,
|
|
67
|
+
'z-skip-link': 60
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/// Overrides for a dark theme. Only colour tokens change.
|
|
71
|
+
$shell-dark-tokens: (
|
|
72
|
+
'color-scheme': dark,
|
|
73
|
+
'surface': #111215,
|
|
74
|
+
'surface-raised': #16171b,
|
|
75
|
+
'sidebar-surface': #141519,
|
|
76
|
+
'topbar-surface': #111215,
|
|
77
|
+
'fg': #ececf1,
|
|
78
|
+
'fg-muted': #9a9ca6,
|
|
79
|
+
'border': #26272d,
|
|
80
|
+
'hover-surface': #1d1e23,
|
|
81
|
+
'active-surface': #26272d,
|
|
82
|
+
'accent': #8f8aff,
|
|
83
|
+
'accent-fg': #0d0d12,
|
|
84
|
+
'accent-surface': rgb(143 138 255 / 16%),
|
|
85
|
+
'focus-ring': #8f8aff,
|
|
86
|
+
'scrim': rgb(0 0 0 / 60%),
|
|
87
|
+
'shadow-panel': 0 16px 40px -12px rgb(0 0 0 / 70%)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/// `tok('sidebar-width')` → `var(--shell-sidebar-width, 264px)`
|
|
91
|
+
/// Errors on unknown names, so a typo fails the build instead of silently
|
|
92
|
+
/// resolving to an empty value.
|
|
93
|
+
@function tok($name) {
|
|
94
|
+
@if not map.has-key($shell-tokens, $name) {
|
|
95
|
+
@error "Unknown shell token '#{$name}'. Known tokens: #{map.keys($shell-tokens)}.";
|
|
96
|
+
}
|
|
97
|
+
@return var(--shell-#{$name}, #{map.get($shell-tokens, $name)});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Raw default, without the `var()` wrapper.
|
|
101
|
+
@function raw($name) {
|
|
102
|
+
@return map.get($shell-tokens, $name);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Per-part tokens. Each defaults to a global token.
|
|
106
|
+
///
|
|
107
|
+
/// Deliberately NOT emitted by `tokens()`: a custom property whose value is
|
|
108
|
+
/// another `var()` is resolved where it is declared, so a `:root` alias would
|
|
109
|
+
/// freeze the global value at `:root` and a theme class further down could no
|
|
110
|
+
/// longer reach the part. The aliases therefore live only as fallbacks inside
|
|
111
|
+
/// the components, where they resolve against the component's own cascade.
|
|
112
|
+
/// Setting a *concrete* value anywhere (`:root { --shell-topbar-bg: #0b1020 }`)
|
|
113
|
+
/// is exactly how they are meant to be used.
|
|
114
|
+
$shell-component-tokens: (
|
|
115
|
+
// ── Shell (the container behind every part) ────────────────────────────
|
|
116
|
+
// Shows wherever a part is transparent or inset: transparent topbar,
|
|
117
|
+
// minimal footer, floating-sidebar gaps, the margin of an inset main.
|
|
118
|
+
'bg': tok('surface'),
|
|
119
|
+
|
|
120
|
+
// ── Topbar ─────────────────────────────────────────────────────────────
|
|
121
|
+
'topbar-bg': tok('topbar-surface'),
|
|
122
|
+
'topbar-fg': tok('fg'),
|
|
123
|
+
// Border tokens take the CSS shorthand — 1 to 4 values, top right bottom
|
|
124
|
+
// left — so each side can differ: `--shell-topbar-border-width: 0 0 2px`.
|
|
125
|
+
'topbar-border-width': 0 0 1px,
|
|
126
|
+
'topbar-border-style': solid,
|
|
127
|
+
'topbar-border-color': tok('border'),
|
|
128
|
+
'topbar-radius': 0px,
|
|
129
|
+
'topbar-shadow': none,
|
|
130
|
+
'topbar-blur': 12px,
|
|
131
|
+
|
|
132
|
+
// ── Sidebar (persistent panel and rail) ────────────────────────────────
|
|
133
|
+
'sidebar-bg': tok('sidebar-surface'),
|
|
134
|
+
'sidebar-fg': tok('fg'),
|
|
135
|
+
'sidebar-border-color': tok('border'),
|
|
136
|
+
'sidebar-shadow': none,
|
|
137
|
+
'sidebar-radius': 0px,
|
|
138
|
+
'sidebar-inset': 8px,
|
|
139
|
+
'sidebar-peek-shadow': tok('shadow-panel'),
|
|
140
|
+
|
|
141
|
+
// ── Drawer (overlay mode) and its scrim ────────────────────────────────
|
|
142
|
+
'drawer-bg': var(--shell-sidebar-bg, #{tok('sidebar-surface')}),
|
|
143
|
+
'drawer-shadow': tok('shadow-panel'),
|
|
144
|
+
'drawer-radius': 0px,
|
|
145
|
+
'drawer-scrim': tok('scrim'),
|
|
146
|
+
'drawer-scrim-blur': 0px,
|
|
147
|
+
|
|
148
|
+
// ── Navigation ─────────────────────────────────────────────────────────
|
|
149
|
+
'nav-item-fg': tok('fg'),
|
|
150
|
+
'nav-item-hover-bg': tok('hover-surface'),
|
|
151
|
+
'nav-item-active-bg': tok('accent-surface'),
|
|
152
|
+
'nav-item-active-fg': tok('accent'),
|
|
153
|
+
'nav-item-radius': tok('radius-sm'),
|
|
154
|
+
'nav-indicator-color': tok('accent'),
|
|
155
|
+
'nav-indicator-size': 3px,
|
|
156
|
+
'nav-heading-fg': tok('fg-muted'),
|
|
157
|
+
'nav-heading-size': tok('font-size-sm'),
|
|
158
|
+
|
|
159
|
+
// ── Topbar overflow (⋯) menu ───────────────────────────────────────────
|
|
160
|
+
'menu-bg': tok('surface'),
|
|
161
|
+
'menu-border-color': tok('border'),
|
|
162
|
+
'menu-shadow': tok('shadow-panel'),
|
|
163
|
+
'menu-radius': tok('radius'),
|
|
164
|
+
|
|
165
|
+
// ── Main & footer ──────────────────────────────────────────────────────
|
|
166
|
+
'main-bg': tok('surface'),
|
|
167
|
+
'main-fg': tok('fg'),
|
|
168
|
+
'main-border-width': 0,
|
|
169
|
+
'main-border-style': solid,
|
|
170
|
+
'main-border-color': tok('border'),
|
|
171
|
+
'main-radius': 0px,
|
|
172
|
+
'main-shadow': none,
|
|
173
|
+
'main-margin': 0,
|
|
174
|
+
'footer-bg': tok('surface-raised'),
|
|
175
|
+
'footer-fg': tok('fg-muted'),
|
|
176
|
+
'footer-border-color': tok('border'),
|
|
177
|
+
'footer-shadow': none,
|
|
178
|
+
'footer-font-size': tok('font-size-sm'),
|
|
179
|
+
|
|
180
|
+
// ── Frame: the rounded card of the `inset` layout ──────────────────────
|
|
181
|
+
// Wraps topbar, main and footer. Its radius and shadow give the card its
|
|
182
|
+
// shape; the parts inside keep their own tokens.
|
|
183
|
+
'frame-bg': tok('surface'),
|
|
184
|
+
'frame-border-width': 0,
|
|
185
|
+
'frame-border-style': solid,
|
|
186
|
+
'frame-border-color': tok('border'),
|
|
187
|
+
'frame-radius': 14px,
|
|
188
|
+
'frame-shadow': (0 1px 2px rgb(0 0 0 / 4%), 0 8px 24px -8px rgb(0 0 0 / 14%)),
|
|
189
|
+
'frame-inset': 8px,
|
|
190
|
+
|
|
191
|
+
// ── Subheader: the breadcrumb row ──────────────────────────────────────
|
|
192
|
+
// Its height is reserved (a minimum), so late labels and actions never
|
|
193
|
+
// shift the page.
|
|
194
|
+
'subheader-height': 44px,
|
|
195
|
+
'subheader-bg': tok('surface'),
|
|
196
|
+
'subheader-fg': tok('fg'),
|
|
197
|
+
'subheader-border-width': 0 0 1px,
|
|
198
|
+
'subheader-border-style': solid,
|
|
199
|
+
'subheader-border-color': tok('border'),
|
|
200
|
+
'subheader-padding-inline': 24px,
|
|
201
|
+
'subheader-shadow': none,
|
|
202
|
+
'subheader-radius': 0px,
|
|
203
|
+
// A margin plus radius/border/shadow gives an inset, pill-shaped row.
|
|
204
|
+
'subheader-margin': 0,
|
|
205
|
+
'subheader-font-size': tok('font-size'),
|
|
206
|
+
|
|
207
|
+
// ── Breadcrumb trail ───────────────────────────────────────────────────
|
|
208
|
+
'breadcrumb-fg': tok('fg-muted'),
|
|
209
|
+
'breadcrumb-hover-fg': tok('fg'),
|
|
210
|
+
'breadcrumb-current-fg': tok('fg'),
|
|
211
|
+
'breadcrumb-separator-color': tok('fg-muted'),
|
|
212
|
+
'breadcrumb-gap': 4px,
|
|
213
|
+
'breadcrumb-skeleton-bg': tok('active-surface'),
|
|
214
|
+
// `1em` = the row's font size, so `subheader-font-size` carries through.
|
|
215
|
+
'breadcrumb-font-size': 1em,
|
|
216
|
+
'breadcrumb-current-weight': 600,
|
|
217
|
+
'breadcrumb-hover-bg': tok('hover-surface'),
|
|
218
|
+
// Longest label before it truncates with an ellipsis.
|
|
219
|
+
'breadcrumb-label-max': 28ch
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
/// `ctok('topbar-bg')` → `var(--shell-topbar-bg, var(--shell-topbar-surface, #fff))`
|
|
223
|
+
/// Pass `$fallback` to change the default for one preset while keeping the
|
|
224
|
+
/// component token in charge: `ctok('topbar-shadow', $elevated-shadow)`.
|
|
225
|
+
@function ctok($name, $fallback: null) {
|
|
226
|
+
@if not map.has-key($shell-component-tokens, $name) {
|
|
227
|
+
@error "Unknown shell component token '#{$name}'. Known: #{map.keys($shell-component-tokens)}.";
|
|
228
|
+
}
|
|
229
|
+
$default: map.get($shell-component-tokens, $name);
|
|
230
|
+
@if $fallback != null {
|
|
231
|
+
$default: $fallback;
|
|
232
|
+
}
|
|
233
|
+
@return var(--shell-#{$name}, #{$default});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// Emit the global tokens as declarations. Include once in a global
|
|
237
|
+
/// stylesheet if you want them inspectable in devtools or inheritable by your
|
|
238
|
+
/// own components:
|
|
239
|
+
///
|
|
240
|
+
/// ```scss
|
|
241
|
+
/// @use 'app-shell/styles/tokens' as shell;
|
|
242
|
+
/// @include shell.tokens; // :root
|
|
243
|
+
/// @include shell.tokens('.theme-compact'); // scoped variant
|
|
244
|
+
/// ```
|
|
245
|
+
@mixin tokens($selector: ':root') {
|
|
246
|
+
#{$selector} {
|
|
247
|
+
@each $name, $value in $shell-tokens {
|
|
248
|
+
--shell-#{$name}: #{$value};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Emit the dark palette under a selector of your choice:
|
|
254
|
+
///
|
|
255
|
+
/// ```scss
|
|
256
|
+
/// @include shell.dark-tokens(':root[data-theme="dark"]');
|
|
257
|
+
/// ```
|
|
258
|
+
@mixin dark-tokens($selector) {
|
|
259
|
+
#{$selector} {
|
|
260
|
+
@each $name, $value in $shell-dark-tokens {
|
|
261
|
+
--shell-#{$name}: #{$value};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Motion ────────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
/// Parts whose timing can be tuned on its own with
|
|
269
|
+
/// `--shell-<scope>-duration` / `--shell-<scope>-easing`.
|
|
270
|
+
///
|
|
271
|
+
/// topbar hover and preset changes in the bar
|
|
272
|
+
/// sidebar hover, peek shadow, colours of the panel
|
|
273
|
+
/// collapse rail ⇄ panel width (grid track, panel, peek)
|
|
274
|
+
/// drawer overlay drawer and scrim
|
|
275
|
+
/// nav nav item hover / active colours
|
|
276
|
+
/// nav-expand opening and closing sections
|
|
277
|
+
/// menu ⋯ overflow menus
|
|
278
|
+
/// subheader breadcrumb row and trail
|
|
279
|
+
/// footer footer colour changes
|
|
280
|
+
$shell-motion-scopes: ('topbar', 'sidebar', 'collapse', 'drawer', 'nav', 'nav-expand', 'menu', 'subheader', 'footer');
|
|
281
|
+
|
|
282
|
+
@function _check-scope($scope) {
|
|
283
|
+
@if $scope != null and not list.index($shell-motion-scopes, $scope) {
|
|
284
|
+
@error "Unknown motion scope '#{$scope}'. Known scopes: #{$shell-motion-scopes}.";
|
|
285
|
+
}
|
|
286
|
+
@return $scope;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Duration for a scope, multiplied by `--shell-motion-scale` — written by
|
|
290
|
+
/// `<app-shell>` from `motion.speed`, and 0 when `motion.enabled` is false.
|
|
291
|
+
/// That one factor gives a global switch and speed control that also governs
|
|
292
|
+
/// durations an app sets per part.
|
|
293
|
+
@function duration($scope: null) {
|
|
294
|
+
$value: tok('transition-duration');
|
|
295
|
+
@if _check-scope($scope) != null {
|
|
296
|
+
$value: var(--shell-#{$scope}-duration, #{$value});
|
|
297
|
+
}
|
|
298
|
+
@return calc(#{$value} * var(--shell-motion-scale, 1));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@function easing($scope: null) {
|
|
302
|
+
$value: tok('transition-easing');
|
|
303
|
+
@if _check-scope($scope) != null {
|
|
304
|
+
$value: var(--shell-#{$scope}-easing, #{$value});
|
|
305
|
+
}
|
|
306
|
+
@return $value;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// `@include motion('drawer', transform, opacity);`
|
|
310
|
+
@mixin motion($scope, $properties...) {
|
|
311
|
+
transition-property: $properties;
|
|
312
|
+
transition-duration: duration($scope);
|
|
313
|
+
transition-timing-function: easing($scope);
|
|
314
|
+
|
|
315
|
+
@media (prefers-reduced-motion: reduce) {
|
|
316
|
+
transition-duration: 1ms;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// Global timing, no scope.
|
|
321
|
+
@mixin transition($properties...) {
|
|
322
|
+
@include motion(null, $properties...);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
@mixin focus-ring($offset: 2px) {
|
|
328
|
+
&:focus-visible {
|
|
329
|
+
outline: 2px solid tok('focus-ring');
|
|
330
|
+
outline-offset: $offset;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Truncate a label that may be squeezed by a shrinking container.
|
|
335
|
+
@mixin truncate {
|
|
336
|
+
min-width: 0;
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
white-space: nowrap;
|
|
339
|
+
text-overflow: ellipsis;
|
|
340
|
+
}
|