canny-ui 6.7.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/canny-ui.min.css +1 -1
  2. package/dist/table-resize.js +16 -1
  3. package/package.json +1 -1
  4. package/src/_utilities.scss +4 -4
  5. package/src/base/_motion.scss +32 -0
  6. package/src/base/_reset.scss +7 -0
  7. package/src/base/_scrollbar-mixin.scss +84 -0
  8. package/src/base/_touch.scss +60 -0
  9. package/src/canny-ui.scss +5 -0
  10. package/src/components/_accordion.scss +10 -1
  11. package/src/components/_alerts.scss +18 -8
  12. package/src/components/_autocomplete.scss +16 -10
  13. package/src/components/_avatar.scss +22 -1
  14. package/src/components/_breadcrumb.scss +36 -2
  15. package/src/components/_buttons.scss +35 -24
  16. package/src/components/_charts-extra.scss +432 -0
  17. package/src/components/_charts.scss +90 -0
  18. package/src/components/_chip.scss +8 -7
  19. package/src/components/_command-palette.scss +27 -5
  20. package/src/components/_confirm.scss +14 -0
  21. package/src/components/_datatable.scss +383 -53
  22. package/src/components/_datepicker.scss +44 -21
  23. package/src/components/_dialog.scss +12 -0
  24. package/src/components/_drawer.scss +12 -0
  25. package/src/components/_fields.scss +115 -27
  26. package/src/components/_forms.scss +28 -10
  27. package/src/components/_gantt.scss +554 -0
  28. package/src/components/_inputs.scss +12 -7
  29. package/src/components/_list.scss +37 -3
  30. package/src/components/_menu.scss +23 -9
  31. package/src/components/_navbar.scss +98 -7
  32. package/src/components/_pagination.scss +105 -2
  33. package/src/components/_panels.scss +19 -5
  34. package/src/components/_popover.scss +3 -5
  35. package/src/components/_progress.scss +31 -0
  36. package/src/components/_scrollbar.scss +16 -159
  37. package/src/components/_sidebar.scss +24 -20
  38. package/src/components/_skeleton.scss +40 -1
  39. package/src/components/_slider.scss +8 -2
  40. package/src/components/_stepper.scss +22 -4
  41. package/src/components/_table.scss +23 -3
  42. package/src/components/_tabs.scss +120 -4
  43. package/src/components/_timeline.scss +26 -0
  44. package/src/components/_toast.scss +24 -12
  45. package/src/components/_tooltip-radix.scss +25 -4
  46. package/src/components/_tree.scss +51 -3
  47. package/src/components/_uploader.scss +97 -1
  48. package/src/scripts/table-resize.js +16 -1
  49. package/src/tokens/_semantic.scss +85 -21
@@ -40,9 +40,24 @@
40
40
  if (!resizer) {
41
41
  resizer = document.createElement('span');
42
42
  resizer.className = 'th-resizer';
43
- resizer.setAttribute('aria-hidden', 'true');
44
43
  th.appendChild(resizer);
45
44
  }
45
+ // keyboard-operable separator: Left / Right resize the column (Shift = bigger steps)
46
+ resizer.setAttribute('role', 'separator');
47
+ resizer.setAttribute('aria-orientation', 'vertical');
48
+ resizer.setAttribute('aria-label', 'Resize ' + (th.textContent || '').trim() + ' column');
49
+ resizer.setAttribute('aria-valuemin', String(MIN_WIDTH));
50
+ resizer.setAttribute('tabindex', '0');
51
+
52
+ function keyDown(e) {
53
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
54
+ e.preventDefault();
55
+ var step = (e.shiftKey ? 40 : 10) * (e.key === 'ArrowRight' ? 1 : -1);
56
+ var next = Math.max(MIN_WIDTH, th.offsetWidth + step);
57
+ th.style.width = next + 'px';
58
+ resizer.setAttribute('aria-valuenow', String(next));
59
+ }
60
+ resizer.addEventListener('keydown', keyDown);
46
61
 
