@ids-group-ltd/ids-design-system 0.2.2 → 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.
@@ -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
@@ -84,17 +90,18 @@
84
90
  The alpha values stay constant across themes — only the
85
91
  hue/saturation/lightness become configurable.
86
92
  ============================================================ */
87
- /* --shadow-tint-h/s/l live in themes/default/_palette.scss — palette-level
93
+
94
+ /* --shadow-tint-h/s/l live in themes/light/_palette.scss — palette-level
88
95
  so dark palettes can override --shadow-tint-l → ~95% for soft light glows. */
89
96
 
90
- --shadow-1: 0 1px 2px hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .06),
91
- 0 1px 3px hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .08);
92
- --shadow-2: 0 4px 8px hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .06),
93
- 0 4px 16px hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .10);
94
- --shadow-3: 0 8px 16px hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .08),
95
- 0 16px 32px hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .14);
96
- --shadow-4: 0 16px 32px hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .12),
97
- 0 24px 48px hsla(var(--shadow-tint-h), var(--shadow-tint-s), var(--shadow-tint-l), .18);
97
+ --shadow-1: 0 1px 2px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 6%),
98
+ 0 1px 3px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 8%);
99
+ --shadow-2: 0 4px 8px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 6%),
100
+ 0 4px 16px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 10%);
101
+ --shadow-3: 0 8px 16px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 8%),
102
+ 0 16px 32px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 14%);
103
+ --shadow-4: 0 16px 32px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 12%),
104
+ 0 24px 48px hsl(var(--shadow-tint-h) var(--shadow-tint-s) var(--shadow-tint-l) / 18%);
98
105
  --shadow-popover: var(--shadow-2);
99
106
  --shadow-modal: var(--shadow-4);
100
107
  --shadow-toast: var(--shadow-3);
@@ -169,16 +176,13 @@
169
176
  --opacity-scrim: 0.55; /* modal/drawer backdrop overlay (light theme) */
170
177
  --opacity-state-hover: 0.08; /* hover layer over surface (overlay vs surface swap) */
171
178
  --opacity-state-pressed: 0.16; /* active/pressed layer */
179
+ --opacity-state-active: 0.24; /* deepest pressed layer (e.g. tag close on :active) */
172
180
 
173
181
  /* ============================================================
174
- 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.
175
185
  ============================================================ */
176
- --bp-xs: 480px;
177
- --bp-sm: 640px;
178
- --bp-md: 768px;
179
- --bp-lg: 1024px;
180
- --bp-xl: 1441px; /* Page-grid wide-tier threshold (12 cols, container caps at --col-cap-content). */
181
- --bp-2xl:1536px;
182
186
 
183
187
  /* ============================================================
184
188
  Page grid — tier-specific gutters + margins.
@@ -202,13 +206,13 @@
202
206
  --scrollbar-thumb: var(--neutral-300);
203
207
  --scrollbar-thumb-hover: var(--neutral-400);
204
208
  --scrollbar-thumb-active: var(--neutral-500);
205
- --select-bg: color-mix(in srgb, var(--primary) 22%, transparent);
209
+ --select-bg-color: color-mix(in srgb, var(--primary) 22%, transparent);
206
210
  --col-cap-text: 720px;
207
211
  --col-cap-content: 1280px;
208
212
 
209
213
  /* ============================================================
210
214
  Typography scale (theme-independent). Font FAMILIES are a theme
211
- choice and live in themes/default/_theme.scss (--font-sans/mono/
215
+ choice and live in themes/light/_theme.scss (--font-sans/mono/
212
216
  display/heading); the size/weight/leading/tracking scale below is
213
217
  constant across themes.
214
218
  ============================================================ */
@@ -282,6 +286,15 @@
282
286
  default their own --ds-*-pad to this, so a consumer retunes all at once. */
283
287
  --ds-container-pad: var(--space-6);
284
288
 
289
+ /* Shared focus indicators — components default their own --ds-*-focus-shadow
290
+ to these, so a consumer retunes focus per-group at once (field vs control)
291
+ or per-component. Two families: fields get the soft halo, interactive
292
+ controls/nav get the crisp ring (see themes/light/_theme.scss). */
293
+ --ds-field-focus-shadow: var(--focus-field);
294
+ --ds-field-focus-shadow-error: var(--focus-field-error);
295
+ --ds-control-focus-shadow: var(--focus-ring);
296
+ --ds-control-focus-shadow-error: var(--focus-ring-error);
297
+
285
298
  /* Table cell padding — density-aware, consumed by .tbl td/th. */
