@databricks/design-system 2.0.5 → 2.0.6

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/design-guidance/docs/README.md +40 -0
  3. package/design-guidance/docs/review-contract.md +22 -0
  4. package/design-guidance/docs/tokens.md +35 -0
  5. package/design-guidance/docs/voice.md +40 -0
  6. package/design-guidance/skills/dbui-build-screen.md +24 -0
  7. package/design-guidance/skills/dbui-check-ecosystem.md +21 -0
  8. package/design-guidance/skills/dbui-check-guidelines.md +29 -0
  9. package/design-guidance/skills/dbui-check-principles.md +26 -0
  10. package/design-guidance/skills/dbui-check-voice.md +17 -0
  11. package/design-guidance/skills/dbui-pick-component.md +23 -0
  12. package/design-guidance/skills/dbui-pick-icon.md +23 -0
  13. package/design-guidance/skills/dbui-review.md +30 -0
  14. package/design-guidance/skills/dbui-validate.md +23 -0
  15. package/dist/{RHFControlledTypeaheadComboboxV2-DsH-nIEk.js → RHFControlledTypeaheadComboboxV2-Ct9x3rC1.js} +5 -5
  16. package/dist/{RHFControlledTypeaheadComboboxV2-DsH-nIEk.js.map → RHFControlledTypeaheadComboboxV2-Ct9x3rC1.js.map} +1 -1
  17. package/dist/{WizardStepContentWrapper-Bq1wm0V7.js → WizardStepContentWrapper-6YTjeEaA.js} +477 -450
  18. package/dist/WizardStepContentWrapper-6YTjeEaA.js.map +1 -0
  19. package/dist/icon-metadata.json +2064 -0
  20. package/dist/{index-BRAVZg-S.js → index-DiRpSwH2.js} +239 -38
  21. package/dist/index-DiRpSwH2.js.map +1 -0
  22. package/dist/index-dark.css +458 -190
  23. package/dist/index-dark.mitigated.css +460 -192
  24. package/dist/index.css +1415 -1082
  25. package/dist/index.js +4 -4
  26. package/dist/index.mitigated.css +1417 -1084
  27. package/dist/patterns.js +3 -3
  28. package/dist-types/design-system/Drawer/Drawer.d.ts +3 -1
  29. package/dist-types/design-system/Icon/AntdIcon.d.ts +3 -0
  30. package/dist-types/design-system/Icon/Icon.d.ts +20 -6
  31. package/dist-types/design-system/Icon/NativeIcon.d.ts +3 -0
  32. package/dist-types/design-system/Icon/__generated/icons/LakewatchIcon.d.ts +4 -0
  33. package/dist-types/design-system/Icon/__generated/icons/index.d.ts +1 -0
  34. package/dist-types/design-system/Icon/index.d.ts +1 -0
  35. package/dist-types/design-system/Icon/utils.d.ts +1 -0
  36. package/dist-types/design-system/_shared_/Tabs/styles.d.ts +2 -3
  37. package/dist-types/~patterns/Wizard/WizardModal.d.ts +1 -1
  38. package/package.json +4 -2
  39. package/dist/WizardStepContentWrapper-Bq1wm0V7.js.map +0 -1
  40. package/dist/index-BRAVZg-S.js.map +0 -1