47
62
  function pointerDown(e) {
48
63
  e.preventDefault();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canny-ui",
3
- "version": "6.7.0",
3
+ "version": "7.0.0",
4
4
  "description": "Pure CSS foundation for the Canny-UI design system — tokens, layout utilities, and a battery of enterprise components, shipped as one flat stylesheet.",
5
5
  "main": "dist/canny-ui.min.css",
6
6
  "style": "dist/canny-ui.min.css",
@@ -123,10 +123,10 @@ $spaces: (
123
123
  .text-secondary { color: var(--text-secondary); }
124
124
  .text-muted { color: var(--text-muted); }
125
125
  .text-accent { color: var(--text-accent); }
126
- .text-danger { color: var(--color-danger); }
127
- .text-success { color: var(--color-success); }
128
- .text-warning { color: var(--color-warning); }
129
- .text-info { color: var(--color-info); }
126
+ .text-danger { color: var(--color-danger-text); }
127
+ .text-success { color: var(--color-success-text); }
128
+ .text-warning { color: var(--color-warning-text); }
129
+ .text-info { color: var(--color-info-text); }
130
130
 
131
131
  // ── Primitives ───────────────────────────────────────────────────────────
132
132
  .cny-label {
@@ -0,0 +1,32 @@
1
+ // ── Reduced motion ───────────────────────────────────────────────
2
+ // One global opt-out for every Canny-UI surface. Durations collapse to ~0
3
+ // (not `none`) so `transitionend` / `animationend` still fire and state
4
+ // still lands on its final value. Component-level rules keep working.
5
+ @media (prefers-reduced-motion: reduce) {
6
+ [class*='cny-'],
7
+ [class*='cny-']::before,
8
+ [class*='cny-']::after,
9
+ .btn,
10
+ .btn::before,
11
+ .btn::after,
12
+ .btn-ripple,
13
+ .checkbox,
14
+ .radio,
15
+ .switch,
16
+ .switch::before,
17
+ .switch-thumb,
18
+ .input {
19
+ animation-duration: 0.01ms !important;
20
+ animation-delay: 0ms !important;
21
+ animation-iteration-count: 1 !important;
22
+ transition-duration: 0.01ms !important;
23
+ transition-delay: 0ms !important;
24
+ scroll-behavior: auto !important;
25
+ }
26
+
27
+ // Scrollbar thumb transitions (see scrollbar-mixin) — pseudo-elements
28
+ // on any scroll container, so they can't be matched by a class prefix.
29
+ *::-webkit-scrollbar-thumb {
30
+ transition-duration: 0.01ms !important;
31
+ }
32
+ }
@@ -14,3 +14,10 @@ body {
14
14
  background: var(--bg-base);
15
15
  color: var(--text-primary);
16
16
  }
17
+
18
+ // Themed text selection — readable in light and dark (the browser default is a
19
+ // fixed blue that clashes with dark surfaces).
20
+ ::selection {
21
+ background: color-mix(in srgb, var(--color-primary) 32%, transparent);
22
+ color: inherit;
23
+ }
@@ -0,0 +1,84 @@
1
+ // ── Expand-on-hover scrollbar ────────────────────────────────────
2
+ // The one scrollbar look used across Canny-UI. A hairline pill at rest that
3
+ // fades back while the container is idle, brightens when the container is
4
+ // hovered, and swells to a chunky grab handle when the
5
+ // pointer reaches the scrollbar itself.
6
+ //
7
+ // Built on ::-webkit-scrollbar, which Chromium ignores on any element whose
8
+ // scrollbar-width / scrollbar-color is not `auto` — so both are reset here.
9
+ // Firefox has no pseudo-element styling; it gets the standard thin scrollbar.
10
+ //
11
+ // Geometry: the 14px gutter is the grab target; the transparent border is
12
+ // what's subtracted from it to get the visible pill. The pill is anchored to
13
+ // the outer edge with a 2px margin: 14 − 8 − 2 = 4px at rest, and the inner
14
+ // border shrinks to 2px on hover, so it grows inward to 14 − 2 − 2 = 10px.
15
+ @mixin expand-scrollbar {
16
+ scrollbar-width: auto;
17
+ scrollbar-color: auto;
18
+
19
+ @supports (-moz-appearance: none) {
20
+ scrollbar-width: thin;
21
+ scrollbar-color: var(--border-color-strong) transparent;
22
+ }
23
+
24
+ &::-webkit-scrollbar {
25
+ width: 14px;
26
+ height: 14px;
27
+ background: transparent;
28
+ }
29
+
30
+ &::-webkit-scrollbar-corner {
31
+ background: transparent;
32
+ }
33
+
34
+ &::-webkit-scrollbar-track {
35
+ background: transparent;
36
+ }
37
+
38
+ &::-webkit-scrollbar-thumb {
39
+ border: 0 solid transparent;
40
+ border-radius: var(--radius-full);
41
+ background-clip: content-box;
42
+ background-color: color-mix(in srgb, var(--text-muted) 40%, transparent);
43
+ // Ease-out-quint: fast start, long soft landing — reads as a glide, not a snap.
44
+ transition:
45
+ border-width 0.32s cubic-bezier(0.22, 1, 0.36, 1),
46
+ background-color 0.24s ease-out;
47
+ }
48
+
49
+ // The pill hugs the outer edge (right for vertical, bottom for horizontal)
50
+ // and grows inward: only the inner border shrinks on hover.
51
+ &::-webkit-scrollbar-thumb:vertical {
52
+ border-width: 3px 2px 3px 8px;
53
+ }
54
+
55
+ &::-webkit-scrollbar-thumb:horizontal {
56
+ border-width: 8px 3px 2px;
57
+ }
58
+
59
+ // Container is hovered or being keyboard-scrolled → the pill firms up.
60
+ &:hover::-webkit-scrollbar-thumb,
61
+ &:focus-within::-webkit-scrollbar-thumb {
62
+ background-color: color-mix(in srgb, var(--text-muted) 65%, transparent);
63
+ }
64
+
65
+ // Pointer on the scrollbar → the thumb swells and darkens.
66
+ &::-webkit-scrollbar-thumb:vertical:hover,
67
+ &::-webkit-scrollbar-thumb:vertical:active {
68
+ border-left-width: 2px;
69
+ }
70
+
71
+ &::-webkit-scrollbar-thumb:horizontal:hover,
72
+ &::-webkit-scrollbar-thumb:horizontal:active {
73
+ border-top-width: 2px;
74
+ }
75
+
76
+ &::-webkit-scrollbar-thumb:hover,
77
+ &::-webkit-scrollbar-thumb:active {
78
+ background-color: var(--text-muted);
79
+ }
80
+
81
+ &::-webkit-scrollbar-thumb:active {
82
+ background-color: var(--text-secondary);
83
+ }
84
+ }
@@ -0,0 +1,60 @@
1
+ // ── Touch targets ────────────────────────────────────────────────
2
+ // Small controls get an invisible >=44px hit area on coarse pointers only;
3
+ // visual size on desktop is untouched. Pseudo-element where the host allows
4
+ // it, a min-size where the host clips overflow (.btn).
5
+ @media (pointer: coarse) {
6
+ // Hit pseudo needs a positioned host.
7
+ .checkbox,
8
+ .radio,
9
+ .switch,
10
+ .cny-slider-thumb,
11
+ .cny-pagination-btn,
12
+ .cny-chip-delete,
13
+ .cny-chip-remove,
14
+ .cny-chip.is-interactive,
15
+ .cny-dialog-close,
16
+ .cny-drawer-close,
17
+ .alert-close,
18
+ .cny-select-clear,
19
+ .cny-ac-clear {
20
+ position: relative;
21
+ }
22
+
23
+ // .cny-input-action and .cny-toast-close are already positioned; they get only the pseudo below.
24
+
25
+ .checkbox,
26
+ .radio,
27
+ .switch,
28
+ .cny-slider-thumb,
29
+ .cny-pagination-btn,
30
+ .cny-chip-delete,
31
+ .cny-chip-remove,
32
+ .cny-chip.is-interactive,
33
+ .cny-input-action,
34
+ .cny-dialog-close,
35
+ .cny-drawer-close,
36
+ .alert-close,
37
+ .cny-toast-close,
38
+ .cny-select-clear,
39
+ .cny-multiselect-clear,
40
+ .cny-ac-clear {
41
+ &::after {
42
+ content: '';
43
+ position: absolute;
44
+ top: 50%;
45
+ left: 50%;
46
+ width: max(100%, 44px);
47
+ height: max(100%, 44px);
48
+ transform: translate(-50%, -50%);
49
+ }
50
+ }
51
+
52
+ // .btn clips overflow for the ripple — grow the box instead. Link buttons are inline text.
53
+ .btn:not(.link) { min-height: 44px; }
54
+ .btn.icon,
55
+ .btn.circle { min-width: 44px; }
56
+
57
+ // Neighbouring cells/rows would fight over an overlapping hit pseudo — grow the box.
58
+ .cny-datepicker-cell { height: 44px; }
59
+ .cny-menu-item { min-height: 44px; }
60
+ }
package/src/canny-ui.scss CHANGED
@@ -3,6 +3,7 @@
3
3
  @use 'tokens/semantic';
4
4
  @use 'base/reset';
5
5
  @use 'base/typography';
6
+ @use 'base/motion';
6
7
  @use 'components/buttons';
7
8
  @use 'components/inputs';
8
9
  @use 'components/cards';
@@ -35,6 +36,8 @@
35
36
  @use 'components/panels';
36
37
  @use 'components/datatable';
37
38
  @use 'components/charts';
39
+ @use 'components/gantt';
40
+ @use 'components/charts-extra';
38
41
  @use 'components/list';
39
42
  @use 'components/tabs';
40
43
  @use 'components/autocomplete';
@@ -43,4 +46,6 @@
43
46
  @use 'components/accordion';
44
47
  @use 'components/slider';
45
48
  @use 'components/stepper';
49
+ // Touch targets override component sizes, so they load after every component.
50
+ @use 'base/touch';
46
51
  @use 'utilities';
@@ -44,10 +44,19 @@
44
44
 
45
45
  &:hover { background: var(--bg-surface-low); }
46
46
  &:focus-visible {
47
- outline: 2px solid var(--color-primary);
47
+ outline: 2px solid var(--border-focus);
48
48
  outline-offset: -2px;
49
49
  }
50
50
 
51
+ &:disabled,
52
+ &[data-disabled] {
53
+ color: var(--text-disabled);
54
+ cursor: not-allowed;
55
+ &:hover { background: transparent; }
56
+ .cny-accordion-trigger-icon,
57
+ .cny-accordion-trigger-chevron { color: var(--text-disabled); }
58
+ }
59
+
51
60
  &-icon {
52
61
  display: inline-flex;
53
62
  align-items: center;
@@ -8,6 +8,10 @@
8
8
  to { opacity: 1; transform: translateY(0); }
9
9
  }
10
10
 
11
+ @keyframes alert-exit {
12
+ to { opacity: 0; transform: translateY(-4px) scale(0.98); }
13
+ }
14
+
11
15
  .alert {
12
16
  --alert-accent: var(--color-info);
13
17
  --alert-accent-soft: var(--color-info-soft);
@@ -31,6 +35,12 @@
31
35
  animation: none;
32
36
  }
33
37
 
38
+ // dismiss: short fade-out before the consumer's onDismiss unmounts it
39
+ &.is-leaving {
40
+ animation: alert-exit 160ms ease-in forwards;
41
+ pointer-events: none;
42
+ }
43
+
34
44
  &-icon {
35
45
  flex: none;
36
46
  display: inline-flex;
@@ -91,7 +101,7 @@
91
101
  background: var(--bg-surface-low);
92
102
  }
93
103
  &:focus-visible {
94
- outline: 2px solid var(--alert-accent);
104
+ outline: 2px solid var(--border-focus);
95
105
  outline-offset: 1px;
96
106
  }
97
107
  }
@@ -103,7 +113,7 @@
103
113
  --alert-accent-on: var(--color-info-on);
104
114
  background: var(--color-info-soft);
105
115
  border-color: color-mix(in srgb, var(--color-info) 22%, transparent);
106
- .alert-title { color: var(--color-info); }
116
+ .alert-title { color: var(--color-info-text); }
107
117
  }
108
118
  &.success {
109
119
  --alert-accent: var(--color-success);
@@ -111,7 +121,7 @@
111
121
  --alert-accent-on: var(--color-success-on);
112
122
  background: var(--color-success-soft);
113
123
  border-color: color-mix(in srgb, var(--color-success) 22%, transparent);
114
- .alert-title { color: var(--color-success); }
124
+ .alert-title { color: var(--color-success-text); }
115
125
  }
116
126
  &.warning {
117
127
  --alert-accent: var(--color-warning);
@@ -119,7 +129,7 @@
119
129
  --alert-accent-on: var(--color-warning-on);
120
130
  background: var(--color-warning-soft);
121
131
  border-color: color-mix(in srgb, var(--color-warning) 24%, transparent);
122
- .alert-title { color: var(--color-warning); }
132
+ .alert-title { color: var(--color-warning-text); }
123
133
  }
124
134
  &.danger {
125
135
  --alert-accent: var(--color-danger);
@@ -127,7 +137,7 @@
127
137
  --alert-accent-on: var(--color-danger-on);
128
138
  background: var(--color-danger-soft);
129
139
  border-color: color-mix(in srgb, var(--color-danger) 26%, transparent);
130
- .alert-title { color: var(--color-danger); }
140
+ .alert-title { color: var(--color-danger-text); }
131
141
  }
132
142
 
133
143
  // Solid variant — flat accent fill
@@ -140,15 +150,15 @@
140
150
  .alert-body { color: var(--alert-accent-on); opacity: 0.88; }
141
151
 
142
152
  .alert-icon {
143
- background: rgba(255, 255, 255, 0.18);
153
+ background: color-mix(in srgb, var(--alert-accent-on) 18%, transparent);
144
154
  color: var(--alert-accent-on);
145
155
  }
146
156
 
147
157
  .alert-close {
148
158
  color: var(--alert-accent-on);
149
159
  opacity: 0.78;
150
- &:hover { opacity: 1; background: rgba(255, 255, 255, 0.14); }
151
- &:focus-visible { outline-color: rgba(255, 255, 255, 0.85); }
160
+ &:hover { opacity: 1; background: color-mix(in srgb, var(--alert-accent-on) 14%, transparent); }
161
+ &:focus-visible { outline-color: var(--alert-accent-on); }
152
162
  }
153
163
  }