286
299
  --table-cell-pad-x: var(--space-4);
287
300
  --table-cell-pad-y: var(--space-3);
@@ -367,7 +380,7 @@
367
380
  --popover-maxh: 360px;
368
381
 
369
382
  /* Field */
370
- --field-gap-label: var(--space-1-5); /* label↔control */
383
+ --field-label-gap: var(--space-1-5); /* field vertical rhythm: label↔control & control↔hint */
371
384
  --field-gap-req: var(--space-1); /* label↔asterisk */
372
385
  --field-h-sm: var(--hit-min);
373
386
  --field-h-md: var(--hit-cozy);
@@ -383,6 +396,19 @@
383
396
  --search-h: var(--hit-md);
384
397
  --search-maxw: 320px;
385
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
+
386
412
  /* ============================================================
387
413
  3b · COMPONENT-SCOPED dimensions (Phase 4 additions)
388
414
  Tokens for atom geometries that previously lived as literals
@@ -433,6 +459,11 @@
433
459
  --menu-minw: 200px; /* dropdown menu min width */
434
460
  --menu-sep-h: var(--border-width-default);
435
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
+
436
467
  /* Tooltip */
437
468
  --tooltip-maxw: 240px;
438
469
  --tooltip-arrow: var(--space-2); /* 8px — caret edge length, halved for offset */
@@ -502,6 +533,16 @@
502
533
  --calendar-bar-h: var(--space-8); /* 32px */
503
534
  --calendar-bar-inset-y: var(--space-2); /* 8px from top of cell */
504
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
+
505
546
  /* Bin pack visualisation */
506
547
  --binpack-bay-h: var(--hit-touch); /* 48px */
507
548
 
@@ -549,13 +590,20 @@
549
590
  --duration-base: 0ms;
550
591
  --duration-slow: 0ms;
551
592
  --duration-slower: 0ms;
552
- --duration-loop-fast: 0ms;
553
- --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;
554
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. */
555
605
  *, *::before, *::after {
556
606
  transition-duration: 0ms !important;
557
- animation-duration: 0ms !important;
558
- animation-iteration-count: 1 !important;
559
607
  }
560
608
  }
561
609
 
@@ -597,6 +645,7 @@
597
645
  }
598
646
  [data-density="cozy"] {
599
647
  --hit-cozy: 40px;
648
+
600
649
  /* defaults */
601
650
  }
