@kahitsan/ksui 0.32.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 (65) 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/components/composite/resource/form-spec.ts +141 -0
  57. package/src/components/composite/resource/route-adapter.ts +63 -0
  58. package/src/components/composite/resource/route-spec.test.ts +125 -0
  59. package/src/components/composite/resource/route-spec.ts +158 -0
  60. package/src/index.ts +8 -0
  61. package/src/utils/INPUT_CLASS.ts +6 -3
  62. package/src/utils/account-icons.ts +29 -7
  63. package/src/utils/highlight.tsx +5 -6
  64. package/src/utils/inject-css.ts +13 -0
  65. package/tailwind.js +11 -7
@@ -13,33 +13,25 @@ import type { JSX } from "solid-js";
13
13
  import { splitProps } from "solid-js";
14
14
  import Sun from "lucide-solid/icons/sun";
15
15
  import Moon from "lucide-solid/icons/moon";
16
+ import { injectCSS } from "../../utils/inject-css";
16
17
 
17
18
  const THEME_TOGGLE_STYLE_ID = "ks-theme-toggle-inline-style";
18
19
  const THEME_TOGGLE_CSS = `
19
20
  .ks-theme-toggle { background: none; border: none; padding: 2px; cursor: pointer; display: flex; align-items: center; }
20
- .ks-theme-toggle-track { position: relative; display: flex; align-items: center; justify-content: space-between; width: 52px; height: 28px; border-radius: 14px; background-color: #3f3f46; padding: 0 2px; transition: background-color 0.3s ease; }
21
- .ks-theme-toggle-track[data-active] { background-color: #d3c5ac; }
21
+ .ks-theme-toggle-track { position: relative; display: flex; align-items: center; justify-content: space-between; width: 52px; height: 28px; border-radius: 14px; background-color: var(--ks-border-strong, #3f3f46); padding: 0 2px; transition: background-color 0.3s ease; }
22
+ .ks-theme-toggle-track[data-active] { background-color: var(--ks-primary-hover, #d4b876); }
22
23
  .ks-theme-toggle-icon { position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; pointer-events: none; transition: color 0.3s ease; }
23
- .ks-theme-toggle-track::after { content: ''; position: absolute; left: 2px; top: 2px; width: 24px; height: 24px; border-radius: 50%; background-color: #52525b; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, background-color 0.3s ease; z-index: 1; }
24
- .ks-theme-toggle-track[data-active]::after { transform: translateX(24px); background-color: #ffffff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); }
25
- .ks-theme-toggle-icon-moon { color: #fbbf24; }
26
- .ks-theme-toggle-icon-sun { color: rgba(255, 255, 255, 0.3); }
27
- .ks-theme-toggle-track[data-active] .ks-theme-toggle-icon-moon { color: rgba(120, 90, 0, 0.3); }
28
- .ks-theme-toggle-track[data-active] .ks-theme-toggle-icon-sun { color: #785a00; }
29
- .ks-theme-toggle:hover .ks-theme-toggle-track { background-color: #52525b; }
30
- .ks-theme-toggle:hover .ks-theme-toggle-track[data-active] { background-color: #c9b99a; }
24
+ .ks-theme-toggle-track::after { content: ''; position: absolute; left: 2px; top: 2px; width: 24px; height: 24px; border-radius: 50%; background-color: var(--ks-border-strong, #3f3f46); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, background-color 0.3s ease; z-index: 1; }
25
+ .ks-theme-toggle-track[data-active]::after { transform: translateX(24px); background-color: var(--ks-fg, #ffffff); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); }
26
+ .ks-theme-toggle-icon-moon { color: var(--ks-accent, #fbbf24); }
27
+ .ks-theme-toggle-icon-sun { color: color-mix(in srgb, var(--ks-fg, #ffffff) 30%, transparent); }
28
+ .ks-theme-toggle-track[data-active] .ks-theme-toggle-icon-moon { color: color-mix(in srgb, var(--ks-accent, #fbbf24) 30%, transparent); }
29
+ .ks-theme-toggle-track[data-active] .ks-theme-toggle-icon-sun { color: var(--ks-accent, #fbbf24); }
30
+ .ks-theme-toggle:hover .ks-theme-toggle-track { background-color: var(--ks-border-strong, #3f3f46); }
31
+ .ks-theme-toggle:hover .ks-theme-toggle-track[data-active] { background-color: var(--ks-primary-hover, #d4b876); }
31
32
  @media (prefers-reduced-motion: reduce) { .ks-theme-toggle-track, .ks-theme-toggle-track::after, .ks-theme-toggle-icon { transition: none; } }
32
33
  `;
33
34
 
34
- function ensureThemeToggleStyle(): void {
35
- if (typeof document === "undefined") return;
36
- if (document.getElementById(THEME_TOGGLE_STYLE_ID)) return;
37
- const el = document.createElement("style");
38
- el.id = THEME_TOGGLE_STYLE_ID;
39
- el.textContent = THEME_TOGGLE_CSS;
40
- document.head.appendChild(el);
41
- }
42
-
43
35
  export type ThemeToggleValue = "dark" | "light";
44
36
 
45
37
  export interface ThemeToggleProps