154
164
  }
@@ -1,3 +1,4 @@
1
+ @use '../base/scrollbar-mixin' as sb;
1
2
  @use '../tokens/primitives' as *;
2
3
 
3
4
  // Autocomplete — searchable combobox built on cmdk + Radix Popover.
@@ -18,7 +19,7 @@
18
19
  cursor: pointer;
19
20
  padding: 0 $space-3;
20
21
 
21
- &.is-open { border-color: var(--border-focus); }
22
+ &.is-open { border-color: var(--border-input-focus); }
22
23
  &[disabled] { cursor: not-allowed; opacity: 0.6; }
23
24
  }
24
25
 
@@ -49,8 +50,8 @@
49
50
 
50
51
  &:hover { color: var(--text-primary); background: var(--bg-surface-low); }
51
52
  &:focus-visible {
52
- outline: none;
53
- box-shadow: 0 0 0 var(--ring-width) var(--ring-color);
53
+ outline: var(--ring-width) solid var(--ring-color);
54
+ outline-offset: var(--ring-offset);
54
55
  }
55
56
  }
56
57
 
@@ -61,13 +62,11 @@
61
62
 
62
63
  // ── Popover content ──────────────────────────────────────────
63
64
  .cny-ac-content {
64
- z-index: 1010;
65
+ z-index: var(--z-popover, 1010);
65
66
  background: var(--bg-surface-lowest);
66
- border: 1px solid var(--border-color);
67
+ border: 1px solid var(--border-color-strong);
67
68
  border-radius: var(--radius-sm);
68
- box-shadow:
69
- 0 1px 0 rgba(15, 23, 42, 0.04),
70
- 0 12px 28px -12px rgba(15, 23, 42, 0.22);
69
+ box-shadow: var(--shadow-popover);
71
70
  overflow: hidden;
72
71
  font-family: var(--font-body);
73
72
  animation: cny-ac-in 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
@@ -98,7 +97,7 @@
98
97
  overflow-y: auto;
99
98
  max-height: 280px;
100
99
  padding: 6px;
101
- scrollbar-width: thin;
100
+ @include sb.expand-scrollbar;
102
101
  }