602
651
  [data-density="comfy"] {
@@ -1,31 +1,31 @@
1
1
  // Type utility classes — ported from CLAUDE_DESIGN_DS/tokens.css §Type utilities.
2
2
  // These are GLOBAL by design — consumers apply them via class.
3
3
 
4
- .t-display { font: var(--font-weight-extrabold) var(--font-size-display)/var(--line-height-tight) var(--font-display); letter-spacing: var(--letter-spacing-tight); }
5
- .t-h1 { font: var(--font-weight-extrabold) var(--font-size-h1)/var(--line-height-tight) var(--font-heading); letter-spacing: var(--letter-spacing-tight); }
6
- .t-h2 { font: var(--font-weight-extrabold) var(--font-size-h2)/var(--line-height-snug) var(--font-heading); letter-spacing: var(--letter-spacing-snug); }
7
- .t-h3 { font: var(--font-weight-extrabold) var(--font-size-h3)/var(--line-height-snug) var(--font-heading); letter-spacing: var(--letter-spacing-snug); }
8
- .t-h4 { font: var(--font-weight-bold) var(--font-size-h4)/var(--line-height-snug) var(--font-heading); }
9
- .t-h5 { font: var(--font-weight-bold) var(--font-size-h5)/var(--line-height-snug) var(--font-heading); }
10
- .t-card-title { font: var(--font-weight-bold) var(--card-title-fs)/var(--line-height-snug) var(--font-heading); margin: 0; color: var(--text-primary); }
11
- .t-l { font: var(--font-weight-regular) var(--font-size-l)/var(--line-height-base) var(--font-sans); }
12
- .t-l-bold { font: var(--font-weight-bold) var(--font-size-l)/var(--line-height-base) var(--font-sans); }
13
- .t-m { font: var(--font-weight-regular) var(--font-size-m)/var(--line-height-base) var(--font-sans); }
14
- .t-m-bold { font: var(--font-weight-bold) var(--font-size-m)/var(--line-height-base) var(--font-sans); }
15
- .t-s { font: var(--font-weight-regular) var(--font-size-s)/var(--line-height-base) var(--font-sans); }
16
- .t-s-bold { font: var(--font-weight-bold) var(--font-size-s)/var(--line-height-base) var(--font-sans); }
17
- .t-xs { font: var(--font-weight-regular) var(--font-size-xs)/var(--line-height-base) var(--font-sans); }
18
- .t-xs-bold { font: var(--font-weight-bold) var(--font-size-xs)/var(--line-height-base) var(--font-sans); }
19
- .t-overline { font: var(--font-weight-bold) var(--font-size-2xs)/var(--line-height-base) var(--font-sans); letter-spacing: var(--letter-spacing-wider); text-transform: uppercase; color: var(--text-tertiary); }
20
- .t-caption { font: var(--font-weight-regular) var(--font-size-xs)/var(--line-height-base) var(--font-sans); color: var(--text-tertiary); }
21
- .t-mono { font: var(--font-weight-medium) var(--font-size-mono)/var(--line-height-base) var(--font-mono); }
22
- .t-mono-inline { font: var(--font-weight-regular) var(--font-size-mono)/1 var(--font-mono); background: var(--surface-secondary); color: var(--text-primary); padding: 1px var(--space-1); border-radius: var(--radius-sm); }
23
- .t-mono-block { font: var(--font-weight-regular) var(--font-size-mono)/var(--line-height-loose) var(--font-mono); background: var(--surface-secondary); border: var(--border-width-default) solid var(--border-divider); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); }
4
+ .t-display { font: var(--font-weight-extrabold) var(--font-size-display) / var(--line-height-tight) var(--font-display); letter-spacing: var(--letter-spacing-tight); }
5
+ .t-h1 { font: var(--font-weight-extrabold) var(--font-size-h1) / var(--line-height-tight) var(--font-heading); letter-spacing: var(--letter-spacing-tight); }
6
+ .t-h2 { font: var(--font-weight-extrabold) var(--font-size-h2) / var(--line-height-snug) var(--font-heading); letter-spacing: var(--letter-spacing-snug); }
7
+ .t-h3 { font: var(--font-weight-extrabold) var(--font-size-h3) / var(--line-height-snug) var(--font-heading); letter-spacing: var(--letter-spacing-snug); }
8
+ .t-h4 { font: var(--font-weight-bold) var(--font-size-h4) / var(--line-height-snug) var(--font-heading); }
9
+ .t-h5 { font: var(--font-weight-bold) var(--font-size-h5) / var(--line-height-snug) var(--font-heading); }
10
+ .t-card-title { font: var(--font-weight-bold) var(--card-title-fs) / var(--line-height-snug) var(--font-heading); margin: 0; color: var(--text-primary); }
11
+ .t-l { font: var(--font-weight-regular) var(--font-size-l) / var(--line-height-base) var(--font-sans); }
12
+ .t-l-bold { font: var(--font-weight-bold) var(--font-size-l) / var(--line-height-base) var(--font-sans); }
13
+ .t-m { font: var(--font-weight-regular) var(--font-size-m) / var(--line-height-base) var(--font-sans); }
14
+ .t-m-bold { font: var(--font-weight-bold) var(--font-size-m) / var(--line-height-base) var(--font-sans); }
15
+ .t-s { font: var(--font-weight-regular) var(--font-size-s) / var(--line-height-base) var(--font-sans); }
16
+ .t-s-bold { font: var(--font-weight-bold) var(--font-size-s) / var(--line-height-base) var(--font-sans); }
17
+ .t-xs { font: var(--font-weight-regular) var(--font-size-xs) / var(--line-height-base) var(--font-sans); }
18
+ .t-xs-bold { font: var(--font-weight-bold) var(--font-size-xs) / var(--line-height-base) var(--font-sans); }
19
+ .t-overline { font: var(--font-weight-bold) var(--font-size-2xs) / var(--line-height-base) var(--font-sans); letter-spacing: var(--letter-spacing-wider); text-transform: uppercase; color: var(--text-tertiary); }
20
+ .t-caption { font: var(--font-weight-regular) var(--font-size-xs) / var(--line-height-base) var(--font-sans); color: var(--text-tertiary); }
21
+ .t-mono { font: var(--font-weight-medium) var(--font-size-mono) / var(--line-height-base) var(--font-mono); }
22
+ .t-mono-inline { font: var(--font-weight-regular) var(--font-size-mono) / 1 var(--font-mono); background: var(--surface-secondary); color: var(--text-primary); padding: 1px var(--space-1); border-radius: var(--radius-sm); }
23
+ .t-mono-block { font: var(--font-weight-regular) var(--font-size-mono) / var(--line-height-loose) var(--font-mono); background: var(--surface-secondary); border: var(--border-width-default) solid var(--border-divider); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); }
24
24
 
