@ids-group-ltd/ids-design-system 0.3.0 → 0.4.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/README.md +2 -2
- package/fesm2022/ids-group-ltd-ids-design-system.mjs +2523 -269
- package/fesm2022/ids-group-ltd-ids-design-system.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/_breakpoints.scss +50 -0
- package/styles/_dropdown-overlay.scss +1 -0
- package/styles/_ds-color.scss +36 -0
- package/styles/_overlay-motion.scss +18 -0
- package/styles/_scrollbar.scss +1 -0
- package/styles/_theme-activation.scss +49 -0
- package/styles/_tokens.scss +54 -17
- package/styles/ds.scss +15 -9
- package/themes/README.md +71 -58
- package/themes/{default/_palette.scss → _base-palette.scss} +35 -18
- package/themes/{default/_theme.scss → _semantic.scss} +77 -64
- package/themes/dark/_palette.scss +39 -0
- package/themes/dark/_theme.scss +122 -0
- package/themes/light/_palette.scss +34 -0
- package/themes/light/_theme.scss +15 -0
- package/types/ids-group-ltd-ids-design-system.d.ts +688 -30
package/package.json
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Breakpoint scale — SINGLE source for both representations:
|
|
2
|
+
// --bp-* runtime custom properties (regular rules, JS via getComputedStyle)
|
|
3
|
+
// $bp-* compile-time Sass aliases, usable inside @media
|
|
4
|
+
//
|
|
5
|
+
// Why $bp-* exist: a custom property CANNOT be used in a @media condition
|
|
6
|
+
// (`@media (width >= var(--bp-md))` is ignored — the condition is evaluated
|
|
7
|
+
// before custom-property resolution). Sass variables resolve at build time, so
|
|
8
|
+
// they can. Use this partial wherever a media query needs the scale:
|
|
9
|
+
// @use 'breakpoints' as bp; // same dir; relative path elsewhere
|
|
10
|
+
// @include bp.up(md) { … } // width >= 768px
|
|
11
|
+
// @include bp.down(sm) { … } // width < 640px
|
|
12
|
+
// @media (width >= #{bp.$bp-lg}) { } // or read an alias directly
|
|
13
|
+
|
|
14
|
+
@use 'sass:map';
|
|
15
|
+
|
|
16
|
+
$breakpoints: (
|
|
17
|
+
'xs': 480px,
|
|
18
|
+
'sm': 640px,
|
|
19
|
+
'md': 768px,
|
|
20
|
+
'lg': 1024px,
|
|
21
|
+
'xl': 1441px, // page-grid wide-tier threshold (12 cols, caps at --col-cap-content)
|
|
22
|
+
'2xl': 1536px,
|
|
23
|
+
);
|
|
24
|
+
$bp-xs: map.get($breakpoints, 'xs');
|
|
25
|
+
$bp-sm: map.get($breakpoints, 'sm');
|
|
26
|
+
$bp-md: map.get($breakpoints, 'md');
|
|
27
|
+
$bp-lg: map.get($breakpoints, 'lg');
|
|
28
|
+
$bp-xl: map.get($breakpoints, 'xl');
|
|
29
|
+
$bp-2xl: map.get($breakpoints, '2xl');
|
|
30
|
+
|
|
31
|
+
// width >= breakpoint — mobile-first / min-width semantics.
|
|
32
|
+
@mixin up($name) {
|
|
33
|
+
@media (width >= #{map.get($breakpoints, $name)}) {
|
|
34
|
+
@content;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// width < breakpoint — max-width semantics, exclusive of the breakpoint itself.
|
|
39
|
+
@mixin down($name) {
|
|
40
|
+
@media (width < #{map.get($breakpoints, $name)}) {
|
|
41
|
+
@content;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Runtime mirror — emitted from the same map so the two never drift.
|
|
46
|
+
:root {
|
|
47
|
+
@each $name, $value in $breakpoints {
|
|
48
|
+
--bp-#{$name}: #{$value};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Scoped accent recolour. A `ds-color="<family>"` attribute — on a zone
|
|
2
|
+
// container, on a component host, or applied via the ds.color() mixin in
|
|
3
|
+
// consumer config — flips the --accent-* pointer family to another brand/status
|
|
4
|
+
// family for its subtree. Components read --accent-* for their accent (the
|
|
5
|
+
// Phase 2 sweep), so the whole subtree recolours from this one rule. Pairs with
|
|
6
|
+
// the button/icon-button `color` input (Phase 1), which flips the same pointers
|
|
7
|
+
// per instance. With no attribute, --accent-* stays the primary family (see
|
|
8
|
+
// themes/_semantic.scss), so the default theme is unchanged.
|
|
9
|
+
|
|
10
|
+
@mixin family-vars($family) {
|
|
11
|
+
--accent: var(--#{$family});
|
|
12
|
+
--accent-hover: var(--#{$family}-hover);
|
|
13
|
+
--accent-pressed: var(--#{$family}-pressed);
|
|
14
|
+
--accent-strong: var(--#{$family}-strong);
|
|
15
|
+
--accent-subtitle: var(--#{$family}-subtitle);
|
|
16
|
+
--accent-muted: var(--#{$family}-muted);
|
|
17
|
+
--accent-muted-strong: var(--#{$family}-muted-strong);
|
|
18
|
+
--accent-on: var(--#{$family}-on);
|
|
19
|
+
--accent-focus-ring: var(--focus-ring-#{$family});
|
|
20
|
+
--accent-focus-field: var(--focus-field-#{$family});
|
|
21
|
+
--accent-surface-tint: var(--#{$family}-subtitle);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Recolour the DS accent for a type or scope from consumer SCSS, e.g.
|
|
25
|
+
/// `ds-switch { @include ds.color(secondary); }`.
|
|
26
|
+
@mixin color($family) {
|
|
27
|
+
@include family-vars($family);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
$families: secondary, tertiary, error, success, warning;
|
|
31
|
+
|
|
32
|
+
@each $family in $families {
|
|
33
|
+
[ds-color='#{$family}'] {
|
|
34
|
+
@include family-vars($family);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Enter/leave motion for CDK-overlay panels that carry the `ds-overlay-pane`
|
|
2
|
+
// panelClass (select, dropdown, combobox, date-picker, popover, tooltip).
|
|
3
|
+
// Animates the panel's ROOT CHILD, not the pane itself, so CDK's positioning
|
|
4
|
+
// transforms on `.cdk-overlay-pane` are left untouched. The leave variant is
|
|
5
|
+
// driven by `ds-leaving`, added by disposeWithLeave() (components/overlay-motion.ts).
|
|
6
|
+
// prefers-reduced-motion zeroes --duration-fast → instant (animationend still
|
|
7
|
+
// fires, so disposeWithLeave still tears down).
|
|
8
|
+
|
|
9
|
+
.cdk-overlay-pane.ds-overlay-pane > * {
|
|
10
|
+
animation: ds-overlay-in var(--duration-fast) var(--ease-decelerate);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@keyframes ds-overlay-in {
|
|
14
|
+
from {
|
|
15
|
+
opacity: 0;
|
|
16
|
+
transform: translateY(-4px);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/styles/_scrollbar.scss
CHANGED
|
@@ -9,6 +9,7 @@ html { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparen
|
|
|
9
9
|
border-radius: var(--radius-pill);
|
|
10
10
|
border: var(--space-0-5) solid transparent;
|
|
11
11
|
background-clip: padding-box;
|
|
12
|
+
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
12
13
|
}
|
|
13
14
|
*::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); background-clip: padding-box; }
|
|
14
15
|
*::-webkit-scrollbar-thumb:active { background: var(--scrollbar-thumb-active); background-clip: padding-box; }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// =========================================================================
|
|
2
|
+
// Theme activation.
|
|
3
|
+
//
|
|
4
|
+
// Light is the DEFAULT — applied at bare :root, no attribute needed. Dark is
|
|
5
|
+
// OPT-IN via <html data-theme="dark">; because the dark block declares the
|
|
6
|
+
// complete token set (palette + roles + overrides), the more-specific
|
|
7
|
+
// [data-theme='dark'] selector fully replaces light when present — it is not
|
|
8
|
+
// a delta-patch.
|
|
9
|
+
//
|
|
10
|
+
// There is no automatic prefers-color-scheme switch. To follow the OS, a
|
|
11
|
+
// consumer opts in at the app root: provideDsTheme({ followSystem: true })
|
|
12
|
+
// (exported from the package; see themes/README.md).
|
|
13
|
+
//
|
|
14
|
+
// Order matters: the dark block comes LAST so it wins over the light-context
|
|
15
|
+
// overrides (brand-gray neutrals) at equal specificity.
|
|
16
|
+
// =========================================================================
|
|
17
|
+
|
|
18
|
+
@use '../themes/light/theme' as light;
|
|
19
|
+
@use '../themes/dark/theme' as dark;
|
|
20
|
+
|
|
21
|
+
:root {
|
|
22
|
+
color-scheme: light; // native UI (scrollbars, form controls) match; opts out of UA force-dark
|
|
23
|
+
|
|
24
|
+
@include light.theme;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Brand-gray neutral family — opt-in light-context switch (ds-docs tweaks panel).
|
|
28
|
+
// A light-theme feature: dark ships its own neutral ramp and wins below.
|
|
29
|
+
:root[data-neutrals='brand'] {
|
|
30
|
+
--neutral-0: var(--brand-gray-0);
|
|
31
|
+
--neutral-50: var(--brand-gray-50);
|
|
32
|
+
--neutral-100: var(--brand-gray-100);
|
|
33
|
+
--neutral-150: var(--brand-gray-150);
|
|
34
|
+
--neutral-200: var(--brand-gray-200);
|
|
35
|
+
--neutral-300: var(--brand-gray-300);
|
|
36
|
+
--neutral-400: var(--brand-gray-400);
|
|
37
|
+
--neutral-500: var(--brand-gray-500);
|
|
38
|
+
--neutral-600: var(--brand-gray-600);
|
|
39
|
+
--neutral-700: var(--brand-gray-700);
|
|
40
|
+
--neutral-800: var(--brand-gray-800);
|
|
41
|
+
--neutral-900: var(--brand-gray-900);
|
|
42
|
+
--neutral-950: var(--brand-gray-950);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:root[data-theme='dark'] {
|
|
46
|
+
color-scheme: dark; // native UI (scrollbars, form controls) render dark to match
|
|
47
|
+
|
|
48
|
+
@include dark.theme;
|
|
49
|
+
}
|
package/styles/_tokens.scss
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Design Tokens — Tier 3 component + scale layer.
|
|
3
3
|
|
|
4
4
|
Tier 1 colour primitives (ramps + HSL channels) live in
|
|
5
|
-
../themes/
|
|
5
|
+
../themes/light/_palette.scss. Tier 2 semantic role assignments
|
|
6
6
|
(surface, border, text, primary/secondary/tertiary, status, focus)
|
|
7
|
-
live in ../themes/
|
|
7
|
+
live in ../themes/light/_theme.scss.
|
|
8
8
|
|
|
9
9
|
THIS file owns the theme-INDEPENDENT layer:
|
|
10
10
|
· scales — spacing, radius, sizes, hit targets, breakpoints
|
|
@@ -43,6 +43,12 @@
|
|
|
43
43
|
--space-24: 96px;
|
|
44
44
|
--space-32: 128px;
|
|
45
45
|
|
|
46
|
+
/* Semantic spacing aliases — default rhythm for the layout primitives
|
|
47
|
+
(ds-stack / ds-inline). Components default their gap to these so the
|
|
48
|
+
vertical/horizontal rhythm retunes in one place. */
|
|
49
|
+
--space-stack: var(--space-4); /* 16px — vertical rhythm between stacked blocks */
|
|
50
|
+
--space-inline: var(--space-2); /* 8px — horizontal cluster gap */
|
|
51
|
+
|
|
46
52
|
/* ============================================================
|
|
47
53
|
Radius — six-stop scale.
|
|
48
54
|
Each step is functionally distinct; adjacent steps are
|
|
@@ -85,7 +91,7 @@
|
|
|
85
91
|
hue/saturation/lightness become configurable.
|
|
86
92
|
============================================================ */
|
|
87
93
|
|
|
88
|
-
/* --shadow-tint-h/s/l live in themes/
|
|
94
|
+
/* --shadow-tint-h/s/l live in themes/light/_palette.scss — palette-level
|
|
89
95
|
so dark palettes can override --shadow-tint-l → ~95% for soft light glows. */
|
|
90
96
|
|
|
91
97
|
--shadow-1: 0 1px 2px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 6%),
|
|
@@ -173,14 +179,10 @@
|
|
|
173
179
|
--opacity-state-active: 0.24; /* deepest pressed layer (e.g. tag close on :active) */
|
|
174
180
|
|
|
175
181
|
/* ============================================================
|
|
176
|
-
Breakpoints
|
|
182
|
+
Breakpoints — moved to _breakpoints.scss, the single source for both
|
|
183
|
+
the --bp-* custom properties (still emitted, now from that partial)
|
|
184
|
+
AND the $bp-* Sass aliases usable inside @media.
|
|
177
185
|
============================================================ */
|
|
178
|
-
--bp-xs: 480px;
|
|
179
|
-
--bp-sm: 640px;
|
|
180
|
-
--bp-md: 768px;
|
|
181
|
-
--bp-lg: 1024px;
|
|
182
|
-
--bp-xl: 1441px; /* Page-grid wide-tier threshold (12 cols, container caps at --col-cap-content). */
|
|
183
|
-
--bp-2xl:1536px;
|
|
184
186
|
|
|
185
187
|
/* ============================================================
|
|
186
188
|
Page grid — tier-specific gutters + margins.
|
|
@@ -210,7 +212,7 @@
|
|
|
210
212
|
|
|
211
213
|
/* ============================================================
|
|
212
214
|
Typography scale (theme-independent). Font FAMILIES are a theme
|
|
213
|
-
choice and live in themes/
|
|
215
|
+
choice and live in themes/light/_theme.scss (--font-sans/mono/
|
|
214
216
|
display/heading); the size/weight/leading/tracking scale below is
|
|
215
217
|
constant across themes.
|
|
216
218
|
============================================================ */
|
|
@@ -287,7 +289,7 @@
|
|
|
287
289
|
/* Shared focus indicators — components default their own --ds-*-focus-shadow
|
|
288
290
|
to these, so a consumer retunes focus per-group at once (field vs control)
|
|
289
291
|
or per-component. Two families: fields get the soft halo, interactive
|
|
290
|
-
controls/nav get the crisp ring (see themes/
|
|
292
|
+
controls/nav get the crisp ring (see themes/light/_theme.scss). */
|
|
291
293
|
--ds-field-focus-shadow: var(--focus-field);
|
|
292
294
|
--ds-field-focus-shadow-error: var(--focus-field-error);
|
|
293
295
|
--ds-control-focus-shadow: var(--focus-ring);
|
|
@@ -378,7 +380,7 @@
|
|
|
378
380
|
--popover-maxh: 360px;
|
|
379
381
|
|
|
380
382
|
/* Field */
|
|
381
|
-
--field-gap
|
|
383
|
+
--field-label-gap: var(--space-1-5); /* field vertical rhythm: label↔control & control↔hint */
|
|
382
384
|
--field-gap-req: var(--space-1); /* label↔asterisk */
|
|
383
385
|
--field-h-sm: var(--hit-min);
|
|
384
386
|
--field-h-md: var(--hit-cozy);
|
|
@@ -394,6 +396,19 @@
|
|
|
394
396
|
--search-h: var(--hit-md);
|
|
395
397
|
--search-maxw: 320px;
|
|
396
398
|
|
|
399
|
+
/* OTP / PIN input — one box per digit. */
|
|
400
|
+
--otp-cell-w: var(--space-12); /* 48px */
|
|
401
|
+
--otp-cell-h: var(--space-14); /* 56px */
|
|
402
|
+
--otp-gap: var(--space-2); /* 8px between cells */
|
|
403
|
+
|
|
404
|
+
/* Color picker */
|
|
405
|
+
--color-picker-swatch: var(--space-6); /* 24px preset swatch */
|
|
406
|
+
--color-picker-preview: var(--space-10); /* 40px panel preview */
|
|
407
|
+
--color-picker-panel-w: 240px; /* picker overlay width */
|
|
408
|
+
--color-picker-sv-h: 160px; /* saturation/value plane height */
|
|
409
|
+
--color-picker-hue-h: var(--space-3); /* 12px hue slider height */
|
|
410
|
+
--color-picker-thumb: 14px; /* draggable thumb diameter */
|
|
411
|
+
|
|
397
412
|
/* ============================================================
|
|
398
413
|
3b · COMPONENT-SCOPED dimensions (Phase 4 additions)
|
|
399
414
|
Tokens for atom geometries that previously lived as literals
|
|
@@ -444,6 +459,11 @@
|
|
|
444
459
|
--menu-minw: 200px; /* dropdown menu min width */
|
|
445
460
|
--menu-sep-h: var(--border-width-default);
|
|
446
461
|
|
|
462
|
+
/* List / list-item — density-aware row padding + leading/trailing gap. */
|
|
463
|
+
--list-item-pad-y: var(--space-3); /* 12px */
|
|
464
|
+
--list-item-pad-x: var(--space-4); /* 16px */
|
|
465
|
+
--list-item-gap: var(--space-3); /* 12px leading↔body↔trailing */
|
|
466
|
+
|
|
447
467
|
/* Tooltip */
|
|
448
468
|
--tooltip-maxw: 240px;
|
|
449
469
|
--tooltip-arrow: var(--space-2); /* 8px — caret edge length, halved for offset */
|
|
@@ -513,6 +533,16 @@
|
|
|
513
533
|
--calendar-bar-h: var(--space-8); /* 32px */
|
|
514
534
|
--calendar-bar-inset-y: var(--space-2); /* 8px from top of cell */
|
|
515
535
|
|
|
536
|
+
/* Timeline / activity feed */
|
|
537
|
+
--timeline-node-size: var(--hit-sm); /* 28px event node */
|
|
538
|
+
--timeline-spine-w: var(--border-width-strong); /* 2px connector spine */
|
|
539
|
+
--timeline-gap: var(--space-5); /* vertical gap between events */
|
|
540
|
+
|
|
541
|
+
/* Tree-view */
|
|
542
|
+
--tree-indent: var(--space-5); /* 20px per nesting level */
|
|
543
|
+
--tree-row-pad-y: var(--space-1-5); /* 6px */
|
|
544
|
+
--tree-row-pad-x: var(--space-3); /* 12px base inset */
|
|
545
|
+
|
|
516
546
|
/* Bin pack visualisation */
|
|
517
547
|
--binpack-bay-h: var(--hit-touch); /* 48px */
|
|
518
548
|
|
|
@@ -560,13 +590,20 @@
|
|
|
560
590
|
--duration-base: 0ms;
|
|
561
591
|
--duration-slow: 0ms;
|
|
562
592
|
--duration-slower: 0ms;
|
|
563
|
-
|
|
564
|
-
|
|
593
|
+
|
|
594
|
+
/* Looping status indicators (spinners, indeterminate progress) stay
|
|
595
|
+
visible but calmer — slowed, never frozen: a stopped spinner reads as
|
|
596
|
+
"hung", not "loading". The one large motion we DO halt is the page-wide
|
|
597
|
+
skeleton sweep, which stops itself in _skeleton-shimmer.scss. */
|
|
598
|
+
--duration-loop-fast: 1400ms;
|
|
599
|
+
--duration-loop-slow: 2800ms;
|
|
565
600
|
}
|
|
601
|
+
|
|
602
|
+
/* Kill one-shot motion only. Every entrance/transition animation is
|
|
603
|
+
token-driven, so zeroing the one-shot durations above already makes them
|
|
604
|
+
instant — loops must NOT be blanket-zeroed here or they freeze. */
|
|
566
605
|
*, *::before, *::after {
|
|
567
606
|
transition-duration: 0ms !important;
|
|
568
|
-
animation-duration: 0ms !important;
|
|
569
|
-
animation-iteration-count: 1 !important;
|
|
570
607
|
}
|
|
571
608
|
}
|
|
572
609
|
|
package/styles/ds.scss
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
// Public DS stylesheet entry. Consumers import once at the top of their styles.scss:
|
|
2
2
|
// @use 'styles/ds';
|
|
3
3
|
//
|
|
4
|
-
// Bundles the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
4
|
+
// Bundles the base palette (theme-independent ramps + channels + statics + fonts)
|
|
5
|
+
// and the theme activation (light + dark, sibling themes + OS-default media rule).
|
|
6
|
+
// Layers:
|
|
7
|
+
// base-palette — Tier 1a theme-independent primitives (ramps, channels,
|
|
8
|
+
// statics, shadow h/s, font families) emitted at :root
|
|
9
|
+
// theme-activation — Tier 1b + Tier 2: per-theme palette + role assignments,
|
|
10
|
+
// scoped to mutually-exclusive :root selectors
|
|
11
|
+
// tokens — Tier 3 geometry / typography scale / motion / component tokens
|
|
12
|
+
// Advanced consumers can compose themes individually instead of using ds:
|
|
13
|
+
// @use 'themes/base-palette'; @use 'styles/theme-activation'; @use 'styles/tokens';
|
|
11
14
|
// @use 'styles/fonts'; @use 'styles/link'; …
|
|
12
15
|
|
|
13
|
-
@forward '../themes/
|
|
14
|
-
@forward '
|
|
16
|
+
@forward '../themes/base-palette';
|
|
17
|
+
@forward 'theme-activation';
|
|
15
18
|
@forward 'tokens';
|
|
19
|
+
@forward 'breakpoints';
|
|
16
20
|
@forward 'tokens-charts';
|
|
17
21
|
@forward 'fonts';
|
|
18
22
|
@forward 'reset';
|
|
19
23
|
@forward 'typography';
|
|
20
24
|
@forward 'link';
|
|
25
|
+
@forward 'ds-color';
|
|
21
26
|
@forward 'layout-utils';
|
|
22
27
|
@forward 'page-grid';
|
|
23
28
|
@forward 'scrollbar';
|
|
24
29
|
@forward 'icon-base';
|
|
25
30
|
@forward 'dropdown-overlay';
|
|
26
31
|
@forward 'toast-overlay';
|
|
32
|
+
@forward 'overlay-motion';
|
|
27
33
|
@forward 'skeleton-shimmer';
|
package/themes/README.md
CHANGED
|
@@ -1,97 +1,110 @@
|
|
|
1
|
-
# Themes — palette
|
|
1
|
+
# Themes — base palette · sibling themes · brands
|
|
2
2
|
|
|
3
|
-
Colour
|
|
3
|
+
Colour is layered in three concerns: a **base palette** (theme-independent primitives), **themes** (light / dark — sibling role maps; light is the default, dark is opt-in), and **brands** (optional overlays authored on a theme). The geometry/component layer is separate (`../styles/_tokens.scss`).
|
|
4
4
|
|
|
5
|
-
## Architecture: three
|
|
5
|
+
## Architecture: three layers
|
|
6
6
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
| **
|
|
10
|
-
| **
|
|
11
|
-
| **
|
|
7
|
+
| Layer | Lives in | Examples | Applied at |
|
|
8
|
+
| ------------------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------- |
|
|
9
|
+
| **Base palette** (primitives) | `themes/_base-palette.scss` | `--blue-600`, `--cool-gray-*`, `--brand-gray-*`, `--red-h/s/l`, `--static-white`, fonts, `--shadow-tint-h/s` | `:root` (theme-independent) |
|
|
10
|
+
| **Theme** (role map) | `themes/_semantic.scss` + `themes/<t>/_*.scss` | `--primary`, `--surface-canvas`, `--text-primary`, the `--neutral-*` slot, `--focus-ring` | `:root` (light) / `:root[data-theme='dark']` |
|
|
11
|
+
| **Component / scale** | `styles/_tokens.scss`, `*.component.scss` | `--space-*`, `--radius-*`, `--ds-button-bg-color`, `--duration-fast` | `:root` (theme-independent) |
|
|
12
12
|
|
|
13
|
-
**Hard rule:** components NEVER consume
|
|
13
|
+
**Hard rule:** components NEVER consume base-palette primitives directly. `var(--blue-600)` inside `button.component.scss` is a bug — use a Tier-2 role (`--primary`, `--text-secondary`) or a component token.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Files
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
- **`_base-palette.scss`** — theme-independent primitives at `:root`: hue ramps (`--blue/red/green/yellow/sky/orange-*`) + HSL channels, `--cool-gray-*` / `--brand-gray-*` ramps, `--static-white` / `--static-ink` (never flip), `--shadow-tint-h/s`, font families. Shared by every theme.
|
|
18
|
+
- **`_semantic.scss`** — `@mixin roles`: the semantic role map (surfaces, text, icons, borders, primary/secondary/tertiary families, status, status-pills, focus). Structure is identical across themes; values resolve through each theme's own `--neutral-*` ramp. Holds the **light-default** values.
|
|
19
|
+
- **`light/_palette.scss` + `light/_theme.scss`** — `@mixin palette` (the `--neutral-*` slot = cool-gray + `--shadow-tint-l: 7%`) and `@mixin theme` (`palette` + `semantic.roles`). Light is the **default** skin and needs no extra overrides.
|
|
20
|
+
- **`dark/_palette.scss` + `dark/_theme.scss`** — `@mixin palette` (dark neutral ramp + `--shadow-tint-l: 95%`) and `@mixin theme` (`palette` + `semantic.roles` + the value overrides that genuinely differ on dark: tint surfaces, on-tint text, hover/pressed mixed toward white, scrim, focus-halo alpha, chart tokens).
|
|
21
|
+
- **`../styles/_theme-activation.scss`** — applies light at `:root` and dark at `:root[data-theme='dark']`, plus the light-context `data-neutrals='brand'` switch.
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
- **`default/_theme.scss`** — Light role map onto the default palette. Both are bundled into `styles/ds.scss` (palette → theme → tokens) so `@use 'styles/ds';` works out of the box.
|
|
23
|
+
`styles/ds.scss` forwards `_base-palette` then `theme-activation` (before `tokens`), so `@use 'styles/ds';` renders light out of the box.
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
**Why sibling themes (not a dark "patch"):** the dark block declares the COMPLETE token set (`@include palette` + `@include semantic.roles` + its own overrides), so `:root[data-theme='dark']` fully replaces light rather than delta-patching it. The `@mixin roles` in `_semantic.scss` is an authoring-DRY device (the light-default structure both themes share), not a runtime cross-theme dependency.
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
## Theme switching
|
|
28
|
+
|
|
29
|
+
**Light is the default — no attribute needed.** Dark is opt-in:
|
|
30
|
+
|
|
31
|
+
- (nothing) — light
|
|
32
|
+
- `<html data-theme="dark">` — dark
|
|
33
|
+
- `<html data-theme="light">` — also light (explicit; same as the default)
|
|
34
|
+
|
|
35
|
+
```scss
|
|
36
|
+
:root { @include light.theme; } // default
|
|
37
|
+
:root[data-theme='dark'] { @include dark.theme; } // opt-in (complete set → fully wins)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
There is **no automatic `prefers-color-scheme` switch** — light is deterministic by default. To follow the OS, opt in at the app root with `provideDsTheme`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// app.config.ts
|
|
44
|
+
import { provideDsTheme } from '@ids-group-ltd/ids-design-system';
|
|
45
|
+
|
|
46
|
+
export const appConfig: ApplicationConfig = {
|
|
47
|
+
providers: [provideDsTheme({ followSystem: true })],
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It sets `data-theme` from `prefers-color-scheme` at startup and keeps it in sync as the OS changes; a manual `data-theme="dark"` still wins.
|
|
52
|
+
|
|
53
|
+
On-fill text uses theme-stable `--static-white` / `--static-ink` so it doesn't flip with the neutrals.
|
|
54
|
+
|
|
55
|
+
**Known limitation — `data-neutrals="brand"` + dark:** the brand-gray (hue-derived) neutral switch is a **light-theme feature**. The dark block ships its own neutral ramp and is declared last, so on `data-theme="dark"` the dark neutrals win and the brand-gray tint is ignored. (Benign — renders correct dark, just not brand-tinted neutrals.)
|
|
26
56
|
|
|
27
57
|
## How a consumer picks a theme
|
|
28
58
|
|
|
29
|
-
### Option A — out
|
|
59
|
+
### Option A — out of the box (light)
|
|
30
60
|
|
|
31
61
|
```scss
|
|
32
|
-
//
|
|
33
|
-
@use 'styles/ds'; // bundles default palette + theme + tokens + reset + ...
|
|
62
|
+
@use 'styles/ds'; // base palette + light/dark activation + tokens + reset + …
|
|
34
63
|
```
|
|
35
64
|
|
|
36
|
-
### Option B —
|
|
65
|
+
### Option B — à la carte (advanced, opt-in global side-effects)
|
|
37
66
|
|
|
38
67
|
```scss
|
|
39
|
-
|
|
40
|
-
@use '
|
|
41
|
-
@use 'themes/dark/theme'; // … then its role map
|
|
68
|
+
@use 'themes/base-palette';
|
|
69
|
+
@use 'styles/theme-activation'; // light at :root + dark at [data-theme='dark']
|
|
42
70
|
@use 'styles/tokens';
|
|
71
|
+
@use 'styles/fonts';
|
|
43
72
|
@use 'styles/reset';
|
|
44
|
-
|
|
45
|
-
@use 'styles/scrollbar';
|
|
46
|
-
@use 'styles/icon-base';
|
|
47
|
-
@use 'styles/dropdown-overlay';
|
|
73
|
+
// …
|
|
48
74
|
```
|
|
49
75
|
|
|
50
|
-
|
|
76
|
+
`base-palette` + `theme-activation` must come BEFORE `tokens` so role aliases resolve. The theme `_palette` / `_theme` files expose **mixins** — don't `@use` them directly; `theme-activation` applies them.
|
|
51
77
|
|
|
52
|
-
### Option C — brand layer on top
|
|
78
|
+
### Option C — brand layer on top
|
|
53
79
|
|
|
54
80
|
```scss
|
|
55
|
-
|
|
56
|
-
@use 'styles/
|
|
57
|
-
@use './app/styles/brand-manage-my'; // overrides palette/roles via [data-brand]
|
|
81
|
+
@use 'styles/ds';
|
|
82
|
+
@use './app/styles/brand-manage-my'; // [data-brand="manage-my"] overrides (light-authored)
|
|
58
83
|
```
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
## Brand vs theme — what's the difference?
|
|
63
|
-
|
|
64
|
-
- **Theme** swaps the entire Tier 1 palette. Affects EVERY semantic role downstream.
|
|
65
|
-
- **Brand** is a thin overlay scoped via attribute selector (`[data-brand="X"]`). Usually overrides a smaller subset (key palette stops + maybe a few roles).
|
|
85
|
+
## Brand vs theme
|
|
66
86
|
|
|
67
|
-
|
|
87
|
+
- **Theme** swaps the whole role map (light ↔ dark). Light is the default; dark is opt-in and, when active, fully replaces light.
|
|
88
|
+
- **Brand** is a thin overlay scoped via attribute (`[data-brand="X"]`), authored on a theme. `manage-my` is authored on **light** — pair it with the default (light) theme. A brand that wants dark adds a separate `[data-theme='dark'][data-brand="X"]` layer.
|
|
68
89
|
|
|
69
90
|
## Adding a new theme
|
|
70
91
|
|
|
71
|
-
1. Create
|
|
72
|
-
2.
|
|
73
|
-
3. Or let consumers `@use 'themes/<name>/palette'; @use 'themes/<name>/theme';` directly.
|
|
92
|
+
1. Create `themes/<name>/_palette.scss` (`@mixin palette` — its `--neutral-*` ramp + `--shadow-tint-l`) and `themes/<name>/_theme.scss` (`@mixin theme` — `@include palette` + `@include semantic.roles` + only the role values that differ for this theme).
|
|
93
|
+
2. Add a selector block in `styles/_theme-activation.scss` (`:root[data-theme='<name>'] { @include <name>.theme; }`), after the light default.
|
|
74
94
|
|
|
75
|
-
Components
|
|
95
|
+
Components never change — they consume semantic roles, which the active theme block defines.
|
|
76
96
|
|
|
77
97
|
## Adding a new brand override
|
|
78
98
|
|
|
79
99
|
1. Create `<consumer-app>/src/app/styles/_brand-X.scss`.
|
|
80
|
-
2. Open with `[data-brand="X"] {
|
|
81
|
-
3. Override
|
|
82
|
-
|
|
83
|
-
- **Blue ramp** (`--blue-50..900`) — the simplest re-skin lever. All brand-derived roles (`--primary`, `--primary-strong`, `--primary-subtitle`, `--primary-muted-strong`, `--text-link`, `--surface-tint`) retint automatically from it. For a fully calibrated brand you MAY also set the `--primary-*` roles directly to hand-picked hex (when ramp derivation doesn't hit the exact stop) — see `_brand-manage-my.scss`. Leave `--primary-hover`/`--primary-pressed` to the DS `color-mix()` derivation.
|
|
84
|
-
- Other roles you want to retint independently (surfaces, borders, text, success/danger ramps, secondary, etc.).
|
|
85
|
-
4. Apply via `<html data-brand="X">` on the host app.
|
|
100
|
+
2. Open with `[data-brand="X"] { … }` (authored on light — the default theme).
|
|
101
|
+
3. Override the brand hue (`--blue-*` ramp and/or `--primary` + channels), and any roles you want to retint (surfaces, text, secondary, status). Leave `--primary-hover`/`--primary-pressed` to the DS `color-mix()` derivation.
|
|
102
|
+
4. Apply via `<html data-brand="X">`.
|
|
86
103
|
|
|
87
104
|
## Rules summary
|
|
88
105
|
|
|
89
|
-
- Components
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
```bash
|
|
95
|
-
grep -nE 'var\(--(violet|neutral|red|green|yellow|blue|orange|cyan|magenta)-[0-9]+\)' \
|
|
96
|
-
prototype/ds/src/lib/components/**/*.scss && exit 1
|
|
97
|
-
```
|
|
106
|
+
- Components never reference base-palette primitives (`--blue-600`, `--neutral-200`) directly — use a Tier-2 role.
|
|
107
|
+
- Light is the default; dark is opt-in (`data-theme="dark"`); OS-follow is a consumer opt-in. No `prefers-color-scheme` by default.
|
|
108
|
+
- Themes are sibling and self-contained; `_semantic.scss`'s `@mixin roles` holds the light-default values, dark overrides the deltas.
|
|
109
|
+
- Brands are theme-authored overlays. `manage-my` is light.
|
|
110
|
+
- `--primary-hover`/`--primary-pressed` are auto-derived via `color-mix()` (toward black in light, toward white in dark).
|