103
102
 
104
103
  .cny-ac-empty,
@@ -164,7 +163,7 @@
164
163
  justify-content: center;
165
164
  width: 16px;
166
165
  height: 16px;
167
- color: var(--color-primary);
166
+ color: var(--text-accent);
168
167
  }
169
168
 
170
169
  .cny-ac-checkbox {
@@ -190,3 +189,10 @@
190
189
  .cny-ac-content { animation: none; }
191
190
  .cny-ac-loading-spinner { animation: none; }
192
191
  }
192
+
193
+ // Matched substring inside an option label (`highlightMatch`)
194
+ .cny-ac-mark {
195
+ background: transparent;
196
+ color: var(--text-accent);
197
+ font-weight: 600;
198
+ }
@@ -11,7 +11,7 @@
11
11
  width: 32px;
12
12
  height: 32px;
13
13
  background: var(--color-primary-soft);
14
- color: var(--color-primary);
14
+ color: var(--text-accent);
15
15
  border-radius: 50%;
16
16
  font-family: var(--font-body);
17
17
  font-size: 0.8125rem;
@@ -29,6 +29,21 @@
29
29
  border-radius: inherit;
30
30
  }
31
31
 
32
+ // Empty avatar (no image / name / children): a person glyph instead of a blank disc.
33
+ .avatar-fallback {
34
+ display: inline-flex;
35
+ width: 58%;
36
+ height: 58%;
37
+ color: inherit;
38
+ opacity: 0.8;
39
+
40
+ svg {
41
+ width: 100%;
42
+ height: 100%;
43
+ border-radius: 0;
44
+ }
45
+ }
46
+
32
47
  // Presence dot — absolutely positioned, escapes the circle by ~15%