25
25
  .sr-only {
26
26
  position: absolute !important;
27
27
  width: 1px; height: 1px;
28
28
  padding: 0; margin: -1px;
29
- overflow: hidden; clip: rect(0,0,0,0);
29
+ overflow: hidden; clip-path: inset(50%);
30
30
  white-space: nowrap; border: 0;
31
31
  }
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,96 +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`, `--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`):
23
- - `themes/dark/` — Dark-mode flip (`--shadow-tint-l` → ~95%, neutrals inverted, etc.).
24
- - `themes/high-contrast/` — A11y boost.
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.
26
+
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.)
25
56
 
26
57
  ## How a consumer picks a theme
27
58
 
28
- ### Option A — out-of-the-box default
59
+ ### Option A — out of the box (light)
29
60
 
30
61
  ```scss
31
- // Consumer styles.scss
32
- @use 'styles/ds'; // bundles default palette + theme + tokens + reset + ...
62
+ @use 'styles/ds'; // base palette + light/dark activation + tokens + reset + …
33
63
  ```
34
64
 
35
- ### Option B — explicit theme choice
65
+ ### Option B — à la carte (advanced, opt-in global side-effects)
36
66
 
37
67
  ```scss
38
- // Consumer styles.scss
39
- @use 'themes/dark/palette'; // pick a non-default theme: palette
40
- @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']
41
70
  @use 'styles/tokens';
71
+ @use 'styles/fonts';
42
72
  @use 'styles/reset';
43
- @use 'styles/typography';
44
- @use 'styles/scrollbar';
45
- @use 'styles/icon-base';
46
- @use 'styles/dropdown-overlay';
73
+ //
47
74
  ```
48
75
 
49
- 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.
50
77
 
51
- ### Option C — brand layer on top of default theme
78
+ ### Option C — brand layer on top
52
79
 
53
80
  ```scss
54
- // Consumer styles.scss
55
- @use 'styles/ds'; // default theme + DS
56
- @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)
57
83
  ```
58
84
 
59
- This is the mm-broker pattern: load DS (default theme included), then overlay a brand at a more-specific selector (`[data-brand="manage-my"]`).
60
-
61
- ## Brand vs theme — what's the difference?
62
-
63
- - **Theme** swaps the entire Tier 1 palette. Affects EVERY semantic role downstream.
64
- - **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
65
86
 
66
- 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.
67
89
 
68
90
  ## Adding a new theme
69
91
 
70
- 1. Create a `themes/<name>/` folder with `_palette.scss` (ramps + HSL channels) and `_theme.scss` (role map), mirroring `themes/default/`.
71
- 2. Optionally add it to `styles/ds.scss` if it should be a default-bundled option.
72
- 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.
73
94
 
74
- 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.
75
96
 
76
97
  ## Adding a new brand override
77
98
 
78
99
  1. Create `<consumer-app>/src/app/styles/_brand-X.scss`.
79
- 2. Open with `[data-brand="X"] { ... }`.
80
- 3. Override:
81
- - **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)`).
82
- - **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.
83
- - Other roles you want to retint independently (surfaces, borders, text, success/danger ramps, secondary, etc.).
84
- 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">`.
85
103
 
86
104
  ## Rules summary
87
105
 
88
- - 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.
89
- - Themes own palette. Brands overlay on top via `[data-brand]`.
90
- - Theme switching = swap one `@use 'themes/X'`. No component changes.
91
- - `--primary-hover`, `--primary-pressed` are auto-derived via `color-mix()`. Don't override unless brand needs non-derived hover.
92
- - Audit guard (suggested pre-commit hook):
93
- ```bash
94
- grep -nE 'var\(--(violet|neutral|red|green|yellow|blue|orange|cyan|magenta)-[0-9]+\)' \
95
- prototype/ds/src/lib/components/**/*.scss && exit 1
96
- ```
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).