@kahitsan/ksui 0.33.0 → 0.34.1

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 +33 -37
  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
@@ -40,11 +40,11 @@ function AccountTile(p: AccountTileProps): JSX.Element {
40
40
  onClick={p.onEditRequest}
41
41
  class={`group flex flex-1 min-w-0 items-center gap-3 border px-4 py-3 text-left transition-colors cursor-pointer ${p.clipClass}`}
42
42
  classList={{
43
- "border-blue-500/40 bg-blue-500/5 hover:bg-blue-500/10":
43
+ "border-[color-mix(in_srgb,var(--ks-info,#38bdf8)_40%,transparent)] bg-[color-mix(in_srgb,var(--ks-info,#38bdf8)_5%,transparent)] hover:bg-[color-mix(in_srgb,var(--ks-info,#38bdf8)_10%,transparent)]":
44
44
  isFilled() && isFrom(),
45
- "border-amber-500/40 bg-amber-500/5 hover:bg-amber-500/10":
45
+ "border-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_40%,transparent)] bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_5%,transparent)] hover:bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_10%,transparent)]":
46
46
  isFilled() && !isFrom(),
47
- "border-dashed border-zinc-700 bg-zinc-900/40 hover:border-amber-500/50 hover:bg-zinc-900/60":
47
+ "border-dashed border-[var(--ks-input-border,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_40%,transparent)] hover:border-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_50%,transparent)] hover:bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_60%,transparent)]":
48
48
  !isFilled(),
49
49
  }}
50
50
  aria-label={
@@ -57,7 +57,7 @@ function AccountTile(p: AccountTileProps): JSX.Element {
57
57
  when={p.account}
58
58
  keyed
59
59
  fallback={
60
- <span class="flex h-8 w-8 items-center justify-center text-zinc-500">
60
+ <span class="flex h-8 w-8 items-center justify-center text-[var(--ks-fg-subtle,#71717a)]">
61
61
  <Plus size={18} />
62
62
  </span>
63
63
  }
@@ -66,7 +66,7 @@ function AccountTile(p: AccountTileProps): JSX.Element {
66
66
  <AccountAvatar
67
67
  account={a}
68
68
  size={28}
69
- iconClass={isFrom() ? "text-blue-300" : "text-amber-300"}
69
+ iconClass={isFrom() ? "text-[var(--ks-info,#38bdf8)]" : "text-[var(--ks-accent-hover,#fcd34d)]"}
70
70
  />
71
71
  )}
72
72
  </Show>
@@ -74,9 +74,9 @@ function AccountTile(p: AccountTileProps): JSX.Element {
74
74
  <span
75
75
  class="text-[10px] font-semibold uppercase tracking-widest"
76
76
  classList={{
77
- "text-blue-300": isFilled() && isFrom(),
78
- "text-amber-300": isFilled() && !isFrom(),
79
- "text-zinc-500": !isFilled(),
77
+ "text-[var(--ks-info,#38bdf8)]": isFilled() && isFrom(),
78
+ "text-[var(--ks-accent-hover,#fcd34d)]": isFilled() && !isFrom(),
79
+ "text-[var(--ks-fg-subtle,#71717a)]": !isFilled(),
80
80
  }}
81
81
  >
82
82
  {isFrom() ? "From" : "To"}
@@ -85,23 +85,23 @@ function AccountTile(p: AccountTileProps): JSX.Element {
85
85
  when={p.account}
86
86
  keyed
87
87
  fallback={
88
- <span class="text-sm text-zinc-500">{p.emptyHint}</span>
88
+ <span class="text-sm text-[var(--ks-fg-subtle,#71717a)]">{p.emptyHint}</span>
89
89
  }
90
90
  >
91
91
  {(a) => (
92
92
  <>
93
- <span class="truncate text-sm font-semibold text-zinc-100">
93
+ <span class="truncate text-sm font-semibold text-[var(--ks-fg,#ffffff)]">
94
94
  {a.name}
95
95
  </span>
96
96
  <Show when={a.balance != null}>
97
- <span class="flex items-baseline gap-1 text-[11px] tabular-nums text-zinc-400">
97
+ <span class="flex items-baseline gap-1 text-[11px] tabular-nums text-[var(--ks-fg-muted,#a1a1aa)]">
98
98
  <span class="truncate">{formatPHP(a.balance!)}</span>
99
99
  <Show when={p.delta !== 0}>
100
100
  <span
101
101
  class="whitespace-nowrap font-semibold"
102
102
  classList={{
103
- "text-red-400": p.delta < 0,
104
- "text-emerald-400": p.delta > 0,
103
+ "text-[var(--ks-danger-fg,#f87171)]": p.delta < 0,
104
+ "text-[var(--ks-success-fg,#34d399)]": p.delta > 0,
105
105
  }}
106
106
  data-testid={`transactions-form-transfer-delta-${p.role}`}
107
107
  >
@@ -168,7 +168,7 @@ export default function TransferAccountsPicker(
168
168
  type="button"
169
169
  data-testid="transactions-form-transfer-swap"
170
170
  onClick={swap}
171
- class="flex h-9 w-9 shrink-0 items-center justify-center self-center border border-zinc-700 bg-zinc-900 text-zinc-300 transition-colors hover:border-amber-500/60 hover:text-amber-300 cursor-pointer ks-hud-clip-button max-sm:rotate-90"
171
+ class="flex h-9 w-9 shrink-0 items-center justify-center self-center border border-[var(--ks-input-border,#3f3f46)] bg-[var(--ks-input-bg,#18181b)] text-[var(--ks-fg,#ffffff)] transition-colors hover:border-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_60%,transparent)] hover:text-[var(--ks-accent-hover,#fcd34d)] cursor-pointer ks-hud-clip-button max-sm:rotate-90"
172
172
  aria-label="Swap source and destination"
173
173
  title="Swap source and destination"
174
174
  >
@@ -193,8 +193,8 @@ export default function TransferAccountsPicker(
193
193
  x2="56"
194
194
  y2="7"
195
195
  >
196
- <stop offset="0" stop-color="#3b82f6" />
197
- <stop offset="1" stop-color="#f59e0b" />
196
+ <stop offset="0" style={{ "stop-color": "var(--ks-info, #38bdf8)" }} />
197
+ <stop offset="1" style={{ "stop-color": "var(--ks-warning, #f59e0b)" }} />
198
198
  </linearGradient>
199
199
  </defs>
200
200
  <line
@@ -206,7 +206,7 @@ export default function TransferAccountsPicker(
206
206
  stroke-width="2"
207
207
  class="fin-flow-dash"
208
208
  />
209
- <path d="M44 1 L56 7 L44 13 Z" fill="#f59e0b" />
209
+ <path d="M44 1 L56 7 L44 13 Z" style={{ fill: "var(--ks-warning, #f59e0b)" }} />
210
210
  </svg>
211
211
  <svg
212
212
  viewBox="0 0 14 40"
@@ -221,8 +221,8 @@ export default function TransferAccountsPicker(
221
221
  x2="7"
222
222
  y2="40"
223
223
  >
224
- <stop offset="0" stop-color="#3b82f6" />
225
- <stop offset="1" stop-color="#f59e0b" />
224
+ <stop offset="0" style={{ "stop-color": "var(--ks-info, #38bdf8)" }} />
225
+ <stop offset="1" style={{ "stop-color": "var(--ks-warning, #f59e0b)" }} />
226
226
  </linearGradient>
227
227
  </defs>
228
228
  <line
@@ -234,7 +234,7 @@ export default function TransferAccountsPicker(
234
234
  stroke-width="2"
235
235
  class="fin-flow-dash"
236
236
  />
237
- <path d="M1 30 L13 30 L7 40 Z" fill="#f59e0b" />
237
+ <path d="M1 30 L13 30 L7 40 Z" style={{ fill: "var(--ks-warning, #f59e0b)" }} />
238
238
  </svg>
239
239
  </div>
240
240
  </Show>
@@ -13,9 +13,9 @@ export default function TransferFeeChip(props: TransferFeeChipProps) {
13
13
  onClick={props.onToggle}
14
14
  class="flex items-center gap-1.5 self-center rounded-md border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider transition-colors cursor-pointer"
15
15
  classList={{
16
- "border-blue-500/50 bg-blue-500/15 text-blue-300 hover:bg-blue-500/25":
16
+ "border-[color-mix(in_srgb,var(--ks-info,#38bdf8)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-info,#38bdf8)_15%,transparent)] text-[var(--ks-info-fg,#7dd3fc)] hover:bg-[color-mix(in_srgb,var(--ks-info,#38bdf8)_25%,transparent)]":
17
17
  props.enabled,
18
- "border-zinc-700 bg-zinc-950/60 text-zinc-400 hover:border-blue-500/40 hover:text-blue-300":
18
+ "border-[var(--ks-input-border,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-surface,#0f0f0f)_60%,transparent)] text-[var(--ks-fg-muted,#a1a1aa)] hover:border-[color-mix(in_srgb,var(--ks-info,#38bdf8)_40%,transparent)] hover:text-[var(--ks-info-fg,#7dd3fc)]":
19
19
  !props.enabled,
20
20
  }}
21
21
  aria-pressed={props.enabled}
@@ -229,15 +229,15 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
229
229
  onClick={() => !props.disabled && setOpen((o) => !o)}
230
230
  class={`${props.compact ? "inline-flex" : "w-full flex"} items-center gap-2 ${
231
231
  props.compact ? "px-2.5 py-2" : "px-3 py-2.5"
232
- } 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`}
232
+ } rounded-lg bg-[color-mix(in_srgb,var(--ks-border,rgba(39,39,42,0.5))_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`}
233
233
  aria-haspopup="listbox"
234
234
  aria-expanded={open()}
235
235
  >
236
- <Ticket size={16} class="shrink-0 text-zinc-400" />
237
- <Show when={props.selected} fallback={<span class="text-zinc-500 italic">No voucher</span>}>
236
+ <Ticket size={16} class="shrink-0 text-[var(--ks-fg-muted,#a1a1aa)]" />
237
+ <Show when={props.selected} fallback={<span class="text-[var(--ks-fg-subtle,#71717a)] italic">No voucher</span>}>
238
238
  <span class="flex-1 min-w-0">
239
- <span class="block truncate text-zinc-100 font-medium">{props.selected!.code}</span>
240
- <span class="block truncate text-[11px] text-emerald-400">
239
+ <span class="block truncate text-[var(--ks-fg,#ffffff)] font-medium">{props.selected!.code}</span>
240
+ <span class="block truncate text-[11px] text-[var(--ks-success-fg,#34d399)]">
241
241
  {formatVoucherDescription(props.selected!)}
242
242
  <Show when={previewDiscount() > 0}> · {formatCurrency(previewDiscount())} off</Show>
243
243
  </span>
@@ -246,7 +246,7 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
246
246
  type="button"
247
247
  data-testid="voucher-picker-clear"
248
248
  onClick={clear}
249
- class="shrink-0 p-1 rounded text-zinc-500 hover:text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer"
249
+ 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"
250
250
  title="Remove voucher"
251
251
  aria-label="Remove voucher"
252
252
  >
@@ -260,14 +260,14 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
260
260
  <div
261
261
  ref={popupRef}
262
262
  data-testid="voucher-picker-popup"
263
- class="z-[100] rounded-md border border-zinc-700 bg-zinc-900/95 backdrop-blur shadow-xl overflow-hidden flex flex-col"
263
+ class="z-[100] rounded-md border border-[var(--ks-input-border,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_95%,transparent)] backdrop-blur shadow-xl overflow-hidden flex flex-col"
264
264
  style={popupStyle()}
265
265
  >
266
- <div class="px-3 py-2 border-b border-zinc-800 flex items-center gap-2">
267
- <Ticket size={14} class="text-zinc-500 shrink-0" />
268
- <span class="text-xs uppercase tracking-widest text-zinc-500 font-bold">Vouchers</span>
266
+ <div class="px-3 py-2 border-b border-[var(--ks-border-strong,#3f3f46)] flex items-center gap-2">
267
+ <Ticket size={14} class="text-[var(--ks-fg-subtle,#71717a)] shrink-0" />
268
+ <span class="text-xs uppercase tracking-widest text-[var(--ks-fg-subtle,#71717a)] font-bold">Vouchers</span>
269
269
  <Show when={loading()}>
270
- <Loader2 size={14} class="animate-spin text-zinc-500 ml-auto shrink-0" />
270
+ <Loader2 size={14} class="animate-spin text-[var(--ks-fg-subtle,#71717a)] ml-auto shrink-0" />
271
271
  </Show>
272
272
  </div>
273
273
  <ul
@@ -277,7 +277,7 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
277
277
  >
278
278
  <Show when={error()}>
279
279
  <li>
280
- <div role="status" class="px-3 py-2 text-xs text-red-400">
280
+ <div role="status" class="px-3 py-2 text-xs text-[var(--ks-danger-fg,#f87171)]">
281
281
  {error()}
282
282
  </div>
283
283
  </li>
@@ -286,7 +286,7 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
286
286
  when={!loading() && !error() && applicable().length === 0 && inapplicable().length === 0}
287
287
  >
288
288
  <li>
289
- <div role="status" class="px-3 py-4 text-xs text-zinc-500 text-center">
289
+ <div role="status" class="px-3 py-4 text-xs text-[var(--ks-fg-subtle,#71717a)] text-center">
290
290
  No vouchers available.
291
291
  </div>
292
292
  </li>
@@ -301,20 +301,20 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
301
301
  type="button"
302
302
  data-testid={`voucher-picker-result-${v.id}`}
303
303
  onClick={() => select(v)}
304
- class="w-full text-left px-3 py-2 hover:bg-amber-500/10 transition-colors flex items-start gap-2 cursor-pointer"
304
+ 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"
305
305
  >
306
- <Ticket size={14} class="shrink-0 mt-0.5 text-emerald-400" aria-hidden="true" />
306
+ <Ticket size={14} class="shrink-0 mt-0.5 text-[var(--ks-success-fg,#34d399)]" aria-hidden="true" />
307
307
  <span class="flex-1 min-w-0">
308
- <span class="block text-sm text-zinc-100 truncate">{v.code}</span>
309
- <span class="block text-[11px] text-zinc-500 truncate">
308
+ <span class="block text-sm text-[var(--ks-fg,#ffffff)] truncate">{v.code}</span>
309
+ <span class="block text-[11px] text-[var(--ks-fg-subtle,#71717a)] truncate">
310
310
  {formatVoucherDescription(v)}
311
311
  </span>
312
312
  </span>
313
- <span class="text-xs text-emerald-300 shrink-0 mt-0.5 font-mono">
313
+ <span class="text-xs text-[var(--ks-success-fg,#34d399)] shrink-0 mt-0.5 font-mono">
314
314
  {formatCurrency(discount())}
315
315
  </span>
316
316
  <Show when={selected()}>
317
- <span class="text-amber-400 shrink-0 mt-0.5">✓</span>
317
+ <span class="text-[var(--ks-accent,#fbbf24)] shrink-0 mt-0.5">✓</span>
318
318
  </Show>
319
319
  </button>
320
320
  </li>
@@ -323,7 +323,7 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
323
323
  </For>
324
324
  <Show when={inapplicable().length > 0}>
325
325
  <li>
326
- <div class="px-3 pt-3 pb-1 text-[10px] uppercase tracking-widest text-zinc-600 font-semibold border-t border-zinc-800 mt-1">
326
+ <div class="px-3 pt-3 pb-1 text-[10px] uppercase tracking-widest text-[var(--ks-fg-subtle,#71717a)] font-semibold border-t border-[var(--ks-border-strong,#3f3f46)] mt-1">
327
327
  Not applicable to this cart
328
328
  </div>
329
329
  </li>
@@ -335,10 +335,10 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
335
335
  class="w-full text-left px-3 py-2 flex items-start gap-2 opacity-50 cursor-not-allowed"
336
336
  aria-disabled="true"
337
337
  >
338
- <Ticket size={14} class="shrink-0 mt-0.5 text-zinc-500" aria-hidden="true" />
338
+ <Ticket size={14} class="shrink-0 mt-0.5 text-[var(--ks-fg-subtle,#71717a)]" aria-hidden="true" />
339
339
  <span class="flex-1 min-w-0">
340
- <span class="block text-sm text-zinc-300 truncate">{v.code}</span>
341
- <span class="block text-[11px] text-zinc-500 truncate">
340
+ <span class="block text-sm text-[var(--ks-fg,#ffffff)] truncate">{v.code}</span>
341
+ <span class="block text-[11px] text-[var(--ks-fg-subtle,#71717a)] truncate">
342
342
  {formatVoucherDescription(v)}
343
343
  </span>
344
344
  </span>
@@ -349,12 +349,12 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
349
349
  </Show>
350
350
  </ul>
351
351
  <Show when={props.selected}>
352
- <div class="border-t border-zinc-800">
352
+ <div class="border-t border-[var(--ks-border-strong,#3f3f46)]">
353
353
  <button
354
354
  type="button"
355
355
  data-testid="voucher-picker-clear-from-list"
356
356
  onClick={() => select(null)}
357
- class="w-full text-left px-3 py-2 text-xs text-red-400 hover:bg-red-500/10 transition-colors flex items-center gap-2 cursor-pointer"
357
+ class="w-full text-left px-3 py-2 text-xs text-[var(--ks-danger-fg,#f87171)] hover:bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_10%,transparent)] transition-colors flex items-center gap-2 cursor-pointer"
358
358
  >
359
359
  <X size={12} />
360
360
  <span>Remove voucher</span>
@@ -76,7 +76,7 @@ export function ResourceForm(props: ResourceFormProps) {
76
76
  <Show when={props.error}>
77
77
  <div
78
78
  data-testid={`${props.spec.testIdPrefix}-form-error`}
79
- class="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-400"
79
+ class="rounded-lg border border-[color-mix(in_srgb,var(--ks-danger,#ef4444)_30%,transparent)] bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_10%,transparent)] px-3 py-2 text-sm text-[var(--ks-danger-fg,#f87171)]"
80
80
  >
81
81
  {props.error}
82
82
  </div>
@@ -316,7 +316,7 @@ export function ResourcePage<T extends ResourceRow>(
316
316
  onChange={(e) =>
317
317
  setFilterState(f.param, e.currentTarget.value)
318
318
  }
319
- class="rounded-lg border border-zinc-800/50 bg-zinc-900 px-3 py-1.5 text-xs text-zinc-400 cursor-pointer"
319
+ class="rounded-lg border border-[var(--ks-border,rgba(39,39,42,0.5))] bg-[var(--ks-input-bg,#18181b)] px-3 py-1.5 text-xs text-[var(--ks-fg-muted,#a1a1aa)] cursor-pointer"
320
320
  >
321
321
  <For each={f.options}>
322
322
  {(o) => <option value={o.value}>{o.label}</option>}
@@ -344,7 +344,7 @@ export function ResourcePage<T extends ResourceRow>(
344
344
  >
345
345
  <div data-testid={`${spec.testIdPrefix}-create-modal`}>
346
346
  <div class="flex items-center justify-between mb-6">
347
- <h2 class="text-lg font-semibold text-zinc-100">
347
+ <h2 class="text-lg font-semibold text-[var(--ks-fg,#ffffff)]">
348
348
  {spec.labels.createTitle}
349
349
  </h2>
350
350
  <button
@@ -352,7 +352,7 @@ export function ResourcePage<T extends ResourceRow>(
352
352
  setCreateOpen(false);
353
353
  resetForm();
354
354
  }}
355
- class="text-zinc-500 hover:text-zinc-300 cursor-pointer"
355
+ class="text-[var(--ks-fg-subtle,#71717a)] hover:text-[var(--ks-fg,#ffffff)] cursor-pointer"
356
356
  aria-label="Close"
357
357
  >
358
358
  <X size={20} />
@@ -387,7 +387,7 @@ export function ResourcePage<T extends ResourceRow>(
387
387
  >
388
388
  <div data-testid={`${spec.testIdPrefix}-detail-modal`}>
389
389
  <div class="flex items-center justify-between mb-6">
390
- <h2 class="text-lg font-semibold text-zinc-100">
390
+ <h2 class="text-lg font-semibold text-[var(--ks-fg,#ffffff)]">
391
391
  {editing()
392
392
  ? spec.labels.editTitle
393
393
  : String(row()[spec.labels.titleField] ?? "")}
@@ -396,7 +396,7 @@ export function ResourcePage<T extends ResourceRow>(
396
396
  <Show when={!editing() && canEdit()}>
397
397
  <button
398
398
  onClick={startEdit}
399
- class="text-zinc-500 hover:text-amber-400 cursor-pointer p-1"
399
+ class="text-[var(--ks-fg-subtle,#71717a)] hover:text-[var(--ks-accent,#fbbf24)] cursor-pointer p-1"
400
400
  title="Edit"
401
401
  aria-label="Edit"
402
402
  >
@@ -407,7 +407,7 @@ export function ResourcePage<T extends ResourceRow>(
407
407
  {row()[spec.softDeleteField] ? (
408
408
  <button
409
409
  onClick={() => handleArchive(row().id)}
410
- class="text-zinc-500 hover:text-red-400 cursor-pointer p-1"
410
+ class="text-[var(--ks-fg-subtle,#71717a)] hover:text-[var(--ks-danger-fg,#f87171)] cursor-pointer p-1"
411
411
  title="Archive"
412
412
  aria-label="Archive"
413
413
  >
@@ -416,7 +416,7 @@ export function ResourcePage<T extends ResourceRow>(
416
416
  ) : (
417
417
  <button
418
418
  onClick={() => handleRestore(row().id)}
419
- class="text-zinc-500 hover:text-emerald-400 cursor-pointer p-1"
419
+ class="text-[var(--ks-fg-subtle,#71717a)] hover:text-[var(--ks-success-fg,#34d399)] cursor-pointer p-1"
420
420
  title="Restore"
421
421
  aria-label="Restore"
422
422
  >
@@ -429,7 +429,7 @@ export function ResourcePage<T extends ResourceRow>(
429
429
  setDetailRow(null);
430
430
  setEditing(false);
431
431
  }}
432
- class="text-zinc-500 hover:text-zinc-300 cursor-pointer p-1"
432
+ class="text-[var(--ks-fg-subtle,#71717a)] hover:text-[var(--ks-fg,#ffffff)] cursor-pointer p-1"
433
433
  aria-label="Close"
434
434
  >
435
435
  <X size={20} />
@@ -18,7 +18,7 @@ export function renderCell(
18
18
  return (
19
19
  <button
20
20
  data-testid={`${spec.testIdPrefix}-row-${row.id}`}
21
- class="text-left text-zinc-200 hover:text-amber-400 transition-colors cursor-pointer"
21
+ class="text-left text-[var(--ks-fg,#ffffff)] hover:text-[var(--ks-accent,#fbbf24)] transition-colors cursor-pointer"
22
22
  onClick={() => onTitleClick(row.id)}
23
23
  >
24
24
  {String(raw ?? "")}
@@ -26,7 +26,7 @@ export function renderCell(
26
26
  );
27
27
  case "enum": {
28
28
  const key = String(raw ?? "");
29
- return <span class="text-zinc-400 text-sm capitalize">{r.labels[key] || key}</span>;
29
+ return <span class="text-[var(--ks-fg-muted,#a1a1aa)] text-sm capitalize">{r.labels[key] || key}</span>;
30
30
  }
31
31
  case "status": {
32
32
  const active = Boolean(raw);
@@ -35,7 +35,10 @@ export function renderCell(
35
35
  }
36
36
  case "text":
37
37
  default: {
38
- const cls = (r.type === "text" && r.muted) ? "text-zinc-500 text-sm" : "text-zinc-400 text-sm";
38
+ const cls =
39
+ r.type === "text" && r.muted
40
+ ? "text-[var(--ks-fg-subtle,#71717a)] text-sm"
41
+ : "text-[var(--ks-fg-muted,#a1a1aa)] text-sm";
39
42
  const text = raw === null || raw === undefined || raw === "" ? "—" : String(raw);
40
43
  return <span class={cls}>{text}</span>;
41
44
  }
@@ -1,7 +1,10 @@
1
1
  // Shared Tailwind class string for text inputs across plugin forms: a
2
- // full-width rounded input with the dark zinc surface and the amber focus
2
+ // full-width rounded input with the themed input surface and the amber focus
3
3
  // ring. Copied verbatim from the packages remote UI atoms so the shared kit
4
4
  // owns the single source of truth. Pure string constant, no dependencies.
5
-
5
+ //
6
+ // bg reads --ks-input-bg (not --ks-border, THEME-SPEC §1.2's dedicated input
7
+ // surface token) — the border token happens to be a plausible dark literal so
8
+ // this silently looked right in the `dark` theme and only broke elsewhere.
6
9
  export const INPUT_CLASS =
7
- "w-full rounded-lg border border-zinc-700 bg-zinc-800/50 px-3 py-2 text-sm text-zinc-200 focus:border-amber-500/50 focus:outline-none";
10
+ "w-full rounded-lg border border-[var(--ks-input-border,#3f3f46)] bg-[var(--ks-input-bg,#18181b)] px-3 py-2 text-sm text-[var(--ks-fg,#ffffff)] focus:border-[color-mix(in_srgb,var(--ks-focus-ring,#c9a961)_50%,transparent)] focus:outline-none";
@@ -74,17 +74,39 @@ export function getAccountIcon(account: { icon?: string | null; type: string }):
74
74
  return DEFAULT_BY_TYPE[account.type] ?? Banknote;
75
75
  }
76
76
 
77
+ // bank has no exact-hue token (blue-400/500 family isn't in §1.2), so it
78
+ // drifts to the nearest semantic bucket (--ks-info, sky). e_wallet/capital's
79
+ // bg (amber-500) and border (amber-400) are each an EXACT hex match to
80
+ // --ks-warning and --ks-accent respectively, so those keep zero-regression
81
+ // color-mix rather than sharing one base. Same for cash: bg is an exact
82
+ // --ks-success match, border an exact --ks-success-fg match.
77
83
  const DEFAULT_TONE_BY_TYPE: Record<string, { text: string; bg: string; border: string }> = {
78
- bank: { text: "text-blue-400", bg: "bg-blue-500/10", border: "border-blue-400/40" },
79
- e_wallet: { text: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-400/40" },
80
- cash: { text: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-400/40" },
81
- capital: { text: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-400/40" },
84
+ bank: {
85
+ text: "text-[var(--ks-info,#38bdf8)]",
86
+ bg: "bg-[color-mix(in_srgb,var(--ks-info,#38bdf8)_10%,transparent)]",
87
+ border: "border-[color-mix(in_srgb,var(--ks-info,#38bdf8)_40%,transparent)]",
88
+ },
89
+ e_wallet: {
90
+ text: "text-[var(--ks-accent,#fbbf24)]",
91
+ bg: "bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_10%,transparent)]",
92
+ border: "border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_40%,transparent)]",
93
+ },
94
+ cash: {
95
+ text: "text-[var(--ks-success-fg,#34d399)]",
96
+ bg: "bg-[color-mix(in_srgb,var(--ks-success,#10b981)_10%,transparent)]",
97
+ border: "border-[color-mix(in_srgb,var(--ks-success-fg,#34d399)_40%,transparent)]",
98
+ },
99
+ capital: {
100
+ text: "text-[var(--ks-accent,#fbbf24)]",
101
+ bg: "bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_10%,transparent)]",
102
+ border: "border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_40%,transparent)]",
103
+ },
82
104
  };
83
105
 
84
106
  const FALLBACK_TONE = {
85
- text: "text-zinc-300",
86
- bg: "bg-zinc-700/30",
87
- border: "border-zinc-700/60",
107
+ text: "text-[var(--ks-fg-muted,#a1a1aa)]",
108
+ bg: "bg-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_30%,transparent)]",
109
+ border: "border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_60%,transparent)]",
88
110
  };
89
111
 
90
112
  // Per-type accent tone for an account chip, or the account's own custom color.
@@ -9,16 +9,15 @@
9
9
  // still pass their own `markClass` to override.
10
10
 
11
11
  import { type JSX } from "solid-js";
12
+ import { injectCSS } from "./inject-css";
12
13
 
13
14
  const MARK_STYLE_ID = "ksui-mark-style";
14
15
 
15
16
  function ensureMarkStyle(): void {
16
- if (typeof document === "undefined") return;
17
- if (document.getElementById(MARK_STYLE_ID)) return;
18
- const style = document.createElement("style");
19
- style.id = MARK_STYLE_ID;
20
- style.textContent = `.ksui-mark{background-color:rgba(245,158,11,0.3);color:inherit;border-radius:2px;}`;
21
- document.head.appendChild(style);
17
+ injectCSS(
18
+ MARK_STYLE_ID,
19
+ `.ksui-mark{background-color:color-mix(in srgb, var(--ks-warning, #f59e0b) 30%, transparent);color:inherit;border-radius:2px;}`,
20
+ );
22
21
  }
23
22
 
24
23
  /** Case-insensitive substring test. Empty query matches everything. */
@@ -0,0 +1,13 @@
1
+ // Shared dedup so a component's CSS-in-JS string is appended to <head> once
2
+ // per id, no matter how many instances mount — replaces the copy-pasted
3
+ // createElement('style') pattern across ~14 components.
4
+ const injected = new Set<string>();
5
+
6
+ export function injectCSS(id: string, css: string): void {
7
+ if (injected.has(id) || typeof document === "undefined") return;
8
+ injected.add(id);
9
+ const style = document.createElement("style");
10
+ style.id = id;
11
+ style.textContent = css;
12
+ document.head.appendChild(style);
13
+ }
package/tailwind.js CHANGED
@@ -19,6 +19,10 @@
19
19
  /** @type {import('tailwindcss').Plugin} */
20
20
  export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
21
21
  // ─── Button intents ────────────────────────────────────────────────
22
+ // Amber/red/slate intent palette mirrors Button.tsx's own documented
23
+ // exception (component-authored tone system, not a 1:1 §1.2 surface/
24
+ // text/border mapping) — none of these rgba alpha values byte-match a
25
+ // §1.2 dark default, so left literal rather than guessed.
22
26
  // Primary (amber)
23
27
  addUtilities({
24
28
  ".ks-btn-primary": {
@@ -134,7 +138,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
134
138
  "align-items": "center",
135
139
  gap: "0.5rem",
136
140
  padding: "0.375rem 0.75rem",
137
- "background-color": "rgb(24 24 27)",
141
+ "background-color": "var(--ks-input-bg, rgb(24 24 27))",
138
142
  border: "1px solid rgb(63 63 70 / 0.6)",
139
143
  "border-radius": "0.25rem",
140
144
  color: "rgb(228 228 231)",
@@ -152,7 +156,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
152
156
  padding: "0.5rem 1rem",
153
157
  "text-align": "left",
154
158
  "font-weight": 500,
155
- color: "rgb(161 161 170)",
159
+ color: "var(--ks-fg-muted, rgb(161 161 170))",
156
160
  "font-size": "0.75rem",
157
161
  "text-transform": "uppercase",
158
162
  "letter-spacing": "0.05em",
@@ -168,7 +172,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
168
172
  ".ks-dt-empty": {
169
173
  padding: "2rem",
170
174
  "text-align": "center",
171
- color: "rgb(113 113 122)",
175
+ color: "var(--ks-fg-subtle, rgb(113 113 122))",
172
176
  },
173
177
  ".ks-dt-pager": {
174
178
  display: "flex",
@@ -177,7 +181,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
177
181
  padding: "0.75rem 1rem",
178
182
  "border-top": "1px solid rgb(39 39 42 / 0.6)",
179
183
  "font-size": "0.8125rem",
180
- color: "rgb(161 161 170)",
184
+ color: "var(--ks-fg-muted, rgb(161 161 170))",
181
185
  },
182
186
  });
183
187
 
@@ -194,7 +198,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
194
198
  padding: "1rem",
195
199
  },
196
200
  ".ks-modal-card": {
197
- "background-color": "rgb(24 24 27)",
201
+ "background-color": "var(--ks-overlay-surface, rgb(24 24 27))",
198
202
  border: "1px solid rgb(63 63 70 / 0.6)",
199
203
  "max-height": "calc(100vh - 2rem)",
200
204
  overflow: "auto",
@@ -246,7 +250,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
246
250
  "background-color": "rgba(59 130 246 / 0.1)",
247
251
  },
248
252
  ".ks-pill-neutral": {
249
- color: "rgb(161 161 170)",
253
+ color: "var(--ks-fg-muted, rgb(161 161 170))",
250
254
  "background-color": "rgb(63 63 70 / 0.3)",
251
255
  },
252
256
  });
@@ -259,7 +263,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
259
263
  gap: "0.25rem",
260
264
  padding: "0.25rem 0.5rem",
261
265
  "font-size": "0.75rem",
262
- color: "rgb(161 161 170)",
266
+ color: "var(--ks-fg-muted, rgb(161 161 170))",
263
267
  "background-color": "transparent",
264
268
  border: "1px solid rgb(63 63 70 / 0.6)",
265
269
  "border-radius": "0.25rem",