@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ids-group-ltd/ids-design-system",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "IDS Group Angular design system: components, tokens, themes",
5
5
  "license": "MIT",
6
6
  "homepage": "https://ids-ds.proto.ids-group.co.uk/",
@@ -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
+ }
@@ -72,6 +72,7 @@
72
72
  cursor: pointer;
73
73
  border-radius: var(--radius-sm);
74
74
  text-align: left;
75
+ transition: background-color var(--duration-fast) var(--ease-standard);
75
76
 
76
77
  .option-icon {
77
78
  color: var(--icon-default);
@@ -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
+ }
@@ -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
+ }
@@ -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/default/_palette.scss. Tier 2 semantic role assignments
5
+ ../themes/light/_palette.scss. Tier 2 semantic role assignments
6
6
  (surface, border, text, primary/secondary/tertiary, status, focus)
7
- live in ../themes/default/_theme.scss.
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/default/_palette.scss — palette-level
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/default/_theme.scss (--font-sans/mono/
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/default/_theme.scss). */
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-label: var(--space-1-5); /* label↔control */
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
- --duration-loop-fast: 0ms;
564
- --duration-loop-slow: 0ms;
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 default palette + theme for out-of-the-box rendering. Layers:
5
- // default/palette Tier 1 concrete colours (ramps + HSL channels)
6
- // default/theme — Tier 2 semantic role → palette assignments + font families
7
- // tokens — Tier 3 geometry / typography scale / motion / component tokens
8
- // Advanced consumers can swap the palette/theme and keep tokens global
9
- // side-effects (fonts, link) are then opt-in:
10
- // @use 'themes/X/palette'; @use 'themes/X/theme'; @use 'styles/tokens';
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/default/palette';
14
- @forward '../themes/default/theme';
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 + role assignments
1
+ # Themes — base palette · sibling themes · brands
2
2
 
3
- Colour lives in two files here: a **palette** (defines the concrete colours) and a **theme** (assigns them to semantic roles). The geometry/component layer is separate (`../styles/_tokens.scss`).
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 tiers
5
+ ## Architecture: three layers
6
6
 
7
- | Tier | Lives in | Examples | Consumers |
8
- | -------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
9
- | **Tier 1 — Palette** (defines colours) | `themes/<name>/_palette.scss` | `--blue-600`, `--cool-gray-0`, `--brand-gray-500`, `--red-500`, `--blue-h/s/l`, `--shadow-tint-h/s/l` | Only the theme file |
10
- | **Tier 2 — Theme** (sets roles) | `themes/<name>/_theme.scss` | `--primary: var(--blue-600)`, `--text-primary`, `--surface-default`, `--success`, `--focus-ring`, `--primary-h: var(--blue-h)`, font families; the `--neutral-*` family slot (cool-gray by default, brand-gray via `[data-neutrals="brand"]`) | Components |
11
- | **Tier 3 — Component / scale** | `styles/_tokens.scss` and inside `*.component.scss` | `--space-*`, `--radius-*`, `--ds-button-bg-color`, `--duration-fast`, `--layer-modal` | Components (their own) |
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 Tier 1 palette directly. If you write `var(--blue-600)` inside `button.component.scss`, that's a bug — introduce or reuse a Tier 2 role.
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
- **Why split palette from theme:** the palette says _what colours exist_; the theme says _what each role is_. A dark theme ships a different palette (different ramp values) + a theme map that re-points roles. Swapping either re-skins the system without touching components.
15
+ ## Files
16
16
 
17
- ## Available palette + theme
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
- - **`default/_palette.scss`** Blue + slate ramps (the concrete colours).
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
- Future (each a `themes/<name>/` folder with `_palette.scss` + `_theme.scss`):
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
- - `themes/dark/` — Dark-mode flip (`--shadow-tint-l` → ~95%, neutrals inverted, etc.).
25
- - `themes/high-contrast/` — A11y boost.
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-of-the-box default
59
+ ### Option A — out of the box (light)
30
60
 
31
61
  ```scss
32
- // Consumer styles.scss
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 — explicit theme choice
65
+ ### Option B — à la carte (advanced, opt-in global side-effects)
37
66
 
38
67
  ```scss
39
- // Consumer styles.scss
40
- @use 'themes/dark/palette'; // pick a non-default theme: palette
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
- @use 'styles/typography';
45
- @use 'styles/scrollbar';
46
- @use 'styles/icon-base';
47
- @use 'styles/dropdown-overlay';
73
+ //
48
74
  ```
49
75
 
50
- Palette + theme must come BEFORE `tokens` so role aliases resolve.
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 of default theme
78
+ ### Option C — brand layer on top
53
79
 
54
80
  ```scss
55
- // Consumer styles.scss
56
- @use 'styles/ds'; // default theme + DS
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
- This is the mm-broker pattern: load DS (default theme included), then overlay a brand at a more-specific selector (`[data-brand="manage-my"]`).
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
- Same mechanism (CSS custom properties + cascade), different scope.
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 a `themes/<name>/` folder with `_palette.scss` (ramps + HSL channels) and `_theme.scss` (role map), mirroring `themes/default/`.
72
- 2. Optionally add it to `styles/ds.scss` if it should be a default-bundled option.
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 don't need any change — they consume semantic roles which automatically pick up the new palette.
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
- - **Palette HSL channels** (`--blue-h/s/l`) — for focus rings and halos that compose hsla() from channels (the theme bridges `--primary-h: var(--blue-h)`).
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 NEVER reference `var(--blue-600)` / `var(--neutral-200)` / `var(--red-500)` directly. Use a Tier 2 role (`--primary`, `--text-secondary`) or a Tier 3 component token.
90
- - Themes own palette. Brands overlay on top via `[data-brand]`.
91
- - Theme switching = swap one `@use 'themes/X'`. No component changes.
92
- - `--primary-hover`, `--primary-pressed` are auto-derived via `color-mix()`. Don't override unless brand needs non-derived hover.
93
- - Audit guard (suggested pre-commit hook):
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).