package/dist/index.css CHANGED
@@ -16046,7 +16046,7 @@ span.du-bois-light-typography-ellipsis-single-line {
16046
16046
 
16047
16047
  --db-surface-selected: var(--db-color-table-background-selected-hover);
16048
16048
  --db-surface-selected-hover: initial; /* TODO */;
16049
- --db-surface-selected-active: initial; /* TODO */;
16049
+ --db-surface-selected-active: var(--db-color-table-background-selected-hover); /* no distinct active value */
16050
16050
 
16051
16051
  /* Surface alpha-blending modifiers */
16052
16052
  --db-surface-backdrop: initial; /* TODO */;
@@ -16072,7 +16072,7 @@ span.du-bois-light-typography-ellipsis-single-line {
16072
16072
  --db-accent-surface-strong: var(--db-color-action-primary-background-hover);
16073
16073
 
16074
16074
  --db-accent-surface-hover: var(--db-color-action-primary-background-hover);
16075
- --db-accent-surface-active: initial; /* TODO */
16075
+ --db-accent-surface-active: var(--db-color-action-primary-background-press); /* no distinct active value */
16076
16076
  --db-accent-surface-selected: var(--db-color-action-primary-background-press);
16077
16077
 
16078
16078
  /* In the current design, the text color for an accent surface does not change with hover, active, etc. */
@@ -16089,7 +16089,7 @@ span.du-bois-light-typography-ellipsis-single-line {
16089
16089
  --db-negative-surface-strong: var(--db-color-action-danger-primary-background-hover);
16090
16090
 
16091
16091
  --db-negative-surface-hover: var(--db-color-action-danger-primary-background-hover);
16092
- --db-negative-surface-active: initial; /* TODO */
16092
+ --db-negative-surface-active: var(--db-color-action-danger-primary-background-press); /* no distinct active value */
16093
16093
  --db-negative-surface-selected: var(--db-color-action-danger-primary-background-press);
16094
16094
 
16095
16095
  --db-negative-text: var(--db-color-text-validation-danger);
@@ -16130,6 +16130,11 @@ span.du-bois-light-typography-ellipsis-single-line {
16130
16130
  --db-positive-border: initial; /* TODO */
16131
16131
  --db-positive-border-hover: initial; /* TODO */
16132
16132
  --db-positive-border-active: initial; /* TODO */
16133
+
16134
+ /* Branded */
16135
+ --db-ai-gradient-start: #4299e0;
16136
+ --db-ai-gradient-mid: #ca42e0;
16137
+ --db-ai-gradient-end: #ff5f46;
16133
16138
  }
16134
16139
 
16135
16140
 
@@ -16210,6 +16215,47 @@ span.du-bois-light-typography-ellipsis-single-line {
16210
16215
  --db-shape-pill: 999px;
16211
16216
  }
16212
16217
 
16218
+ :where(:root), :where(.du-bois-light) {
16219
+ /* Shape */
16220
+ /* Mirrors the ShapeTokens host-override var used by list items; falls back to the theme radius. */
16221
+ --db-list-item-border-radius: var(--list-item-border-radius, 4px);
16222
+
16223
+ /* Color */
16224
+ --db-list-surface: var(--db-surface-base);
16225
+ --db-list-text: var(--db-text-base);
16226
+ --db-list-header-text: var(--db-text-subtle);
16227
+ --db-list-separator: var(--db-border-subtle);
16228
+
16229
+ --db-list-item-surface-hover: var(--db-surface-hover);
16230
+ --db-list-item-surface-active: var(--db-surface-active);
16231
+ --db-list-item-surface-selected: var(--db-accent-surface-subtle);
16232
+ --db-list-item-surface-disabled: var(--db-surface-disabled);
16233
+
16234
+ --db-list-item-text-selected: var(--db-accent-text);
16235
+ --db-list-item-text-disabled: var(--db-text-disabled);
16236
+
16237
+ --db-list-item-danger-text: var(--db-negative-text);
16238
+ --db-list-item-danger-surface-hover: var(--db-negative-surface-subtle);
16239
+
16240
+ /* Density */
16241
+ --db-list-item-padding: var(--db-space-2);
16242
+ --db-list-item-height: var(--db-size-8);
16243
+ }
16244
+
16245
+ :where(:root), :where(.du-bois-light) {
16246
+ /* Shape */
16247
+ /* Mirrors the ShapeTokens host-override var; falls back to the v1 value (borderRadiusSm). */
16248
+ --db-popup-border-radius: var(--popup-border-radius, 4px);
16249
+
16250
+ /* Color */
16251
+ --db-popup-surface: var(--db-surface-base);
16252
+ --db-popup-text: var(--db-text-base);
16253
+ --db-popup-border: var(--db-border-base);
16254
+
16255
+ /* Elevation */
16256
+ --db-popup-elevation: var(--db-elevation-med);
16257
+ }
16258
+
16213
16259
  :where(:root), :where(.du-bois-light) {
16214
16260
  /* Shape */
16215
16261
  --db-selection-toggle-border-radius: 4px;
@@ -16221,15 +16267,15 @@ span.du-bois-light-typography-ellipsis-single-line {
16221
16267
  --db-selection-toggle-border: var(--db-border-strong);
16222
16268
 
16223
16269
  --db-selection-toggle-surface-hover: var(--db-surface-hover);
16224
- --db-selection-toggle-surface-press: var(--db-surface-selected);
16270
+ --db-selection-toggle-surface-active: var(--db-surface-selected-active);
16225
16271
  --db-selection-toggle-surface-checked: var(--db-accent-surface);
16226
16272
  --db-selection-toggle-surface-checked-hover: var(--db-accent-surface-hover);
16227
- --db-selection-toggle-surface-checked-press: var(--db-accent-surface-selected);
16273
+ --db-selection-toggle-surface-checked-active: var(--db-accent-surface-active);
16228
16274
  --db-selection-toggle-surface-disabled: var(--db-surface-disabled);
16229
16275
  --db-selection-toggle-surface-checked-disabled: var(--db-surface-disabled);
16230
16276
 
16231
16277
  --db-selection-toggle-border-hover: var(--db-accent-surface-hover);
16232
- --db-selection-toggle-border-press: var(--db-accent-surface-selected);
16278
+ --db-selection-toggle-border-active: var(--db-accent-surface-active);
16233
16279
  --db-selection-toggle-border-disabled: var(--db-border-base);
16234
16280
 
16235
16281
  --db-selection-toggle-mark-disabled: var(--db-text-disabled);
@@ -16245,1322 +16291,1638 @@ span.du-bois-light-typography-ellipsis-single-line {
16245
16291
  --db-selection-toggle-transition-duration: 120ms;
16246
16292
  }
16247
16293
 
16248
- :where(:root), :where(.du-bois-light) {
16249
- /* Shape */
16250
- --db-checkbox-border-radius: var(--db-selection-toggle-border-radius);
16251
- --db-checkbox-border-width: 1px;
16252
-
16253
- /* Color */
16254
- --db-checkbox-surface: var(--db-selection-toggle-surface);
16255
- --db-checkbox-surface-hover: var(--db-color-action-default-background-hover);
16256
- --db-checkbox-surface-press: var(--db-color-action-default-background-press);
16257
- --db-checkbox-surface-checked: var(--db-selection-toggle-surface-checked);
16258
- --db-checkbox-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16259
- --db-checkbox-surface-checked-press: var(--db-selection-toggle-surface-checked-press);
16260
- --db-checkbox-surface-disabled: var(--db-selection-toggle-surface-disabled);
16261
-
16262
- --db-checkbox-border: var(--db-color-action-default-border-default);
16263
- --db-checkbox-border-hover: var(--db-selection-toggle-border-hover);
16264
- --db-checkbox-border-press: var(--db-selection-toggle-border-press);
16265
- --db-checkbox-focus-ring: var(--db-focus-ring-color);
16266
- --db-checkbox-border-disabled: var(--db-color-action-disabled-border);
16267
16294
 
16268
- --db-checkbox-mark: #ffffff;
16269
- --db-checkbox-mark-disabled: var(--db-selection-toggle-mark-disabled);
16270
- --db-checkbox-text: var(--db-selection-toggle-text);
16271
- --db-checkbox-text-disabled: var(--db-selection-toggle-text-disabled);
16272
16295
 
16273
- --db-checkbox-shadow-checked: var(--db-selection-toggle-shadow-checked);
16296
+ /* Component-tier tokens for the native <Button>, grouped by area (density, shape, typography, color).
16297
+ * Colors alias the action-* system tokens; sizes alias the system density scale.
16298
+ *
16299
+ * Follows the mortar/action.css pattern: one semantic set (surface / border / text, each with
16300
+ * -hover / -active / -disabled) whose defaults are the secondary variant. The variant classes that
16301
+ * re-set these tokens per variant live in NativeButton.css; this file declares only tokens. */
16274
16302
 
16275
- /* Density */
16276
- --db-checkbox-size: var(--db-selection-toggle-size);
16277
- --db-checkbox-label-spacing: var(--db-selection-toggle-label-spacing);
16278
- --db-checkbox-focus-ring-width: var(--db-focus-ring-width);
16279
- --db-checkbox-indeterminate-mark-width: var(--db-size-2); /* 8px */
16280
- --db-checkbox-indeterminate-mark-height: calc(var(--db-size-1) * 0.75); /* 3px */
16303
+ /* Declared under the theme classes too (not just :root) so the var() chain re-resolves inside a
16304
+ * `.du-bois-dark` subtree (or a portal that re-applies the theme class) instead of freezing at the root values. */
16305
+ :where(:root), :where(.du-bois-light) {
16306
+ /* ── Density ──────────────────────────────────────────────────────────── */
16307
+ --db-btn-height: var(--db-size-8);
16308
+ --db-btn-height-small: var(--db-size-6);
16309
+ --db-btn-icon-size: var(--db-size-4);
16310
+ --db-btn-border-width: 1px;
16311
+ --db-btn-padding-vertical: var(--db-space-1);
16312
+ --db-btn-padding-horizontal: var(--db-space-3);
16313
+ --db-btn-padding-horizontal-small: var(--db-space-2);
16314
+ /* Icon/force-icon horizontal padding: half the standard horizontal padding. */
16315
+ --db-btn-padding-horizontal-icon: calc(var(--db-space-3) / 2); /* 6px */
16316
+ --db-btn-gap: var(--db-space-1);
16317
+ --db-btn-end-icon-gap: var(--db-space-2);
16318
+ --db-btn-end-icon-gap-small: var(--db-space-1);
16319
+ /* Width of a content-less button while loading, so the standalone spinner has room. */
16320
+ --db-btn-loading-empty-width: var(--db-size-10);
16281
16321
 
16282
- /* Motion */
16283
- --db-checkbox-transition-duration: var(--db-selection-toggle-transition-duration);
16284
- }
16322
+ /* ── Shape ────────────────────────────────────────────────────────────── */
16323
+ --db-btn-border-radius: var(--db-shape-control);
16285
16324
 
16286
- /* Native-input <Checkbox> styles. Classic CSS only — no native nesting (the
16287
- * `&`-style nesting feature ships in Chrome/Edge 120, Safari 17.2, Firefox 117
16288
- * which leaves a non-trivial slice of our production traffic uncovered today;
16289
- * see PR #1954620 thread). `:has()` lookups are still used to read state from
16290
- * the native <input>; that feature has the support coverage we need.
16291
- *
16292
- * Structure (assumed by the React component):
16293
- *
16294
- * <label class="db-checkbox">
16295
- * <input type="checkbox" class="db-checkbox-input" />
16296
- * <span class="db-checkbox-indicator"> ← border + fill react to state
16297
- * <svg class="db-checkbox-checkmark" .../> ← visible when :checked
16298
- * <span class="db-checkbox-dash" /> ← visible when :indeterminate
16299
- * </span>
16300
- * <span class="db-checkbox-label">{children}</span>
16301
- * </label>
16302
- *
16303
- * State and hiding rules target `.db-checkbox-input` — the checkbox's OWN input —
16304
- * never a bare `input`: consumers may render an inline <input> (e.g. a number
16305
- * field) inside the label children, and a descendant `input` selector would hide
16306
- * / absolutely-position that field and mis-drive the `:has(…)` state rules. */
16325
+ /* ── Typography ───────────────────────────────────────────────────────── */
16326
+ --db-btn-font-size: var(--db-font-size-base);
16327
+ --db-btn-icon-only-font-size: var(--db-font-size-lg); /* 15px */
16328
+ --db-btn-font-weight: var(--db-font-weight-normal);
16329
+ /* Line heights are hard-coded until the typography scale ships computed per-level line-height tokens. */
16330
+ --db-btn-line-height: 1.25rem; /* 20px */
16331
+ --db-btn-line-height-small: 1rem; /* 16px */
16307
16332
 
16308
- .db-checkbox {
16309
- /* `display: flex` (block-level), NOT `inline-flex`: an inline-flex root puts
16310
- * the label inside an inline-line-box whose leading pushes block-level siblings
16311
- * (e.g. `<FormMessage>`) down by a few pixels — making validation messages sit
16312
- * lower under the native checkbox than under the AntD checkbox (which itself is
16313
- * wrapped in a block `<div>`, so its label's inline-line-box is consumed inside
16314
- * that div and never leaks into sibling spacing). `width: fit-content` keeps the
16315
- * label behaving like an inline-flex item inside a flex-row parent (the group,
16316
- * or any consumer's flex container). */
16317
- display: flex;
16318
- align-items: flex-start;
16319
- position: relative;
16320
- cursor: pointer;
16321
- user-select: none;
16322
- /* No `gap` here — spacing between indicator and label, plus the trailing
16323
- * whitespace that gives sibling checkboxes their visual separation, lives
16324
- * on `.db-checkbox-label` as padding-inline. Matches AntD's `.{prefix}-checkbox + span`
16325
- * rule and the prior Emotion-based NativeCheckbox implementation. */
16326
- min-height: var(--db-line-height-base, 20px);
16327
- width: fit-content;
16328
- }
16333
+ /* ── Color: semantic set (defaults = secondary variant) ───────────────── */
16334
+ --db-btn-surface: var(--db-color-action-default-background-default);
16335
+ --db-btn-surface-hover: var(--db-color-action-default-background-hover);
16336
+ --db-btn-surface-active: var(--db-color-action-default-background-press);
16337
+ --db-btn-surface-disabled: transparent;
16338
+ --db-btn-border: var(--db-color-action-default-border-default);
16339
+ --db-btn-border-hover: var(--db-color-action-default-border-hover);
16340
+ --db-btn-border-active: var(--db-color-action-default-border-press);
16341
+ --db-btn-border-disabled: var(--db-color-action-disabled-border);
16342
+ --db-btn-text: var(--db-color-action-default-text-default);
16343
+ --db-btn-text-hover: var(--db-color-action-default-text-hover);
16344
+ --db-btn-text-active: var(--db-color-action-default-text-press);
16345
+ --db-btn-text-disabled: var(--db-color-action-disabled-text);
16329
16346
 
16330
- /* Hover/press live on the root (label), not on the input: the input is
16331
- * visually hidden (opacity:0), so an `input:hover` rule wouldn't be visible
16332
- * anyway. Putting `:hover` on the root also makes the entire label area
16333
- * — not just the indicator square — drive the visual feedback. */
16334
- .db-checkbox:hover:has(.db-checkbox-input:not(:disabled, :checked, :indeterminate)) .db-checkbox-indicator {
16335
- background-color: var(--db-checkbox-surface-hover);
16336
- border-color: var(--db-checkbox-border-hover);
16347
+ /* ── Focus & icon ─────────────────────────────────────────────────────── */
16348
+ --db-btn-focus-ring: var(--db-color-action-default-border-focus);
16349
+ --db-btn-danger-focus-ring: var(--db-color-action-danger-primary-background-default);
16350
+ --db-btn-icon-glyph: var(--db-color-text-secondary);
16337
16351
  }
16338
16352
 
16339
- .db-checkbox:active:has(.db-checkbox-input:not(:disabled, :checked, :indeterminate)) .db-checkbox-indicator {
16340
- background-color: var(--db-checkbox-surface-press);
16341
- border-color: var(--db-checkbox-border-press);
16342
- }
16353
+ /* Native <Button> visual styles. Consumes the component-tier `--db-btn-*` tokens (see tokens.css);
16354
+ * the variant classes below re-set those tokens per variant and the rules that follow paint them.
16355
+ * Shipped via mortar.css. */
16343
16356
 
16344
- /* Selected fill checked and indeterminate share the same surface. */
16345
- .db-checkbox:has(.db-checkbox-input:is(:checked, :indeterminate)) .db-checkbox-indicator {
16346
- background-color: var(--db-checkbox-surface-checked);
16347
- border-color: var(--db-checkbox-surface-checked);
16357
+ /* ── 0. Variant token re-sets ──────────────────────────────────────────────
16358
+ * Each variant class re-sets the semantic tokens declared in tokens.css; combined classes win by
16359
+ * specificity, which beats the `:where(:root)` (specificity 0) defaults regardless of file order. */
16360
+ .db-btn-primary {
16361
+ --db-btn-surface: var(--db-color-action-primary-background-default);
16362
+ --db-btn-surface-hover: var(--db-color-action-primary-background-hover);
16363
+ --db-btn-surface-active: var(--db-color-action-primary-background-press);
16364
+ --db-btn-surface-disabled: var(--db-color-action-disabled-border);
16365
+ --db-btn-border: transparent;
16366
+ --db-btn-border-hover: transparent;
16367
+ --db-btn-border-active: transparent;
16368
+ --db-btn-border-disabled: transparent;
16369
+ --db-btn-text: var(--db-color-action-primary-text-default);
16370
+ --db-btn-text-hover: var(--db-color-action-primary-text-hover);
16371
+ --db-btn-text-active: var(--db-color-action-primary-text-press);
16372
+ --db-btn-text-disabled: var(--db-color-action-primary-text-default);
16348
16373
  }
16349
-
16350
- .db-checkbox:hover:has(.db-checkbox-input:is(:checked, :indeterminate):not(:disabled)) .db-checkbox-indicator {
16351
- background-color: var(--db-checkbox-surface-checked-hover);
16352
- border-color: var(--db-checkbox-surface-checked-hover);
16374
+ .db-btn-tertiary,
16375
+ .db-btn-link {
16376
+ --db-btn-surface: var(--db-color-action-tertiary-background-default);
16377
+ --db-btn-surface-disabled: var(--db-color-action-tertiary-background-default);
16378
+ --db-btn-border: var(--db-color-action-tertiary-background-default);
16379
+ --db-btn-border-hover: transparent;
16380
+ --db-btn-border-active: transparent;
16381
+ --db-btn-border-disabled: transparent;
16382
+ --db-btn-text: var(--db-color-action-tertiary-text-default);
16383
+ --db-btn-text-hover: var(--db-color-action-tertiary-text-hover);
16384
+ --db-btn-text-active: var(--db-color-action-tertiary-text-press);
16385
+ --db-btn-text-disabled: var(--db-color-action-disabled-text);
16353
16386
  }
16354
-
16355
- .db-checkbox:active:has(.db-checkbox-input:is(:checked, :indeterminate):not(:disabled)) .db-checkbox-indicator {
16356
- background-color: var(--db-checkbox-surface-checked-press);
16357
- border-color: var(--db-checkbox-surface-checked-press);
16387
+ .db-btn-tertiary {
16388
+ --db-btn-surface-hover: var(--db-color-action-tertiary-background-hover);
16389
+ --db-btn-surface-active: var(--db-color-action-tertiary-background-press);
16358
16390
  }
16359
-
16360
- /* Mark visibility. */
16361
- .db-checkbox:has(.db-checkbox-input:checked) .db-checkbox-checkmark {
16362
- opacity: 1;
16391
+ /* Link is borderless and text-only, so its hover/press surface stays transparent. */
16392
+ .db-btn-link {
16393
+ --db-btn-surface-hover: transparent;
16394
+ --db-btn-surface-active: transparent;
16363
16395
  }
16364
-
16365
- .db-checkbox:has(.db-checkbox-input:indeterminate) .db-checkbox-dash {
16366
- opacity: 1;
16396
+ /* Every danger button (icon included) disables to the filled treatment. */
16397
+ .db-btn-danger {
16398
+ --db-btn-surface-disabled: var(--db-color-action-disabled-border);
16399
+ --db-btn-border-disabled: transparent;
16400
+ --db-btn-text-disabled: var(--db-color-action-primary-text-default);
16367
16401
  }
16368
-
16369
- /* Focus-visible (keyboard only). */
16370
- .db-checkbox:has(.db-checkbox-input:focus-visible) .db-checkbox-indicator {
16371
- outline: var(--db-checkbox-focus-ring-width) solid var(--db-checkbox-focus-ring);
16372
- outline-offset: 4px;
16402
+ /* Danger, filled (primary): re-set only the surface; text/transparent border come from primary. */
16403
+ .db-btn-primary.db-btn-danger:not(.db-btn-icon-only) {
16404
+ --db-btn-surface: var(--db-color-action-danger-primary-background-default);
16405
+ --db-btn-surface-hover: var(--db-color-action-danger-primary-background-hover);
16406
+ --db-btn-surface-active: var(--db-color-action-danger-primary-background-press);
16373
16407
  }
16374
-
16375
- /* Disabled (covers unchecked, checked, indeterminate). Sourced from
16376
- * `input:disabled` so the standalone-disabled and group-disabled paths both
16377
- * land here without needing a separate root modifier class. */
16378
- .db-checkbox:has(.db-checkbox-input:disabled) {
16379
- cursor: not-allowed;
16408
+ /* Danger, secondary/outlined. */
16409
+ .db-btn-danger:not(.db-btn-primary):not(.db-btn-tertiary):not(.db-btn-link):not(.db-btn-icon-only) {
16410
+ --db-btn-surface: var(--db-color-action-danger-default-background-default);
16411
+ --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
16412
+ --db-btn-surface-active: var(--db-color-action-danger-default-background-press);
16413
+ --db-btn-border: var(--db-color-action-danger-default-border-default);
16414
+ --db-btn-border-hover: var(--db-color-action-danger-default-border-hover);
16415
+ --db-btn-border-active: var(--db-color-action-danger-default-border-press);
16416
+ --db-btn-text: var(--db-color-action-danger-default-text-default);
16417
+ --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
16418
+ --db-btn-text-active: var(--db-color-action-danger-default-text-press);
16380
16419
  }
16381
-
16382
- .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-label {
16383
- color: var(--db-checkbox-text-disabled);
16420
+ /* Danger, tertiary/link: danger text everywhere, danger surface on hover/press; resting/disabled
16421
+ * surface stay the tertiary ghost (override the filled danger-disabled set above). */
16422
+ .db-btn-tertiary.db-btn-danger,
16423
+ .db-btn-link.db-btn-danger {
16424
+ --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
16425
+ --db-btn-surface-active: var(--db-color-action-danger-default-background-press);
16426
+ --db-btn-surface-disabled: var(--db-color-action-tertiary-background-default);
16427
+ --db-btn-text: var(--db-color-action-danger-default-text-default);
16428
+ --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
16429
+ --db-btn-text-active: var(--db-color-action-danger-default-text-press);
16430
+ --db-btn-text-disabled: var(--db-color-action-disabled-text);
16384
16431
  }
16385
-
16386
- .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-indicator {
16387
- background-color: var(--db-checkbox-surface-disabled);
16388
- border-color: var(--db-checkbox-border-disabled);
16389
- box-shadow: none;
16432
+ /* Icon-only renders as the ghost (tertiary) treatment regardless of variant; re-set the semantic set
16433
+ * so the icon-only rules paint from these component tokens. */
16434
+ .db-btn-icon-only {
16435
+ --db-btn-surface: var(--db-color-action-tertiary-background-default);
16436
+ --db-btn-surface-hover: var(--db-color-action-tertiary-background-hover);
16437
+ --db-btn-surface-active: var(--db-color-action-tertiary-background-press);
16438
+ --db-btn-text: var(--db-color-action-tertiary-text-default);
16439
+ --db-btn-text-hover: var(--db-color-action-tertiary-text-hover);
16440
+ --db-btn-text-active: var(--db-color-action-tertiary-text-press);
16390
16441
  }
16391
-
16392
- .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-checkmark {
16393
- color: var(--db-checkbox-mark-disabled);
16442
+ /* Danger icon-only keeps the ghost resting surface (inherited above) but uses danger text and a
16443
+ * danger hover/press surface. */
16444
+ .db-btn-icon-only.db-btn-danger {
16445
+ --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
16446
+ --db-btn-surface-active: var(--db-color-action-danger-default-background-press);
16447
+ --db-btn-text: var(--db-color-action-danger-default-text-default);
16448
+ --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
16449
+ --db-btn-text-active: var(--db-color-action-danger-default-text-press);
16394
16450
  }
16395
16451
 
16396
- .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-dash {
16397
- background-color: var(--db-checkbox-mark-disabled);
16452
+ /* ── 1. Base — paints the default (secondary) variant ────────────────────── */
16453
+ .db-btn {
16454
+ appearance: none;
16455
+ box-sizing: border-box;
16456
+ position: relative; /* anchors the loading spinner */
16457
+ display: inline-flex !important;
16458
+ align-items: center !important;
16459
+ justify-content: center !important;
16460
+ vertical-align: middle;
16461
+ white-space: nowrap;
16462
+ user-select: none;
16463
+ cursor: pointer;
16464
+ height: var(--db-btn-height) !important;
16465
+ font-family: inherit;
16466
+ font-size: var(--db-btn-font-size);
16467
+ font-weight: var(--db-btn-font-weight);
16468
+ line-height: var(--db-btn-line-height) !important;
16469
+ text-decoration: none !important;
16470
+ border: var(--db-btn-border-width) solid var(--db-btn-border);
16471
+ border-color: var(--db-btn-border) !important;
16472
+ border-radius: var(--db-btn-border-radius) !important;
16473
+ box-shadow: none !important;
16474
+ background-color: var(--db-btn-surface) !important;
16475
+ color: var(--db-btn-text) !important;
16398
16476
  }
16399
16477
 
16400
- /* Visually-hidden native input (opacity:0) overlaying the indicator square,
16401
- * kept interactive and in the tab order so `:has()` lookups above can read its
16402
- * state. It stays pointer-clickable — matching AntD's `.ant-checkbox-input`
16403
- * so it is the top-most hit-test target over the box: `getByRole('checkbox').click()`
16404
- * (Playwright, and any coordinate click that lands on the box) resolves to the
16405
- * real control instead of the decorative indicator overlay. Making the input
16406
- * `pointer-events: none` (the prior native-only behavior) is what regressed
16407
- * interaction tests vs. the AntD variant. `cursor: inherit` tracks the label's
16408
- * pointer / disabled `not-allowed` cursor so the box keeps AntD's cursor too. */
16409
- .db-checkbox .db-checkbox-input {
16410
- position: absolute;
16411
- inset: 0;
16412
- inline-size: var(--db-checkbox-size);
16413
- block-size: var(--db-checkbox-size);
16414
- margin: 0;
16415
- opacity: 0;
16416
- cursor: inherit;
16478
+ /* ── 2. Padding ──────────────────────────────────────────────────────────── */
16479
+ .db-btn.db-btn:where(:not(.db-btn-icon-only, .db-btn-force-icon, .db-btn-link)) {
16480
+ padding: var(--db-btn-padding-vertical) var(--db-btn-padding-horizontal) !important;
16417
16481
  }
16418
-
16419
- /* Indicator square with the border/fill that responds to state above.
16420
- * Vertical math: aligns the indicator to the cap-height of the first text
16421
- * line so checkbox + label sit on the same visual baseline even when the
16422
- * label wraps. */
16423
- .db-checkbox .db-checkbox-indicator {
16424
- display: grid;
16425
- place-items: center;
16426
- flex-shrink: 0;
16427
- /* Decorative only — the overlaid input owns hit-testing. Without this, the
16428
- * indicator (and its inherited checkmark/dash children) intercept
16429
- * `getByRole('checkbox').click()`; clicks on any edge sliver the input does
16430
- * not cover fall through to the label, which toggles via label/input
16431
- * association. */
16432
- pointer-events: none;
16433
- inline-size: var(--db-checkbox-size);
16434
- block-size: var(--db-checkbox-size);
16435
- margin-top: calc((var(--db-line-height-base, 20px) - var(--db-checkbox-size)) / 2);
16436
- border: var(--db-checkbox-border-width) solid var(--db-checkbox-border);
16437
- border-radius: var(--db-checkbox-border-radius);
16438
- background-color: var(--db-checkbox-surface);
16439
- box-sizing: border-box;
16440
- transition:
16441
- background-color var(--db-checkbox-transition-duration) ease,
16442
- border-color var(--db-checkbox-transition-duration) ease,
16443
- box-shadow var(--db-checkbox-transition-duration) ease;
16482
+ .db-btn.db-btn-small:where(:not(.db-btn-icon-only, .db-btn-force-icon, .db-btn-link)) {
16483
+ padding: 0 var(--db-btn-padding-horizontal-small) !important;
16444
16484
  }
16445
-
16446
- .db-checkbox .db-checkbox-indicator > .db-checkbox-checkmark {
16447
- grid-area: 1 / 1;
16448
- inline-size: 100%;
16449
- block-size: 100%;
16450
- color: var(--db-checkbox-mark);
16451
- opacity: 0;
16452
- transition: opacity var(--db-checkbox-transition-duration) ease;
16485
+ .db-btn.db-btn-link {
16486
+ padding: 0 !important;
16453
16487
  }
16454
-
16455
- .db-checkbox .db-checkbox-indicator > .db-checkbox-checkmark path {
16456
- fill: none;
16457
- stroke: currentColor;
16458
- stroke-width: 2;
16459
- stroke-linecap: round;
16460
- stroke-linejoin: round;
16488
+ .db-btn.db-btn-force-icon {
16489
+ padding: 0 var(--db-btn-padding-horizontal-icon) !important;
16461
16490
  }
16462
16491
 
16463
- .db-checkbox .db-checkbox-indicator > .db-checkbox-dash {
16464
- grid-area: 1 / 1;
16465
- inline-size: var(--db-checkbox-indeterminate-mark-width);
16466
- block-size: var(--db-checkbox-indeterminate-mark-height);
16467
- /* CSS clamps this to half the 3px height, giving fully rounded end caps so the
16468
- * dash reads as a pill rather than a flat bar. */
16469
- border-radius: 4px;
16470
- background-color: var(--db-checkbox-mark);
16471
- opacity: 0;
16472
- transition: opacity var(--db-checkbox-transition-duration) ease;
16492
+ /* ── 3. Interactive states ───────────────────────────────────────────────── */
16493
+ .db-btn:hover:not(:disabled, .db-btn-loading) {
16494
+ background-color: var(--db-btn-surface-hover) !important;
16495
+ border-color: var(--db-btn-border-hover) !important;
16496
+ color: var(--db-btn-text-hover) !important;
16473
16497
  }
16474
-
16475
- /* Padding-inline replicates AntD's `.{prefix}-checkbox + span { padding: 0 8px }`.
16476
- * `padding-left` provides the visible gap between indicator and label text.
16477
- * `padding-right` extends the checkbox's bounding box so adjacent checkboxes in a
16478
- * flex row sit ~16px apart (8px trailing + 8px group `gap`) — same visual rhythm
16479
- * as the AntD variant. */
16480
- .db-checkbox .db-checkbox-label {
16481
- color: var(--db-checkbox-text);
16482
- padding-inline: var(--db-checkbox-label-spacing);
16483
- word-break: break-word;
16498
+ .db-btn:active:not(:disabled, .db-btn-loading) {
16499
+ background-color: var(--db-btn-surface-active) !important;
16500
+ border-color: var(--db-btn-border-active) !important;
16501
+ color: var(--db-btn-text-active) !important;
16484
16502
  }
16485
16503
 
16486
- /* Form-flow integration: when a checkbox or group sits next to a `<Hint>` or
16487
- * `<FormMessage>` in a form layout, collapse the inherited sibling margin and
16488
- * align the helper text under the checkbox label. Mirrors the AntD-era rules
16489
- * carried in `getWrapperStyle` and `getNativeCheckboxWrapperStyles`.
16490
- *
16491
- * Each helper-text class is listed once per theme (`du-bois-light-*` /
16492
- * `du-bois-dark-*`). The selectors deliberately repeat the root/group class
16493
- * (`.db-checkbox.db-checkbox`) to push specificity to (0,3,0). That
16494
- * overrides `FormMessage`'s own `&&`-doubled `margin-top: 8px` declared with
16495
- * Emotion at runtime — without doubling we'd lose on the specificity tie and
16496
- * the validation message would keep an 8px gap below the checkbox. */
16497
-
16498
- /* Single checkbox followed by Hint and/or FormMessage. Indents the helper text
16499
- * by 24px so it aligns with the start of the label, and removes the default
16500
- * vertical margin Hint/FormMessage carry. */
16501
- .db-checkbox.db-checkbox + .du-bois-light-hint,
16502
- .db-checkbox.db-checkbox + .du-bois-dark-hint,
16503
- .db-checkbox.db-checkbox + .du-bois-light-form-message,
16504
- .db-checkbox.db-checkbox + .du-bois-dark-form-message,
16505
- .db-checkbox.db-checkbox + .du-bois-light-hint + .du-bois-light-form-message,
16506
- .db-checkbox.db-checkbox + .du-bois-dark-hint + .du-bois-dark-form-message {
16507
- padding-left: var(--db-space-6);
16508
- margin-top: 0;
16504
+ /* ── 4. Focus ────────────────────────────────────────────────────────────── */
16505
+ .db-btn:focus-visible {
16506
+ outline: 2px solid var(--db-btn-focus-ring);
16507
+ outline-offset: -2px;
16509
16508
  }
16510
-
16511
- /* Group spacing against sibling form text — 8px gap above the group when it
16512
- * follows a Hint, 8px gap above a FormMessage when it follows the group.
16513
- * Doubled class on the group side beats FormMessage's `&&` for the same reason. */
16514
- .du-bois-light-hint + .db-checkbox-group.db-checkbox-group,
16515
- .du-bois-dark-hint + .db-checkbox-group.db-checkbox-group,
16516
- .db-checkbox-group.db-checkbox-group + .du-bois-light-form-message,
16517
- .db-checkbox-group.db-checkbox-group + .du-bois-dark-form-message {
16518
- margin-top: var(--db-space-2);
16509
+ .db-btn-primary:focus-visible {
16510
+ outline-offset: 1px;
16519
16511
  }
16520
-
16521
- /* CheckboxGroup layout. Vertical delegates to the `.db-stack` primitive; the
16522
- * horizontal default stays bespoke because it must NOT wrap (a long row of
16523
- * checkboxes overflows horizontally rather than reflowing onto multiple lines),
16524
- * which is the one thing `.db-cluster` won't do. Both wire their gap to the
16525
- * checkbox spacing token so a retheme moves with the rest of the component. */
16526
- .db-checkbox-group {
16527
- display: flex;
16512
+ .db-btn-use-focus-pseudo-class:focus {
16513
+ outline: 2px solid var(--db-btn-focus-ring);
16514
+ outline-offset: 1px;
16528
16515
  }
16529
16516
 
16530
- /* Horizontal (default): single non-wrapping row. `:not(.db-stack)` keeps this
16531
- * off the vertical element so it never fights the stack primitive's gap. */
16532
- .db-checkbox-group:not(.db-stack) {
16533
- flex-wrap: nowrap;
16534
- column-gap: var(--db-checkbox-label-spacing);
16535
- row-gap: 0;
16517
+ /* ── 5. Small / block (full-width) / empty ────────────────────────────────── */
16518
+ .db-btn-small {
16519
+ height: var(--db-btn-height-small) !important;
16536
16520
  }
16537
-
16538
- /* Vertical: .db-stack supplies flex-direction + gap; we only retarget the knob. */
16539
- .db-checkbox-group.db-stack {
16540
- --db-stack-gap: var(--db-checkbox-label-spacing);
16521
+ .db-btn-block {
16522
+ width: 100%;
16523
+ }
16524
+ .db-btn-empty {
16525
+ background-color: transparent !important;
16526
+ border-color: transparent !important;
16527
+ }
16528
+ .db-btn-default.db-btn-empty {
16529
+ color: var(--db-btn-icon-glyph) !important;
16530
+ }
16531
+ .db-btn-empty.db-btn-loading {
16532
+ width: var(--db-btn-loading-empty-width);
16541
16533
  }
16542
16534
 
16543
- :where(:root), :where(.du-bois-light) {
16544
- /* Shape */
16545
- --db-radio-border-radius: var(--db-radius-full);
16546
- --db-radio-border-width: 1px;
16547
-
16548
- /* Color */
16549
- --db-radio-surface: var(--db-selection-toggle-surface);
16550
- --db-radio-surface-hover: var(--db-color-action-default-background-hover);
16551
- --db-radio-surface-press: var(--db-color-action-default-background-press);
16552
- --db-radio-surface-checked: var(--db-selection-toggle-surface-checked);
16553
- --db-radio-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16554
- --db-radio-surface-checked-press: var(--db-color-action-default-background-press);
16555
- --db-radio-surface-disabled: var(--db-selection-toggle-surface-disabled);
16556
-
16557
- --db-radio-border: var(--db-color-action-default-border-default);
16558
- --db-radio-border-hover: var(--db-selection-toggle-border-hover);
16559
- --db-radio-border-press: var(--db-selection-toggle-border-press);
16560
- --db-radio-border-focus: var(--db-focus-ring-color);
16561
- --db-radio-border-disabled: var(--db-color-action-disabled-border);
16562
- --db-radio-border-checked-hover: var(--db-selection-toggle-border-press);
16563
- --db-radio-border-checked-press: var(--db-color-action-default-border-press);
16564
- --db-radio-border-unchecked-focus: var(--db-color-action-primary-background-default);
16565
- --db-radio-outline-checked-focus: var(--db-color-action-primary-background-default);
16566
-
16567
- --db-radio-mark: #ffffff;
16568
- --db-radio-mark-disabled: var(--db-selection-toggle-mark-disabled);
16569
- --db-radio-text: var(--db-selection-toggle-text);
16570
- --db-radio-text-disabled: var(--db-selection-toggle-text-disabled);
16571
-
16572
- /* Density */
16573
- --db-radio-size: var(--db-selection-toggle-size);
16574
- --db-radio-label-spacing: var(--db-selection-toggle-label-spacing);
16575
- --db-radio-mark-size: var(--db-size-1);
16535
+ /* ── 6. Tertiary & link ──────────────────────────────────────────────────── */
16536
+ .db-btn-link {
16537
+ height: auto !important;
16538
+ border: none !important;
16539
+ }
16540
+ /* Re-declare the text color for a non-icon-only tertiary/link at two-class weight so a plain
16541
+ * call-site `color` override loses; an icon-only glyph stays at base weight so its override wins. */
16542
+ .db-btn-tertiary:not(.db-btn-icon-only),
16543
+ .db-btn-link:not(.db-btn-icon-only) {
16544
+ color: var(--db-btn-text) !important;
16545
+ }
16546
+ /* Keep the resting border at two-class weight (parity with the shielded border-color). */
16547
+ .db-btn.db-btn-tertiary,
16548
+ .db-btn.db-btn-link {
16549
+ border-color: var(--db-btn-border) !important;
16550
+ }
16576
16551
 
16577
- /* Motion */
16578
- --db-radio-transition-duration: var(--db-selection-toggle-transition-duration);
16552
+ /* ── 7. Danger ───────────────────────────────────────────────────────────── */
16553
+ .db-btn-danger:focus-visible {
16554
+ outline-color: var(--db-btn-danger-focus-ring);
16579
16555
  }
16580
16556
 
16581
- /* Native-input <Radio> styles. Classic CSS only no native nesting (the
16582
- * `&`-style nesting feature ships in Chrome/Edge 120, Safari 17.2, Firefox 117
16583
- * which leaves a non-trivial slice of our production traffic uncovered today;
16584
- * see PR #1954620 thread). `:has()` lookups are still used to read state from
16585
- * the native <input>; that feature has the support coverage we need.
16586
- *
16587
- * Structure (assumed by the React component):
16588
- *
16589
- * <label class="db-radio">
16590
- * <input type="radio" />
16591
- * <span class="db-radio-indicator"> ← circle border + fill react to state
16592
- * <span class="db-radio-dot" /> ← visible when :checked
16593
- * </span>
16594
- * <span class="db-radio-label">{children}</span>
16595
- * </label>
16596
- *
16597
- * The root only ever has one <input> descendant, so a bare `input` selector is
16598
- * safe and clearer than a class. Mirrors NativeCheckbox.css; the differences are
16599
- * the circular indicator (border-radius: full) and the filled dot instead of a
16600
- * checkmark/dash, and there is no indeterminate state.
16557
+ /* ── 8. Icon-only ────────────────────────────────────────────────────────── */
16558
+ .db-btn-icon-only {
16559
+ padding: 0;
16560
+ border: none !important;
16561
+ background-color: var(--db-btn-surface) !important;
16562
+ font-size: var(--db-btn-icon-only-font-size);
16563
+ }
16564
+ /* `:where()` keeps this at (0,1,0) so a call-site `css` width override wins, as it does for `height`. */
16565
+ .db-btn-icon-only:where(:not(.db-btn-small)) {
16566
+ width: var(--db-btn-height) !important;
16567
+ }
16568
+ .db-btn-icon-only:not(.db-btn-link, .db-btn-tertiary, [href], :hover, :active, :disabled) {
16569
+ color: var(--db-btn-icon-glyph) !important;
16570
+ }
16571
+ /* `:where()` on the exclusion holds this at (0,2,0): enough to tint an un-overridden icon-only link
16572
+ * over the base color, while still letting a consumer's own icon-color override win on source order. */
16573
+ .db-btn-icon-only[href]:where(:not(.db-btn-tertiary, .db-btn-link, .db-btn-danger, :disabled)) {
16574
+ color: var(--db-btn-text) !important;
16575
+ }
16576
+ .db-btn-icon-only:where(.db-btn-small) {
16577
+ width: var(--db-btn-height-small);
16578
+ }
16579
+ .db-btn-icon-only.db-btn-small {
16580
+ line-height: var(--db-btn-line-height-small) !important;
16581
+ }
16582
+ .db-btn-icon-only:hover:not(:disabled, .db-btn-loading) {
16583
+ background-color: var(--db-btn-surface-hover) !important;
16584
+ color: var(--db-btn-text-hover) !important;
16585
+ }
16586
+ .db-btn-icon-only:active:not(:disabled, .db-btn-loading) {
16587
+ background-color: var(--db-btn-surface-active) !important;
16588
+ color: var(--db-btn-text-active) !important;
16589
+ }
16590
+ .db-btn-icon-only:enabled:not(.db-btn-primary, .db-btn-tertiary, .db-btn-link, .db-btn-danger, :hover, :active)
16591
+ > .anticon {
16592
+ color: var(--db-btn-icon-glyph);
16593
+ }
16594
+ .db-btn-icon-only.db-btn-danger:not(:hover, :active) > .anticon {
16595
+ color: var(--db-btn-text);
16596
+ }
16597
+ .db-btn-icon-only.db-btn-danger:hover:not(:disabled, .db-btn-loading) {
16598
+ background-color: var(--db-btn-surface-hover) !important;
16599
+ color: var(--db-btn-text-hover) !important;
16600
+ }
16601
+ .db-btn-icon-only.db-btn-danger:active:not(:disabled, .db-btn-loading) {
16602
+ background-color: var(--db-btn-surface-active) !important;
16603
+ color: var(--db-btn-text-active) !important;
16604
+ }
16605
+
16606
+ /* ── 9. Disabled — base rules paint the per-variant `*-disabled` tokens (see tokens.css) ──────── */
16607
+ .db-btn:disabled {
16608
+ cursor: not-allowed;
16609
+ background-color: var(--db-btn-surface-disabled) !important;
16610
+ color: var(--db-btn-text-disabled) !important;
16611
+ }
16612
+ /* Empty disabled buttons keep a transparent border (set elsewhere), so exclude them.
16613
+ * `:where()` on the exclusion holds this at (0,2,0): enough to beat the base border, while still
16614
+ * letting a consumer's own `[disabled]` border override win on source order. */
16615
+ .db-btn:disabled:where(:not(.db-btn-empty)) {
16616
+ border-color: var(--db-btn-border-disabled) !important;
16617
+ }
16618
+ .db-btn-icon-only:disabled {
16619
+ border: none;
16620
+ }
16621
+ .db-btn-icon-only:disabled:hover {
16622
+ background-color: transparent !important;
16623
+ }
16624
+ .db-btn:disabled > .anticon {
16625
+ color: inherit !important;
16626
+ }
16627
+
16628
+ /* ── 10. Icon & content layout ───────────────────────────────────────────── */
16629
+ /* The leading icon renders unwrapped as the button's direct `.anticon` child (see NativeButton.tsx),
16630
+ * so `> .anticon` targets only a real icon glyph — a non-glyph `icon` (e.g. a Spinner) keeps its own
16631
+ * sizing — and out-specifies AntD's runtime-injected global `.anticon` rule. */
16632
+ .db-btn > .anticon,
16633
+ .db-btn-end-icon,
16634
+ .db-btn-end-icon > .anticon {
16635
+ display: inline-flex;
16636
+ align-items: center;
16637
+ font-size: var(--db-btn-icon-size) !important;
16638
+ line-height: 0;
16639
+ }
16640
+ .db-btn-small > .anticon .anticon {
16641
+ line-height: 0;
16642
+ }
16643
+ .db-btn > .db-btn-content > .anticon {
16644
+ display: inline-flex;
16645
+ align-items: center;
16646
+ line-height: 0;
16647
+ }
16648
+ .db-btn-content {
16649
+ display: inline-flex;
16650
+ align-items: center;
16651
+ }
16652
+ .db-btn > .anticon + .db-btn-content {
16653
+ margin-inline-start: var(--db-btn-gap) !important;
16654
+ }
16655
+ .db-btn-end-icon {
16656
+ margin-inline-start: var(--db-btn-end-icon-gap) !important;
16657
+ }
16658
+ .db-btn-small .db-btn-end-icon {
16659
+ margin-inline-start: var(--db-btn-end-icon-gap-small) !important;
16660
+ }
16661
+ .db-btn-default:not(.db-btn-danger, [href], :hover, :active, :disabled) > .anticon {
16662
+ color: var(--db-btn-icon-glyph);
16663
+ }
16664
+ .db-btn-default[href]:not(.db-btn-danger, :hover, :active, :disabled) .db-btn-end-icon {
16665
+ color: var(--db-btn-icon-glyph);
16666
+ }
16667
+ .db-btn-default:not([href], .db-btn-danger, :hover, :active, :disabled)
16668
+ > .db-btn-content
16669
+ > .db-btn-end-icon
16670
+ > .anticon {
16671
+ color: var(--db-btn-icon-glyph);
16672
+ }
16673
+
16674
+ /* ── 11. Loading ─────────────────────────────────────────────────────────── */
16675
+ .db-btn-loading {
16676
+ cursor: default;
16677
+ }
16678
+ .db-btn-force-icon.db-btn-loading:disabled {
16679
+ border-color: transparent !important;
16680
+ }
16681
+ .db-btn-spinner {
16682
+ color: inherit !important;
16683
+ pointer-events: none;
16684
+ }
16685
+ .db-btn-loading-with-icon .db-btn-content {
16686
+ padding-inline-start: calc(var(--db-btn-icon-size) + var(--db-btn-gap));
16687
+ }
16688
+ .db-btn-loading .db-btn-spinner {
16689
+ position: absolute;
16690
+ left: 50%;
16691
+ top: 50%;
16692
+ transform: translate(-50%, -50%);
16693
+ }
16694
+ .db-btn-loading.db-btn-icon-only .db-btn-spinner {
16695
+ position: static;
16696
+ transform: none;
16697
+ }
16698
+
16699
+ /* ── 12. dangerouslySetForceIconStyles ───────────────────────────────────── */
16700
+ .db-btn-force-icon {
16701
+ width: var(--db-btn-height) !important;
16702
+ color: var(--db-btn-icon-glyph) !important;
16703
+ }
16704
+ .db-btn-force-icon.db-btn-small {
16705
+ width: var(--db-btn-height-small) !important;
16706
+ }
16707
+
16708
+
16709
+ :where(:root), :where(.du-bois-light) {
16710
+ /* Shape */
16711
+ --db-checkbox-border-radius: var(--db-selection-toggle-border-radius);
16712
+ --db-checkbox-border-width: 1px;
16713
+
16714
+ /* Color */
16715
+ --db-checkbox-surface: var(--db-selection-toggle-surface);
16716
+ --db-checkbox-surface-hover: var(--db-color-action-default-background-hover);
16717
+ --db-checkbox-surface-active: var(--db-color-action-default-background-press);
16718
+ --db-checkbox-surface-checked: var(--db-selection-toggle-surface-checked);
16719
+ --db-checkbox-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16720
+ --db-checkbox-surface-checked-active: var(--db-selection-toggle-surface-checked-active);
16721
+ --db-checkbox-surface-disabled: var(--db-selection-toggle-surface-disabled);
16722
+
16723
+ --db-checkbox-border: var(--db-color-action-default-border-default);
16724
+ --db-checkbox-border-hover: var(--db-selection-toggle-border-hover);
16725
+ --db-checkbox-border-active: var(--db-selection-toggle-border-active);
16726
+ --db-checkbox-focus-ring: var(--db-focus-ring-color);
16727
+ --db-checkbox-border-disabled: var(--db-color-action-disabled-border);
16728
+
16729
+ --db-checkbox-mark: #ffffff;
16730
+ --db-checkbox-mark-disabled: var(--db-selection-toggle-mark-disabled);
16731
+ --db-checkbox-text: var(--db-selection-toggle-text);
16732
+ --db-checkbox-text-disabled: var(--db-selection-toggle-text-disabled);
16733
+
16734
+ --db-checkbox-shadow-checked: var(--db-selection-toggle-shadow-checked);
16735
+
16736
+ /* Density */
16737
+ --db-checkbox-size: var(--db-selection-toggle-size);
16738
+ --db-checkbox-label-spacing: var(--db-selection-toggle-label-spacing);
16739
+ --db-checkbox-focus-ring-width: var(--db-focus-ring-width);
16740
+ --db-checkbox-indeterminate-mark-width: var(--db-size-2); /* 8px */
16741
+ --db-checkbox-indeterminate-mark-height: calc(var(--db-size-1) * 0.75); /* 3px */
16742
+
16743
+ /* Motion */
16744
+ --db-checkbox-transition-duration: var(--db-selection-toggle-transition-duration);
16745
+ }
16746
+
16747
+ /* Native-input <Checkbox> styles. Classic CSS only — no native nesting (the
16748
+ * `&`-style nesting feature ships in Chrome/Edge 120, Safari 17.2, Firefox 117
16749
+ * which leaves a non-trivial slice of our production traffic uncovered today;
16750
+ * see PR #1954620 thread). `:has()` lookups are still used to read state from
16751
+ * the native <input>; that feature has the support coverage we need.
16601
16752
  *
16602
- * TODO(DUBOIS-452): the indicator/dot state styles below are identical to
16603
- * NativeCheckbox.css — consolidate them into a shared `.db-selection-toggle` class. */
16753
+ * Structure (assumed by the React component):
16754
+ *
16755
+ * <label class="db-checkbox">
16756
+ * <input type="checkbox" class="db-checkbox-input" />
16757
+ * <span class="db-checkbox-indicator"> ← border + fill react to state
16758
+ * <svg class="db-checkbox-checkmark" .../> ← visible when :checked
16759
+ * <span class="db-checkbox-dash" /> ← visible when :indeterminate
16760
+ * </span>
16761
+ * <span class="db-checkbox-label">{children}</span>
16762
+ * </label>
16763
+ *
16764
+ * State and hiding rules target `.db-checkbox-input` — the checkbox's OWN input —
16765
+ * never a bare `input`: consumers may render an inline <input> (e.g. a number
16766
+ * field) inside the label children, and a descendant `input` selector would hide
16767
+ * / absolutely-position that field and mis-drive the `:has(…)` state rules. */
16604
16768
 
16605
- .db-radio {
16606
- /* `display: flex` (block-level), NOT `inline-flex`: see NativeCheckbox.css for
16607
- * the line-box / sibling-spacing rationale. */
16769
+ .db-checkbox {
16770
+ /* `display: flex` (block-level), NOT `inline-flex`: an inline-flex root puts
16771
+ * the label inside an inline-line-box whose leading pushes block-level siblings
16772
+ * (e.g. `<FormMessage>`) down by a few pixels — making validation messages sit
16773
+ * lower under the native checkbox than under the AntD checkbox (which itself is
16774
+ * wrapped in a block `<div>`, so its label's inline-line-box is consumed inside
16775
+ * that div and never leaks into sibling spacing). `width: fit-content` keeps the
16776
+ * label behaving like an inline-flex item inside a flex-row parent (the group,
16777
+ * or any consumer's flex container). */
16608
16778
  display: flex;
16609
16779
  align-items: flex-start;
16610
16780
  position: relative;
16611
16781
  cursor: pointer;
16612
16782
  user-select: none;
16783
+ /* No `gap` here — spacing between indicator and label, plus the trailing
16784
+ * whitespace that gives sibling checkboxes their visual separation, lives
16785
+ * on `.db-checkbox-label` as padding-inline. Matches AntD's `.{prefix}-checkbox + span`
16786
+ * rule and the prior Emotion-based NativeCheckbox implementation. */
16613
16787
  min-height: var(--db-line-height-base, 20px);
16614
16788
  width: fit-content;
16615
16789
  }
16616
16790
 
16617
16791
  /* Hover/press live on the root (label), not on the input: the input is
16618
16792
  * visually hidden (opacity:0), so an `input:hover` rule wouldn't be visible
16619
- * anyway, and the whole label area should drive the feedback. */
16620
- .db-radio:hover:has(input:not(:disabled, :checked)) .db-radio-indicator {
16621
- background-color: var(--db-radio-surface-hover);
16622
- border-color: var(--db-radio-border-hover);
16793
+ * anyway. Putting `:hover` on the root also makes the entire label area
16794
+ * — not just the indicator square — drive the visual feedback. */
16795
+ .db-checkbox:hover:has(.db-checkbox-input:not(:disabled, :checked, :indeterminate)) .db-checkbox-indicator {
16796
+ background-color: var(--db-checkbox-surface-hover);
16797
+ border-color: var(--db-checkbox-border-hover);
16623
16798
  }
16624
16799
 
16625
- .db-radio:active:has(input:not(:disabled, :checked)) .db-radio-indicator {
16626
- background-color: var(--db-radio-surface-press);
16627
- border-color: var(--db-radio-border-press);
16800
+ .db-checkbox:active:has(.db-checkbox-input:not(:disabled, :checked, :indeterminate)) .db-checkbox-indicator {
16801
+ background-color: var(--db-checkbox-surface-active);
16802
+ border-color: var(--db-checkbox-border-active);
16628
16803
  }
16629
16804
 
16630
- /* Selected fill. */
16631
- .db-radio:has(input:checked) .db-radio-indicator {
16632
- background-color: var(--db-radio-surface-checked);
16633
- border-color: var(--db-radio-surface-checked);
16805
+ /* Selected fill — checked and indeterminate share the same surface. */
16806
+ .db-checkbox:has(.db-checkbox-input:is(:checked, :indeterminate)) .db-checkbox-indicator {
16807
+ background-color: var(--db-checkbox-surface-checked);
16808
+ border-color: var(--db-checkbox-surface-checked);
16634
16809
  }
16635
16810
 
16636
- /* AntD getRadioInputStyles checked hover: background = actionPrimaryBackgroundHover,
16637
- * borderColor = actionPrimaryBackgroundPress (a darker ring than the fill). */
16638
- .db-radio:hover:has(input:checked:not(:disabled)) .db-radio-indicator {
16639
- background-color: var(--db-radio-surface-checked-hover);
16640
- border-color: var(--db-radio-border-checked-hover);
16811
+ .db-checkbox:hover:has(.db-checkbox-input:is(:checked, :indeterminate):not(:disabled)) .db-checkbox-indicator {
16812
+ background-color: var(--db-checkbox-surface-checked-hover);
16813
+ border-color: var(--db-checkbox-surface-checked-hover);
16641
16814
  }
16642
16815
 
16643
- /* AntD getRadioInputStyles checked active: background = actionDefaultBackgroundPress,
16644
- * borderColor = actionDefaultBorderPress (two-tone, not flat). */
16645
- .db-radio:active:has(input:checked:not(:disabled)) .db-radio-indicator {
16646
- background-color: var(--db-radio-surface-checked-press);
16647
- border-color: var(--db-radio-border-checked-press);
16816
+ .db-checkbox:active:has(.db-checkbox-input:is(:checked, :indeterminate):not(:disabled)) .db-checkbox-indicator {
16817
+ background-color: var(--db-checkbox-surface-checked-active);
16818
+ border-color: var(--db-checkbox-surface-checked-active);
16648
16819
  }
16649
16820
 
16650
- /* Dot visibility. */
16651
- .db-radio:has(input:checked) .db-radio-dot {
16821
+ /* Mark visibility. */
16822
+ .db-checkbox:has(.db-checkbox-input:checked) .db-checkbox-checkmark {
16652
16823
  opacity: 1;
16653
16824
  }
16654
16825
 
16655
- /* Focus-visible (keyboard only). AntD getRadioInputStyles splits focus by checked
16656
- * state, so we do too.
16657
- *
16658
- * UNCHECKED focus (AntD `&:not(disabled) > input:focus + inner`): only the border
16659
- * color changes to actionPrimaryBackgroundDefault — NO outline ring. */
16660
- .db-radio:has(input:focus-visible:not(:checked)) .db-radio-indicator {
16661
- border-color: var(--db-radio-border-unchecked-focus);
16826
+ .db-checkbox:has(.db-checkbox-input:indeterminate) .db-checkbox-dash {
16827
+ opacity: 1;
16662
16828
  }
16663
16829
 
16664
- /* CHECKED focus-visible (AntD `&.checked &:not(disabled) > input:focus-visible + inner`):
16665
- * background = actionPrimaryBackgroundDefault, borderColor = actionDefaultBorderFocus,
16666
- * outline = 1px solid actionPrimaryBackgroundDefault, outlineOffset = 1. */
16667
- .db-radio:has(input:focus-visible:checked) .db-radio-indicator {
16668
- background-color: var(--db-radio-surface-checked);
16669
- border-color: var(--db-radio-border-focus);
16670
- outline: 1px solid var(--db-radio-outline-checked-focus);
16671
- outline-offset: 1px;
16830
+ /* Focus-visible (keyboard only). */
16831
+ .db-checkbox:has(.db-checkbox-input:focus-visible) .db-checkbox-indicator {
16832
+ outline: var(--db-checkbox-focus-ring-width) solid var(--db-checkbox-focus-ring);
16833
+ outline-offset: 4px;
16672
16834
  }
16673
16835
 
16674
- /* Disabled (covers checked and unchecked). Sourced from `input:disabled` so the
16675
- * standalone-disabled and group-disabled paths both land here. */
16676
- .db-radio:has(input:disabled) {
16836
+ /* Disabled (covers unchecked, checked, indeterminate). Sourced from
16837
+ * `input:disabled` so the standalone-disabled and group-disabled paths both
16838
+ * land here without needing a separate root modifier class. */
16839
+ .db-checkbox:has(.db-checkbox-input:disabled) {
16677
16840
  cursor: not-allowed;
16678
16841
  }
16679
16842
 
16680
- .db-radio:has(input:disabled) .db-radio-label {
16681
- color: var(--db-radio-text-disabled);
16843
+ .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-label {
16844
+ color: var(--db-checkbox-text-disabled);
16682
16845
  }
16683
16846
 
16684
- .db-radio:has(input:disabled) .db-radio-indicator {
16685
- background-color: var(--db-radio-surface-disabled);
16686
- border-color: var(--db-radio-border-disabled);
16847
+ .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-indicator {
16848
+ background-color: var(--db-checkbox-surface-disabled);
16849
+ border-color: var(--db-checkbox-border-disabled);
16687
16850
  box-shadow: none;
16688
16851
  }
16689
16852
 
16690
- .db-radio:has(input:disabled) .db-radio-dot {
16691
- background-color: var(--db-radio-mark-disabled);
16853
+ .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-checkmark {
16854
+ color: var(--db-checkbox-mark-disabled);
16692
16855
  }
16693
16856
 
16694
- /* Visually-hidden native input (opacity:0) overlaying the indicator circle,
16695
- * kept interactive and in the tab order so the `:has()` lookups above can read
16696
- * its state. It stays pointer-clickable — matching AntD's `.ant-radio-input` —
16697
- * so it is the top-most hit-test target over the circle: `getByRole('radio').click()`
16698
- * (Playwright, and any coordinate click that lands on the circle) resolves to the
16857
+ .db-checkbox:has(.db-checkbox-input:disabled) .db-checkbox-dash {
16858
+ background-color: var(--db-checkbox-mark-disabled);
16859
+ }
16860
+
16861
+ /* Visually-hidden native input (opacity:0) overlaying the indicator square,
16862
+ * kept interactive and in the tab order so `:has()` lookups above can read its
16863
+ * state. It stays pointer-clickable — matching AntD's `.ant-checkbox-input` —
16864
+ * so it is the top-most hit-test target over the box: `getByRole('checkbox').click()`
16865
+ * (Playwright, and any coordinate click that lands on the box) resolves to the
16699
16866
  * real control instead of the decorative indicator overlay. Making the input
16700
16867
  * `pointer-events: none` (the prior native-only behavior) is what regressed
16701
16868
  * interaction tests vs. the AntD variant. `cursor: inherit` tracks the label's
16702
- * pointer / disabled `not-allowed` cursor so the circle keeps AntD's cursor too. */
16703
- .db-radio input {
16869
+ * pointer / disabled `not-allowed` cursor so the box keeps AntD's cursor too. */
16870
+ .db-checkbox .db-checkbox-input {
16704
16871
  position: absolute;
16705
16872
  inset: 0;
16706
- inline-size: var(--db-radio-size);
16707
- block-size: var(--db-radio-size);
16873
+ inline-size: var(--db-checkbox-size);
16874
+ block-size: var(--db-checkbox-size);
16708
16875
  margin: 0;
16709
16876
  opacity: 0;
16710
16877
  cursor: inherit;
16711
16878
  }
16712
16879
 
16713
- /* Indicator — circle with the border/fill that responds to state above.
16714
- * Vertical math aligns the indicator to the cap-height of the first text line
16715
- * so radio + label sit on the same visual baseline even when the label wraps. */
16716
- .db-radio .db-radio-indicator {
16880
+ /* Indicator — square with the border/fill that responds to state above.
16881
+ * Vertical math: aligns the indicator to the cap-height of the first text
16882
+ * line so checkbox + label sit on the same visual baseline even when the
16883
+ * label wraps. */
16884
+ .db-checkbox .db-checkbox-indicator {
16717
16885
  display: grid;
16718
16886
  place-items: center;
16719
16887
  flex-shrink: 0;
16720
16888
  /* Decorative only — the overlaid input owns hit-testing. Without this, the
16721
- * indicator (and its inherited dot child) intercept `getByRole('radio').click()`;
16722
- * clicks on any edge sliver the input does not cover fall through to the label,
16723
- * which toggles via label/input association. */
16889
+ * indicator (and its inherited checkmark/dash children) intercept
16890
+ * `getByRole('checkbox').click()`; clicks on any edge sliver the input does
16891
+ * not cover fall through to the label, which toggles via label/input
16892
+ * association. */
16724
16893
  pointer-events: none;
16725
- inline-size: var(--db-radio-size);
16726
- block-size: var(--db-radio-size);
16727
- margin-top: calc((var(--db-line-height-base, 20px) - var(--db-radio-size)) / 2);
16728
- border: var(--db-radio-border-width) solid var(--db-radio-border);
16729
- border-radius: var(--db-radio-border-radius);
16730
- background-color: var(--db-radio-surface);
16894
+ inline-size: var(--db-checkbox-size);
16895
+ block-size: var(--db-checkbox-size);
16896
+ margin-top: calc((var(--db-line-height-base, 20px) - var(--db-checkbox-size)) / 2);
16897
+ border: var(--db-checkbox-border-width) solid var(--db-checkbox-border);
16898
+ border-radius: var(--db-checkbox-border-radius);
16899
+ background-color: var(--db-checkbox-surface);
16731
16900
  box-sizing: border-box;
16732
16901
  transition:
16733
- background-color var(--db-radio-transition-duration) ease,
16734
- border-color var(--db-radio-transition-duration) ease,
16735
- box-shadow var(--db-radio-transition-duration) ease;
16902
+ background-color var(--db-checkbox-transition-duration) ease,
16903
+ border-color var(--db-checkbox-transition-duration) ease,
16904
+ box-shadow var(--db-checkbox-transition-duration) ease;
16736
16905
  }
16737
16906
 
16738
- .db-radio .db-radio-indicator > .db-radio-dot {
16907
+ .db-checkbox .db-checkbox-indicator > .db-checkbox-checkmark {
16739
16908
  grid-area: 1 / 1;
16740
- inline-size: var(--db-radio-mark-size);
16741
- block-size: var(--db-radio-mark-size);
16742
- border-radius: var(--db-radius-full);
16743
- background-color: var(--db-radio-mark);
16909
+ inline-size: 100%;
16910
+ block-size: 100%;
16911
+ color: var(--db-checkbox-mark);
16744
16912
  opacity: 0;
16745
- transition: opacity var(--db-radio-transition-duration) ease;
16913
+ transition: opacity var(--db-checkbox-transition-duration) ease;
16746
16914
  }
16747
16915
 
16748
- /* Honour the user's reduced-motion preference: drop the state-change
16749
- * transitions on the indicator and dot so checking/hovering is instant. */
16750
- @media (prefers-reduced-motion: reduce) {
16751
- .db-radio .db-radio-indicator,
16752
- .db-radio .db-radio-indicator > .db-radio-dot {
16753
- transition: none;
16754
- }
16916
+ .db-checkbox .db-checkbox-indicator > .db-checkbox-checkmark path {
16917
+ fill: none;
16918
+ stroke: currentColor;
16919
+ stroke-width: 2;
16920
+ stroke-linecap: round;
16921
+ stroke-linejoin: round;
16755
16922
  }
16756
16923
 
16757
- /* Padding-inline replicates the AntD `.{prefix} + span` label gap and the
16758
- * trailing whitespace that separates adjacent radios in a row. */
16759
- .db-radio .db-radio-label {
16760
- color: var(--db-radio-text);
16761
- padding-inline: var(--db-radio-label-spacing);
16762
- word-break: break-word;
16924
+ .db-checkbox .db-checkbox-indicator > .db-checkbox-dash {
16925
+ grid-area: 1 / 1;
16926
+ inline-size: var(--db-checkbox-indeterminate-mark-width);
16927
+ block-size: var(--db-checkbox-indeterminate-mark-height);
16928
+ /* CSS clamps this to half the 3px height, giving fully rounded end caps so the
16929
+ * dash reads as a pill rather than a flat bar. */
16930
+ border-radius: 4px;
16931
+ background-color: var(--db-checkbox-mark);
16932
+ opacity: 0;
16933
+ transition: opacity var(--db-checkbox-transition-duration) ease;
16763
16934
  }
16764
16935
 
16765
- /* Form-flow integration: align helper text under the radio label. The `paddingLeft`
16766
- * indent comes from AntD's getCommonRadioGroupStyles (`& > label + hint`) and applies
16767
- * to BOTH layouts. The doubled root class pushes specificity to (0,3,0) to beat
16768
- * FormMessage's runtime `&&` margin. */
16769
- .db-radio.db-radio + .du-bois-light-hint,
16770
- .db-radio.db-radio + .du-bois-dark-hint,
16771
- .db-radio.db-radio + .du-bois-light-form-message,
16772
- .db-radio.db-radio + .du-bois-dark-form-message,
16773
- .db-radio.db-radio + .du-bois-light-hint + .du-bois-light-form-message,
16774
- .db-radio.db-radio + .du-bois-dark-hint + .du-bois-dark-form-message {
16775
- padding-inline-start: var(--db-space-6);
16776
- margin-top: 0;
16936
+ /* Padding-inline replicates AntD's `.{prefix}-checkbox + span { padding: 0 8px }`.
16937
+ * `padding-left` provides the visible gap between indicator and label text.
16938
+ * `padding-right` extends the checkbox's bounding box so adjacent checkboxes in a
16939
+ * flex row sit ~16px apart (8px trailing + 8px group `gap`) — same visual rhythm
16940
+ * as the AntD variant. */
16941
+ .db-checkbox .db-checkbox-label {
16942
+ color: var(--db-checkbox-text);
16943
+ padding-inline: var(--db-checkbox-label-spacing);
16944
+ word-break: break-word;
16777
16945
  }
16778
16946
 
16779
- /* Vertical hint rhythm (AntD getVerticalRadioGroupStyles, vertical-only): a hint
16780
- * after a radio gets `marginTop: -sm` (cancels the preceding radio's
16781
- * `paddingBottom: sm` so the hint tucks directly under its label) and
16782
- * `marginBottom: sm` (gap before the next radio). The negative top margin relies on
16783
- * the .db-stack gap being 0 (set below). Scoped to .db-stack so it never perturbs
16784
- * the horizontal grid, where hints are positioned by grid-row instead. */
16785
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-hint,
16786
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-hint,
16787
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-form-message,
16788
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-form-message,
16789
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-hint + .du-bois-light-form-message,
16790
- .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-hint + .du-bois-dark-form-message {
16791
- margin-top: calc(-1 * var(--db-space-2));
16792
- margin-bottom: var(--db-space-2);
16793
- }
16947
+ /* Form-flow integration: when a checkbox or group sits next to a `<Hint>` or
16948
+ * `<FormMessage>` in a form layout, collapse the inherited sibling margin and
16949
+ * align the helper text under the checkbox label. Mirrors the AntD-era rules
16950
+ * carried in `getWrapperStyle` and `getNativeCheckboxWrapperStyles`.
16951
+ *
16952
+ * Each helper-text class is listed once per theme (`du-bois-light-*` /
16953
+ * `du-bois-dark-*`). The selectors deliberately repeat the root/group class
16954
+ * (`.db-checkbox.db-checkbox`) to push specificity to (0,3,0). That
16955
+ * overrides `FormMessage`'s own `&&`-doubled `margin-top: 8px` declared with
16956
+ * Emotion at runtime — without doubling we'd lose on the specificity tie and
16957
+ * the validation message would keep an 8px gap below the checkbox. */
16794
16958
 
16795
- /* AntD `& > label:last-of-type + hint { marginTop: 0 }`: the last radio has no
16796
- * `paddingBottom` to cancel, so its trailing hint keeps its natural top position. */
16797
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-hint,
16798
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-hint,
16799
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-form-message,
16800
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-form-message,
16801
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-hint + .du-bois-light-form-message,
16802
- .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-hint + .du-bois-dark-form-message {
16959
+ /* Single checkbox followed by Hint and/or FormMessage. Indents the helper text
16960
+ * by 24px so it aligns with the start of the label, and removes the default
16961
+ * vertical margin Hint/FormMessage carry. */
16962
+ .db-checkbox.db-checkbox + .du-bois-light-hint,
16963
+ .db-checkbox.db-checkbox + .du-bois-dark-hint,
16964
+ .db-checkbox.db-checkbox + .du-bois-light-form-message,
16965
+ .db-checkbox.db-checkbox + .du-bois-dark-form-message,
16966
+ .db-checkbox.db-checkbox + .du-bois-light-hint + .du-bois-light-form-message,
16967
+ .db-checkbox.db-checkbox + .du-bois-dark-hint + .du-bois-dark-form-message {
16968
+ padding-left: var(--db-space-6);
16803
16969
  margin-top: 0;
16804
16970
  }
16805
16971
 
16806
- .du-bois-light-hint + .db-radio-group.db-radio-group,
16807
- .du-bois-dark-hint + .db-radio-group.db-radio-group,
16808
- .db-radio-group.db-radio-group + .du-bois-light-form-message,
16809
- .db-radio-group.db-radio-group + .du-bois-dark-form-message {
16972
+ /* Group spacing against sibling form text — 8px gap above the group when it
16973
+ * follows a Label or Hint, 8px gap above a FormMessage when it follows the group.
16974
+ * Doubled class on the group side beats FormMessage's `&&` for the same reason. */
16975
+ .du-bois-light-label + .db-checkbox-group.db-checkbox-group,
16976
+ .du-bois-dark-label + .db-checkbox-group.db-checkbox-group,
16977
+ .du-bois-light-hint + .db-checkbox-group.db-checkbox-group,
16978
+ .du-bois-dark-hint + .db-checkbox-group.db-checkbox-group,
16979
+ .db-checkbox-group.db-checkbox-group + .du-bois-light-form-message,
16980
+ .db-checkbox-group.db-checkbox-group + .du-bois-dark-form-message {
16810
16981
  margin-top: var(--db-space-2);
16811
16982
  }
16812
16983
 
16813
- /* RadioGroup layout. Vertical (the default) delegates flex-direction to the
16814
- * .db-stack primitive; horizontal is a two-row grid (mirroring AntD). */
16815
- .db-radio-group {
16984
+ /* CheckboxGroup layout. Vertical delegates to the `.db-stack` primitive; the
16985
+ * horizontal default stays bespoke because it must NOT wrap (a long row of
16986
+ * checkboxes overflows horizontally rather than reflowing onto multiple lines),
16987
+ * which is the one thing `.db-cluster` won't do. Both wire their gap to the
16988
+ * checkbox spacing token so a retheme moves with the rest of the component. */
16989
+ .db-checkbox-group {
16816
16990
  display: flex;
16817
16991
  }
16818
16992
 
16819
- /* Vertical: .db-stack supplies flex-direction: column. AntD's vertical helper
16820
- * (getVerticalRadioGroupStyles) produces its item rhythm purely from
16821
- * `& > label { paddingBottom: sm }` plus the negative-margin hint trick above — it
16822
- * uses NO flex gap. The negative `marginTop` on hints only works if the stack adds
16823
- * no gap of its own, so we zero out the gap and reproduce the rhythm via
16824
- * padding-bottom on each radio. */
16825
- .db-radio-group.db-stack {
16826
- --db-stack-gap: 0;
16993
+ /* Horizontal (default): single non-wrapping row. `:not(.db-stack)` keeps this
16994
+ * off the vertical element so it never fights the stack primitive's gap. */
16995
+ .db-checkbox-group:not(.db-stack) {
16996
+ flex-wrap: nowrap;
16997
+ column-gap: var(--db-checkbox-label-spacing);
16998
+ row-gap: 0;
16827
16999
  }
16828
17000
 
16829
- /* AntD `& > label { paddingBottom: sm }`. */
16830
- .db-radio-group.db-stack > .db-radio {
16831
- padding-bottom: var(--db-space-2);
17001
+ /* Vertical: .db-stack supplies flex-direction + gap; we only retarget the knob. */
17002
+ .db-checkbox-group.db-stack {
17003
+ --db-stack-gap: var(--db-checkbox-label-spacing);
16832
17004
  }
16833
17005
 
16834
- /* AntD `& > label:last-of-type { paddingBottom: 0 }`. */
16835
- .db-radio-group.db-stack > .db-radio:last-of-type {
16836
- padding-bottom: 0;
16837
- }
16838
17006
 
16839
- /* AntD getVerticalRadioGroupStyles `.${prefix}-radio-tile + .${prefix}-radio-tile {
16840
- * marginTop: md }`: RadioTiles are not matched by the `.db-radio` padding-bottom
16841
- * rhythm above (the tile's own class is du-bois-{light,dark}-radio-tile), so space
16842
- * consecutive tiles explicitly. The .db-stack gap is 0, so this margin is the spacing. */
16843
- .db-radio-group.db-stack > .du-bois-light-radio-tile + .du-bois-light-radio-tile,
16844
- .db-radio-group.db-stack > .du-bois-light-radio-tile + .du-bois-dark-radio-tile,
16845
- .db-radio-group.db-stack > .du-bois-dark-radio-tile + .du-bois-light-radio-tile,
16846
- .db-radio-group.db-stack > .du-bois-dark-radio-tile + .du-bois-dark-radio-tile {
16847
- margin-top: var(--db-space-4);
16848
- }
16849
17007
 
16850
- /* Horizontal: AntD `getHorizontalRadioGroupStyles` is a TWO-ROW grid (NOT a flex
16851
- * row) so each radio sits in the [label] row and its following hint drops into the
16852
- * [hint] row of the same auto-column. `&&` in AntD doubles specificity; the doubled
16853
- * group class here matches that.
16854
- * AntD: `display: grid; gridTemplateRows: [label] auto [hint] auto;
16855
- * gridAutoColumns: max-content; gridColumnGap: md`. */
16856
- .db-radio-group--horizontal.db-radio-group--horizontal {
16857
- display: grid;
16858
- grid-template-rows: [label] auto [hint] auto;
16859
- grid-auto-columns: max-content;
16860
- column-gap: var(--db-space-4);
16861
- }
17008
+ :where(:root), :where(.du-bois-light) {
17009
+ /* Density */
17010
+ --db-drawer-padding-horizontal: var(--db-space-6);
17011
+ --db-drawer-padding-horizontal-small: var(--db-space-4);
17012
+ --db-drawer-content-padding-top: var(--db-space-4);
17013
+ --db-drawer-content-padding-top-small: var(--db-space-2);
17014
+ --db-drawer-footer-padding-top: var(--db-space-4);
17015
+ --db-drawer-footer-padding-bottom: var(--db-space-6);
17016
+ --db-drawer-footer-padding-bottom-small: var(--db-space-4);
17017
+ --db-drawer-title-margin-bottom: var(--db-space-2);
17018
+ --db-drawer-close-button-margin: var(--db-space-1);
16862
17019
 
16863
- /* Equal column widths: AntD `gridAutoColumns: minmax(0, 1fr)`. */
16864
- .db-radio-group--horizontal.db-radio-group--equal-width {
16865
- grid-auto-columns: minmax(0, 1fr);
17020
+ /* Color */
17021
+ --db-drawer-surface: var(--db-surface-base);
17022
+ --db-drawer-text: var(--db-text-base);
17023
+ --db-drawer-border: var(--db-border-base);
17024
+ /* No v2 token holds the drawer scrim yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17025
+ --db-drawer-overlay: var(--db-color-overlay-overlay);
16866
17026
  }
16867
17027
 
16868
- /* AntD `& > label, & > .${prefix}-radio-tile { gridRow: label / label; marginRight: 0 }`:
16869
- * every radio (and every RadioTile button, whose own class is
16870
- * du-bois-{light,dark}-radio-tile and is NOT matched by `> .db-radio`) lives in the
16871
- * label row. grid-auto-flow defaults to row, so successive items advance the implicit
16872
- * column. */
16873
- .db-radio-group--horizontal > .db-radio,
16874
- .db-radio-group--horizontal > .du-bois-light-radio-tile,
16875
- .db-radio-group--horizontal > .du-bois-dark-radio-tile {
16876
- grid-row: label / label;
16877
- margin-inline-end: 0;
17028
+ :where(:root), :where(.du-bois-light) {
17029
+ /* Elevation */
17030
+ --db-drawer-shadow: var(--db-elevation-high);
16878
17031
  }
16879
17032
 
16880
- /* AntD `& > label + hint { display: inline-block; gridRow: hint / hint }`: a hint
16881
- * following a radio drops into the hint row of the column its radio occupies. The
16882
- * vertical negative-margin rules are scoped to .db-stack, so nothing to reset here. */
16883
- .db-radio-group--horizontal > .du-bois-light-hint,
16884
- .db-radio-group--horizontal > .du-bois-dark-hint {
16885
- display: inline-block;
16886
- grid-row: hint / hint;
16887
- }
16888
17033
 
16889
- /* AntD `:has(> hint) { marginTop: sm }`: when the horizontal group contains a hint,
16890
- * the group itself gets a top margin (room above the two-row grid). */
16891
- .db-radio-group--horizontal:has(> .du-bois-light-hint),
16892
- .db-radio-group--horizontal:has(> .du-bois-dark-hint) {
16893
- margin-top: var(--db-space-2);
16894
- }
16895
17034
 
16896
- /* AntD `&& + form-message { marginTop: sm }` (horizontal): a form-message following
16897
- * the whole group is separated by sm. */
16898
- .db-radio-group--horizontal + .du-bois-light-form-message,
16899
- .db-radio-group--horizontal + .du-bois-dark-form-message {
16900
- margin-top: var(--db-space-2);
16901
- }
16902
17035
 
16903
- :where(:root), :where(.du-bois-light) {
16904
- /* Shape */
16905
- --db-switch-border-radius: var(--db-radius-full);
16906
- --db-switch-border-width: 1px;
16907
17036
 
16908
- /* Color */
16909
- --db-switch-surface: var(--db-selection-toggle-surface);
16910
- --db-switch-surface-hover: var(--db-color-action-default-background-hover);
16911
- --db-switch-surface-press: var(--db-color-action-default-background-press);
16912
- --db-switch-surface-checked: var(--db-selection-toggle-surface-checked);
16913
- --db-switch-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16914
- --db-switch-surface-checked-press: var(--db-selection-toggle-surface-checked-press);
16915
- --db-switch-surface-disabled: var(--db-selection-toggle-surface-disabled);
16916
- --db-switch-surface-checked-disabled: var(--db-color-action-disabled-text);
16917
17037
 
16918
- --db-switch-border: var(--db-color-action-default-border-default);
16919
- --db-switch-border-hover: var(--db-selection-toggle-border-hover);
16920
- --db-switch-border-press: var(--db-selection-toggle-border-press);
16921
- --db-switch-border-disabled: var(--db-color-action-disabled-border);
16922
- --db-switch-border-checked: var(--db-selection-toggle-surface-checked);
16923
- --db-switch-border-checked-hover: var(--db-selection-toggle-surface-checked-hover);
16924
- --db-switch-border-checked-press: var(--db-selection-toggle-surface-checked-press);
16925
- --db-switch-border-focus: var(--db-selection-toggle-surface-checked);
16926
17038
 
16927
- --db-switch-thumb: var(--db-selection-toggle-surface);
16928
17039
 
16929
- --db-switch-text: var(--db-selection-toggle-text);
16930
- --db-switch-text-disabled: var(--db-selection-toggle-text-disabled);
16931
17040
 
16932
- /* Density */
16933
- --db-switch-track-width: calc(var(--db-size-4) * 1.75); /* 28px */
16934
- --db-switch-track-height: var(--db-size-4); /* 16px */
16935
- --db-switch-thumb-size: var(--db-switch-track-height);
16936
- --db-switch-label-spacing: var(--db-selection-toggle-label-spacing);
16937
17041
 
16938
- /* Motion */
16939
- --db-switch-transition-duration: var(--db-selection-toggle-transition-duration);
17042
+ /* Component tokens for the native Icon. */
17043
+ :where(:root),
17044
+ :where(.du-bois-light) {
17045
+ --db-icon-size: var(--db-font-size-xl);
17046
+ --db-icon-color-danger: var(--db-color-text-validation-danger);
17047
+ --db-icon-color-warning: var(--db-color-text-validation-warning);
17048
+ --db-icon-color-success: var(--db-color-text-validation-success);
17049
+ --db-icon-ai-gradient-start: var(--db-ai-gradient-start);
17050
+ --db-icon-ai-gradient-mid: var(--db-ai-gradient-mid);
17051
+ --db-icon-ai-gradient-end: var(--db-ai-gradient-end);
16940
17052
  }
16941
17053
 
16942
- .db-switch {
16943
- /* flex, not inline-flex: an inline-flex root's line-box leading pushes block siblings
16944
- * (e.g. a following FormMessage) down a few px, breaking the form-flow spacing. */
16945
- display: flex;
16946
- align-items: center;
16947
- cursor: pointer;
16948
- user-select: none;
16949
- width: fit-content;
16950
- margin-inline-end: var(--db-switch-label-spacing);
17054
+ /* Native Icon styles. Ships through mortar.css and reads only component-tier --db-icon-* tokens.
17055
+ * Icon size is driven solely by the `--db-icon-size` design token: to resize an icon, set that
17056
+ * token in scope (default 16px in tokens.css) rather than overriding `font-size`. */
17057
+ .db-icon {
17058
+ display: inline-block;
17059
+ color: inherit;
17060
+ font-style: normal;
17061
+ font-size: var(--db-icon-size);
17062
+ line-height: 0;
17063
+ text-align: center;
17064
+ text-transform: none;
17065
+ vertical-align: -0.125em;
17066
+ text-rendering: optimizelegibility;
17067
+ -webkit-font-smoothing: antialiased;
17068
+ -moz-osx-font-smoothing: grayscale;
16951
17069
  }
16952
17070
 
16953
- .db-switch:has(input:disabled) {
16954
- cursor: not-allowed;
17071
+ .db-icon svg {
17072
+ display: inline-block;
17073
+ line-height: 1;
16955
17074
  }
16956
17075
 
16957
- /* The track. `appearance: none` strips the native checkbox rendering so the input itself can be styled. */
16958
- .db-switch input {
16959
- appearance: none;
16960
- position: relative;
16961
- flex-shrink: 0;
16962
- margin: 0;
17076
+ /* TODO: remove once clickable-icon callsites are gone. Kept for backwards compatibility with the
17077
+ * AntD icon, which made icons clickable via onClick; some callsites still rely on it. Actual icons
17078
+ * should never carry a tabindex — a clickable icon belongs inside a button. */
17079
+ .db-icon[tabindex] {
16963
17080
  cursor: pointer;
16964
- inline-size: var(--db-switch-track-width);
16965
- block-size: var(--db-switch-track-height);
16966
- border: var(--db-switch-border-width) solid var(--db-switch-border);
16967
- border-radius: var(--db-switch-border-radius);
16968
- background-color: var(--db-switch-surface);
16969
- box-sizing: border-box;
16970
- transition:
16971
- background-color var(--db-switch-transition-duration) ease,
16972
- border-color var(--db-switch-transition-duration) ease;
16973
- }
16974
-
16975
- /* The thumb. A pseudo-element keeps the input the switch's only element, so nothing can overlay it
16976
- * and intercept clicks. Offset left by the border width to sit flush with the track's outer edge. */
16977
- .db-switch input::before {
16978
- content: '';
16979
- position: absolute;
16980
- top: 50%;
16981
- left: calc(-1 * var(--db-switch-border-width));
16982
- inline-size: var(--db-switch-thumb-size);
16983
- block-size: var(--db-switch-thumb-size);
16984
- border: var(--db-switch-border-width) solid;
16985
- border-color: inherit;
16986
- border-radius: var(--db-switch-border-radius);
16987
- background-color: var(--db-switch-thumb);
16988
- box-sizing: border-box;
16989
- transform: translateY(-50%);
16990
- transition: transform var(--db-switch-transition-duration) ease;
16991
17081
  }
16992
17082
 
16993
- /* `<label for>` propagates `:hover` to the input, so scope hover/press to the control, not `input:hover`. */
16994
- .db-switch:hover input:not(:disabled):not(:checked) {
16995
- background-color: var(--db-switch-surface-hover);
16996
- border-color: var(--db-switch-border-hover);
17083
+ /* Semantic colors set `color` only; the SVG glyph paints its `fill`/`stroke` from `currentColor`,
17084
+ * so setting the text color recolors the icon. */
17085
+ .db-icon-danger {
17086
+ color: var(--db-icon-color-danger);
16997
17087
  }
16998
17088
 
16999
- .db-switch:active input:not(:disabled):not(:checked) {
17000
- background-color: var(--db-switch-surface-press);
17001
- border-color: var(--db-switch-border-press);
17089
+ .db-icon-warning {
17090
+ color: var(--db-icon-color-warning);
17002
17091
  }
17003
17092
 
17004
- .db-switch input:checked {
17005
- background-color: var(--db-switch-surface-checked);
17006
- border-color: var(--db-switch-border-checked);
17093
+ .db-icon-success {
17094
+ color: var(--db-icon-color-success);
17007
17095
  }
17008
17096
 
17009
- .db-switch:hover input:checked:not(:disabled) {
17010
- background-color: var(--db-switch-surface-checked-hover);
17011
- border-color: var(--db-switch-border-checked-hover);
17097
+ .db-icon-ai svg * {
17098
+ fill: var(--db-icon-gradient);
17012
17099
  }
17013
17100
 
17014
- .db-switch:active input:checked:not(:disabled) {
17015
- background-color: var(--db-switch-surface-checked-press);
17016
- border-color: var(--db-switch-border-checked-press);
17101
+ .db-icon-ai-gradient-start {
17102
+ stop-color: var(--db-icon-ai-gradient-start);
17017
17103
  }
17018
17104
 
17019
- .db-switch input:checked::before {
17020
- transform: translateY(-50%) translateX(calc(var(--db-switch-track-width) - var(--db-switch-thumb-size)));
17105
+ .db-icon-ai-gradient-mid {
17106
+ stop-color: var(--db-icon-ai-gradient-mid);
17021
17107
  }
17022
17108
 
17023
- /* On focus, recolor the border to the checked-fill color on top of the outline ring. Matching AntD,
17024
- * the ring sits flush when off and gains a 1px offset when checked.
17025
- * TODO: width/offset are hardcoded 1px to preserve the AntD look; adopt the system 2px focus ring
17026
- * (var(--db-focus-ring-width)) in a separate change. */
17027
- .db-switch input:focus-visible {
17028
- border-color: var(--db-switch-border-focus);
17029
- outline: 1px solid var(--db-focus-ring-color);
17030
- outline-offset: 0;
17109
+ .db-icon-ai-gradient-end {
17110
+ stop-color: var(--db-icon-ai-gradient-end);
17031
17111
  }
17032
17112
 
17033
- .db-switch input:checked:focus-visible {
17034
- outline-offset: 1px;
17113
+ .db-icon-gradient-definitions {
17114
+ position: absolute;
17115
+ width: 1px;
17116
+ height: 1px;
17117
+ padding: 0;
17118
+ margin: -1px;
17119
+ overflow: hidden;
17120
+ clip: rect(0, 0, 0, 0);
17121
+ white-space: nowrap;
17122
+ border: 0;
17035
17123
  }
17036
17124
 
17037
- .db-switch input:disabled {
17038
- cursor: not-allowed;
17039
- background-color: var(--db-switch-surface-disabled);
17040
- border-color: var(--db-switch-border-disabled);
17125
+ .db-icon-spin {
17126
+ display: inline-block;
17127
+ animation: db-icon-loading-circle 1s infinite linear;
17041
17128
  }
17042
17129
 
17043
- /* Disabled + on: more specific than the plain disabled rule, so it wins regardless of source order. */
17044
- .db-switch input:checked:disabled {
17045
- background-color: var(--db-switch-surface-checked-disabled);
17046
- border-color: var(--db-switch-surface-checked-disabled);
17130
+ @keyframes db-icon-loading-circle {
17131
+ 100% {
17132
+ transform: rotate(360deg);
17133
+ }
17047
17134
  }
17048
17135
 
17049
17136
  @media (prefers-reduced-motion: reduce) {
17050
- .db-switch input,
17051
- .db-switch input::before {
17052
- transition: none;
17137
+ .db-icon-spin {
17138
+ animation: none;
17053
17139
  }
17054
17140
  }
17055
17141
 
17056
- .db-switch-wrapper {
17057
- display: flex;
17058
- align-items: center;
17059
- }
17060
17142
 
17061
- .db-switch-wrapper .db-switch-state-message {
17062
- margin-inline-start: auto;
17063
- margin-inline-end: var(--db-switch-label-spacing);
17064
- }
17065
17143
 
17066
- /* `:first-child` scopes this to the label-first layout; the plain layout renders the label last. */
17067
- .db-switch-wrapper > .du-bois-light-label:first-child,
17068
- .db-switch-wrapper > .du-bois-dark-label:first-child {
17069
- margin-inline-end: var(--db-switch-label-spacing);
17070
- }
17071
17144
 
17072
- .db-switch-state-message {
17073
- color: var(--db-switch-text);
17074
- }
17145
+ :where(:root), :where(.du-bois-light) {
17146
+ /* Shape */
17147
+ /* Must not scale with density; no non-scaling system token holds it. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17148
+ --db-navigation-menu-indicator-thickness: 4px;
17149
+ /* No system token holds this control-specific inset. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17150
+ --db-navigation-menu-focus-ring-offset: -1px;
17075
17151
 
17076
- /* Target the <Label>'s class, not a bare `label` — that would also match the .db-switch root <label>. */
17077
- .db-switch-wrapper:has(input:disabled) .du-bois-light-label,
17078
- .db-switch-wrapper:has(input:disabled) .du-bois-dark-label {
17079
- color: var(--db-switch-text-disabled);
17080
- }
17152
+ /* Color */
17153
+ --db-navigation-menu-indicator: var(--db-accent-surface);
17154
+ --db-navigation-menu-focus-ring: var(--db-focus-ring-color);
17155
+ --db-navigation-menu-text: var(--db-text-subtle);
17156
+ --db-navigation-menu-text-selected: var(--db-text-base);
17157
+ /* No v2 semantic for default-action link text yet; bridges on the v1 --db-color-* token. */
17158
+ --db-navigation-menu-text-hover: var(--db-color-action-default-text-hover);
17159
+ --db-navigation-menu-text-active: var(--db-color-action-default-text-press);
17081
17160
 
17082
- /* Indent a following hint/message past the track. The class is doubled to out-specify FormMessage's
17083
- * own margin rule. Each rule's key (rightmost) selector is a single class, not a multi-item :is(),
17084
- * so Chromium can hash-reject it instead of attempting the match against every element on every
17085
- * recalc (the leading/middle :is() groups aren't the key selector, so they're left as-is). */
17086
- :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-hint, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-hint, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17087
- + :is(.du-bois-light-hint, .du-bois-dark-hint)
17088
- + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17089
- + :is(.du-bois-light-hint, .du-bois-dark-hint)
17090
- + .du-bois-dark-form-message {
17091
- padding-inline-start: calc(var(--db-space-2) + var(--db-switch-track-width));
17161
+ /* Density */
17162
+ --db-navigation-menu-item-height: var(--db-size-8);
17163
+ --db-navigation-menu-item-min-width: var(--db-size-6);
17164
+ --db-navigation-menu-link-padding-top: var(--db-space-1);
17165
+ --db-navigation-menu-link-padding-bottom: var(--db-space-2);
17166
+ --db-navigation-menu-focus-ring-width: var(--db-focus-ring-width);
17092
17167
  }
17093
17168
 
17094
- :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17095
- + :is(.du-bois-light-hint, .du-bois-dark-hint)
17096
- + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17097
- + :is(.du-bois-light-hint, .du-bois-dark-hint)
17098
- + .du-bois-dark-form-message {
17099
- margin-top: 0;
17169
+
17170
+ :where(:root), :where(.du-bois-light) {
17171
+ /* Color */
17172
+ /* No v2 token holds tertiary action-text states yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17173
+ --db-overflow-trigger-text: var(--db-color-action-tertiary-text-default);
17174
+ --db-overflow-trigger-text-hover: var(--db-color-action-tertiary-text-hover);
17175
+ --db-overflow-trigger-text-press: var(--db-color-action-tertiary-text-press);
17176
+
17177
+ /* Density */
17178
+ --db-overflow-spacing: var(--db-space-2);
17179
+ --db-overflow-menu-spacing: var(--db-space-1);
17100
17180
  }
17101
17181
 
17102
- /* Component-tier tokens for the native <Button>, grouped by area (density, shape, typography, color).
17103
- * Colors alias the action-* system tokens; sizes alias the system density scale.
17104
- *
17105
- * Follows the mortar/action.css pattern: one semantic set (surface / border / text, each with
17106
- * -hover / -press / -disabled) whose defaults are the secondary variant. The variant classes that
17107
- * re-set these tokens per variant live in NativeButton.css; this file declares only tokens. */
17108
17182
 
17109
- /* Declared under the theme classes too (not just :root) so the var() chain re-resolves inside a
17110
- * `.du-bois-dark` subtree (or a portal that re-applies the theme class) instead of freezing at the root values. */
17183
+
17184
+ /* Popover is a neutral-card member of the popup family: surface, text, border, and radius come
17185
+ * from --db-popup-*. Link, focus, and spacing are popover-specific and reference system tokens
17186
+ * directly. The popup elevation scale is light-only, so the dark shadow is re-declared below. */
17187
+
17111
17188
  :where(:root), :where(.du-bois-light) {
17112
- /* ── Density ──────────────────────────────────────────────────────────── */
17113
- --db-btn-height: var(--db-size-8);
17114
- --db-btn-height-small: var(--db-size-6);
17115
- --db-btn-icon-size: var(--db-size-4);
17116
- --db-btn-border-width: 1px;
17117
- --db-btn-padding-vertical: var(--db-space-1);
17118
- --db-btn-padding-horizontal: var(--db-space-3);
17119
- --db-btn-padding-horizontal-small: var(--db-space-2);
17120
- /* Icon/force-icon horizontal padding: half the standard horizontal padding. */
17121
- --db-btn-padding-horizontal-icon: calc(var(--db-space-3) / 2); /* 6px */
17122
- --db-btn-gap: var(--db-space-1);
17123
- --db-btn-end-icon-gap: var(--db-space-2);
17124
- --db-btn-end-icon-gap-small: var(--db-space-1);
17125
- /* Width of a content-less button while loading, so the standalone spinner has room. */
17126
- --db-btn-loading-empty-width: var(--db-size-10);
17189
+ /* Shape */
17190
+ --db-popover-border-radius: var(--db-popup-border-radius);
17127
17191
 
17128
- /* ── Shape ────────────────────────────────────────────────────────────── */
17129
- --db-btn-border-radius: var(--db-shape-control);
17192
+ /* Color */
17193
+ --db-popover-surface: var(--db-popup-surface);
17194
+ --db-popover-text: var(--db-popup-text);
17195
+ --db-popover-border: var(--db-popup-border);
17196
+ /* No v2 token holds the tertiary link text yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17197
+ --db-popover-link-text: var(--db-color-action-tertiary-text-default);
17198
+ --db-popover-link-text-hover: var(--db-color-action-tertiary-text-hover);
17199
+ --db-popover-focus-ring: var(--db-focus-ring-color);
17130
17200
 
17131
- /* ── Typography ───────────────────────────────────────────────────────── */
17132
- --db-btn-font-size: var(--db-font-size-base);
17133
- --db-btn-icon-only-font-size: var(--db-font-size-lg); /* 15px */
17134
- --db-btn-font-weight: var(--db-font-weight-normal);
17135
- /* Line heights are hard-coded until the typography scale ships computed per-level line-height tokens. */
17136
- --db-btn-line-height: 1.25rem; /* 20px */
17137
- --db-btn-line-height-small: 1rem; /* 16px */
17201
+ /* Density */
17202
+ --db-popover-spacing: var(--db-space-2);
17203
+ --db-popover-focus-ring-width: var(--db-focus-ring-width);
17138
17204
 
17139
- /* ── Color: semantic set (defaults = secondary variant) ───────────────── */
17140
- --db-btn-surface: var(--db-color-action-default-background-default);
17141
- --db-btn-surface-hover: var(--db-color-action-default-background-hover);
17142
- --db-btn-surface-press: var(--db-color-action-default-background-press);
17143
- --db-btn-surface-disabled: transparent;
17144
- --db-btn-border: var(--db-color-action-default-border-default);
17145
- --db-btn-border-hover: var(--db-color-action-default-border-hover);
17146
- --db-btn-border-press: var(--db-color-action-default-border-press);
17147
- --db-btn-border-disabled: var(--db-color-action-disabled-border);
17148
- --db-btn-text: var(--db-color-action-default-text-default);
17149
- --db-btn-text-hover: var(--db-color-action-default-text-hover);
17150
- --db-btn-text-press: var(--db-color-action-default-text-press);
17151
- --db-btn-text-disabled: var(--db-color-action-disabled-text);
17205
+ /* Elevation */
17206
+ --db-popover-shadow: var(--db-popup-elevation);
17207
+ }
17152
17208
 
17153
- /* ── Focus & icon ─────────────────────────────────────────────────────── */
17154
- --db-btn-focus-ring: var(--db-color-action-default-border-focus);
17155
- --db-btn-danger-focus-ring: var(--db-color-action-danger-primary-background-default);
17156
- --db-btn-icon-glyph: var(--db-color-text-secondary);
17209
+
17210
+ /* Declared under the theme classes too (not just :root) so the var() chain re-resolves inside a
17211
+ * `.du-bois-dark` subtree instead of freezing at the light values. */
17212
+ :where(:root), :where(.du-bois-light) {
17213
+ /* Shape */
17214
+ --db-progress-border-radius: var(--db-shape-pill);
17215
+
17216
+ /* Color */
17217
+ --db-progress-track: var(--db-color-progress-track);
17218
+ --db-progress-fill: var(--db-color-progress-fill);
17219
+
17220
+ /* Density */
17221
+ --db-progress-size: var(--db-size-2);
17222
+
17223
+ /* Motion */
17224
+ /* No v2 motion token holds this duration. TODO(FEINF-6505): adopt a v2 motion token when one is added. */
17225
+ --db-progress-transition-duration: 300ms;
17157
17226
  }
17158
17227
 
17159
- /* Native <Button> visual styles. Consumes the component-tier `--db-btn-*` tokens (see tokens.css);
17160
- * the variant classes below re-set those tokens per variant and the rules that follow paint them.
17161
- * Shipped via mortar.css. */
17228
+ :where(:root), :where(.du-bois-light) {
17229
+ /* Shape */
17230
+ --db-radio-border-radius: var(--db-radius-full);
17231
+ --db-radio-border-width: 1px;
17162
17232
 
17163
- /* ── 0. Variant token re-sets ──────────────────────────────────────────────
17164
- * Each variant class re-sets the semantic tokens declared in tokens.css; combined classes win by
17165
- * specificity, which beats the `:where(:root)` (specificity 0) defaults regardless of file order. */
17166
- .db-btn-primary {
17167
- --db-btn-surface: var(--db-color-action-primary-background-default);
17168
- --db-btn-surface-hover: var(--db-color-action-primary-background-hover);
17169
- --db-btn-surface-press: var(--db-color-action-primary-background-press);
17170
- --db-btn-surface-disabled: var(--db-color-action-disabled-border);
17171
- --db-btn-border: transparent;
17172
- --db-btn-border-hover: transparent;
17173
- --db-btn-border-press: transparent;
17174
- --db-btn-border-disabled: transparent;
17175
- --db-btn-text: var(--db-color-action-primary-text-default);
17176
- --db-btn-text-hover: var(--db-color-action-primary-text-hover);
17177
- --db-btn-text-press: var(--db-color-action-primary-text-press);
17178
- --db-btn-text-disabled: var(--db-color-action-primary-text-default);
17233
+ /* Color */
17234
+ --db-radio-surface: var(--db-selection-toggle-surface);
17235
+ --db-radio-surface-hover: var(--db-color-action-default-background-hover);
17236
+ --db-radio-surface-active: var(--db-color-action-default-background-press);
17237
+ --db-radio-surface-checked: var(--db-selection-toggle-surface-checked);
17238
+ --db-radio-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
17239
+ --db-radio-surface-checked-active: var(--db-color-action-default-background-press);
17240
+ --db-radio-surface-disabled: var(--db-selection-toggle-surface-disabled);
17241
+
17242
+ --db-radio-border: var(--db-color-action-default-border-default);
17243
+ --db-radio-border-hover: var(--db-selection-toggle-border-hover);
17244
+ --db-radio-border-active: var(--db-selection-toggle-border-active);
17245
+ --db-radio-border-focus: var(--db-focus-ring-color);
17246
+ --db-radio-border-disabled: var(--db-color-action-disabled-border);
17247
+ --db-radio-border-checked-hover: var(--db-selection-toggle-border-active);
17248
+ --db-radio-border-checked-active: var(--db-color-action-default-border-press);
17249
+ --db-radio-border-unchecked-focus: var(--db-color-action-primary-background-default);
17250
+ --db-radio-outline-checked-focus: var(--db-color-action-primary-background-default);
17251
+
17252
+ --db-radio-mark: #ffffff;
17253
+ --db-radio-mark-disabled: var(--db-selection-toggle-mark-disabled);
17254
+ --db-radio-text: var(--db-selection-toggle-text);
17255
+ --db-radio-text-disabled: var(--db-selection-toggle-text-disabled);
17256
+
17257
+ /* Density */
17258
+ --db-radio-size: var(--db-selection-toggle-size);
17259
+ --db-radio-label-spacing: var(--db-selection-toggle-label-spacing);
17260
+ --db-radio-mark-size: var(--db-size-1);
17261
+
17262
+ /* Motion */
17263
+ --db-radio-transition-duration: var(--db-selection-toggle-transition-duration);
17179
17264
  }
17180
- .db-btn-tertiary,
17181
- .db-btn-link {
17182
- --db-btn-surface: var(--db-color-action-tertiary-background-default);
17183
- --db-btn-surface-disabled: var(--db-color-action-tertiary-background-default);
17184
- --db-btn-border: var(--db-color-action-tertiary-background-default);
17185
- --db-btn-border-hover: transparent;
17186
- --db-btn-border-press: transparent;
17187
- --db-btn-border-disabled: transparent;
17188
- --db-btn-text: var(--db-color-action-tertiary-text-default);
17189
- --db-btn-text-hover: var(--db-color-action-tertiary-text-hover);
17190
- --db-btn-text-press: var(--db-color-action-tertiary-text-press);
17191
- --db-btn-text-disabled: var(--db-color-action-disabled-text);
17265
+
17266
+ /* Native-input <Radio> styles. Classic CSS only — no native nesting (the
17267
+ * `&`-style nesting feature ships in Chrome/Edge 120, Safari 17.2, Firefox 117
17268
+ * which leaves a non-trivial slice of our production traffic uncovered today;
17269
+ * see PR #1954620 thread). `:has()` lookups are still used to read state from
17270
+ * the native <input>; that feature has the support coverage we need.
17271
+ *
17272
+ * Structure (assumed by the React component):
17273
+ *
17274
+ * <label class="db-radio">
17275
+ * <input type="radio" />
17276
+ * <span class="db-radio-indicator"> ← circle border + fill react to state
17277
+ * <span class="db-radio-dot" /> ← visible when :checked
17278
+ * </span>
17279
+ * <span class="db-radio-label">{children}</span>
17280
+ * </label>
17281
+ *
17282
+ * The root only ever has one <input> descendant, so a bare `input` selector is
17283
+ * safe and clearer than a class. Mirrors NativeCheckbox.css; the differences are
17284
+ * the circular indicator (border-radius: full) and the filled dot instead of a
17285
+ * checkmark/dash, and there is no indeterminate state.
17286
+ *
17287
+ * TODO(DUBOIS-452): the indicator/dot state styles below are identical to
17288
+ * NativeCheckbox.css — consolidate them into a shared `.db-selection-toggle` class. */
17289
+
17290
+ .db-radio {
17291
+ /* `display: flex` (block-level), NOT `inline-flex`: see NativeCheckbox.css for
17292
+ * the line-box / sibling-spacing rationale. */
17293
+ display: flex;
17294
+ align-items: flex-start;
17295
+ position: relative;
17296
+ cursor: pointer;
17297
+ user-select: none;
17298
+ min-height: var(--db-line-height-base, 20px);
17299
+ width: fit-content;
17192
17300
  }
17193
- .db-btn-tertiary {
17194
- --db-btn-surface-hover: var(--db-color-action-tertiary-background-hover);
17195
- --db-btn-surface-press: var(--db-color-action-tertiary-background-press);
17301
+
17302
+ /* Hover/press live on the root (label), not on the input: the input is
17303
+ * visually hidden (opacity:0), so an `input:hover` rule wouldn't be visible
17304
+ * anyway, and the whole label area should drive the feedback. */
17305
+ .db-radio:hover:has(input:not(:disabled, :checked)) .db-radio-indicator {
17306
+ background-color: var(--db-radio-surface-hover);
17307
+ border-color: var(--db-radio-border-hover);
17196
17308
  }
17197
- /* Link is borderless and text-only, so its hover/press surface stays transparent. */
17198
- .db-btn-link {
17199
- --db-btn-surface-hover: transparent;
17200
- --db-btn-surface-press: transparent;
17309
+
17310
+ .db-radio:active:has(input:not(:disabled, :checked)) .db-radio-indicator {
17311
+ background-color: var(--db-radio-surface-active);
17312
+ border-color: var(--db-radio-border-active);
17201
17313
  }
17202
- /* Every danger button (icon included) disables to the filled treatment. */
17203
- .db-btn-danger {
17204
- --db-btn-surface-disabled: var(--db-color-action-disabled-border);
17205
- --db-btn-border-disabled: transparent;
17206
- --db-btn-text-disabled: var(--db-color-action-primary-text-default);
17314
+
17315
+ /* Selected fill. */
17316
+ .db-radio:has(input:checked) .db-radio-indicator {
17317
+ background-color: var(--db-radio-surface-checked);
17318
+ border-color: var(--db-radio-surface-checked);
17207
17319
  }
17208
- /* Danger, filled (primary): re-set only the surface; text/transparent border come from primary. */
17209
- .db-btn-primary.db-btn-danger:not(.db-btn-icon-only) {
17210
- --db-btn-surface: var(--db-color-action-danger-primary-background-default);
17211
- --db-btn-surface-hover: var(--db-color-action-danger-primary-background-hover);
17212
- --db-btn-surface-press: var(--db-color-action-danger-primary-background-press);
17320
+
17321
+ /* AntD getRadioInputStyles checked hover: background = actionPrimaryBackgroundHover,
17322
+ * borderColor = actionPrimaryBackgroundPress (a darker ring than the fill). */
17323
+ .db-radio:hover:has(input:checked:not(:disabled)) .db-radio-indicator {
17324
+ background-color: var(--db-radio-surface-checked-hover);
17325
+ border-color: var(--db-radio-border-checked-hover);
17213
17326
  }
17214
- /* Danger, secondary/outlined. */
17215
- .db-btn-danger:not(.db-btn-primary):not(.db-btn-tertiary):not(.db-btn-link):not(.db-btn-icon-only) {
17216
- --db-btn-surface: var(--db-color-action-danger-default-background-default);
17217
- --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
17218
- --db-btn-surface-press: var(--db-color-action-danger-default-background-press);
17219
- --db-btn-border: var(--db-color-action-danger-default-border-default);
17220
- --db-btn-border-hover: var(--db-color-action-danger-default-border-hover);
17221
- --db-btn-border-press: var(--db-color-action-danger-default-border-press);
17222
- --db-btn-text: var(--db-color-action-danger-default-text-default);
17223
- --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
17224
- --db-btn-text-press: var(--db-color-action-danger-default-text-press);
17327
+
17328
+ /* AntD getRadioInputStyles checked active: background = actionDefaultBackgroundPress,
17329
+ * borderColor = actionDefaultBorderPress (two-tone, not flat). */
17330
+ .db-radio:active:has(input:checked:not(:disabled)) .db-radio-indicator {
17331
+ background-color: var(--db-radio-surface-checked-active);
17332
+ border-color: var(--db-radio-border-checked-active);
17225
17333
  }
17226
- /* Danger, tertiary/link: danger text everywhere, danger surface on hover/press; resting/disabled
17227
- * surface stay the tertiary ghost (override the filled danger-disabled set above). */
17228
- .db-btn-tertiary.db-btn-danger,
17229
- .db-btn-link.db-btn-danger {
17230
- --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
17231
- --db-btn-surface-press: var(--db-color-action-danger-default-background-press);
17232
- --db-btn-surface-disabled: var(--db-color-action-tertiary-background-default);
17233
- --db-btn-text: var(--db-color-action-danger-default-text-default);
17234
- --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
17235
- --db-btn-text-press: var(--db-color-action-danger-default-text-press);
17236
- --db-btn-text-disabled: var(--db-color-action-disabled-text);
17334
+
17335
+ /* Dot visibility. */
17336
+ .db-radio:has(input:checked) .db-radio-dot {
17337
+ opacity: 1;
17237
17338
  }
17238
- /* Icon-only renders as the ghost (tertiary) treatment regardless of variant; re-set the semantic set
17239
- * so the icon-only rules paint from these component tokens. */
17240
- .db-btn-icon-only {
17241
- --db-btn-surface: var(--db-color-action-tertiary-background-default);
17242
- --db-btn-surface-hover: var(--db-color-action-tertiary-background-hover);
17243
- --db-btn-surface-press: var(--db-color-action-tertiary-background-press);
17244
- --db-btn-text: var(--db-color-action-tertiary-text-default);
17245
- --db-btn-text-hover: var(--db-color-action-tertiary-text-hover);
17246
- --db-btn-text-press: var(--db-color-action-tertiary-text-press);
17339
+
17340
+ /* Focus-visible (keyboard only). AntD getRadioInputStyles splits focus by checked
17341
+ * state, so we do too.
17342
+ *
17343
+ * UNCHECKED focus (AntD `&:not(disabled) > input:focus + inner`): only the border
17344
+ * color changes to actionPrimaryBackgroundDefault — NO outline ring. */
17345
+ .db-radio:has(input:focus-visible:not(:checked)) .db-radio-indicator {
17346
+ border-color: var(--db-radio-border-unchecked-focus);
17247
17347
  }
17248
- /* Danger icon-only keeps the ghost resting surface (inherited above) but uses danger text and a
17249
- * danger hover/press surface. */
17250
- .db-btn-icon-only.db-btn-danger {
17251
- --db-btn-surface-hover: var(--db-color-action-danger-default-background-hover);
17252
- --db-btn-surface-press: var(--db-color-action-danger-default-background-press);
17253
- --db-btn-text: var(--db-color-action-danger-default-text-default);
17254
- --db-btn-text-hover: var(--db-color-action-danger-default-text-hover);
17255
- --db-btn-text-press: var(--db-color-action-danger-default-text-press);
17348
+
17349
+ /* CHECKED focus-visible (AntD `&.checked &:not(disabled) > input:focus-visible + inner`):
17350
+ * background = actionPrimaryBackgroundDefault, borderColor = actionDefaultBorderFocus,
17351
+ * outline = 1px solid actionPrimaryBackgroundDefault, outlineOffset = 1. */
17352
+ .db-radio:has(input:focus-visible:checked) .db-radio-indicator {
17353
+ background-color: var(--db-radio-surface-checked);
17354
+ border-color: var(--db-radio-border-focus);
17355
+ outline: 1px solid var(--db-radio-outline-checked-focus);
17356
+ outline-offset: 1px;
17256
17357
  }
17257
17358
 
17258
- /* ── 1. Base paints the default (secondary) variant ────────────────────── */
17259
- .db-btn {
17260
- appearance: none;
17359
+ /* Disabled (covers checked and unchecked). Sourced from `input:disabled` so the
17360
+ * standalone-disabled and group-disabled paths both land here. */
17361
+ .db-radio:has(input:disabled) {
17362
+ cursor: not-allowed;
17363
+ }
17364
+
17365
+ .db-radio:has(input:disabled) .db-radio-label {
17366
+ color: var(--db-radio-text-disabled);
17367
+ }
17368
+
17369
+ .db-radio:has(input:disabled) .db-radio-indicator {
17370
+ background-color: var(--db-radio-surface-disabled);
17371
+ border-color: var(--db-radio-border-disabled);
17372
+ box-shadow: none;
17373
+ }
17374
+
17375
+ .db-radio:has(input:disabled) .db-radio-dot {
17376
+ background-color: var(--db-radio-mark-disabled);
17377
+ }
17378
+
17379
+ /* Visually-hidden native input (opacity:0) overlaying the indicator circle,
17380
+ * kept interactive and in the tab order so the `:has()` lookups above can read
17381
+ * its state. It stays pointer-clickable — matching AntD's `.ant-radio-input` —
17382
+ * so it is the top-most hit-test target over the circle: `getByRole('radio').click()`
17383
+ * (Playwright, and any coordinate click that lands on the circle) resolves to the
17384
+ * real control instead of the decorative indicator overlay. Making the input
17385
+ * `pointer-events: none` (the prior native-only behavior) is what regressed
17386
+ * interaction tests vs. the AntD variant. `cursor: inherit` tracks the label's
17387
+ * pointer / disabled `not-allowed` cursor so the circle keeps AntD's cursor too. */
17388
+ .db-radio input {
17389
+ position: absolute;
17390
+ inset: 0;
17391
+ inline-size: var(--db-radio-size);
17392
+ block-size: var(--db-radio-size);
17393
+ margin: 0;
17394
+ opacity: 0;
17395
+ cursor: inherit;
17396
+ }
17397
+
17398
+ /* Indicator — circle with the border/fill that responds to state above.
17399
+ * Vertical math aligns the indicator to the cap-height of the first text line
17400
+ * so radio + label sit on the same visual baseline even when the label wraps. */
17401
+ .db-radio .db-radio-indicator {
17402
+ display: grid;
17403
+ place-items: center;
17404
+ flex-shrink: 0;
17405
+ /* Decorative only — the overlaid input owns hit-testing. Without this, the
17406
+ * indicator (and its inherited dot child) intercept `getByRole('radio').click()`;
17407
+ * clicks on any edge sliver the input does not cover fall through to the label,
17408
+ * which toggles via label/input association. */
17409
+ pointer-events: none;
17410
+ inline-size: var(--db-radio-size);
17411
+ block-size: var(--db-radio-size);
17412
+ margin-top: calc((var(--db-line-height-base, 20px) - var(--db-radio-size)) / 2);
17413
+ border: var(--db-radio-border-width) solid var(--db-radio-border);
17414
+ border-radius: var(--db-radio-border-radius);
17415
+ background-color: var(--db-radio-surface);
17261
17416
  box-sizing: border-box;
17262
- position: relative; /* anchors the loading spinner */
17263
- display: inline-flex !important;
17264
- align-items: center !important;
17265
- justify-content: center !important;
17266
- vertical-align: middle;
17267
- white-space: nowrap;
17268
- user-select: none;
17269
- cursor: pointer;
17270
- height: var(--db-btn-height) !important;
17271
- font-family: inherit;
17272
- font-size: var(--db-btn-font-size);
17273
- font-weight: var(--db-btn-font-weight);
17274
- line-height: var(--db-btn-line-height) !important;
17275
- text-decoration: none !important;
17276
- border: var(--db-btn-border-width) solid var(--db-btn-border);
17277
- border-color: var(--db-btn-border) !important;
17278
- border-radius: var(--db-btn-border-radius) !important;
17279
- box-shadow: none !important;
17280
- background-color: var(--db-btn-surface) !important;
17281
- color: var(--db-btn-text) !important;
17417
+ transition:
17418
+ background-color var(--db-radio-transition-duration) ease,
17419
+ border-color var(--db-radio-transition-duration) ease,
17420
+ box-shadow var(--db-radio-transition-duration) ease;
17282
17421
  }
17283
17422
 
17284
- /* ── 2. Padding ──────────────────────────────────────────────────────────── */
17285
- .db-btn.db-btn:where(:not(.db-btn-icon-only, .db-btn-force-icon, .db-btn-link)) {
17286
- padding: var(--db-btn-padding-vertical) var(--db-btn-padding-horizontal) !important;
17423
+ .db-radio .db-radio-indicator > .db-radio-dot {
17424
+ grid-area: 1 / 1;
17425
+ inline-size: var(--db-radio-mark-size);
17426
+ block-size: var(--db-radio-mark-size);
17427
+ border-radius: var(--db-radius-full);
17428
+ background-color: var(--db-radio-mark);
17429
+ opacity: 0;
17430
+ transition: opacity var(--db-radio-transition-duration) ease;
17287
17431
  }
17288
- .db-btn.db-btn-small:where(:not(.db-btn-icon-only, .db-btn-force-icon, .db-btn-link)) {
17289
- padding: 0 var(--db-btn-padding-horizontal-small) !important;
17432
+
17433
+ /* Honour the user's reduced-motion preference: drop the state-change
17434
+ * transitions on the indicator and dot so checking/hovering is instant. */
17435
+ @media (prefers-reduced-motion: reduce) {
17436
+ .db-radio .db-radio-indicator,
17437
+ .db-radio .db-radio-indicator > .db-radio-dot {
17438
+ transition: none;
17439
+ }
17440
+ }
17441
+
17442
+ /* Padding-inline replicates the AntD `.{prefix} + span` label gap and the
17443
+ * trailing whitespace that separates adjacent radios in a row. */
17444
+ .db-radio .db-radio-label {
17445
+ color: var(--db-radio-text);
17446
+ padding-inline: var(--db-radio-label-spacing);
17447
+ word-break: break-word;
17448
+ }
17449
+
17450
+ /* Form-flow integration: align helper text under the radio label. The `paddingLeft`
17451
+ * indent comes from AntD's getCommonRadioGroupStyles (`& > label + hint`) and applies
17452
+ * to BOTH layouts. The doubled root class pushes specificity to (0,3,0) to beat
17453
+ * FormMessage's runtime `&&` margin. */
17454
+ .db-radio.db-radio + .du-bois-light-hint,
17455
+ .db-radio.db-radio + .du-bois-dark-hint,
17456
+ .db-radio.db-radio + .du-bois-light-form-message,
17457
+ .db-radio.db-radio + .du-bois-dark-form-message,
17458
+ .db-radio.db-radio + .du-bois-light-hint + .du-bois-light-form-message,
17459
+ .db-radio.db-radio + .du-bois-dark-hint + .du-bois-dark-form-message {
17460
+ padding-inline-start: var(--db-space-6);
17461
+ margin-top: 0;
17462
+ }
17463
+
17464
+ /* Vertical hint rhythm (AntD getVerticalRadioGroupStyles, vertical-only): a hint
17465
+ * after a radio gets `marginTop: -sm` (cancels the preceding radio's
17466
+ * `paddingBottom: sm` so the hint tucks directly under its label) and
17467
+ * `marginBottom: sm` (gap before the next radio). The negative top margin relies on
17468
+ * the .db-stack gap being 0 (set below). Scoped to .db-stack so it never perturbs
17469
+ * the horizontal grid, where hints are positioned by grid-row instead. */
17470
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-hint,
17471
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-hint,
17472
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-form-message,
17473
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-form-message,
17474
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-light-hint + .du-bois-light-form-message,
17475
+ .db-radio-group.db-stack > .db-radio.db-radio + .du-bois-dark-hint + .du-bois-dark-form-message {
17476
+ margin-top: calc(-1 * var(--db-space-2));
17477
+ margin-bottom: var(--db-space-2);
17478
+ }
17479
+
17480
+ /* AntD `& > label:last-of-type + hint { marginTop: 0 }`: the last radio has no
17481
+ * `paddingBottom` to cancel, so its trailing hint keeps its natural top position. */
17482
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-hint,
17483
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-hint,
17484
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-form-message,
17485
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-form-message,
17486
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-light-hint + .du-bois-light-form-message,
17487
+ .db-radio-group.db-stack > .db-radio.db-radio:last-of-type + .du-bois-dark-hint + .du-bois-dark-form-message {
17488
+ margin-top: 0;
17489
+ }
17490
+
17491
+ .du-bois-light-label + .db-radio-group.db-radio-group,
17492
+ .du-bois-dark-label + .db-radio-group.db-radio-group,
17493
+ .du-bois-light-hint + .db-radio-group.db-radio-group,
17494
+ .du-bois-dark-hint + .db-radio-group.db-radio-group,
17495
+ .db-radio-group.db-radio-group + .du-bois-light-form-message,
17496
+ .db-radio-group.db-radio-group + .du-bois-dark-form-message {
17497
+ margin-top: var(--db-space-2);
17290
17498
  }
17291
- .db-btn.db-btn-link {
17292
- padding: 0 !important;
17499
+
17500
+ /* RadioGroup layout. Vertical (the default) delegates flex-direction to the
17501
+ * .db-stack primitive; horizontal is a two-row grid (mirroring AntD). */
17502
+ .db-radio-group {
17503
+ display: flex;
17293
17504
  }
17294
- .db-btn.db-btn-force-icon {
17295
- padding: 0 var(--db-btn-padding-horizontal-icon) !important;
17505
+
17506
+ /* Vertical: .db-stack supplies flex-direction: column. AntD's vertical helper
17507
+ * (getVerticalRadioGroupStyles) produces its item rhythm purely from
17508
+ * `& > label { paddingBottom: sm }` plus the negative-margin hint trick above — it
17509
+ * uses NO flex gap. The negative `marginTop` on hints only works if the stack adds
17510
+ * no gap of its own, so we zero out the gap and reproduce the rhythm via
17511
+ * padding-bottom on each radio. */
17512
+ .db-radio-group.db-stack {
17513
+ --db-stack-gap: 0;
17296
17514
  }
17297
17515
 
17298
- /* ── 3. Interactive states ───────────────────────────────────────────────── */
17299
- .db-btn:hover:not(:disabled, .db-btn-loading) {
17300
- background-color: var(--db-btn-surface-hover) !important;
17301
- border-color: var(--db-btn-border-hover) !important;
17302
- color: var(--db-btn-text-hover) !important;
17516
+ /* AntD `& > label { paddingBottom: sm }`. */
17517
+ .db-radio-group.db-stack > .db-radio {
17518
+ padding-bottom: var(--db-space-2);
17303
17519
  }
17304
- .db-btn:active:not(:disabled, .db-btn-loading) {
17305
- background-color: var(--db-btn-surface-press) !important;
17306
- border-color: var(--db-btn-border-press) !important;
17307
- color: var(--db-btn-text-press) !important;
17520
+
17521
+ /* AntD `& > label:last-of-type { paddingBottom: 0 }`. */
17522
+ .db-radio-group.db-stack > .db-radio:last-of-type {
17523
+ padding-bottom: 0;
17308
17524
  }
17309
17525
 
17310
- /* ── 4. Focus ────────────────────────────────────────────────────────────── */
17311
- .db-btn:focus-visible {
17312
- outline: 2px solid var(--db-btn-focus-ring);
17313
- outline-offset: -2px;
17526
+ /* AntD getVerticalRadioGroupStyles `.${prefix}-radio-tile + .${prefix}-radio-tile {
17527
+ * marginTop: md }`: RadioTiles are not matched by the `.db-radio` padding-bottom
17528
+ * rhythm above (the tile's own class is du-bois-{light,dark}-radio-tile), so space
17529
+ * consecutive tiles explicitly. The .db-stack gap is 0, so this margin is the spacing. */
17530
+ .db-radio-group.db-stack > .du-bois-light-radio-tile + .du-bois-light-radio-tile,
17531
+ .db-radio-group.db-stack > .du-bois-light-radio-tile + .du-bois-dark-radio-tile,
17532
+ .db-radio-group.db-stack > .du-bois-dark-radio-tile + .du-bois-light-radio-tile,
17533
+ .db-radio-group.db-stack > .du-bois-dark-radio-tile + .du-bois-dark-radio-tile {
17534
+ margin-top: var(--db-space-4);
17314
17535
  }
17315
- .db-btn-primary:focus-visible {
17316
- outline-offset: 1px;
17536
+
17537
+ /* Horizontal: AntD `getHorizontalRadioGroupStyles` is a TWO-ROW grid (NOT a flex
17538
+ * row) so each radio sits in the [label] row and its following hint drops into the
17539
+ * [hint] row of the same auto-column. `&&` in AntD doubles specificity; the doubled
17540
+ * group class here matches that.
17541
+ * AntD: `display: grid; gridTemplateRows: [label] auto [hint] auto;
17542
+ * gridAutoColumns: max-content; gridColumnGap: md`. */
17543
+ .db-radio-group--horizontal.db-radio-group--horizontal {
17544
+ display: grid;
17545
+ grid-template-rows: [label] auto [hint] auto;
17546
+ grid-auto-columns: max-content;
17547
+ column-gap: var(--db-space-4);
17317
17548
  }
17318
- .db-btn-use-focus-pseudo-class:focus {
17319
- outline: 2px solid var(--db-btn-focus-ring);
17320
- outline-offset: 1px;
17549
+
17550
+ /* Equal column widths: AntD `gridAutoColumns: minmax(0, 1fr)`. */
17551
+ .db-radio-group--horizontal.db-radio-group--equal-width {
17552
+ grid-auto-columns: minmax(0, 1fr);
17321
17553
  }
17322
17554
 
17323
- /* ── 5. Small / full-width / empty ────────────────────────────────────────── */
17324
- .db-btn-small {
17325
- height: var(--db-btn-height-small) !important;
17555
+ /* AntD `& > label, & > .${prefix}-radio-tile { gridRow: label / label; marginRight: 0 }`:
17556
+ * every radio (and every RadioTile button, whose own class is
17557
+ * du-bois-{light,dark}-radio-tile and is NOT matched by `> .db-radio`) lives in the
17558
+ * label row. grid-auto-flow defaults to row, so successive items advance the implicit
17559
+ * column. */
17560
+ .db-radio-group--horizontal > .db-radio,
17561
+ .db-radio-group--horizontal > .du-bois-light-radio-tile,
17562
+ .db-radio-group--horizontal > .du-bois-dark-radio-tile {
17563
+ grid-row: label / label;
17564
+ margin-inline-end: 0;
17326
17565
  }
17327
- .db-btn-full-width {
17328
- width: 100%;
17566
+
17567
+ /* AntD `& > label + hint { display: inline-block; gridRow: hint / hint }`: a hint
17568
+ * following a radio drops into the hint row of the column its radio occupies. The
17569
+ * vertical negative-margin rules are scoped to .db-stack, so nothing to reset here. */
17570
+ .db-radio-group--horizontal > .du-bois-light-hint,
17571
+ .db-radio-group--horizontal > .du-bois-dark-hint {
17572
+ display: inline-block;
17573
+ grid-row: hint / hint;
17329
17574
  }
17330
- .db-btn-empty {
17331
- background-color: transparent !important;
17332
- border-color: transparent !important;
17575
+
17576
+ /* AntD `:has(> hint) { marginTop: sm }`: when the horizontal group contains a hint,
17577
+ * the group itself gets a top margin (room above the two-row grid). */
17578
+ .db-radio-group--horizontal:has(> .du-bois-light-hint),
17579
+ .db-radio-group--horizontal:has(> .du-bois-dark-hint) {
17580
+ margin-top: var(--db-space-2);
17333
17581
  }
17334
- .db-btn-default.db-btn-empty {
17335
- color: var(--db-btn-icon-glyph) !important;
17582
+
17583
+ /* AntD `&& + form-message { marginTop: sm }` (horizontal): a form-message following
17584
+ * the whole group is separated by sm. */
17585
+ .db-radio-group--horizontal + .du-bois-light-form-message,
17586
+ .db-radio-group--horizontal + .du-bois-dark-form-message {
17587
+ margin-top: var(--db-space-2);
17336
17588
  }
17337
- .db-btn-empty.db-btn-loading {
17338
- width: var(--db-btn-loading-empty-width);
17589
+
17590
+
17591
+
17592
+
17593
+
17594
+ :where(:root), :where(.du-bois-light) {
17595
+ /* Shape */
17596
+ --db-slider-border-radius: var(--db-shape-pill);
17597
+
17598
+ /* Color */
17599
+ /* No v2 token holds the track + disabled-range surface yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17600
+ --db-slider-track-surface: #e8ecf0;
17601
+ --db-slider-range-surface-disabled: #e8ecf0;
17602
+ /* No v2 token holds the range fill yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17603
+ --db-slider-range-surface: #2272b4;
17604
+
17605
+ --db-slider-thumb-surface: var(--db-accent-surface);
17606
+ --db-slider-thumb-surface-hover: var(--db-accent-surface-hover);
17607
+ --db-slider-thumb-surface-press: var(--db-accent-surface-active);
17608
+ /* No v2 token holds the disabled thumb yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17609
+ --db-slider-thumb-surface-disabled: #d1d9e1;
17610
+
17611
+ /* Density */
17612
+ --db-slider-size: var(--db-size-5); /* 20px */
17613
+ --db-slider-thumb-size: var(--db-size-5); /* 20px */
17614
+ --db-slider-track-thickness: calc(var(--db-size-1) * 0.75); /* 3px */
17339
17615
  }
17340
17616
 
17341
- /* ── 6. Tertiary & link ──────────────────────────────────────────────────── */
17342
- .db-btn-link {
17343
- height: auto !important;
17344
- border: none !important;
17617
+ :where(:root), :where(.du-bois-light) {
17618
+ /* Shape */
17619
+ /* TODO(FEINF-6505): adopt --db-shape-container-* once a 4px container radius exists; kept on --db-radius-sm to preserve 4px. */
17620
+ --db-snackbar-border-radius: var(--db-radius-sm);
17621
+
17622
+ /* Density */
17623
+ --db-snackbar-spacing-vertical: var(--db-space-3);
17624
+ --db-snackbar-spacing-horizontal: var(--db-space-4);
17625
+ --db-snackbar-close-button-spacing: var(--db-space-2);
17626
+
17627
+ /* Color */
17628
+ /* No v1/v2 system token holds this theme-invariant white. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17629
+ --db-snackbar-text: #ffffff;
17630
+ /* No v1/v2 system token holds these theme-invariant close-icon colors. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17631
+ --db-snackbar-close-icon: #e8ecf0;
17632
+ --db-snackbar-close-icon-hover: #8acaff;
17633
+ --db-snackbar-close-icon-active: #bae1fc;
17345
17634
  }
17346
- /* Re-declare the text color for a non-icon-only tertiary/link at two-class weight so a plain
17347
- * call-site `color` override loses; an icon-only glyph stays at base weight so its override wins. */
17348
- .db-btn-tertiary:not(.db-btn-icon-only),
17349
- .db-btn-link:not(.db-btn-icon-only) {
17350
- color: var(--db-btn-text) !important;
17635
+
17636
+ :where(:root), :where(.du-bois-light) {
17637
+ /* Color */
17638
+ /* No v1/v2 system token holds this light/dark surface pair. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17639
+ --db-snackbar-surface: #11171c;
17640
+
17641
+ /* Elevation */
17642
+ --db-snackbar-shadow: var(--db-elevation-med);
17351
17643
  }
17352
- /* Keep the resting border at two-class weight (parity with the shielded border-color). */
17353
- .db-btn.db-btn-tertiary,
17354
- .db-btn.db-btn-link {
17355
- border-color: var(--db-btn-border) !important;
17644
+
17645
+ /* Spacer's height is a spacing step. The size prop (xs/sm/mid/md/lg) selects one component token,
17646
+ * each aliasing the system space scale — theme-invariant, so a single block (no light/dark split). */
17647
+
17648
+ :where(:root), :where(.du-bois-light) {
17649
+ /* Density */
17650
+ --db-spacer-size-xs: var(--db-space-1); /* 4px */
17651
+ --db-spacer-size-sm: var(--db-space-2); /* 8px */
17652
+ --db-spacer-size-mid: var(--db-space-3); /* 12px */
17653
+ --db-spacer-size-md: var(--db-space-4); /* 16px */
17654
+ --db-spacer-size-lg: var(--db-space-6); /* 24px */
17356
17655
  }
17357
17656
 
17358
- /* ── 7. Danger ───────────────────────────────────────────────────────────── */
17359
- .db-btn-danger:focus-visible {
17360
- outline-color: var(--db-btn-danger-focus-ring);
17657
+
17658
+
17659
+ :where(:root), :where(.du-bois-light) {
17660
+ /* Shape */
17661
+ --db-switch-border-radius: var(--db-radius-full);
17662
+ --db-switch-border-width: 1px;
17663
+
17664
+ /* Color */
17665
+ --db-switch-surface: var(--db-selection-toggle-surface);
17666
+ --db-switch-surface-hover: var(--db-color-action-default-background-hover);
17667
+ --db-switch-surface-active: var(--db-color-action-default-background-press);
17668
+ --db-switch-surface-checked: var(--db-selection-toggle-surface-checked);
17669
+ --db-switch-surface-checked-hover: var(--db-selection-toggle-surface-checked-hover);
17670
+ --db-switch-surface-checked-active: var(--db-selection-toggle-surface-checked-active);
17671
+ --db-switch-surface-disabled: var(--db-selection-toggle-surface-disabled);
17672
+ --db-switch-surface-checked-disabled: var(--db-color-action-disabled-text);
17673
+
17674
+ --db-switch-border: var(--db-color-action-default-border-default);
17675
+ --db-switch-border-hover: var(--db-selection-toggle-border-hover);
17676
+ --db-switch-border-active: var(--db-selection-toggle-border-active);
17677
+ --db-switch-border-disabled: var(--db-color-action-disabled-border);
17678
+ --db-switch-border-checked: var(--db-selection-toggle-surface-checked);
17679
+ --db-switch-border-checked-hover: var(--db-selection-toggle-surface-checked-hover);
17680
+ --db-switch-border-checked-active: var(--db-selection-toggle-surface-checked-active);
17681
+ --db-switch-border-focus: var(--db-selection-toggle-surface-checked);
17682
+
17683
+ --db-switch-thumb: var(--db-selection-toggle-surface);
17684
+
17685
+ --db-switch-text: var(--db-selection-toggle-text);
17686
+ --db-switch-text-disabled: var(--db-selection-toggle-text-disabled);
17687
+
17688
+ /* Density */
17689
+ --db-switch-track-width: calc(var(--db-size-4) * 1.75); /* 28px */
17690
+ --db-switch-track-height: var(--db-size-4); /* 16px */
17691
+ --db-switch-thumb-size: var(--db-switch-track-height);
17692
+ --db-switch-label-spacing: var(--db-selection-toggle-label-spacing);
17693
+
17694
+ /* Motion */
17695
+ --db-switch-transition-duration: var(--db-selection-toggle-transition-duration);
17361
17696
  }
17362
17697
 
17363
- /* ── 8. Icon-only ────────────────────────────────────────────────────────── */
17364
- .db-btn-icon-only {
17365
- padding: 0;
17366
- border: none !important;
17367
- background-color: var(--db-btn-surface) !important;
17368
- font-size: var(--db-btn-icon-only-font-size);
17698
+ .db-switch {
17699
+ /* flex, not inline-flex: an inline-flex root's line-box leading pushes block siblings
17700
+ * (e.g. a following FormMessage) down a few px, breaking the form-flow spacing. */
17701
+ display: flex;
17702
+ align-items: center;
17703
+ cursor: pointer;
17704
+ user-select: none;
17705
+ width: fit-content;
17706
+ margin-inline-end: var(--db-switch-label-spacing);
17369
17707
  }
17370
- /* `:where()` keeps this at (0,1,0) so a call-site `css` width override wins, as it does for `height`. */
17371
- .db-btn-icon-only:where(:not(.db-btn-small)) {
17372
- width: var(--db-btn-height) !important;
17708
+
17709
+ .db-switch:has(input:disabled) {
17710
+ cursor: not-allowed;
17373
17711
  }
17374
- .db-btn-icon-only:not(.db-btn-link, .db-btn-tertiary, [href], :hover, :active, :disabled) {
17375
- color: var(--db-btn-icon-glyph) !important;
17712
+
17713
+ /* The track. `appearance: none` strips the native checkbox rendering so the input itself can be styled. */
17714
+ .db-switch input {
17715
+ appearance: none;
17716
+ position: relative;
17717
+ flex-shrink: 0;
17718
+ margin: 0;
17719
+ cursor: pointer;
17720
+ inline-size: var(--db-switch-track-width);
17721
+ block-size: var(--db-switch-track-height);
17722
+ border: var(--db-switch-border-width) solid var(--db-switch-border);
17723
+ border-radius: var(--db-switch-border-radius);
17724
+ background-color: var(--db-switch-surface);
17725
+ box-sizing: border-box;
17726
+ transition:
17727
+ background-color var(--db-switch-transition-duration) ease,
17728
+ border-color var(--db-switch-transition-duration) ease;
17376
17729
  }
17377
- /* `:where()` on the exclusion holds this at (0,2,0): enough to tint an un-overridden icon-only link
17378
- * over the base color, while still letting a consumer's own icon-color override win on source order. */
17379
- .db-btn-icon-only[href]:where(:not(.db-btn-tertiary, .db-btn-link, .db-btn-danger, :disabled)) {
17380
- color: var(--db-btn-text) !important;
17730
+
17731
+ /* The thumb. A pseudo-element keeps the input the switch's only element, so nothing can overlay it
17732
+ * and intercept clicks. Offset left by the border width to sit flush with the track's outer edge. */
17733
+ .db-switch input::before {
17734
+ content: '';
17735
+ position: absolute;
17736
+ top: 50%;
17737
+ left: calc(-1 * var(--db-switch-border-width));
17738
+ inline-size: var(--db-switch-thumb-size);
17739
+ block-size: var(--db-switch-thumb-size);
17740
+ border: var(--db-switch-border-width) solid;
17741
+ border-color: inherit;
17742
+ border-radius: var(--db-switch-border-radius);
17743
+ background-color: var(--db-switch-thumb);
17744
+ box-sizing: border-box;
17745
+ transform: translateY(-50%);
17746
+ transition: transform var(--db-switch-transition-duration) ease;
17381
17747
  }
17382
- .db-btn-icon-only:where(.db-btn-small) {
17383
- width: var(--db-btn-height-small);
17748
+
17749
+ /* `<label for>` propagates `:hover` to the input, so scope hover/press to the control, not `input:hover`. */
17750
+ .db-switch:hover input:not(:disabled):not(:checked) {
17751
+ background-color: var(--db-switch-surface-hover);
17752
+ border-color: var(--db-switch-border-hover);
17384
17753
  }
17385
- .db-btn-icon-only.db-btn-small {
17386
- line-height: var(--db-btn-line-height-small) !important;
17754
+
17755
+ .db-switch:active input:not(:disabled):not(:checked) {
17756
+ background-color: var(--db-switch-surface-active);
17757
+ border-color: var(--db-switch-border-active);
17387
17758
  }
17388
- .db-btn-icon-only:hover:not(:disabled, .db-btn-loading) {
17389
- background-color: var(--db-btn-surface-hover) !important;
17390
- color: var(--db-btn-text-hover) !important;
17759
+
17760
+ .db-switch input:checked {
17761
+ background-color: var(--db-switch-surface-checked);
17762
+ border-color: var(--db-switch-border-checked);
17391
17763
  }
17392
- .db-btn-icon-only:active:not(:disabled, .db-btn-loading) {
17393
- background-color: var(--db-btn-surface-press) !important;
17394
- color: var(--db-btn-text-press) !important;
17764
+
17765
+ .db-switch:hover input:checked:not(:disabled) {
17766
+ background-color: var(--db-switch-surface-checked-hover);
17767
+ border-color: var(--db-switch-border-checked-hover);
17395
17768
  }
17396
- .db-btn-icon-only:enabled:not(.db-btn-primary, .db-btn-tertiary, .db-btn-link, .db-btn-danger, :hover, :active)
17397
- > .anticon {
17398
- color: var(--db-btn-icon-glyph);
17769
+
17770
+ .db-switch:active input:checked:not(:disabled) {
17771
+ background-color: var(--db-switch-surface-checked-active);
17772
+ border-color: var(--db-switch-border-checked-active);
17399
17773
  }
17400
- .db-btn-icon-only.db-btn-danger:not(:hover, :active) > .anticon {
17401
- color: var(--db-btn-text);
17774
+
17775
+ .db-switch input:checked::before {
17776
+ transform: translateY(-50%) translateX(calc(var(--db-switch-track-width) - var(--db-switch-thumb-size)));
17402
17777
  }
17403
- .db-btn-icon-only.db-btn-danger:hover:not(:disabled, .db-btn-loading) {
17404
- background-color: var(--db-btn-surface-hover) !important;
17405
- color: var(--db-btn-text-hover) !important;
17778
+
17779
+ /* On focus, recolor the border to the checked-fill color on top of the outline ring. Matching AntD,
17780
+ * the ring sits flush when off and gains a 1px offset when checked.
17781
+ * TODO: width/offset are hardcoded 1px to preserve the AntD look; adopt the system 2px focus ring
17782
+ * (var(--db-focus-ring-width)) in a separate change. */
17783
+ .db-switch input:focus-visible {
17784
+ border-color: var(--db-switch-border-focus);
17785
+ outline: 1px solid var(--db-focus-ring-color);
17786
+ outline-offset: 0;
17406
17787
  }
17407
- .db-btn-icon-only.db-btn-danger:active:not(:disabled, .db-btn-loading) {
17408
- background-color: var(--db-btn-surface-press) !important;
17409
- color: var(--db-btn-text-press) !important;
17788
+
17789
+ .db-switch input:checked:focus-visible {
17790
+ outline-offset: 1px;
17410
17791
  }
17411
17792
 
17412
- /* ── 9. Disabled — base rules paint the per-variant `*-disabled` tokens (see tokens.css) ──────── */
17413
- .db-btn:disabled {
17793
+ .db-switch input:disabled {
17414
17794
  cursor: not-allowed;
17415
- background-color: var(--db-btn-surface-disabled) !important;
17416
- color: var(--db-btn-text-disabled) !important;
17417
- }
17418
- /* Empty and loading disabled buttons keep a transparent border (set elsewhere), so exclude them.
17419
- * `:where()` on the exclusion holds this at (0,2,0): enough to beat the base border, while still
17420
- * letting a consumer's own `[disabled]` border override win on source order. */
17421
- .db-btn:disabled:where(:not(.db-btn-empty, .db-btn-loading)) {
17422
- border-color: var(--db-btn-border-disabled) !important;
17423
- }
17424
- .db-btn-icon-only:disabled {
17425
- border: none;
17426
- }
17427
- .db-btn-icon-only:disabled:hover {
17428
- background-color: transparent !important;
17429
- }
17430
- .db-btn:disabled > .anticon {
17431
- color: inherit !important;
17795
+ background-color: var(--db-switch-surface-disabled);
17796
+ border-color: var(--db-switch-border-disabled);
17432
17797
  }
17433
17798
 
17434
- /* ── 10. Icon & content layout ───────────────────────────────────────────── */
17435
- /* The leading icon renders unwrapped as the button's direct `.anticon` child (see NativeButton.tsx),
17436
- * so `> .anticon` targets only a real icon glyph — a non-glyph `icon` (e.g. a Spinner) keeps its own
17437
- * sizing — and out-specifies AntD's runtime-injected global `.anticon` rule. */
17438
- .db-btn > .anticon,
17439
- .db-btn-end-icon,
17440
- .db-btn-end-icon > .anticon {
17441
- display: inline-flex;
17442
- align-items: center;
17443
- font-size: var(--db-btn-icon-size) !important;
17444
- line-height: 0;
17445
- }
17446
- .db-btn-small > .anticon .anticon {
17447
- line-height: 0;
17799
+ /* Disabled + on: more specific than the plain disabled rule, so it wins regardless of source order. */
17800
+ .db-switch input:checked:disabled {
17801
+ background-color: var(--db-switch-surface-checked-disabled);
17802
+ border-color: var(--db-switch-surface-checked-disabled);
17448
17803
  }
17449
- .db-btn > .db-btn-content > .anticon {
17450
- display: inline-flex;
17451
- align-items: center;
17452
- line-height: 0;
17804
+
17805
+ @media (prefers-reduced-motion: reduce) {
17806
+ .db-switch input,
17807
+ .db-switch input::before {
17808
+ transition: none;
17809
+ }
17453
17810
  }
17454
- .db-btn-content {
17455
- display: inline-flex;
17811
+
17812
+ .db-switch-wrapper {
17813
+ display: flex;
17456
17814
  align-items: center;
17457
17815
  }
17458
- .db-btn > .anticon + .db-btn-content {
17459
- margin-inline-start: var(--db-btn-gap) !important;
17460
- }
17461
- .db-btn-end-icon {
17462
- margin-inline-start: var(--db-btn-end-icon-gap) !important;
17463
- }
17464
- .db-btn-small .db-btn-end-icon {
17465
- margin-inline-start: var(--db-btn-end-icon-gap-small) !important;
17466
- }
17467
- .db-btn-default:not(.db-btn-danger, [href], :hover, :active, :disabled) > .anticon {
17468
- color: var(--db-btn-icon-glyph);
17469
- }
17470
- .db-btn-default[href]:not(.db-btn-danger, :hover, :active, :disabled) .db-btn-end-icon {
17471
- color: var(--db-btn-icon-glyph);
17472
- }
17473
- .db-btn-default:not([href], .db-btn-danger, :hover, :active, :disabled)
17474
- > .db-btn-content
17475
- > .db-btn-end-icon
17476
- > .anticon {
17477
- color: var(--db-btn-icon-glyph);
17478
- }
17479
17816
 
17480
- /* ── 11. Loading ─────────────────────────────────────────────────────────── */
17481
- .db-btn-loading {
17482
- cursor: default;
17483
- }
17484
- .db-btn-loading:disabled {
17485
- border-color: transparent !important;
17486
- }
17487
- .db-btn-spinner {
17488
- color: inherit !important;
17489
- pointer-events: none;
17817
+ .db-switch-wrapper .db-switch-state-message {
17818
+ margin-inline-start: auto;
17819
+ margin-inline-end: var(--db-switch-label-spacing);
17490
17820
  }
17491
- .db-btn-loading-with-icon .db-btn-content {
17492
- padding-inline-start: calc(var(--db-btn-icon-size) + var(--db-btn-gap));
17821
+
17822
+ /* `:first-child` scopes this to the label-first layout; the plain layout renders the label last. */
17823
+ .db-switch-wrapper > .du-bois-light-label:first-child,
17824
+ .db-switch-wrapper > .du-bois-dark-label:first-child {
17825
+ margin-inline-end: var(--db-switch-label-spacing);
17493
17826
  }
17494
- .db-btn-loading .db-btn-spinner {
17495
- position: absolute;
17496
- left: 50%;
17497
- top: 50%;
17498
- transform: translate(-50%, -50%);
17827
+
17828
+ .db-switch-state-message {
17829
+ color: var(--db-switch-text);
17499
17830
  }
17500
- .db-btn-loading.db-btn-icon-only .db-btn-spinner {
17501
- position: static;
17502
- transform: none;
17831
+
17832
+ /* Target the <Label>'s class, not a bare `label` — that would also match the .db-switch root <label>. */
17833
+ .db-switch-wrapper:has(input:disabled) .du-bois-light-label,
17834
+ .db-switch-wrapper:has(input:disabled) .du-bois-dark-label {
17835
+ color: var(--db-switch-text-disabled);
17503
17836
  }
17504
17837
 
17505
- /* ── 12. dangerouslySetForceIconStyles ───────────────────────────────────── */
17506
- .db-btn-force-icon {
17507
- width: var(--db-btn-height) !important;
17508
- color: var(--db-btn-icon-glyph) !important;
17838
+ /* Indent a following hint/message past the track. The class is doubled to out-specify FormMessage's
17839
+ * own margin rule. Each rule's key (rightmost) selector is a single class, not a multi-item :is(),
17840
+ * so Chromium can hash-reject it instead of attempting the match against every element on every
17841
+ * recalc (the leading/middle :is() groups aren't the key selector, so they're left as-is). */
17842
+ :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-hint, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-hint, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17843
+ + :is(.du-bois-light-hint, .du-bois-dark-hint)
17844
+ + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17845
+ + :is(.du-bois-light-hint, .du-bois-dark-hint)
17846
+ + .du-bois-dark-form-message {
17847
+ padding-inline-start: calc(var(--db-space-2) + var(--db-switch-track-width));
17509
17848
  }
17510
- .db-btn-force-icon.db-btn-small {
17511
- width: var(--db-btn-height-small) !important;
17849
+
17850
+ :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper) + .du-bois-dark-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17851
+ + :is(.du-bois-light-hint, .du-bois-dark-hint)
17852
+ + .du-bois-light-form-message, :is(.db-switch.db-switch, .db-switch-wrapper.db-switch-wrapper)
17853
+ + :is(.du-bois-light-hint, .du-bois-dark-hint)
17854
+ + .du-bois-dark-form-message {
17855
+ margin-top: 0;
17512
17856
  }
17513
17857
 
17858
+ /* Component tokens for Tabs (no family). Re-declared under the theme classes, not only :root, so the
17859
+ * var() chain re-resolves inside a .du-bois-dark subtree instead of freezing at the light value. */
17514
17860
  :where(:root), :where(.du-bois-light) {
17515
- /* Shape */
17516
- /* Must not scale with density; no non-scaling system token holds it. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17517
- --db-navigation-menu-indicator-thickness: 4px;
17518
- /* No system token holds this control-specific inset. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17519
- --db-navigation-menu-focus-ring-offset: -1px;
17861
+ /* ── Density ──────────────────────────────────────────────────────────── */
17862
+ --db-tabs-height: var(--db-size-8);
17863
+ --db-tabs-trigger-min-width: var(--db-size-6);
17864
+ --db-tabs-trigger-min-width-closable: var(--db-size-10);
17865
+ --db-tabs-list-margin-bottom: var(--db-space-4);
17866
+ --db-tabs-trigger-gap: var(--db-space-4);
17867
+ --db-tabs-trigger-padding-top: var(--db-space-1);
17868
+ --db-tabs-trigger-padding-bottom: var(--db-space-2);
17869
+ --db-tabs-close-icon-gap: var(--db-space-1);
17870
+ --db-tabs-add-button-margin-top: var(--db-space-0-5);
17871
+ /* No 1.5-unit space step exists; synthesize 6px from the nearest. TODO: use a direct step if one lands. */
17872
+ --db-tabs-add-button-margin-bottom: calc(var(--db-space-1) * 1.5);
17520
17873
 
17521
- /* Color */
17522
- --db-navigation-menu-indicator: var(--db-accent-surface);
17523
- --db-navigation-menu-focus-ring: var(--db-focus-ring-color);
17524
- --db-navigation-menu-text: var(--db-text-subtle);
17525
- --db-navigation-menu-text-active: var(--db-text-base);
17526
- /* No v2 semantic for default-action link text yet; bridges on the v1 --db-color-* token. */
17527
- --db-navigation-menu-text-hover: var(--db-color-action-default-text-hover);
17528
- --db-navigation-menu-text-press: var(--db-color-action-default-text-press);
17874
+ /* ── Shape ────────────────────────────────────────────────────────────── */
17875
+ --db-tabs-scrollbar-radius: var(--db-shape-control);
17876
+ --db-tabs-focus-ring-width: var(--db-focus-ring-width);
17529
17877
 
17530
- /* Density */
17531
- --db-navigation-menu-item-height: var(--db-size-8);
17532
- --db-navigation-menu-item-min-width: var(--db-size-6);
17533
- --db-navigation-menu-link-padding-top: var(--db-space-1);
17534
- --db-navigation-menu-link-padding-bottom: var(--db-space-2);
17535
- --db-navigation-menu-focus-ring-width: var(--db-focus-ring-width);
17878
+ /* ── Typography ───────────────────────────────────────────────────────── */
17879
+ --db-tabs-font-size: var(--db-font-size-base);
17880
+ --db-tabs-font-weight: var(--db-font-weight-semibold);
17881
+ /* No v2 token holds the tab line-height yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17882
+ --db-tabs-line-height: 1.25rem;
17883
+
17884
+ /* ── Color ────────────────────────────────────────────────────────────── */
17885
+ /* No v2 token holds the tab text / hover / press / active / disabled colors yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17886
+ --db-tabs-text: var(--db-color-text-secondary);
17887
+ --db-tabs-text-hover: var(--db-color-action-default-text-hover);
17888
+ --db-tabs-text-press: var(--db-color-action-default-text-press);
17889
+ --db-tabs-text-active: var(--db-color-text-primary);
17890
+ --db-tabs-text-disabled: var(--db-color-action-disabled-text);
17891
+ /* No v2 token holds the tab panel content text color yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17892
+ --db-tabs-content-text: var(--db-color-text-primary);
17893
+ /* No v2 token holds the active-tab underline yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17894
+ --db-tabs-indicator: var(--db-color-action-primary-background-default);
17895
+ /* No v2 token holds the list divider yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17896
+ --db-tabs-border: var(--db-color-action-default-border-default);
17897
+ /* No v2 token holds the focus ring color yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17898
+ --db-tabs-focus-ring: var(--db-color-action-default-border-focus);
17536
17899
  }
17537
17900
 
17901
+ /* No v2 token holds the scrollbar thumb yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17538
17902
  :where(:root), :where(.du-bois-light) {
17539
- /* Shape */
17540
- /* TODO(FEINF-6505): adopt --db-shape-container-* once a 4px container radius exists; kept on --db-radius-sm to preserve 4px. */
17541
- --db-snackbar-border-radius: var(--db-radius-sm);
17903
+ --db-tabs-scrollbar-thumb: rgba(17, 23, 28, 0.2);
17904
+ --db-tabs-scrollbar-thumb-hover: rgba(17, 23, 28, 0.3);
17905
+ }
17542
17906
 
17543
- /* Density */
17544
- --db-snackbar-spacing-vertical: var(--db-space-3);
17545
- --db-snackbar-spacing-horizontal: var(--db-space-4);
17546
- --db-snackbar-close-button-spacing: var(--db-space-2);
17547
17907
 
17548
- /* Color */
17549
- /* No v1/v2 system token holds this theme-invariant white. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17550
- --db-snackbar-text: #ffffff;
17551
- /* No v1/v2 system token holds these theme-invariant close-icon colors. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17552
- --db-snackbar-close-icon: #e8ecf0;
17553
- --db-snackbar-close-icon-hover: #8acaff;
17554
- --db-snackbar-close-icon-press: #bae1fc;
17555
- }
17556
17908
 
17557
17909
  :where(:root), :where(.du-bois-light) {
17910
+ /* Shape */
17911
+ --db-toolbar-border-radius: var(--db-shape-control);
17912
+
17558
17913
  /* Color */
17559
- /* No v1/v2 system token holds this light/dark surface pair. TODO(FEINF-6505): adopt a v2 system token when one is added. */
17560
- --db-snackbar-surface: #11171c;
17914
+ --db-toolbar-surface: var(--db-surface-subtle);
17915
+ --db-toolbar-border: var(--db-border-base);
17916
+ --db-toolbar-separator-surface: var(--db-border-base);
17917
+ --db-toolbar-toggle-item-text: var(--db-text-base);
17918
+ /* No v2 token holds the toggle hover / press text yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17919
+ --db-toolbar-toggle-item-text-hover: var(--db-color-action-default-text-hover);
17920
+ --db-toolbar-toggle-item-text-press: var(--db-color-action-default-text-press);
17921
+ --db-toolbar-shadow: var(--db-elevation-med);
17561
17922
 
17562
- /* Elevation */
17563
- --db-snackbar-shadow: var(--db-elevation-med);
17923
+ /* Density */
17924
+ --db-toolbar-gap: var(--db-space-4);
17925
+ --db-toolbar-spacing: var(--db-space-2);
17564
17926
  }
17565
17927
 
17566
17928
  /* Tooltip renders an inverted surface (dark chip in light mode, light chip in dark mode), so it does
@@ -17587,38 +17949,9 @@ span.du-bois-light-typography-ellipsis-single-line {
17587
17949
  /* v1 blue primitives, held as literals until a v2 link-on-tooltip semantic exists. */
17588
17950
  --db-tooltip-link-text: #4299e0; /* blue500 */
17589
17951
  --db-tooltip-link-text-hover: #8acaff; /* blue400 */
17590
- --db-tooltip-link-text-press: #bae1fc; /* blue300 */
17952
+ --db-tooltip-link-text-active: #bae1fc; /* blue300 */
17591
17953
 
17592
17954
  /* Elevation */
17593
17955
  --db-tooltip-shadow: var(--db-elevation-med);
17594
17956
  }
17595
17957
 
17596
- /* Declared under the theme classes too (not just :root) so the var() chain re-resolves inside a
17597
- * `.du-bois-dark` subtree instead of freezing at the light values. */
17598
- :where(:root), :where(.du-bois-light) {
17599
- /* Shape */
17600
- --db-progress-border-radius: var(--db-shape-pill);
17601
-
17602
- /* Color */
17603
- --db-progress-track: var(--db-color-progress-track);
17604
- --db-progress-fill: var(--db-color-progress-fill);
17605
-
17606
- /* Density */
17607
- --db-progress-size: var(--db-size-2);
17608
-
17609
- /* Motion */
17610
- /* No v2 motion token holds this duration. TODO(FEINF-6505): adopt a v2 motion token when one is added. */
17611
- --db-progress-transition-duration: 300ms;
17612
- }
17613
-
17614
- :where(:root), :where(.du-bois-light) {
17615
- /* Color */
17616
- /* No v2 token holds tertiary action-text states yet. TODO(FEINF-6505): adopt a v2 token when one lands. */
17617
- --db-overflow-trigger-text: var(--db-color-action-tertiary-text-default);
17618
- --db-overflow-trigger-text-hover: var(--db-color-action-tertiary-text-hover);
17619
- --db-overflow-trigger-text-press: var(--db-color-action-tertiary-text-press);
17620
-
17621
- /* Density */
17622
- --db-overflow-spacing: var(--db-space-2);
17623
- --db-overflow-menu-spacing: var(--db-space-1);
17624
- }