@@ -69,7 +61,7 @@ const defaultAriaLabel = (next: ThemeToggleValue): string => `Switch to ${next}
69
61
  * next value through `onToggle`. The parent owns the theme source of truth.
70
62
  */
71
63
  export default function ThemeToggle(props: ThemeToggleProps): JSX.Element {
72
- ensureThemeToggleStyle();
64
+ injectCSS(THEME_TOGGLE_STYLE_ID, THEME_TOGGLE_CSS);
73
65
 
74
66
  const [local, rest] = splitProps(props, [
75
67
  "value",
@@ -46,7 +46,7 @@ interface TooltipProps {
46
46
  // prefers describedby on the focusable trigger itself; since the
47
47
  // trigger is `props.children` (any element) and Solid has no
48
48
  // cloneElement, the trigger-level wiring lives with the caller in
49
- // specific cases that need it. See #627 for the broader sweep that
49
+ // specific cases that need it. See issue 627 for the broader sweep that
50
50
  // introduces a directive-based wiring API.
51
51
  export default function Tooltip(props: TooltipProps): JSX.Element {
52
52
  const tooltipId = props.id ?? createUniqueId();
@@ -62,7 +62,7 @@ export default function Tooltip(props: TooltipProps): JSX.Element {
62
62
  <span
63
63
  role="tooltip"
64
64
  id={tooltipId}
65
- class={`pointer-events-none absolute px-2 py-1 rounded-md bg-zinc-900 border border-zinc-700/80 text-[11px] text-zinc-100 opacity-0 group-hover/tt:opacity-100 group-focus-within/tt:opacity-100 transition-opacity duration-150 delay-150 z-50 shadow-lg ${
65
+ class={`pointer-events-none absolute px-2 py-1 rounded-md bg-[var(--ks-overlay-surface,#18181b)] border border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_80%,transparent)] text-[11px] text-[var(--ks-fg,#ffffff)] opacity-0 group-hover/tt:opacity-100 group-focus-within/tt:opacity-100 transition-opacity duration-150 delay-150 z-50 shadow-lg ${
66
66
  align() === "start" ? "left-0" : "left-1/2 -translate-x-1/2"
67
67
  } ${props.wrap ? "whitespace-normal w-max max-w-[260px]" : "whitespace-nowrap"} ${
68
68
  placement() === "bottom" ? "top-full mt-1" : "bottom-full mb-1"
@@ -73,7 +73,7 @@ export default function AccountRadioPicker(props: AccountRadioPickerProps) {
73
73
  onInput={(e) => props.onChange(e.currentTarget.value)}
74
74
  aria-label={props.ariaLabel}
75
75
  placeholder="Account ID (financial-accounts module unavailable)"
76
- class="w-full bg-zinc-900/60 border border-zinc-800/60 px-3 py-3 text-sm text-zinc-200 ks-hud-clip-button focus:outline-none focus:border-amber-500/50"
76
+ class="w-full bg-[color-mix(in_srgb,var(--ks-surface,#0f0f0f)_60%,transparent)] border border-[color-mix(in_srgb,var(--ks-border,rgba(39,39,42,0.5))_60%,transparent)] px-3 py-3 text-sm text-[var(--ks-fg,#ffffff)] ks-hud-clip-button focus:outline-none focus:border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_50%,transparent)]"
77
77
  />
78
78
  }
79
79
  >
@@ -98,16 +98,20 @@ export default function AccountRadioPicker(props: AccountRadioPickerProps) {
98
98
  onClick={() => props.onChange(a.id.toString())}
99
99
  class="group flex items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm transition-colors cursor-pointer ks-hud-clip-top-left-bottom-right"
100
100
  classList={{
101
- "border-amber-500/50 bg-amber-600/10 text-amber-300":
101
+ "border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_10%,transparent)] text-[var(--ks-accent-hover,#fcd34d)]":
102
102
  selected(),
103
- "border-zinc-700 bg-zinc-800/50 text-zinc-300 hover:border-zinc-600 hover:bg-zinc-800":
103
+ "border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_50%,transparent)] text-[var(--ks-fg-muted,#a1a1aa)] hover:border-[var(--ks-border-strong,#3f3f46)] hover:bg-[var(--ks-surface-raised,#1a1a1a)]":
104
104
  !selected(),
105
105
  }}
106
106
  >
107
107
  <AccountAvatar
108
108
  account={a}
109
109
  size={24}
110
- iconClass={selected() ? "text-amber-300" : "text-zinc-400"}
110
+ iconClass={
111
+ selected()
112
+ ? "text-[var(--ks-accent-hover,#fcd34d)]"
113
+ : "text-[var(--ks-fg-muted,#a1a1aa)]"
114
+ }
111
115
  />
112
116
  <span class="truncate">{a.name}</span>
113
117
  </button>
@@ -178,20 +178,20 @@ function SingleComboBox<T>(props: ComboBoxSingleProps<T>): JSX.Element {
178
178
  data-testid={tid("trigger")}
179
179
  disabled={props.disabled}
180
180
  onClick={() => !props.disabled && eng.setOpen((o) => !o)}
181
- class="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg bg-zinc-800/30 border border-zinc-700/50 hover:border-amber-500/40 hover:bg-amber-500/5 transition-colors text-sm text-left cursor-pointer disabled:cursor-not-allowed disabled:opacity-60"
181
+ class="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_30%,transparent)] border border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_50%,transparent)] hover:border-[color-mix(in_srgb,var(--ks-primary,#c9a961)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--ks-primary,#c9a961)_5%,transparent)] transition-colors text-sm text-left cursor-pointer disabled:cursor-not-allowed disabled:opacity-60"
182
182
  aria-haspopup="listbox"
183
183
  aria-expanded={eng.open()}
184
184
  >
185
- <Icon size={16} class="shrink-0 text-zinc-400" />
186
- <Show when={triggerLabel()} fallback={<span class="text-zinc-500 italic">{placeholder()}</span>}>
185
+ <Icon size={16} class="shrink-0 text-[var(--ks-fg-muted,#a1a1aa)]" />
186
+ <Show when={triggerLabel()} fallback={<span class="text-[var(--ks-fg-subtle,#71717a)] italic">{placeholder()}</span>}>
187
187
  <span class="flex-1 min-w-0">
188
- <span class="block truncate text-zinc-100 font-medium">{triggerLabel()}</span>
188
+ <span class="block truncate text-[var(--ks-fg,#ffffff)] font-medium">{triggerLabel()}</span>
189
189
  </span>
190
190
  <button
191
191
  type="button"
192
192
  data-testid={tid("clear")}
193
193
  onClick={clear}
194
- class="shrink-0 p-1 rounded text-zinc-500 hover:text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer"
194
+ class="shrink-0 p-1 rounded text-[var(--ks-fg-subtle,#71717a)] hover:text-[var(--ks-danger-fg,#f87171)] hover:bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_10%,transparent)] transition-colors cursor-pointer"
195
195
  title="Clear"
196
196
  aria-label={`Clear ${props.noun}`}
197
197
  >
@@ -205,11 +205,11 @@ function SingleComboBox<T>(props: ComboBoxSingleProps<T>): JSX.Element {
205
205
  <div
206
206
  ref={popupRef}
207
207
  data-testid={tid("popup")}
208
- class="z-[10000] rounded-md border border-zinc-700 bg-zinc-900/95 backdrop-blur shadow-xl overflow-hidden flex flex-col"
208
+ class="z-[10000] rounded-md border border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_95%,transparent)] backdrop-blur shadow-xl overflow-hidden flex flex-col"
209
209
  style={eng.popupStyle()}
210
210
  >
211
- <div class="px-2 py-2 border-b border-zinc-800 flex items-center gap-2">
212
- <Search size={14} class="text-zinc-500 shrink-0 ml-1" />
211
+ <div class="px-2 py-2 border-b border-[var(--ks-border-subtle,rgba(39,39,42,0.25))] flex items-center gap-2">
212
+ <Search size={14} class="text-[var(--ks-fg-subtle,#71717a)] shrink-0 ml-1" />
213
213
  <input
214
214
  ref={inputRef}
215
215
  type="text"
@@ -222,20 +222,20 @@ function SingleComboBox<T>(props: ComboBoxSingleProps<T>): JSX.Element {
222
222
  value={eng.query()}
223
223
  onInput={(e) => eng.setQuery(e.currentTarget.value)}
224
224
  placeholder={props.onCreate ? `Search or add a new ${props.noun}…` : `Search ${props.noun}s…`}
225
- class="w-full px-1 py-1 text-sm bg-transparent text-zinc-200 placeholder:text-zinc-600 focus:outline-none"
225
+ class="w-full px-1 py-1 text-sm bg-transparent text-[var(--ks-fg,#ffffff)] placeholder:text-[var(--ks-input-placeholder,#71717a)] focus:outline-none"
226
226
  />
227
227
  <Show when={eng.loading()}>
228
- <Loader2 size={14} class="animate-spin text-zinc-500 mr-1 shrink-0" />
228
+ <Loader2 size={14} class="animate-spin text-[var(--ks-fg-subtle,#71717a)] mr-1 shrink-0" />
229
229
  </Show>
230
230
  </div>
231
231
  <div class="flex-1 overflow-y-auto">
232
232
  <Show when={eng.error()}>
233
- <div role="status" class="px-3 py-2 text-xs text-red-400">
233
+ <div role="status" class="px-3 py-2 text-xs text-[var(--ks-danger-fg,#f87171)]">
234
234
  {eng.error()}
235
235
  </div>
236
236
  </Show>
237
237
  <Show when={!eng.loading() && eng.results().length === 0 && !showCreateOption() && !eng.error()}>
238
- <div role="status" class="px-3 py-4 text-xs text-zinc-500 text-center">
238
+ <div role="status" class="px-3 py-4 text-xs text-[var(--ks-fg-subtle,#71717a)] text-center">
239
239
  {eng.trimmedQuery() ? "No matches" : "Start typing or pick from your list…"}
240
240
  </div>
241
241
  </Show>
@@ -258,21 +258,21 @@ function SingleComboBox<T>(props: ComboBoxSingleProps<T>): JSX.Element {
258
258
  type="button"
259
259
  data-testid={`${tid("result")}-${props.idOf(item)}`}
260
260
  onClick={() => select(item)}
261
- class="w-full text-left px-3 py-2 hover:bg-amber-500/10 transition-colors flex items-start gap-2 cursor-pointer"
261
+ class="w-full text-left px-3 py-2 hover:bg-[color-mix(in_srgb,var(--ks-primary,#c9a961)_10%,transparent)] transition-colors flex items-start gap-2 cursor-pointer"
262
262
  >
263
- <Icon size={14} class="text-zinc-500 shrink-0 mt-0.5" />
263
+ <Icon size={14} class="text-[var(--ks-fg-subtle,#71717a)] shrink-0 mt-0.5" />
264
264
  <span class="flex-1 min-w-0">
265
- <span class="block text-sm text-zinc-100 truncate">
265
+ <span class="block text-sm text-[var(--ks-fg,#ffffff)] truncate">
266
266
  {highlightMatch(props.labelOf(item), eng.debouncedQuery().trim())}
267
267
  </span>
268
268
  <Show when={secondary}>
269
- <span class="block text-[11px] text-zinc-500 truncate">
269
+ <span class="block text-[11px] text-[var(--ks-fg-subtle,#71717a)] truncate">
270
270
  {highlightMatch(secondary!, eng.debouncedQuery().trim())}
271
271
  </span>
272
272
  </Show>
273
273
  </span>
274
274
  <Show when={isSel()}>
275
- <span class="text-amber-400 text-xs shrink-0 mt-0.5">✓</span>
275
+ <span class="text-[var(--ks-accent,#fbbf24)] text-xs shrink-0 mt-0.5">✓</span>
276
276
  </Show>
277
277
  </button>
278
278
  </li>
@@ -282,21 +282,21 @@ function SingleComboBox<T>(props: ComboBoxSingleProps<T>): JSX.Element {
282
282
  </ul>
283
283
  </Show>
284
284
  <Show when={showCreateOption()}>
285
- <div class="border-t border-zinc-800">
285
+ <div class="border-t border-[var(--ks-border-subtle,rgba(39,39,42,0.25))]">
286
286
  <button
287
287
  type="button"
288
288
  data-testid={tid("create")}
289
289
  onClick={createAndSelect}
290
290
  disabled={eng.creating()}
291
- class="w-full text-left px-3 py-2.5 hover:bg-emerald-500/10 transition-colors flex items-center gap-2 cursor-pointer disabled:cursor-wait disabled:opacity-60"
291
+ class="w-full text-left px-3 py-2.5 hover:bg-[color-mix(in_srgb,var(--ks-success,#10b981)_10%,transparent)] transition-colors flex items-center gap-2 cursor-pointer disabled:cursor-wait disabled:opacity-60"
292
292
  >
293
293
  <Show
294
294
  when={!eng.creating()}
295
- fallback={<Loader2 size={14} class="animate-spin text-emerald-400 shrink-0" />}
295
+ fallback={<Loader2 size={14} class="animate-spin text-[var(--ks-success-fg,#34d399)] shrink-0" />}
296
296
  >
297
- <UserPlus size={14} class="text-emerald-400 shrink-0" />
297
+ <UserPlus size={14} class="text-[var(--ks-success-fg,#34d399)] shrink-0" />
298
298
  </Show>
299
- <span class="text-sm text-emerald-300">
299
+ <span class="text-sm text-[var(--ks-success-fg,#34d399)]">
300
300
  New {props.noun} "<span class="font-medium">{eng.trimmedQuery()}</span>"
301
301
  </span>
302
302
  </button>
@@ -435,10 +435,11 @@ function MultiComboBox<T>(props: ComboBoxMultiProps<T>): JSX.Element {
435
435
  const isEmpty = () => props.value.length === 0;
436
436
 
437
437
  const wrapperTone = () => {
438
- if (props.disabled) return "opacity-60 cursor-not-allowed bg-zinc-800/30 border-zinc-700/50";
438
+ if (props.disabled)
439
+ return "opacity-60 cursor-not-allowed bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_30%,transparent)] border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_50%,transparent)]";
439
440
  if (props.invalid)
440
- return "bg-red-500/5 border-red-500/40 hover:bg-red-500/10 hover:border-red-500/60";
441
- return "bg-zinc-800/30 border-zinc-700/50 hover:border-amber-500/40 focus-within:border-amber-500/60";
441
+ return "bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_5%,transparent)] border-[color-mix(in_srgb,var(--ks-danger,#ef4444)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_10%,transparent)] hover:border-[color-mix(in_srgb,var(--ks-danger,#ef4444)_60%,transparent)]";
442
+ return "bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_30%,transparent)] border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_50%,transparent)] hover:border-[color-mix(in_srgb,var(--ks-primary,#c9a961)_40%,transparent)] focus-within:border-[color-mix(in_srgb,var(--ks-primary,#c9a961)_60%,transparent)]";
442
443
  };
443
444
 
444
445
  return (
@@ -469,12 +470,12 @@ function MultiComboBox<T>(props: ComboBoxMultiProps<T>): JSX.Element {
469
470
  data-testid={`${tid("chip")}-${idOf(item)}`}
470
471
  class={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs ${
471
472
  primary()
472
- ? "border-amber-500/50 bg-amber-500/10 text-amber-200"
473
- : "border-zinc-700 bg-zinc-800/40 text-zinc-200"
473
+ ? "border-[color-mix(in_srgb,var(--ks-primary,#c9a961)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-primary,#c9a961)_10%,transparent)] text-[var(--ks-accent,#fbbf24)]"
474
+ : "border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_40%,transparent)] text-[var(--ks-fg,#ffffff)]"
474
475
  }`}
475
476
  >
476
477
  <Show when={primary()}>
477
- <Star size={10} class="text-amber-400 shrink-0" aria-label="Primary" />
478
+ <Star size={10} class="text-[var(--ks-accent,#fbbf24)] shrink-0" aria-label="Primary" />
478
479
  </Show>
479
480
  <Show
480
481
  when={props.primaryStar}
@@ -510,7 +511,7 @@ function MultiComboBox<T>(props: ComboBoxMultiProps<T>): JSX.Element {
510
511
  removeFromPool(item);
511
512
  }}
