@kahitsan/ksui 0.33.0 → 0.34.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 (60) hide show
  1. package/package.json +4 -1
  2. package/src/components/base/AccountAvatar.tsx +1 -1
  3. package/src/components/base/AddAttachmentTile.tsx +4 -4
  4. package/src/components/base/BadgeSelect.tsx +10 -10
  5. package/src/components/base/Button.tsx +70 -38
  6. package/src/components/base/CameraCapture.tsx +8 -8
  7. package/src/components/base/ChartLegend.tsx +3 -3
  8. package/src/components/base/CopyButton.tsx +1 -1
  9. package/src/components/base/DataTable.tsx +68 -72
  10. package/src/components/base/DatePicker.tsx +77 -82
  11. package/src/components/base/DateTile.tsx +7 -7
  12. package/src/components/base/DetailRow.tsx +2 -2
  13. package/src/components/base/Dropdown.tsx +15 -15
  14. package/src/components/base/ExistingAttachmentTile.tsx +6 -6
  15. package/src/components/base/EyebrowBadge.tsx +15 -15
  16. package/src/components/base/FormErrorBanner.tsx +1 -1
  17. package/src/components/base/FormField.tsx +2 -2
  18. package/src/components/base/ImageCropper.tsx +19 -19
  19. package/src/components/base/ImageViewer.tsx +7 -11
  20. package/src/components/base/KpiCard.tsx +7 -7
  21. package/src/components/base/Modal.tsx +10 -14
  22. package/src/components/base/ProgressBar.tsx +63 -59
  23. package/src/components/base/RadioCardGroup.tsx +13 -6
  24. package/src/components/base/SectionHeading.tsx +4 -4
  25. package/src/components/base/SegmentedFilter.tsx +60 -4
  26. package/src/components/base/SocialLinksBar.tsx +3 -15
  27. package/src/components/base/StatusIndicator.tsx +11 -11
  28. package/src/components/base/StatusPill.tsx +15 -15
  29. package/src/components/base/TagPill.tsx +1 -1
  30. package/src/components/base/ThemeToggle.tsx +12 -20
  31. package/src/components/base/Tooltip.tsx +2 -2
  32. package/src/components/composite/AccountRadioPicker.tsx +8 -4
  33. package/src/components/composite/ComboBox.tsx +51 -44
  34. package/src/components/composite/CustomRenderer.tsx +6 -13
  35. package/src/components/composite/FileField.tsx +10 -17
  36. package/src/components/composite/FlowGraph.tsx +29 -36
  37. package/src/components/composite/FlowRunner.tsx +11 -18
  38. package/src/components/composite/FormAdvancedSection.tsx +31 -31
  39. package/src/components/composite/LiveTimer.tsx +3 -3
  40. package/src/components/composite/MarkdownNotes.tsx +11 -11
  41. package/src/components/composite/MentionTextarea.tsx +10 -10
  42. package/src/components/composite/NotFound.tsx +7 -7
  43. package/src/components/composite/PaymentAccountPicker.tsx +19 -19
  44. package/src/components/composite/SalesBodyEditor.tsx +26 -26
  45. package/src/components/composite/SearchableSelect.tsx +14 -14
  46. package/src/components/composite/SecretReveal.tsx +5 -5
  47. package/src/components/composite/TransactionAccountFields.tsx +78 -0
  48. package/src/components/composite/TransactionForm.tsx +83 -169
  49. package/src/components/composite/TransactionPayableFields.tsx +97 -0
  50. package/src/components/composite/TransferAccountsPicker.tsx +20 -20
  51. package/src/components/composite/TransferFeeChip.tsx +2 -2
  52. package/src/components/composite/VoucherPicker.tsx +25 -25
  53. package/src/components/composite/resource/ResourceForm.tsx +1 -1
  54. package/src/components/composite/resource/ResourcePage.tsx +8 -8
  55. package/src/components/composite/resource/cells.tsx +6 -3
  56. package/src/utils/INPUT_CLASS.ts +6 -3
  57. package/src/utils/account-icons.ts +29 -7
  58. package/src/utils/highlight.tsx +5 -6
  59. package/src/utils/inject-css.ts +13 -0
  60. package/tailwind.js +11 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahitsan/ksui",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "ksui is a standalone set of SolidJS UI components for KahitSan/Hilinga and any SolidJS app. Published to the public npm registry and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with only solid-js externalized; it depends on nothing but solid-js + lucide-solid and injects its own CSS.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -32,6 +32,9 @@
32
32
  ],
33
33
  "scripts": {
34
34
  "typecheck": "tsc --noEmit",
35
+ "check:tokens": "node scripts/check-token-fallbacks.mjs",
36
+ "check:coverage": "node scripts/check-token-fallbacks.mjs --coverage",
37
+ "check:palette": "node scripts/check-palette-drift.mjs",
35
38
  "test": "vitest run",
36
39
  "test:watch": "vitest",
37
40
  "test:e2e": "playwright test",
@@ -117,7 +117,7 @@ export default function AccountAvatar(props: AccountAvatarProps) {
117
117
  <Dynamic
118
118
  component={getAccountIcon(props.account)}
119
119
  size={iconSize()}
120
- class={props.iconClass ?? "text-zinc-300"}
120
+ class={props.iconClass ?? "text-[var(--ks-fg-muted,#a1a1aa)]"}
121
121
  style={iconStyle()}
122
122
  />
123
123
  );
@@ -53,7 +53,7 @@ export default function AddAttachmentTile(props: Props) {
53
53
  setOpen(!open());
54
54
  }}
55
55
  disabled={props.uploading}
56
- class="w-24 h-24 flex flex-col items-center justify-center gap-1 border border-dashed border-zinc-700 bg-zinc-800/30 text-zinc-400 hover:bg-zinc-800/60 hover:border-amber-500/40 hover:text-amber-400 active:bg-zinc-800/80 transition-colors ks-hud-clip-top-left-bottom-right cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
56
+ class="w-24 h-24 flex flex-col items-center justify-center gap-1 border border-dashed border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_30%,transparent)] text-[var(--ks-fg-muted,#a1a1aa)] hover:bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_60%,transparent)] hover:border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_40%,transparent)] hover:text-[var(--ks-accent,#fbbf24)] active:bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_80%,transparent)] transition-colors ks-hud-clip-top-left-bottom-right cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
57
57
  aria-label="Add attachment"
58
58
  >
59
59
  <Plus size={22} />