33
48
  // and is ringed in the page surface color so it reads on any background.
34
49
  &.online::after,
@@ -90,3 +105,9 @@
90
105
  box-shadow: 0 0 0 2px var(--bg-surface-lowest);
91
106
  }
92
107
  }
108
+
109
+ // "+N" overflow avatar in an AvatarGroup
110
+ .avatar.avatar-overflow {
111
+ background: var(--bg-surface-low);
112
+ color: var(--text-muted);
113
+ }
@@ -33,8 +33,24 @@
33
33
  transition: color 120ms ease, background-color 120ms ease;
34
34
  &:hover { color: var(--text-primary); background: var(--bg-surface-low); }
35
35
  &:focus-visible {
36
- outline: 2px solid var(--color-primary);
37
- outline-offset: 1px;
36
+ outline: 2px solid var(--border-focus);
37
+ outline-offset: 2px;
38
+ }
39
+ }
40
+
41
+ &-ellipsis {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ padding: 2px 4px;
45
+ background: transparent;
46
+ border: 0;
47
+ border-radius: var(--radius-sm);
48
+ color: var(--text-secondary);
49
+ cursor: pointer;
50
+ &:hover { color: var(--text-primary); background: var(--bg-surface-low); }
51
+ &:focus-visible {
52
+ outline: 2px solid var(--border-focus);
53
+ outline-offset: 2px;
38
54
  }
39
55
  }