512
513
  aria-label={`Remove ${props.labelOf(item)}`}
513
- class="shrink-0 rounded-full p-0.5 text-zinc-400 hover:text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer"
514
+ class="shrink-0 rounded-full p-0.5 text-[var(--ks-fg-muted,#a1a1aa)] hover:text-[var(--ks-danger-fg,#f87171)] hover:bg-red-500/10 transition-colors cursor-pointer"
514
515
  >
515
516
  <X size={11} />
516
517
  </button>
@@ -520,7 +521,7 @@ function MultiComboBox<T>(props: ComboBoxMultiProps<T>): JSX.Element {
520
521
  }}
521
522
  </For>
522
523
  <Show when={isEmpty()}>
523
- <Icon size={14} class={`shrink-0 ml-1 ${props.invalid ? "text-red-300" : "text-zinc-400"}`} />
524
+ <Icon size={14} class={`shrink-0 ml-1 ${props.invalid ? "text-[var(--ks-danger-fg,#f87171)]" : "text-[var(--ks-fg-muted,#a1a1aa)]"}`} />
524
525
  </Show>
525
526
  <input
526
527
  ref={inputRef}
@@ -535,8 +536,10 @@ function MultiComboBox<T>(props: ComboBoxMultiProps<T>): JSX.Element {
535
536
  if (!eng.open()) eng.setOpen(true);
536
537
  }}