@@ -64,11 +64,11 @@ export default function AddAttachmentTile(props: Props) {
64
64
  <div
65
65
  ref={menu}
66
66
  style={{ top: `${pos().top}px`, left: `${pos().left}px` }}
67
- class="fixed z-[60] min-w-[160px] rounded-lg border border-zinc-600 bg-zinc-800 shadow-2xl p-1 ks-hud-clip-top-left-bottom-right"
67
+ class="fixed z-[60] min-w-[160px] rounded-lg border border-[var(--ks-border-strong,#3f3f46)] bg-[var(--ks-surface-raised,#1a1a1a)] shadow-2xl p-1 ks-hud-clip-top-left-bottom-right"
68
68
  >
69
69
  <button
70
70
  type="button"
71
- class="w-full flex items-center gap-2 px-3 py-2 text-sm text-zinc-200 hover:bg-zinc-800 rounded cursor-pointer"
71
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm text-[var(--ks-fg,#ffffff)] hover:bg-[var(--ks-surface-sunken,#141414)] rounded cursor-pointer"
72
72
  onClick={() => {
73
73
  setOpen(false);
74
74
  props.onPickCamera();
@@ -79,7 +79,7 @@ export default function AddAttachmentTile(props: Props) {
79
79
  </button>
80
80
  <button
81
81
  type="button"
82
- class="w-full flex items-center gap-2 px-3 py-2 text-sm text-zinc-200 hover:bg-zinc-800 rounded cursor-pointer"
82
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm text-[var(--ks-fg,#ffffff)] hover:bg-[var(--ks-surface-sunken,#141414)] rounded cursor-pointer"
83
83
  onClick={() => {
84
84
  setOpen(false);
85
85
  props.onPickFile();
@@ -76,7 +76,7 @@ export default function BadgeSelect(props: BadgeSelectProps): JSX.Element {
76
76
  );
77
77
  });
78
78
 
79
- const defaultBadge = () => "bg-zinc-800 text-zinc-400 border border-transparent";
79
+ const defaultBadge = () => "bg-[var(--ks-surface-raised,#1a1a1a)] text-[var(--ks-fg-muted,#a1a1aa)] border border-transparent";
80
80
  const badgeClass = () => props.badgeClass?.(props.value) ?? defaultBadge();
81
81
 
82
82
  const emptyStateMessage = () => {
@@ -164,7 +164,7 @@ export default function BadgeSelect(props: BadgeSelectProps): JSX.Element {
164
164
  aria-expanded={open()}
165
165
  disabled={props.disabled || busy()}
166
166
  onClick={() => !props.disabled && setOpen((o) => !o)}
167
- class={`text-xs font-medium px-1.5 py-0.5 rounded cursor-pointer transition-colors hover:ring-1 hover:ring-amber-500/40 ${badgeClass()} ${
167
+ class={`text-xs font-medium px-1.5 py-0.5 rounded cursor-pointer transition-colors hover:ring-1 hover:ring-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_40%,transparent)] ${badgeClass()} ${
168
168
  props.disabled ? "cursor-not-allowed opacity-60" : ""
169
169
  }`}
170
170
  title={props.disabled ? "" : (props.title ?? "Click to change")}
@@ -176,25 +176,25 @@ export default function BadgeSelect(props: BadgeSelectProps): JSX.Element {
176
176
  <div
177
177
  ref={popupRef}
178
178
  role="listbox"
179
- class="z-[100] rounded-md border border-zinc-700 bg-zinc-900/95 backdrop-blur shadow-xl overflow-hidden flex flex-col"
179
+ class="z-[100] rounded-md border border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_95%,transparent)] backdrop-blur shadow-xl overflow-hidden flex flex-col"
180
180
  style={popupStyle()}
181
181
  >
182
182
  <Show when={showSearch()}>
183
- <div class="px-2 py-1.5 border-b border-zinc-800">
183
+ <div class="px-2 py-1.5 border-b border-[var(--ks-border,rgba(39,39,42,0.5))]">
184
184
  <input
185
185
  ref={searchRef}
186
186
  type="text"
187
187
  value={query()}
188
188
  onInput={(e) => setQuery(e.currentTarget.value)}
189
189
  placeholder={props.searchPlaceholder ?? "Search…"}
190
- class="w-full px-2 py-1 text-xs bg-zinc-950 border border-zinc-800 rounded text-zinc-200 placeholder:text-zinc-600 focus:outline-none focus:border-amber-500/50"
190
+ class="w-full px-2 py-1 text-xs bg-[var(--ks-surface-sunken,#141414)] border border-[var(--ks-border,rgba(39,39,42,0.5))] rounded text-[var(--ks-fg,#ffffff)] placeholder:text-[var(--ks-input-placeholder,#71717a)] focus:outline-none focus:border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_50%,transparent)]"
191
191
  />
192
192
  </div>
193
193
  </Show>
194
194
  <div class="flex-1 overflow-y-auto">
195
195
  <Show
196
196
  when={!props.loading && filtered().length > 0}
197
- fallback={<div class="px-3 py-2 text-xs text-zinc-500">{emptyStateMessage()}</div>}
197
+ fallback={<div class="px-3 py-2 text-xs text-[var(--ks-fg-subtle,#71717a)]">{emptyStateMessage()}</div>}
198
198
  >
199
199
  <For each={filtered()}>
200
200
  {(opt) => (
@@ -203,18 +203,18 @@ export default function BadgeSelect(props: BadgeSelectProps): JSX.Element {
203
203
  role="option"
204
204
  aria-selected={opt.value === props.value}
205
205
  onClick={() => handleSelect(opt.value)}
206
- class={`w-full text-left px-3 py-2 text-xs hover:bg-amber-500/10 transition-colors flex items-center justify-between gap-2 ${
207
- opt.value === props.value ? "text-amber-400" : "text-zinc-200"
206
+ class={`w-full text-left px-3 py-2 text-xs hover:bg-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_10%,transparent)] transition-colors flex items-center justify-between gap-2 ${
207
+ opt.value === props.value ? "text-[var(--ks-accent,#fbbf24)]" : "text-[var(--ks-fg,#ffffff)]"
208
208
  }`}
209
209
  >
210
210
  <span class="flex flex-col">
211
211
  <span class="font-medium">{opt.label}</span>
212
212
  <Show when={opt.description}>
213
- <span class="text-[10px] text-zinc-500">{opt.description}</span>
213
+ <span class="text-[10px] text-[var(--ks-fg-subtle,#71717a)]">{opt.description}</span>
214
214
  </Show>
215
215
  </span>
216
216
  <Show when={opt.value === props.value}>
217
- <span class="text-amber-400" aria-hidden="true">✓</span>
217
+ <span class="text-[var(--ks-accent,#fbbf24)]" aria-hidden="true">✓</span>
218
218
  </Show>
219
219
  </button>
220
220
  )}
@@ -23,6 +23,7 @@ import {
23
23
  type Component,
24
24
  } from "solid-js";
25
25
  import { Dynamic } from "solid-js/web";
26
+ import { injectCSS } from "../../utils/inject-css";
26
27
 
27
28
  const BUTTON_STYLE_ID = "ks-button-inline-style";
28
29
  const BUTTON_CSS = `
@@ -49,12 +50,7 @@ const BUTTON_CSS = `
49
50
  `;
50
51
 
51
52
  function ensureButtonStyle() {
52
- if (typeof document === "undefined") return;
53
- if (document.getElementById(BUTTON_STYLE_ID)) return;
54
- const el = document.createElement("style");
55
- el.id = BUTTON_STYLE_ID;
56
- el.textContent = BUTTON_CSS;
57
- document.head.appendChild(el);
53
+ injectCSS(BUTTON_STYLE_ID, BUTTON_CSS);
58
54
  }
59
55
 
60
56
  const styles: Record<string, string> = {
@@ -106,12 +102,24 @@ function cn(...classes: Array<string | undefined | null | false>): string {
106
102
  return classes.filter(Boolean).join(" ");
107
103
  }
108
104
 
105
+ // Intent → design token: the button's fill/border/glow effects are all
106
+ // derived from a single base token per intent. Every effect string below is
107
+ // written out literally (not built from a template at runtime) because the
108
+ // theming rollout's check-token-fallbacks.mjs gate statically greps source
109
+ // text for the color-mix CSS function with a literal --ks-* var name — an
110
+ // interpolated `${token.name}` would read as malformed to that gate even
111
+ // though the runtime output is correct.
109
112
  interface IntentColors {
110
113
  ripple: string;
111
- effect: string;
114
+ effectColor: string;
112
115
  effectBg: string;
113
116
  effectBorder: string;
117
+ effectBgHover: string;
118
+ effectBorderHover: string;
114
119
  effectGlow: string;
120
+ effectGlowHover: string;
121
+ effectPulseBgMid: string;
122
+ effectPulseGlow: string;
115
123
  }
116
124
 
117
125
  interface IntentConfig {
@@ -121,41 +129,64 @@ interface IntentConfig {
121
129
  colors: IntentColors;
122
130
  }
123
131
 
132
+ const RIPPLE = "color-mix(in srgb, var(--ks-fg, #ffffff) 30%, transparent)";
133
+
124
134
  const buttonIntentConfig: Record<ButtonIntent, IntentConfig> = {
125
135
  primary: {
126
- textColor: "text-amber-400",
127
- background: "bg-amber-600/20 border-amber-600/60",
128
- hover: "hover:bg-amber-600/30 hover:border-amber-500",
136
+ textColor: "text-[var(--ks-accent,#fbbf24)]",
137
+ background:
138
+ "bg-[color-mix(in_srgb,var(--ks-primary,#c9a961)_20%,transparent)] border-[color-mix(in_srgb,var(--ks-primary,#c9a961)_60%,transparent)]",
139
+ hover:
140
+ "hover:bg-[color-mix(in_srgb,var(--ks-primary-hover,#d4b876)_30%,transparent)] hover:border-[var(--ks-primary-hover,#d4b876)]",
129
141
  colors: {
130
- ripple: "rgba(255, 255, 255, 0.3)",
131
- effect: "rgba(201, 169, 97, 0.4)",
132
- effectBg: "rgba(201, 169, 97, 0.1)",
133
- effectBorder: "rgba(201, 169, 97, 0.4)",
134
- effectGlow: "rgba(201, 169, 97, 0.3)",
142
+ ripple: RIPPLE,
143
+ effectColor: "color-mix(in srgb, var(--ks-primary, #c9a961) 40%, transparent)",
144
+ effectBg: "color-mix(in srgb, var(--ks-primary, #c9a961) 10%, transparent)",
145
+ effectBorder: "color-mix(in srgb, var(--ks-primary, #c9a961) 40%, transparent)",
146
+ effectBgHover: "color-mix(in srgb, var(--ks-primary, #c9a961) 20%, transparent)",
147
+ effectBorderHover: "color-mix(in srgb, var(--ks-primary, #c9a961) 60%, transparent)",
148
+ effectGlow: "color-mix(in srgb, var(--ks-primary, #c9a961) 30%, transparent)",
149
+ effectGlowHover: "color-mix(in srgb, var(--ks-primary, #c9a961) 50%, transparent)",
150
+ effectPulseBgMid: "color-mix(in srgb, var(--ks-primary, #c9a961) 20%, transparent)",
151
+ effectPulseGlow: "color-mix(in srgb, var(--ks-primary, #c9a961) 20%, transparent)",
135
152
  },
136
153
  },
137
154
  danger: {
138
- textColor: "text-red-400",
139
- background: "bg-red-600/20 border-red-600/60",
140
- hover: "hover:bg-red-600/30 hover:border-red-500",
155
+ textColor: "text-[var(--ks-danger-fg,#f87171)]",
156
+ background:
157
+ "bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_20%,transparent)] border-[color-mix(in_srgb,var(--ks-danger,#ef4444)_60%,transparent)]",
158
+ hover:
159
+ "hover:bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_30%,transparent)] hover:border-[var(--ks-danger,#ef4444)]",
141
160
  colors: {
142
- ripple: "rgba(255, 255, 255, 0.3)",
143
- effect: "rgba(255, 68, 68, 0.4)",
144
- effectBg: "rgba(255, 68, 68, 0.1)",
145
- effectBorder: "rgba(255, 68, 68, 0.4)",
146
- effectGlow: "rgba(255, 68, 68, 0.3)",
161
+ ripple: RIPPLE,
162
+ effectColor: "color-mix(in srgb, var(--ks-danger, #ef4444) 40%, transparent)",
163
+ effectBg: "color-mix(in srgb, var(--ks-danger, #ef4444) 10%, transparent)",
164
+ effectBorder: "color-mix(in srgb, var(--ks-danger, #ef4444) 40%, transparent)",
165
+ effectBgHover: "color-mix(in srgb, var(--ks-danger, #ef4444) 20%, transparent)",
166
+ effectBorderHover: "color-mix(in srgb, var(--ks-danger, #ef4444) 60%, transparent)",
167
+ effectGlow: "color-mix(in srgb, var(--ks-danger, #ef4444) 30%, transparent)",
168
+ effectGlowHover: "color-mix(in srgb, var(--ks-danger, #ef4444) 50%, transparent)",
169
+ effectPulseBgMid: "color-mix(in srgb, var(--ks-danger, #ef4444) 20%, transparent)",
170
+ effectPulseGlow: "color-mix(in srgb, var(--ks-danger, #ef4444) 20%, transparent)",
147
171
  },
148
172
  },
149
173
  secondary: {
150
- textColor: "text-slate-400",
151
- background: "bg-slate-600/20 border-slate-600/60",
152
- hover: "hover:bg-slate-600/30 hover:border-slate-500",
174
+ textColor: "text-[var(--ks-fg-muted,#a1a1aa)]",
175
+ background:
176
+ "bg-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_20%,transparent)] border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_60%,transparent)]",
177
+ hover:
178
+ "hover:bg-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_30%,transparent)] hover:border-[var(--ks-fg-muted,#a1a1aa)]",
153
179
  colors: {
154
- ripple: "rgba(255, 255, 255, 0.3)",
155
- effect: "rgba(148, 163, 184, 0.4)",
156
- effectBg: "rgba(148, 163, 184, 0.1)",
157
- effectBorder: "rgba(148, 163, 184, 0.4)",
158
- effectGlow: "rgba(148, 163, 184, 0.3)",
180
+ ripple: RIPPLE,
181
+ effectColor: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 40%, transparent)",
182
+ effectBg: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 10%, transparent)",
183
+ effectBorder: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 40%, transparent)",
184
+ effectBgHover: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 20%, transparent)",
185
+ effectBorderHover: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 60%, transparent)",
186
+ effectGlow: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 30%, transparent)",
187
+ effectGlowHover: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 50%, transparent)",
188
+ effectPulseBgMid: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 20%, transparent)",
189
+ effectPulseGlow: "color-mix(in srgb, var(--ks-border-strong, #3f3f46) 20%, transparent)",
159
190
  },
160
191
  },
161
192
  };
@@ -289,16 +320,16 @@ const Button = (props: ButtonProps): JSX.Element => {
289
320
  const customProperties = createMemo(() => {
290
321
  const colors = intentConfig().colors;
291
322
  return {
292
- "--ks-effect-color": colors.effect,
323
+ "--ks-effect-color": colors.effectColor,
293
324
  "--ks-effect-bg": colors.effectBg,
294
325
  "--ks-effect-border": colors.effectBorder,
295
- "--ks-effect-bg-hover": colors.effect.replace("0.1", "0.2"),
296
- "--ks-effect-border-hover": colors.effect.replace("0.4", "0.6"),
326
+ "--ks-effect-bg-hover": colors.effectBgHover,
327
+ "--ks-effect-border-hover": colors.effectBorderHover,
297
328
  "--ks-effect-glow": colors.effectGlow,
298
- "--ks-effect-glow-hover": colors.effectGlow.replace("0.3", "0.5"),
329
+ "--ks-effect-glow-hover": colors.effectGlowHover,
299
330
  "--ks-effect-pulse-bg": colors.effectBg,
300
- "--ks-effect-pulse-bg-mid": colors.effect.replace("0.4", "0.2"),
301
- "--ks-effect-pulse-glow": colors.effectGlow.replace("0.3", "0.2"),
331
+ "--ks-effect-pulse-bg-mid": colors.effectPulseBgMid,
332
+ "--ks-effect-pulse-glow": colors.effectPulseGlow,
302
333
  "--ks-effect-pulse-glow-mid": colors.effectGlow,
303
334
  "--ks-ripple-color": colors.ripple,
304
335
  } as JSX.CSSProperties;
@@ -416,7 +447,8 @@ const Button = (props: ButtonProps): JSX.Element => {
416
447
  top: `${ripple.y - ripple.size / 2}px`,
417
448
  width: `${ripple.size}px`,
418
449
  height: `${ripple.size}px`,
419
- "background-color": "var(--ks-ripple-color, rgba(255, 255, 255, 0.3))",
450
+ "background-color":
451
+ "var(--ks-ripple-color, color-mix(in srgb, var(--ks-fg, #ffffff) 30%, transparent))",
420
452
  }}
421
453
  />
422
454
  )}
@@ -75,19 +75,19 @@ export default function CameraCapture(props: Props) {
75
75
  return (
76
76
  <div
77
77
  data-testid="camera-capture-modal"
78
- class="fixed inset-0 z-[60] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
78
+ class="fixed inset-0 z-[60] flex items-center justify-center bg-[var(--ks-overlay,rgba(0,0,0,0.7))] backdrop-blur-sm p-4"
79
79
  >
80
- <div class="card-bg rounded-xl border border-amber-500/30 overflow-hidden max-w-lg w-full shadow-2xl">
81
- <div class="flex items-center justify-between p-3 border-b border-zinc-800/50">
80
+ <div class="card-bg rounded-xl border border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_30%,transparent)] overflow-hidden max-w-lg w-full shadow-2xl">
81
+ <div class="flex items-center justify-between p-3 border-b border-[var(--ks-border,rgba(39,39,42,0.5))]">
82
82
  <span
83
- class="text-sm text-zinc-200 font-medium flex items-center gap-2"
83
+ class="text-sm text-[var(--ks-fg,#ffffff)] font-medium flex items-center gap-2"
84
84
  data-testid="camera-capture-title"
85
85
  >
86
86
  <Camera size={16} /> Camera
87
87
  </span>
88
88
  <button
89
89
  onClick={() => props.onClose()}
90
- class="text-zinc-500 hover:text-zinc-300 cursor-pointer"
90
+ class="text-[var(--ks-fg-subtle,#71717a)] hover:text-[var(--ks-fg-muted,#a1a1aa)] cursor-pointer"
91
91
  aria-label="Close camera"
92
92
  >
93
93
  <X size={18} />
@@ -96,7 +96,7 @@ export default function CameraCapture(props: Props) {
96
96
 
97
97
  <Show when={error()}>
98
98
  <div class="p-6 text-center">
99
- <p class="text-sm text-red-400 mb-3">{error()}</p>
99
+ <p class="text-sm text-[var(--ks-danger-fg,#f87171)] mb-3">{error()}</p>
100
100
  <Button intent="secondary" variant="ghost" onClick={() => props.onClose()}>
101
101
  Close
102
102
  </Button>
@@ -104,7 +104,7 @@ export default function CameraCapture(props: Props) {
104
104
  </Show>
105
105
 
106
106
  <Show when={!error()}>
107
- <div class="relative bg-black aspect-video">
107
+ <div class="relative bg-[var(--ks-surface,#0f0f0f)] aspect-video">
108
108
  <Show when={!captured()}>
109
109
  <video ref={videoRef} autoplay playsinline muted class="w-full h-full object-cover" />
110
110
  </Show>
@@ -131,7 +131,7 @@ export default function CameraCapture(props: Props) {
131
131
  <button
132
132
  type="button"
133
133
  onClick={takePhoto}
134
- class="w-14 h-14 rounded-full border-4 border-zinc-400 bg-zinc-200 hover:bg-white active:scale-95 transition-all cursor-pointer"
134
+ class="w-14 h-14 rounded-full border-4 border-[var(--ks-border-strong,#3f3f46)] bg-[var(--ks-fg,#ffffff)] hover:bg-[var(--ks-fg,#ffffff)] active:scale-95 transition-all cursor-pointer"
135
135
  title="Take photo"
136
136
  aria-label="Take photo"
137
137
  />
@@ -11,7 +11,7 @@ interface ChartLegendProps {
11
11
  label: string;
12
12
  /** Pre-formatted value string (e.g. a currency string). */
13
13
  value: string;
14
- /** Optional Tailwind text color class for the value. Defaults to text-zinc-100. */
14
+ /** Optional Tailwind text color class for the value. Defaults to var(--ks-fg). */
15
15
  valueColor?: string;
16
16
  }
17
17
 
@@ -20,11 +20,11 @@ export default function ChartLegend(props: ChartLegendProps) {
20
20
  <div class="flex items-center gap-2">
21
21
  <span class={`w-2 h-2 rounded-sm ${props.dot}`} />
22
22
  <div class="flex flex-col">
23
- <span class="text-[9px] uppercase tracking-widest text-zinc-500 font-medium leading-tight">
23
+ <span class="text-[9px] uppercase tracking-widest text-[var(--ks-fg-subtle,#71717a)] font-medium leading-tight">
24
24
  {props.label}
25
25
  </span>
26
26
  <span
27
- class={`text-xs font-bold tabular-nums leading-tight ${props.valueColor || "text-zinc-100"}`}
27
+ class={`text-xs font-bold tabular-nums leading-tight ${props.valueColor || "text-[var(--ks-fg,#ffffff)]"}`}
28
28
  >
29
29
  {props.value}
30
30
  </span>
@@ -44,7 +44,7 @@ export default function CopyButton(props: CopyButtonProps): JSX.Element {
44
44
  type="button"
45
45
  onClick={handleClick}
46
46
  aria-label={props.ariaLabel ?? "Copy"}
47
- class={`ks-interactive px-2 py-1.5 rounded bg-zinc-800 border border-zinc-700 text-zinc-300 hover:text-white text-xs flex items-center gap-1 ${props.class ?? ""}`}
47
+ class={`ks-interactive px-2 py-1.5 rounded bg-[var(--ks-surface-raised,#1a1a1a)] border border-[var(--ks-border-strong,#3f3f46)] text-[var(--ks-fg-muted,#a1a1aa)] hover:text-[var(--ks-fg,#ffffff)] text-xs flex items-center gap-1 ${props.class ?? ""}`}
48
48
  >
49
49
  {copied() ? <Check size={props.size ?? 12} /> : <Copy size={props.size ?? 12} />}
50
50
  {copied() ? (props.copiedLabel ?? "Copied") : (props.label ?? "Copy")}
@@ -46,6 +46,7 @@ import ChevronDown from "lucide-solid/icons/chevron-down";
46
46
  import ChevronUp from "lucide-solid/icons/chevron-up";
47
47
  import ChevronsUpDown from "lucide-solid/icons/chevrons-up-down";
48
48
  import DatePicker from "./DatePicker";
49
+ import { injectCSS } from "../../utils/inject-css";
49
50
 
50
51
  // ---------------------------------------------------------------------------
51
52
  // Injected CSS
@@ -58,37 +59,37 @@ const STYLE_ID = "ksui-datatable-style";
58
59
  // ---------------------------------------------------------------------------
59
60
  //
60
61
  // Every color in the injected stylesheet is driven by a `--ksui-dt-*` CSS
61
- // custom property; the fallback after each `var(...)` is the host
62
- // DataTable's exact value (resolved from its Tailwind classes zinc/amber
63
- // dark theme). To retint, wrap the table in a container that sets the vars,
64
- // e.g. `<div style={{ "--ksui-dt-card-bg": "#000", "--ksui-dt-accent": "#0af" }}>`.
62
+ // custom property; each one now falls through to a `--ks-*` design token
63
+ // (THEME-SPEC §1.2) before the literal dark-theme fallback, so a themed host
64
+ // restyles the table without any ksui code change. To retint further, wrap
65
+ // the table in a container that sets the `--ksui-dt-*` vars directly — they
66
+ // still win over the token layer per the old-name-stays-the-read-point rule.
65
67
  //
66
- // Full list of overridable vars (default = host value):
67
- // --ksui-dt-card-bg outer card background
68
- // (host `.card-bg`: linear-gradient(135deg,#0f0f0f,#1a1a1a))
69
- // --ksui-dt-radius outer card / control corner radius (0.625rem)
70
- // --ksui-dt-border card / header / footer / control border (zinc-800/50, rgba(39,39,42,0.5))
71
- // --ksui-dt-row-border row divider + expansion-row top border (zinc-800/30, rgba(39,39,42,0.3))
72
- // --ksui-dt-control-bg filter button / menu / select / search / show-more bg (zinc-900, #18181b)
68
+ // Full list of overridable vars (role -> token):
69
+ // --ksui-dt-card-bg outer card background -> --ks-surface / --ks-surface-raised gradient
70
+ // --ksui-dt-radius outer card / control corner radius (0.625rem, not a color)
71
+ // --ksui-dt-border card / header / footer / control border -> --ks-border
72
+ // --ksui-dt-row-border row divider + expansion-row top border -> --ks-border-subtle
73
+ // --ksui-dt-control-bg filter button / menu / select / search / show-more bg -> --ks-input-bg
73
74
  // (the date filter is the ksui DatePicker; it reads the same vars)
74
- // --ksui-dt-fg primary text: row/search text (zinc-200, #e4e4e7)
75
- // --ksui-dt-fg-strong hover-to-full-contrast text on controls/pager (white, #ffffff)
76
- // --ksui-dt-text secondary text: filter btn / select / pager nums+arrows (zinc-400, #a1a1aa)
77
- // --ksui-dt-text-strong show-more label + sortable-header hover (zinc-300, #d4d4d8)
78
- // --ksui-dt-muted header th / search icon+placeholder / empty / info text (zinc-500, #71717a)
79
- // --ksui-dt-faint sort caret / per-row date separator / pager ellipsis (zinc-600, #52525b)
80
- // --ksui-dt-row-hover row hover + pager hover bg (zinc-800/50, rgba(39,39,42,0.5))
81
- // --ksui-dt-row-active row :active bg (zinc-800/70, rgba(39,39,42,0.7))
82
- // --ksui-dt-expansion-bg per-row expansion panel bg (zinc-950/40, rgba(9,9,11,0.4))
83
- // --ksui-dt-skeleton loading skeleton shimmer bg (zinc-800/50, rgba(39,39,42,0.5))
84
- // --ksui-dt-accent active accent text: active pager num / show-more hover (amber-400, #fbbf24)
85
- // --ksui-dt-accent-bg active pager num bg (amber-600/20, rgba(217,119,6,0.2))
86
- // --ksui-dt-accent-border focus/hover accent border: search focus, show-more hover (amber-500/40, rgba(245,158,11,0.4))
87
- // --ksui-dt-good positive-status badge text, e.g. "Active" (emerald-400, #34d399)
88
- // --ksui-dt-good-bg positive-status badge bg (emerald-400/14, rgba(52,211,153,0.14))
89
- // --ksui-dt-danger negative-status badge text, e.g. "Overdue"/"Expired" (red-400, #f87171)
90
- // --ksui-dt-danger-bg negative-status badge bg (red-400/14, rgba(248,113,113,0.14))
91
- // --ksui-dt-thead-bg header-row tint, layered over card-bg to set it apart from data rows (white/3%, rgba(255,255,255,0.03))
75
+ // --ksui-dt-fg primary text: row/search text -> --ks-fg
76
+ // --ksui-dt-fg-strong hover-to-full-contrast text on controls/pager -> --ks-fg
77
+ // --ksui-dt-text secondary text: filter btn / select / pager nums+arrows -> --ks-fg-muted
78
+ // --ksui-dt-text-strong show-more label + sortable-header hover -> --ks-fg
79
+ // --ksui-dt-muted header th / search icon+placeholder / empty / info text -> --ks-fg-subtle
80
+ // --ksui-dt-faint sort caret / per-row date separator / pager ellipsis -> --ks-fg-subtle
81
+ // --ksui-dt-row-hover row hover + pager hover bg -> --ks-border
82
+ // --ksui-dt-row-active row :active bg -> --ks-border-strong (color-mix)
83
+ // --ksui-dt-expansion-bg per-row expansion panel bg -> --ks-bg (color-mix)
84
+ // --ksui-dt-skeleton loading skeleton shimmer bg -> --ks-border
85
+ // --ksui-dt-accent active accent text: active pager num / show-more hover -> --ks-accent
86
+ // --ksui-dt-accent-bg active pager num bg -> --ks-accent (color-mix)
87
+ // --ksui-dt-accent-border focus/hover accent border: search focus, show-more hover -> --ks-focus-ring (color-mix)
88
+ // --ksui-dt-good positive-status badge text, e.g. "Active" -> --ks-success-fg
89
+ // --ksui-dt-good-bg positive-status badge bg -> --ks-success-fg (color-mix)
90
+ // --ksui-dt-danger negative-status badge text, e.g. "Overdue"/"Expired" -> --ks-danger-fg
91
+ // --ksui-dt-danger-bg negative-status badge bg -> --ks-danger-fg (color-mix)
92
+ // --ksui-dt-thead-bg header-row tint, layered over card-bg to set it apart from data rows -> --ks-fg (color-mix)
92
93
  //
93
94
  // `.ksui-datatable-td-num` / `.ksui-datatable-th-num` (mono, tabular-nums,
94
95
  // right-aligned, nowrap) and `.ksui-datatable-badge` (+ `-ok` / `-warn` /
@@ -97,72 +98,67 @@ const STYLE_ID = "ksui-datatable-style";
97
98
  // infers column semantics from data.
98
99
  //
99
100
  const DATATABLE_CSS = `
100
- .ksui-datatable{background:var(--ksui-dt-card-bg,linear-gradient(135deg,#0f0f0f 0%,#1a1a1a 100%));border:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));border-radius:var(--ksui-dt-radius,0.625rem);color:var(--ksui-dt-fg,#e4e4e7);box-shadow:0 1px 2px rgba(0,0,0,0.3),0 8px 20px -10px rgba(0,0,0,0.5);}
101
- .ksui-datatable-header{display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem;border-bottom:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));padding:0.75rem 0.875rem;}
101
+ .ksui-datatable{background:var(--ksui-dt-card-bg,linear-gradient(135deg,var(--ks-surface,#0f0f0f) 0%,var(--ks-surface-raised,#1a1a1a) 100%));border:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));border-radius:var(--ksui-dt-radius,0.625rem);color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));box-shadow:0 1px 2px rgba(0,0,0,0.3),0 8px 20px -10px rgba(0,0,0,0.5);}
102
+ .ksui-datatable-header{display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem;border-bottom:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));padding:0.75rem 0.875rem;}
102
103
  .ksui-datatable-filters-inline{display:none;flex:1 1 0%;}
103
104
  .ksui-datatable-filters-mobile{display:block;flex:1 1 0%;}
104
105
  @media (min-width:768px){.ksui-datatable-filters-inline{display:block;}.ksui-datatable-filters-mobile{display:none;}}
105
106
  .ksui-datatable-spacer{flex:1 1 0%;}
106
107
  .ksui-datatable-controls{display:flex;align-items:center;gap:0.5rem;}
107
108
  .ksui-datatable-filter-toggle-wrap{position:relative;}
108
- .ksui-datatable-filter-toggle{display:inline-flex;cursor:pointer;align-items:center;gap:0.5rem;border-radius:0.5rem;border:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));background:var(--ksui-dt-control-bg,#18181b);padding:0.5rem 0.75rem;font-size:0.75rem;line-height:1rem;color:var(--ksui-dt-text,#a1a1aa);transition:color 0.15s ease;}
109
- .ksui-datatable-filter-toggle:hover{color:var(--ksui-dt-fg-strong,#ffffff);}
110
- .ksui-datatable-filter-menu{position:absolute;left:0;top:100%;z-index:50;margin-top:0.5rem;border-radius:0.5rem;border:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));background:var(--ksui-dt-control-bg,#18181b);padding:0.75rem;box-shadow:0 20px 25px -5px rgba(0,0,0,0.4),0 8px 10px -6px rgba(0,0,0,0.4);}
111
- .ksui-datatable-select{border-radius:0.375rem;border:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));background:var(--ksui-dt-control-bg,#18181b);padding:0.5rem;font-size:0.75rem;line-height:1rem;color:var(--ksui-dt-text,#a1a1aa);}
109
+ .ksui-datatable-filter-toggle{display:inline-flex;cursor:pointer;align-items:center;gap:0.5rem;border-radius:0.5rem;border:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));background:var(--ksui-dt-control-bg,var(--ks-input-bg,#18181b));padding:0.5rem 0.75rem;font-size:0.75rem;line-height:1rem;color:var(--ksui-dt-text,var(--ks-fg-muted,#a1a1aa));transition:color 0.15s ease;}
110
+ .ksui-datatable-filter-toggle:hover{color:var(--ksui-dt-fg-strong,var(--ks-fg,#ffffff));}
111
+ .ksui-datatable-filter-menu{position:absolute;left:0;top:100%;z-index:50;margin-top:0.5rem;border-radius:0.5rem;border:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));background:var(--ksui-dt-control-bg,var(--ks-input-bg,#18181b));padding:0.75rem;box-shadow:0 20px 25px -5px rgba(0,0,0,0.4),0 8px 10px -6px rgba(0,0,0,0.4);}
112
+ .ksui-datatable-select{border-radius:0.375rem;border:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));background:var(--ksui-dt-control-bg,var(--ks-input-bg,#18181b));padding:0.5rem;font-size:0.75rem;line-height:1rem;color:var(--ksui-dt-text,var(--ks-fg-muted,#a1a1aa));}
112
113
  .ksui-datatable-search-wrap{position:relative;}
113
- .ksui-datatable-search-icon{position:absolute;left:0.75rem;top:50%;transform:translateY(-50%);color:var(--ksui-dt-muted,#71717a);pointer-events:none;}
114
- .ksui-datatable-search-input{width:100%;border-radius:0.5rem;border:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));background:var(--ksui-dt-control-bg,#18181b);padding:0.5rem 1rem 0.5rem 2.25rem;font-size:0.875rem;line-height:1.25rem;color:var(--ksui-dt-fg,#e4e4e7);outline:none;transition:border-color 0.15s ease;}
115
- .ksui-datatable-search-input::placeholder{color:var(--ksui-dt-muted,#71717a);}
116
- .ksui-datatable-search-input:focus{border-color:var(--ksui-dt-accent-border,rgba(245,158,11,0.4));}
114
+ .ksui-datatable-search-icon{position:absolute;left:0.75rem;top:50%;transform:translateY(-50%);color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));pointer-events:none;}
115
+ .ksui-datatable-search-input{width:100%;border-radius:0.5rem;border:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));background:var(--ksui-dt-control-bg,var(--ks-input-bg,#18181b));padding:0.5rem 1rem 0.5rem 2.25rem;font-size:0.875rem;line-height:1.25rem;color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));outline:none;transition:border-color 0.15s ease;}
116
+ .ksui-datatable-search-input::placeholder{color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
117
+ .ksui-datatable-search-input:focus{border-color:var(--ksui-dt-accent-border,color-mix(in srgb, var(--ks-focus-ring, #c9a961) 40%, transparent));}
117
118
  @media (min-width:640px){.ksui-datatable-search-input{width:18rem;}}
118
119
  .ksui-datatable-scroll{overflow-x:auto;transition:opacity 0.15s ease;}
119
120
  .ksui-datatable-table{width:100%;text-align:left;font-size:0.78125rem;line-height:1.3;border-collapse:collapse;}
120
- .ksui-datatable-thead{border-bottom:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));background:var(--ksui-dt-thead-bg,rgba(255,255,255,0.03));font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;font-size:0.65625rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--ksui-dt-muted,#71717a);}
121
- .ksui-datatable-th{padding:0.5625rem 0.875rem;background:var(--ksui-dt-thead-bg,rgba(255,255,255,0.03));white-space:nowrap;}
121
+ .ksui-datatable-thead{border-bottom:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));background:var(--ksui-dt-thead-bg,color-mix(in srgb, var(--ks-fg, #ffffff) 3%, transparent));font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;font-size:0.65625rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
122
+ .ksui-datatable-th{padding:0.5625rem 0.875rem;background:var(--ksui-dt-thead-bg,color-mix(in srgb, var(--ks-fg, #ffffff) 3%, transparent));white-space:nowrap;}
122
123
  .ksui-datatable-th-num{text-align:right;}
123
124
  .ksui-datatable-th-sortable{cursor:pointer;transition:color 0.15s ease;}
124
- .ksui-datatable-th-sortable:hover{color:var(--ksui-dt-text-strong,#d4d4d8);}
125
+ .ksui-datatable-th-sortable:hover{color:var(--ksui-dt-text-strong,var(--ks-fg,#ffffff));}
125
126
  .ksui-datatable-th-inner{display:inline-flex;align-items:center;}
126
- .ksui-datatable-sort-icon{margin-left:0.25rem;display:inline-flex;color:var(--ksui-dt-faint,#52525b);}
127
- .ksui-datatable-row{border-top:1px solid var(--ksui-dt-row-border,rgba(39,39,42,0.3));transition:background-color 0.15s ease;}
128
- .ksui-datatable-row:hover{background-color:var(--ksui-dt-row-hover,rgba(39,39,42,0.5));}
127
+ .ksui-datatable-sort-icon{margin-left:0.25rem;display:inline-flex;color:var(--ksui-dt-faint,var(--ks-fg-subtle,#71717a));}
128
+ .ksui-datatable-row{border-top:1px solid var(--ksui-dt-row-border,var(--ks-border-subtle,rgba(39,39,42,0.25)));transition:background-color 0.15s ease;}
129
+ .ksui-datatable-row:hover{background-color:var(--ksui-dt-row-hover,var(--ks-border,rgba(39,39,42,0.5)));}
129
130
  .ksui-datatable-row-clickable{cursor:pointer;}
130
- .ksui-datatable-row-clickable:active{background-color:var(--ksui-dt-row-active,rgba(39,39,42,0.7));}
131
+ .ksui-datatable-row-clickable:active{background-color:var(--ksui-dt-row-active,color-mix(in srgb, var(--ks-border-strong, #3f3f46) 70%, transparent));}
131
132
  .ksui-datatable-td{padding:0.5rem 0.875rem;vertical-align:middle;white-space:nowrap;}
132
- .ksui-datatable-td-num{font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap;color:var(--ksui-dt-text-strong,#d4d4d8);}
133
- .ksui-datatable-code{display:block;font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;font-size:0.71875rem;color:var(--ksui-dt-fg,#e4e4e7);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:34ch;}
134
- .ksui-datatable-badge{display:inline-block;font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;font-size:0.625rem;padding:0.125rem 0.4375rem;border-radius:0.3125rem;letter-spacing:0.02em;background:var(--ksui-dt-control-bg,#18181b);color:var(--ksui-dt-text,#a1a1aa);}
135
- .ksui-datatable-badge-ok{background:var(--ksui-dt-good-bg,rgba(52,211,153,0.14));color:var(--ksui-dt-good,#34d399);}
136
- .ksui-datatable-badge-warn{background:var(--ksui-dt-accent-bg,rgba(217,119,6,0.2));color:var(--ksui-dt-accent,#fbbf24);}
137
- .ksui-datatable-badge-danger{background:var(--ksui-dt-danger-bg,rgba(248,113,113,0.14));color:var(--ksui-dt-danger,#f87171);}
138
- .ksui-datatable-expansion-row{border-top:1px solid var(--ksui-dt-row-border,rgba(39,39,42,0.3));background-color:var(--ksui-dt-expansion-bg,rgba(9,9,11,0.4));}
133
+ .ksui-datatable-td-num{font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap;color:var(--ksui-dt-text-strong,var(--ks-fg,#ffffff));}
134
+ .ksui-datatable-code{display:block;font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;font-size:0.71875rem;color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:34ch;}
135
+ .ksui-datatable-badge{display:inline-block;font-family:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;font-size:0.625rem;padding:0.125rem 0.4375rem;border-radius:0.3125rem;letter-spacing:0.02em;background:var(--ksui-dt-control-bg,var(--ks-input-bg,#18181b));color:var(--ksui-dt-text,var(--ks-fg-muted,#a1a1aa));}
136
+ .ksui-datatable-badge-ok{background:var(--ksui-dt-good-bg,color-mix(in srgb, var(--ks-success-fg, #34d399) 14%, transparent));color:var(--ksui-dt-good,var(--ks-success-fg,#34d399));}
137
+ .ksui-datatable-badge-warn{background:var(--ksui-dt-accent-bg,color-mix(in srgb, var(--ks-accent, #fbbf24) 20%, transparent));color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
138
+ .ksui-datatable-badge-danger{background:var(--ksui-dt-danger-bg,color-mix(in srgb, var(--ks-danger-fg, #f87171) 14%, transparent));color:var(--ksui-dt-danger,var(--ks-danger-fg,#f87171));}
139
+ .ksui-datatable-expansion-row{border-top:1px solid var(--ksui-dt-row-border,var(--ks-border-subtle,rgba(39,39,42,0.25)));background-color:var(--ksui-dt-expansion-bg,color-mix(in srgb, var(--ks-bg, #0a0a0a) 40%, transparent));}
139
140
  .ksui-datatable-expansion-td{padding:0;}
140
- .ksui-datatable-skeleton{height:1rem;width:100%;border-radius:0.25rem;background:var(--ksui-dt-skeleton,rgba(39,39,42,0.5));animation:ksuiDatatablePulse 1.5s cubic-bezier(0.4,0,0.6,1) infinite;}
141
+ .ksui-datatable-skeleton{height:1rem;width:100%;border-radius:0.25rem;background:var(--ksui-dt-skeleton,var(--ks-border,rgba(39,39,42,0.5)));animation:ksuiDatatablePulse 1.5s cubic-bezier(0.4,0,0.6,1) infinite;}
141
142
  @keyframes ksuiDatatablePulse{0%,100%{opacity:1;}50%{opacity:0.5;}}
142
- .ksui-datatable-empty{padding:3rem 1rem;text-align:center;color:var(--ksui-dt-muted,#71717a);}
143
- .ksui-datatable-footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));padding:0.625rem 0.875rem;}
144
- .ksui-datatable-info{font-size:0.75rem;color:var(--ksui-dt-muted,#71717a);}
145
- .ksui-datatable-showmore{border-radius:0.5rem;border:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));background:var(--ksui-dt-control-bg,#18181b);padding:0.5rem 1rem;font-size:0.75rem;font-weight:500;color:var(--ksui-dt-text-strong,#d4d4d8);transition:border-color 0.15s ease,color 0.15s ease;cursor:pointer;}
146
- .ksui-datatable-showmore:hover:not(:disabled){border-color:var(--ksui-dt-accent-border,rgba(245,158,11,0.4));color:var(--ksui-dt-accent,#fbbf24);}
143
+ .ksui-datatable-empty{padding:3rem 1rem;text-align:center;color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
144
+ .ksui-datatable-footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));padding:0.625rem 0.875rem;}
145
+ .ksui-datatable-info{font-size:0.75rem;color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
146
+ .ksui-datatable-showmore{border-radius:0.5rem;border:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));background:var(--ksui-dt-control-bg,var(--ks-input-bg,#18181b));padding:0.5rem 1rem;font-size:0.75rem;font-weight:500;color:var(--ksui-dt-text-strong,var(--ks-fg,#ffffff));transition:border-color 0.15s ease,color 0.15s ease;cursor:pointer;}
147
+ .ksui-datatable-showmore:hover:not(:disabled){border-color:var(--ksui-dt-accent-border,color-mix(in srgb, var(--ks-focus-ring, #c9a961) 40%, transparent));color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
147
148
  .ksui-datatable-showmore:disabled{cursor:not-allowed;opacity:0.5;}
148
149
  .ksui-datatable-pager{display:flex;align-items:center;gap:0.25rem;}
149
- .ksui-datatable-pager-arrow{border-radius:0.25rem;padding:0.375rem;color:var(--ksui-dt-text,#a1a1aa);transition:background-color 0.15s ease,color 0.15s ease;background:transparent;border:0;cursor:pointer;display:inline-flex;}
150
- .ksui-datatable-pager-arrow:hover:not(:disabled){background-color:var(--ksui-dt-row-hover,rgba(39,39,42,0.5));color:var(--ksui-dt-fg-strong,#ffffff);}
150
+ .ksui-datatable-pager-arrow{border-radius:0.25rem;padding:0.375rem;color:var(--ksui-dt-text,var(--ks-fg-muted,#a1a1aa));transition:background-color 0.15s ease,color 0.15s ease;background:transparent;border:0;cursor:pointer;display:inline-flex;}
151
+ .ksui-datatable-pager-arrow:hover:not(:disabled){background-color:var(--ksui-dt-row-hover,var(--ks-border,rgba(39,39,42,0.5)));color:var(--ksui-dt-fg-strong,var(--ks-fg,#ffffff));}
151
152
  .ksui-datatable-pager-arrow:disabled{cursor:not-allowed;opacity:0.3;}
152
- .ksui-datatable-pager-num{border-radius:0.25rem;padding:0.25rem 0.625rem;font-size:0.75rem;color:var(--ksui-dt-text,#a1a1aa);transition:background-color 0.15s ease,color 0.15s ease;background:transparent;border:0;cursor:pointer;}
153
- .ksui-datatable-pager-num:hover{background-color:var(--ksui-dt-row-hover,rgba(39,39,42,0.5));color:var(--ksui-dt-fg-strong,#ffffff);}
154
- .ksui-datatable-pager-num-active{background-color:var(--ksui-dt-accent-bg,rgba(217,119,6,0.2));font-weight:500;color:var(--ksui-dt-accent,#fbbf24);}
155
- .ksui-datatable-pager-num-active:hover{background-color:var(--ksui-dt-accent-bg,rgba(217,119,6,0.2));color:var(--ksui-dt-accent,#fbbf24);}
156
- .ksui-datatable-pager-ellipsis{padding:0 0.375rem;font-size:0.75rem;color:var(--ksui-dt-faint,#52525b);}
153
+ .ksui-datatable-pager-num{border-radius:0.25rem;padding:0.25rem 0.625rem;font-size:0.75rem;color:var(--ksui-dt-text,var(--ks-fg-muted,#a1a1aa));transition:background-color 0.15s ease,color 0.15s ease;background:transparent;border:0;cursor:pointer;}
154
+ .ksui-datatable-pager-num:hover{background-color:var(--ksui-dt-row-hover,var(--ks-border,rgba(39,39,42,0.5)));color:var(--ksui-dt-fg-strong,var(--ks-fg,#ffffff));}
155
+ .ksui-datatable-pager-num-active{background-color:var(--ksui-dt-accent-bg,color-mix(in srgb, var(--ks-accent, #fbbf24) 20%, transparent));font-weight:500;color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
156
+ .ksui-datatable-pager-num-active:hover{background-color:var(--ksui-dt-accent-bg,color-mix(in srgb, var(--ks-accent, #fbbf24) 20%, transparent));color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
157
+ .ksui-datatable-pager-ellipsis{padding:0 0.375rem;font-size:0.75rem;color:var(--ksui-dt-faint,var(--ks-fg-subtle,#71717a));}
157
158
  `;
158
159
 
159
160
  function ensureDataTableStyle(): void {
160
- if (typeof document === "undefined") return;
161
- if (document.getElementById(STYLE_ID)) return;
162
- const el = document.createElement("style");
163
- el.id = STYLE_ID;
164
- el.textContent = DATATABLE_CSS;
165
- document.head.appendChild(el);
161
+ injectCSS(STYLE_ID, DATATABLE_CSS);
166
162
  }
167
163
 
168
164
  // ---------------------------------------------------------------------------