40
56
 
@@ -54,3 +70,21 @@
54
70
  .cny-breadcrumb-list { gap: 4px; }
55
71
  }
56
72
  }
73
+
74
+ // Coarse pointers: invisible >=44px hit area around the small link / ellipsis targets.
75
+ @media (pointer: coarse) {
76
+ .cny-breadcrumb-link,
77
+ .cny-breadcrumb-ellipsis {
78
+ position: relative;
79
+
80
+ &::after {
81
+ content: '';
82
+ position: absolute;
83
+ top: 50%;
84
+ left: 50%;
85
+ width: max(100%, 44px);
86
+ height: 44px;
87
+ transform: translate(-50%, -50%);
88
+ }
89
+ }
90
+ }
@@ -30,6 +30,8 @@
30
30
  cursor: pointer;
31
31
  user-select: none;
32
32
  white-space: nowrap;
33
+ max-width: 100%;
34
+ min-width: 0;
33
35
  -webkit-tap-highlight-color: transparent;
34
36
  transition:
35
37
  background-color var(--transition-fast),
@@ -45,8 +47,8 @@
45
47
  }
46
48
 
47
49
  &:focus-visible {
48
- outline: none;
49
- box-shadow: 0 0 0 var(--ring-width) var(--ring-color);
50
+ outline: var(--ring-width) solid var(--ring-color);
51
+ outline-offset: var(--ring-offset);
50
52
  }