537
538
  onKeyDown={onKeyDown}
538
- class={`flex-1 min-w-[120px] bg-transparent outline-none text-sm text-zinc-100 ${
539
- props.invalid && isEmpty() ? "placeholder-red-300/80 italic" : "placeholder-zinc-500"
539
+ class={`flex-1 min-w-[120px] bg-transparent outline-none text-sm text-[var(--ks-fg,#ffffff)] ${
540
+ props.invalid && isEmpty()
541
+ ? "placeholder-[color-mix(in_srgb,var(--ks-danger-fg,#f87171)_80%,transparent)] italic"
542
+ : "placeholder-[var(--ks-input-placeholder,#71717a)]"
540
543
  }`}
541
544
  />
542
545
  </div>
@@ -548,20 +551,20 @@ function MultiComboBox<T>(props: ComboBoxMultiProps<T>): JSX.Element {
548
551
  data-testid={tid("popup")}
549
552
  role="listbox"
550
553
  aria-label={`${props.noun} search results`}
551
- class="z-[110] rounded-md border border-zinc-700 bg-zinc-900/95 backdrop-blur shadow-xl overflow-hidden flex flex-col"
554
+ class="z-[110] rounded-md border border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_95%,transparent)] backdrop-blur shadow-xl overflow-hidden flex flex-col"
552
555
  style={eng.popupStyle()}
553
556
  >
554
557
  <Show when={eng.error()}>
555
- <div role="status" class="px-3 py-2 text-xs text-red-400 border-b border-zinc-800">
558
+ <div role="status" class="px-3 py-2 text-xs text-[var(--ks-danger-fg,#f87171)] border-b border-[var(--ks-border-subtle,rgba(39,39,42,0.25))]">
556
559
  {eng.error()}
557
560
  </div>
558
561
  </Show>
559
562
  <div class="flex-1 overflow-y-auto">
560
563
  <Show when={eng.loading() && eng.results().length === 0}>
561
- <div role="status" class="px-3 py-3 text-xs text-zinc-500 italic">Searching…</div>
564
+ <div role="status" class="px-3 py-3 text-xs text-[var(--ks-fg-subtle,#71717a)] italic">Searching…</div>
562
565
  </Show>
563
566
  <Show when={displayOptions().length === 0 && !eng.loading()}>
564
- <div role="status" class="px-3 py-3 text-xs text-zinc-500 italic">
567
+ <div role="status" class="px-3 py-3 text-xs text-[var(--ks-fg-subtle,#71717a)] italic">
565
568
  {eng.trimmedQuery() ? `No matching ${props.noun}s.` : `Start typing to find a ${props.noun}…`}
566
569
  </div>
567
570
  </Show>
@@ -580,17 +583,19 @@ function MultiComboBox<T>(props: ComboBoxMultiProps<T>): JSX.Element {
580
583
  onMouseEnter={() => setFocusedIdx(0)}
581
584
  onClick={() => void createAndAdd()}
582
585
  disabled={eng.creating()}
583
- class={`w-full flex items-start gap-2 px-3 py-2 text-left text-sm transition-colors border-b border-zinc-800 ${
584
- focusedIdx() === 0 ? "bg-amber-500/15 text-amber-200" : "text-zinc-100 hover:bg-zinc-800"
586
+ class={`w-full flex items-start gap-2 px-3 py-2 text-left text-sm transition-colors border-b border-[var(--ks-border-subtle,rgba(39,39,42,0.25))] ${
587
+ focusedIdx() === 0
588
+ ? "bg-[color-mix(in_srgb,var(--ks-primary,#c9a961)_15%,transparent)] text-[var(--ks-accent,#fbbf24)]"
589
+ : "text-[var(--ks-fg,#ffffff)] hover:bg-[var(--ks-surface-raised,#1a1a1a)]"
585
590
  }`}
586
591
  >
587
592
  <Show
588
593
  when={!eng.creating()}
589
- fallback={<Loader2 size={14} class="animate-spin text-emerald-400 shrink-0 mt-0.5" />}
594
+ fallback={<Loader2 size={14} class="animate-spin text-[var(--ks-success-fg,#34d399)] shrink-0 mt-0.5" />}
590
595
  >
591
- <UserPlus size={14} class="text-emerald-400 shrink-0 mt-0.5" />
596
+ <UserPlus size={14} class="text-[var(--ks-success-fg,#34d399)] shrink-0 mt-0.5" />
592
597
  </Show>
593
- <span class="flex-1 text-emerald-300">
598
+ <span class="flex-1 text-[var(--ks-success-fg,#34d399)]">
594
599
  New {props.noun} "<span class="font-medium">{eng.trimmedQuery()}</span>"
595
600
  </span>
596
601
  </button>
@@ -609,16 +614,18 @@ function MultiComboBox<T>(props: ComboBoxMultiProps<T>): JSX.Element {
609
614
  onMouseEnter={() => setFocusedIdx(displayIdx())}
610
615
  onClick={() => addToPool(item)}
611
616
  class={`w-full flex items-start gap-2 px-3 py-2 text-left text-sm transition-colors ${
612
- isFocused() ? "bg-amber-500/15 text-amber-200" : "text-zinc-100 hover:bg-zinc-800"
617
+ isFocused()
618
+ ? "bg-[color-mix(in_srgb,var(--ks-primary,#c9a961)_15%,transparent)] text-[var(--ks-accent,#fbbf24)]"
619
+ : "text-[var(--ks-fg,#ffffff)] hover:bg-[var(--ks-surface-raised,#1a1a1a)]"
613
620
  }`}
614
621
  >
615
- <Icon size={14} class="text-zinc-500 shrink-0 mt-0.5" />
622
+ <Icon size={14} class="text-[var(--ks-fg-subtle,#71717a)] shrink-0 mt-0.5" />
616
623
  <span class="flex-1 min-w-0">
617
624
  <span class="block truncate font-medium">
618
625
  {highlightMatch(props.labelOf(item), eng.debouncedQuery().trim())}
619
626
  </span>
620
627
  <Show when={secondary()}>
621
- <span class="block truncate text-[11px] text-zinc-500">
628
+ <span class="block truncate text-[11px] text-[var(--ks-fg-subtle,#71717a)]">
622
629
  {highlightMatch(secondary()!, eng.debouncedQuery().trim())}
623
630
  </span>
624
631
  </Show>
@@ -16,22 +16,15 @@ import {
16
16
  validateConsumes,
17
17
  type RendererProps,
18
18
  } from "../../utils/renderers";
19
+ import { injectCSS } from "../../utils/inject-css";
19
20
 
20
21
  const STYLE_ID = "ksui-custom-renderer-style";
21
-
22
- function ensureStyle(): void {
23
- if (typeof document === "undefined") return;
24
- if (document.getElementById(STYLE_ID)) return;
25
- const style = document.createElement("style");
26
- style.id = STYLE_ID;
27
- // Unscoped ksui-* classes + CSS custom properties so a host can retint without
28
- // forking; no Tailwind, no host-brand classes (standalone-library rule).
29
- style.textContent = `
30
- .ksui-cr-fallback{display:flex;align-items:center;gap:0.5rem;padding:0.625rem 0.75rem;border-radius:0.5rem;font-size:0.8125rem;background:var(--ksui-cr-fallback-bg,rgba(245,158,11,0.08));border:1px solid var(--ksui-cr-fallback-border,rgba(245,158,11,0.25));color:var(--ksui-cr-fallback-fg,#fbbf24);}
22
+ // Unscoped ksui-* classes + CSS custom properties so a host can retint without
23
+ // forking; no Tailwind, no host-brand classes (standalone-library rule).
24
+ const STYLE_CSS = `
25
+ .ksui-cr-fallback{display:flex;align-items:center;gap:0.5rem;padding:0.625rem 0.75rem;border-radius:0.5rem;font-size:0.8125rem;background:var(--ksui-cr-fallback-bg,rgba(245,158,11,0.08));border:1px solid var(--ksui-cr-fallback-border,rgba(245,158,11,0.25));color:var(--ksui-cr-fallback-fg,var(--ks-warning-fg,#fbbf24));}
31
26
  .ksui-cr-fallback svg{flex:0 0 auto;}
32
27
  `;
33
- document.head.appendChild(style);
34
- }
35
28
 
36
29
  export interface CustomRendererProps {
37
30
  /** Registered renderer id to look up (§8). */
@@ -52,7 +45,7 @@ export interface CustomRendererProps {
52
45
  }
53
46
 
54
47
  const DefaultFallback: Component<{ id: string; reason: string }> = (props) => {
55
- ensureStyle();
48
+ injectCSS(STYLE_ID, STYLE_CSS);
56
49
  return (
57
50
  <div class="ksui-cr-fallback" role="status" data-testid="ksui-cr-fallback">
58
51
  <AlertTriangle size={14} />
@@ -19,6 +19,7 @@ import UploadCloud from "lucide-solid/icons/cloud-upload";
19
19
  import FileIcon from "lucide-solid/icons/file";
20
20
  import ImageOff from "lucide-solid/icons/image-off";
21
21
  import X from "lucide-solid/icons/x";
22
+ import { injectCSS } from "../../utils/inject-css";
22
23
 
23
24
  /** The opaque asset handle the field stores as its value (§11). */
24
25
  export interface AssetHandle {
@@ -32,29 +33,21 @@ export interface AssetHandle {
32
33
  export type FileFieldStatus = "empty" | "uploading" | "done" | "failed";
33
34
 
34
35
  const STYLE_ID = "ksui-file-field-style";
35
-
36
- function ensureStyle(): void {
37
- if (typeof document === "undefined") return;
38
- if (document.getElementById(STYLE_ID)) return;
39
- const style = document.createElement("style");
40
- style.id = STYLE_ID;
41
- style.textContent = `
36
+ const STYLE_CSS = `
42
37
  .ksui-ff{display:flex;flex-direction:column;gap:0.5rem;}
43
- .ksui-ff-drop{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.375rem;padding:1.25rem;border-radius:0.625rem;border:1.5px dashed var(--ksui-ff-border,rgba(255,255,255,0.2));background:var(--ksui-ff-bg,rgba(255,255,255,0.03));color:var(--ksui-ff-fg,inherit);cursor:pointer;text-align:center;font-size:0.82rem;transition:border-color .15s,background .15s;}
44
- .ksui-ff-drop.dragging{border-color:var(--ksui-ff-accent,#c9a961);background:var(--ksui-ff-accent-bg,rgba(201,169,97,0.08));}
38
+ .ksui-ff-drop{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.375rem;padding:1.25rem;border-radius:0.625rem;border:1.5px dashed var(--ksui-ff-border,var(--ks-input-border,#3f3f46));background:var(--ksui-ff-bg,var(--ks-input-bg,#18181b));color:var(--ksui-ff-fg,inherit);cursor:pointer;text-align:center;font-size:0.82rem;transition:border-color .15s,background .15s;}
39
+ .ksui-ff-drop.dragging{border-color:var(--ksui-ff-accent,var(--ks-primary,#c9a961));background:var(--ksui-ff-accent-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 8%, transparent));}
45
40
  .ksui-ff-drop:disabled{opacity:0.5;cursor:not-allowed;}
46
41
  .ksui-ff-hint{font-size:0.72rem;opacity:0.6;}
47
- .ksui-ff-card{display:flex;align-items:center;gap:0.625rem;padding:0.5rem 0.625rem;border-radius:0.5rem;border:1px solid var(--ksui-ff-border,rgba(255,255,255,0.15));background:var(--ksui-ff-bg,rgba(255,255,255,0.03));}
48
- .ksui-ff-thumb{width:2.5rem;height:2.5rem;border-radius:0.375rem;object-fit:cover;flex:0 0 auto;background:rgba(255,255,255,0.06);display:flex;align-items:center;justify-content:center;}
42
+ .ksui-ff-card{display:flex;align-items:center;gap:0.625rem;padding:0.5rem 0.625rem;border-radius:0.5rem;border:1px solid var(--ksui-ff-border,var(--ks-input-border,#3f3f46));background:var(--ksui-ff-bg,var(--ks-input-bg,#18181b));}
43
+ .ksui-ff-thumb{width:2.5rem;height:2.5rem;border-radius:0.375rem;object-fit:cover;flex:0 0 auto;background:var(--ks-surface-sunken,#141414);display:flex;align-items:center;justify-content:center;}
49
44
  .ksui-ff-meta{display:flex;flex-direction:column;min-width:0;flex:1;}
50
45
  .ksui-ff-name{font-size:0.82rem;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
51
46
  .ksui-ff-sub{font-size:0.72rem;opacity:0.6;}
52
- .ksui-ff-sub.failed{color:var(--ksui-ff-danger,#f87171);}
53
- .ksui-ff-remove{margin-left:auto;flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:0.375rem;border:1px solid var(--ksui-ff-border,rgba(255,255,255,0.15));background:transparent;color:inherit;cursor:pointer;}
54
- .ksui-ff-retry{font-size:0.72rem;text-decoration:underline;cursor:pointer;color:var(--ksui-ff-accent,#c9a961);background:none;border:none;padding:0;}
47
+ .ksui-ff-sub.failed{color:var(--ksui-ff-danger,var(--ks-danger-fg,#f87171));}
48
+ .ksui-ff-remove{margin-left:auto;flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:0.375rem;border:1px solid var(--ksui-ff-border,var(--ks-input-border,#3f3f46));background:transparent;color:inherit;cursor:pointer;}
49
+ .ksui-ff-retry{font-size:0.72rem;text-decoration:underline;cursor:pointer;color:var(--ksui-ff-accent,var(--ks-primary,#c9a961));background:none;border:none;padding:0;}
55
50
  `;
56
- document.head.appendChild(style);
57
- }
58
51
 
59
52
  export interface FileFieldProps {
60
53
  /** Field label. */
@@ -89,7 +82,7 @@ function formatSize(bytes: number): string {
89
82
  }
90
83
 
91
84
  export const FileField: Component<FileFieldProps> = (props) => {
92
- ensureStyle();
85
+ injectCSS(STYLE_ID, STYLE_CSS);
93
86
  const [status, setStatus] = createSignal<FileFieldStatus>(props.value ? "done" : "empty");
94
87
  // The handle currently shown. Seeded from props.value, and updated locally on a
95
88
  // fresh upload so the done card shows even when the parent doesn't echo onChange
@@ -35,6 +35,7 @@ import {
35
35
  type GraphNode,
36
36
  type PositionedNode,
37
37
  } from "../../utils/graph";
38
+ import { injectCSS } from "../../utils/inject-css";
38
39
 
39
40
  type IconComp = Component<{ size?: number; class?: string }>;
40
41
 
@@ -62,58 +63,50 @@ const ZOOM_MIN = 0.3;
62
63
  const ZOOM_MAX = 3;
63
64
 
64
65
  const STYLE_ID = "ksui-flow-graph-style";
65
-
66
- function ensureStyle(): void {
67
- if (typeof document === "undefined") return;
68
- if (document.getElementById(STYLE_ID)) return;
69
- const style = document.createElement("style");
70
- style.id = STYLE_ID;
71
- style.textContent = `
66
+ const STYLE_CSS = `
72
67
  .ksui-fg-wrap{width:100%;overflow:auto;position:relative;user-select:none;-webkit-user-select:none;}
73
68
  .ksui-fg-card,.ksui-fg-title,.ksui-fg-kind,.ksui-fg-elabel{user-select:none;-webkit-user-select:none;}
74
- .ksui-fg-wrap.interactive{overflow:hidden;border:1px solid var(--ksui-fg-node-border,rgba(255,255,255,0.12));border-radius:10px;background:var(--ksui-fg-canvas,#101014);background-image:radial-gradient(var(--ksui-fg-dot,rgba(255,255,255,0.05)) 1px,transparent 1px);background-size:20px 20px;cursor:grab;touch-action:none;}
69
+ .ksui-fg-wrap.interactive{overflow:hidden;border:1px solid var(--ksui-fg-node-border,color-mix(in srgb, var(--ks-fg, #ffffff) 12%, transparent));border-radius:10px;background:var(--ksui-fg-canvas,var(--ks-bg,#0a0a0a));background-image:radial-gradient(var(--ksui-fg-dot,color-mix(in srgb, var(--ks-fg, #ffffff) 5%, transparent)) 1px,transparent 1px);background-size:20px 20px;cursor:grab;touch-action:none;}
75
70
  .ksui-fg-wrap.interactive.grabbing{cursor:grabbing;}
76
- .ksui-fg-wrap.scroll{overflow:auto;border:1px solid var(--ksui-fg-node-border,rgba(255,255,255,0.12));border-radius:10px;background:var(--ksui-fg-canvas,#101014);background-image:radial-gradient(var(--ksui-fg-dot,rgba(255,255,255,0.05)) 1px,transparent 1px);background-size:20px 20px;}
71
+ .ksui-fg-wrap.scroll{overflow:auto;border:1px solid var(--ksui-fg-node-border,color-mix(in srgb, var(--ks-fg, #ffffff) 12%, transparent));border-radius:10px;background:var(--ksui-fg-canvas,var(--ks-bg,#0a0a0a));background-image:radial-gradient(var(--ksui-fg-dot,color-mix(in srgb, var(--ks-fg, #ffffff) 5%, transparent)) 1px,transparent 1px);background-size:20px 20px;}
77
72
  .ksui-fg-svg{display:block;max-width:100%;height:auto;font-family:inherit;}
78
73
  .ksui-fg-wrap.scroll .ksui-fg-svg{margin:0 auto;}
79
74
  .ksui-fg-wrap.interactive .ksui-fg-svg{max-width:none;width:100%;height:100%;}
80
- .ksui-fg-edge{fill:none;stroke:var(--ksui-fg-edge,rgba(255,255,255,0.28));stroke-width:1.75;}
75
+ .ksui-fg-edge{fill:none;stroke:var(--ksui-fg-edge,color-mix(in srgb, var(--ks-fg, #ffffff) 28%, transparent));stroke-width:1.75;}
81
76
  .ksui-fg-edge.dashed{stroke-dasharray:5 4;}
82
- .ksui-fg-edge.primary{stroke:var(--ksui-fg-primary,#c9a961);}
83
- .ksui-fg-edge.info{stroke:#5b9bf0;}
84
- .ksui-fg-edge.success{stroke:#43c478;}
85
- .ksui-fg-edge.danger{stroke:#ef6a6a;}
86
- .ksui-fg-edge.muted{stroke:rgba(255,255,255,0.2);}
77
+ .ksui-fg-edge.primary{stroke:var(--ksui-fg-primary,var(--ks-primary,#c9a961));}
78
+ .ksui-fg-edge.info{stroke:var(--ks-info,#38bdf8);}
79
+ .ksui-fg-edge.success{stroke:var(--ks-success,#10b981);}
80
+ .ksui-fg-edge.danger{stroke:var(--ks-danger,#ef4444);}
81
+ .ksui-fg-edge.muted{stroke:color-mix(in srgb, var(--ks-fg, #ffffff) 20%, transparent);}
87
82
  .ksui-fg-edge.flow{stroke-dasharray:6 5;animation:ksui-fg-march .8s linear infinite;}
88
83
  @keyframes ksui-fg-march{to{stroke-dashoffset:-11;}}
89
84
  @media (prefers-reduced-motion:reduce){.ksui-fg-edge.flow{animation:none;}}
90
- .ksui-fg-handle{fill:var(--ksui-fg-canvas,#101014);stroke:var(--ksui-fg-edge,rgba(255,255,255,0.4));stroke-width:1.5;}
85
+ .ksui-fg-handle{fill:var(--ksui-fg-canvas,var(--ks-bg,#0a0a0a));stroke:var(--ksui-fg-edge,color-mix(in srgb, var(--ks-fg, #ffffff) 40%, transparent));stroke-width:1.5;}
91
86
  .ksui-fg-node,.ksui-fg-edge,.ksui-fg-elabel,.ksui-fg-elabel-bg,.ksui-fg-handle{transition:opacity .12s ease;}
92
87
  .ksui-fg-dim{opacity:0.14;}
93
- .ksui-fg-elabel{fill:var(--ksui-fg-muted,rgba(255,255,255,0.78));font-size:9.5px;}
94
- .ksui-fg-elabel-bg{fill:var(--ksui-fg-canvas,#101014);opacity:0.92;}
95
- .ksui-fg-card{box-sizing:border-box;height:100%;display:flex;align-items:center;gap:9px;padding:0 11px;border-radius:9px;background:var(--ksui-fg-card,#1c1c22);border:1px solid var(--ksui-fg-node-border,rgba(255,255,255,0.14));border-left-width:3px;overflow:hidden;}
88
+ .ksui-fg-elabel{fill:var(--ksui-fg-muted,color-mix(in srgb, var(--ks-fg, #ffffff) 78%, transparent));font-size:9.5px;}
89
+ .ksui-fg-elabel-bg{fill:var(--ksui-fg-canvas,var(--ks-bg,#0a0a0a));opacity:0.92;}
90
+ .ksui-fg-card{box-sizing:border-box;height:100%;display:flex;align-items:center;gap:9px;padding:0 11px;border-radius:9px;background:var(--ksui-fg-card,var(--ks-surface-raised,#1a1a1a));border:1px solid var(--ksui-fg-node-border,rgba(255,255,255,0.14));border-left-width:3px;overflow:hidden;}
96
91
  .ksui-fg-node.clickable .ksui-fg-card{cursor:pointer;}
97
- .ksui-fg-node.clickable .ksui-fg-card:hover{background:#23232b;}
92
+ .ksui-fg-node.clickable .ksui-fg-card:hover{background:var(--ks-surface-raised,#1a1a1a);}
98
93
  .ksui-fg-node:focus{outline:none;}
99
- .ksui-fg-node:focus-visible .ksui-fg-card{border-color:var(--ksui-fg-primary,#c9a961);}
100
- .ksui-fg-chip{flex:none;width:30px;height:30px;border-radius:7px;display:flex;align-items:center;justify-content:center;color:#fff;}
94
+ .ksui-fg-node:focus-visible .ksui-fg-card{border-color:var(--ksui-fg-primary,var(--ks-primary,#c9a961));}
95
+ .ksui-fg-chip{flex:none;width:30px;height:30px;border-radius:7px;display:flex;align-items:center;justify-content:center;color:var(--ks-fg,#ffffff);}
101
96
  .ksui-fg-txt{min-width:0;display:flex;flex-direction:column;line-height:1.15;}
102
- .ksui-fg-title{font-size:12px;font-weight:600;color:var(--ksui-fg-fg,#ececef);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
103
- .ksui-fg-kind{font-size:9px;text-transform:uppercase;letter-spacing:0.06em;color:var(--ksui-fg-muted,rgba(255,255,255,0.45));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
104
- .ksui-fg-card.primary{border-left-color:#c9a961;} .ksui-fg-card.primary .ksui-fg-chip{background:#c9a961;color:#18181b;}
105
- .ksui-fg-card.info{border-left-color:#5b9bf0;} .ksui-fg-card.info .ksui-fg-chip{background:#3f6fb0;}
106
- .ksui-fg-card.success{border-left-color:#43c478;} .ksui-fg-card.success .ksui-fg-chip{background:#2f8e57;}
107
- .ksui-fg-card.danger{border-left-color:#ef6a6a;} .ksui-fg-card.danger .ksui-fg-chip{background:#b04545;}
108
- .ksui-fg-card.muted{border-left-color:rgba(255,255,255,0.3);} .ksui-fg-card.muted .ksui-fg-chip{background:#3a3a42;}
109
- .ksui-fg-empty{padding:1.75rem 1rem;text-align:center;font-size:0.82rem;color:var(--ksui-fg-muted,rgba(255,255,255,0.55));}
97
+ .ksui-fg-title{font-size:12px;font-weight:600;color:var(--ksui-fg-fg,var(--ks-fg,#ffffff));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
98
+ .ksui-fg-kind{font-size:9px;text-transform:uppercase;letter-spacing:0.06em;color:var(--ksui-fg-muted,color-mix(in srgb, var(--ks-fg, #ffffff) 45%, transparent));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
99
+ .ksui-fg-card.primary{border-left-color:var(--ks-primary,#c9a961);} .ksui-fg-card.primary .ksui-fg-chip{background:var(--ks-primary,#c9a961);color:var(--ks-fg-on-primary,#0a0a0a);}
100
+ .ksui-fg-card.info{border-left-color:var(--ks-info,#38bdf8);} .ksui-fg-card.info .ksui-fg-chip{background:var(--ks-info-bg,rgba(56,189,248,0.12));}
101
+ .ksui-fg-card.success{border-left-color:var(--ks-success,#10b981);} .ksui-fg-card.success .ksui-fg-chip{background:var(--ks-success-bg,rgba(16,185,129,0.12));}
102
+ .ksui-fg-card.danger{border-left-color:var(--ks-danger,#ef4444);} .ksui-fg-card.danger .ksui-fg-chip{background:var(--ks-danger-bg,rgba(239,68,68,0.12));}
103
+ .ksui-fg-card.muted{border-left-color:color-mix(in srgb, var(--ks-fg, #ffffff) 30%, transparent);} .ksui-fg-card.muted .ksui-fg-chip{background:var(--ks-border-strong,#3f3f46);}
104
+ .ksui-fg-empty{padding:1.75rem 1rem;text-align:center;font-size:0.82rem;color:var(--ksui-fg-muted,color-mix(in srgb, var(--ks-fg, #ffffff) 55%, transparent));}
110
105
  .ksui-fg-controls{position:absolute;right:8px;bottom:8px;display:flex;gap:4px;z-index:1;}
111
- .ksui-fg-ctrl{width:24px;height:24px;display:grid;place-items:center;border-radius:5px;border:1px solid var(--ksui-fg-node-border,rgba(255,255,255,0.18));background:var(--ksui-fg-card,#1c1c22);color:var(--ksui-fg-fg,#ececef);font-size:14px;line-height:1;cursor:pointer;user-select:none;}
112
- .ksui-fg-ctrl:hover{background:#23232b;}
113
- .ksui-fg-hint{position:absolute;left:8px;bottom:8px;font-size:9.5px;color:var(--ksui-fg-muted,rgba(255,255,255,0.4));pointer-events:none;}
106
+ .ksui-fg-ctrl{width:24px;height:24px;display:grid;place-items:center;border-radius:5px;border:1px solid var(--ksui-fg-node-border,color-mix(in srgb, var(--ks-fg, #ffffff) 18%, transparent));background:var(--ksui-fg-card,var(--ks-surface-raised,#1a1a1a));color:var(--ksui-fg-fg,var(--ks-fg,#ffffff));font-size:14px;line-height:1;cursor:pointer;user-select:none;}
107
+ .ksui-fg-ctrl:hover{background:var(--ks-surface-raised,#1a1a1a);}
108
+ .ksui-fg-hint{position:absolute;left:8px;bottom:8px;font-size:9.5px;color:var(--ksui-fg-muted,color-mix(in srgb, var(--ks-fg, #ffffff) 40%, transparent));pointer-events:none;}
114
109
  `;
115
- document.head.appendChild(style);
116
- }
117
110
 
118
111
  export interface FlowGraphProps {
119
112
  nodes: GraphNode[];
@@ -146,7 +139,7 @@ function clip(text: string, max: number): string {
146
139
  }
147
140
 
148
141
  export const FlowGraph: Component<FlowGraphProps> = (props) => {
149
- ensureStyle();
142
+ injectCSS(STYLE_ID, STYLE_CSS);
150
143
  const tid = (s: string) => (props.testId ? `${props.testId}-${s}` : undefined);
151
144
 
152
145
  const vertical = () => props.direction === "vertical";