51
53
 
52
54
  // ── Content wrapper — keeps children above the ripple layer ───────────
@@ -57,6 +59,15 @@
57
59
  align-items: center;
58
60
  justify-content: center;
59
61
  gap: inherit;
62
+ min-width: 0;
63
+ max-width: 100%;
64
+
65
+ // A long label truncates instead of overflowing the button.
66
+ > .btn-label {
67
+ min-width: 0;
68
+ overflow: hidden;
69
+ text-overflow: ellipsis;
70
+ }
60
71
 
61
72
  // An icon is a flex item here, so it inherits flex-shrink: 1 and squashes
62
73
  // out of square as soon as the label needs the room — a 15px lucide icon
@@ -144,10 +155,10 @@
144
155
  box-shadow: inset 0 0 0 1px var(--border-color-strong), inset 0 1px 0 0 rgba(0, 0, 0, 0.04);
145
156
  }
146
157
 
147
- &.danger { color: var(--color-danger); }
148
- &.warning { color: var(--color-warning); }
149
- &.success { color: var(--color-success); }
150
- &.info { color: var(--color-info); }
158
+ &.danger { color: var(--color-danger-text); }
159
+ &.warning { color: var(--color-warning-text); }
160
+ &.success { color: var(--color-success-text); }
161
+ &.info { color: var(--color-info-text); }
151
162
  }
152
163
 
153
164
  // ── Dashed — Ant Design-style, border intent-colors on hover ───────────
@@ -164,17 +175,17 @@
164
175
  &:hover:not(:disabled) {
165
176
  background: var(--bg-surface-low);
166
177
  border-color: var(--color-primary);
167
- color: var(--color-primary);
178
+ color: var(--text-accent);
168
179
  }
169
180
  &:active:not(:disabled) {
170
181
  background: var(--bg-surface-edge);
171
182
  border-color: var(--color-primary-active);
172
183
  }
173
184
 
174
- &.danger { &:hover:not(:disabled) { border-color: var(--color-danger); color: var(--color-danger); } }
175
- &.warning { &:hover:not(:disabled) { border-color: var(--color-warning); color: var(--color-warning); } }
176
- &.success { &:hover:not(:disabled) { border-color: var(--color-success); color: var(--color-success); } }
177
- &.info { &:hover:not(:disabled) { border-color: var(--color-info); color: var(--color-info); } }
185
+ &.danger { &:hover:not(:disabled) { border-color: var(--color-danger); color: var(--color-danger-text); } }
186
+ &.warning { &:hover:not(:disabled) { border-color: var(--color-warning); color: var(--color-warning-text); } }
187
+ &.success { &:hover:not(:disabled) { border-color: var(--color-success); color: var(--color-success-text); } }
188
+ &.info { &:hover:not(:disabled) { border-color: var(--color-info); color: var(--color-info-text); } }
178
189
  }
179
190
 
180
191
  // ── Ghost — text-only, tints on hover ──────────────────────────────────
@@ -185,23 +196,23 @@
185
196
  &:hover:not(:disabled) { background: var(--bg-surface-low); }
186
197
  &:active:not(:disabled) { background: var(--bg-surface-edge); }
187
198
 
188
- &.danger { color: var(--color-danger); &:hover:not(:disabled) { background: var(--color-danger-soft); } }
189
- &.warning { color: var(--color-warning); &:hover:not(:disabled) { background: var(--color-warning-soft); } }
190
- &.success { color: var(--color-success); &:hover:not(:disabled) { background: var(--color-success-soft); } }
191
- &.info { color: var(--color-info); &:hover:not(:disabled) { background: var(--color-info-soft); } }
199
+ &.danger { color: var(--color-danger-text); &:hover:not(:disabled) { background: var(--color-danger-soft); } }
200
+ &.warning { color: var(--color-warning-text); &:hover:not(:disabled) { background: var(--color-warning-soft); } }
201
+ &.success { color: var(--color-success-text); &:hover:not(:disabled) { background: var(--color-success-soft); } }
202
+ &.info { color: var(--color-info-text); &:hover:not(:disabled) { background: var(--color-info-soft); } }
192
203
  }
193
204
 
194
205
  // ── Soft — tonal fill, gentle presence ─────────────────────────────────
195
206
  &.soft {
196
207
  background: var(--color-primary-soft);
197
- color: var(--color-primary);
208
+ color: var(--text-accent);
198
209
 
199
210
  &:hover:not(:disabled) { background: var(--bg-surface-edge); }
200
211
 
201
- &.danger { background: var(--color-danger-soft); color: var(--color-danger); }
202
- &.warning { background: var(--color-warning-soft); color: var(--color-warning); }
203
- &.success { background: var(--color-success-soft); color: var(--color-success); }
204
- &.info { background: var(--color-info-soft); color: var(--color-info); }
212
+ &.danger { background: var(--color-danger-soft); color: var(--color-danger-text); }
213
+ &.warning { background: var(--color-warning-soft); color: var(--color-warning-text); }
214
+ &.success { background: var(--color-success-soft); color: var(--color-success-text); }
215
+ &.info { background: var(--color-info-soft); color: var(--color-info-text); }
205
216
  }
206
217
 
207
218
  // ── Link — inline text action (no ripple, no overflow clip) ────────────
@@ -216,7 +227,7 @@
216
227
  text-decoration-thickness: 1px;
217
228
  text-underline-offset: 0.2em;
218
229
 
219
- &:hover:not(:disabled) { color: var(--color-primary-hover); }
230
+ &:hover:not(:disabled) { color: var(--color-primary-active); }
220
231
 
221
232
  .btn-ripple { display: none; }
222
233
  }
@@ -269,10 +280,10 @@
269
280
  // ── Toggle (pressed) state ─ aria-pressed="true" driven by Radix Toggle
270
281
  &[aria-pressed='true'],
271
282
  &[data-state='on'] {
272
- &.ghost { background: var(--color-primary-soft); color: var(--color-primary); }
273
- &.outline { background: var(--color-primary-soft); color: var(--color-primary); box-shadow: inset 0 0 0 1px var(--color-primary), var(--shadow-xs); }
283
+ &.ghost { background: var(--color-primary-soft); color: var(--text-accent); }
284
+ &.outline { background: var(--color-primary-soft); color: var(--text-accent); box-shadow: inset 0 0 0 1px var(--color-primary), var(--shadow-xs); }
274
285
  &.soft { background: var(--color-primary); color: var(--color-primary-on); }
275
- &.dashed { background: var(--color-primary-soft); color: var(--color-primary); border-color: var(--color-primary); }
286
+ &.dashed { background: var(--color-primary-soft); color: var(--text-accent); border-color: var(--color-primary); }
276
287
  }
277
288
  }
278
289