@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
@@ -25,35 +25,28 @@ import {
25
25
  type FlowNode,
26
26
  type FlowState,
27
27
  } from "../../utils/flow";
28
+ import { injectCSS } from "../../utils/inject-css";
28
29
 
29
30
  const STYLE_ID = "ksui-flow-runner-style";
30
-
31
- function ensureStyle(): void {
32
- if (typeof document === "undefined") return;
33
- if (document.getElementById(STYLE_ID)) return;
34
- const style = document.createElement("style");
35
- style.id = STYLE_ID;
36
- style.textContent = `
31
+ const STYLE_CSS = `
37
32
  .ksui-fr{display:flex;flex-direction:column;gap:0.875rem;color:var(--ksui-fr-fg,inherit);}
38
33
  .ksui-fr-title{font-size:0.95rem;font-weight:600;margin:0;}
39
34
  .ksui-fr-prompt,.ksui-fr-body{font-size:0.85rem;opacity:0.85;margin:0;}
40
35
  .ksui-fr-field{display:flex;flex-direction:column;gap:0.25rem;}
41
36
  .ksui-fr-label{font-size:0.78rem;font-weight:500;}
42
- .ksui-fr-input{padding:0.5rem 0.625rem;border-radius:0.5rem;border:1px solid var(--ksui-fr-border,rgba(255,255,255,0.15));background:var(--ksui-fr-input-bg,rgba(255,255,255,0.04));color:inherit;font-size:0.85rem;}
37
+ .ksui-fr-input{padding:0.5rem 0.625rem;border-radius:0.5rem;border:1px solid var(--ksui-fr-border,var(--ks-input-border,#3f3f46));background:var(--ksui-fr-input-bg,var(--ks-input-bg,#18181b));color:inherit;font-size:0.85rem;}
43
38
  .ksui-fr-actions{display:flex;gap:0.5rem;flex-wrap:wrap;}
44
- .ksui-fr-btn{padding:0.5rem 0.875rem;border-radius:0.5rem;border:1px solid var(--ksui-fr-border,rgba(255,255,255,0.15));background:var(--ksui-fr-btn-bg,rgba(255,255,255,0.06));color:inherit;font-size:0.82rem;cursor:pointer;}
39
+ .ksui-fr-btn{padding:0.5rem 0.875rem;border-radius:0.5rem;border:1px solid var(--ksui-fr-border,var(--ks-input-border,#3f3f46));background:var(--ksui-fr-btn-bg,var(--ks-surface-raised,#1a1a1a));color:inherit;font-size:0.82rem;cursor:pointer;}
45
40
  .ksui-fr-btn:disabled{opacity:0.5;cursor:not-allowed;}
46
- .ksui-fr-btn.primary{background:var(--ksui-fr-primary,#c9a961);color:var(--ksui-fr-primary-fg,#18181b);border-color:transparent;}
47
- .ksui-fr-btn.danger{background:var(--ksui-fr-danger,#ef4444);color:#fff;border-color:transparent;}
41
+ .ksui-fr-btn.primary{background:var(--ksui-fr-primary,var(--ks-primary,#c9a961));color:var(--ksui-fr-primary-fg,#18181b);border-color:transparent;}
42
+ .ksui-fr-btn.danger{background:var(--ksui-fr-danger,var(--ks-danger,#ef4444));color:var(--ks-fg,#ffffff);border-color:transparent;}
48
43
  .ksui-fr-msg{padding:0.625rem 0.75rem;border-radius:0.5rem;font-size:0.85rem;}
49
- .ksui-fr-msg.info{background:rgba(59,130,246,0.1);}
50
- .ksui-fr-msg.success{background:rgba(34,197,94,0.12);}
51
- .ksui-fr-msg.error{background:rgba(239,68,68,0.12);}
52
- .ksui-fr-error{padding:0.625rem 0.75rem;border-radius:0.5rem;font-size:0.82rem;background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.3);}
44
+ .ksui-fr-msg.info{background:var(--ks-info-bg,rgba(56,189,248,0.12));}
45
+ .ksui-fr-msg.success{background:var(--ks-success-bg,rgba(16,185,129,0.12));}
46
+ .ksui-fr-msg.error{background:var(--ks-danger-bg,rgba(239,68,68,0.12));}
47
+ .ksui-fr-error{padding:0.625rem 0.75rem;border-radius:0.5rem;font-size:0.82rem;background:var(--ks-danger-bg,rgba(239,68,68,0.12));border:1px solid color-mix(in srgb, var(--ks-danger, #ef4444) 30%, transparent);}
53
48
  .ksui-fr-loading{font-size:0.82rem;opacity:0.7;}
54
49
  `;
55
- document.head.appendChild(style);
56
- }
57
50
 
58
51
  export interface FlowRunnerProps {
59
52
  /** The first node the server handed the client (the flow entry). */
@@ -77,7 +70,7 @@ export interface FlowRunnerProps {
77
70
  }
78
71
 
79
72
  export const FlowRunner: Component<FlowRunnerProps> = (props) => {
80
- ensureStyle();
73
+ injectCSS(STYLE_ID, STYLE_CSS);
81
74
  const [node, setNode] = createSignal<FlowNode>(props.initialNode);
82
75
  const [busy, setBusy] = createSignal(false);
83
76
  const [error, setError] = createSignal<string | null>(null);
@@ -36,13 +36,13 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
36
36
  return (
37
37
  <div
38
38
  data-testid="advanced-fields-container"
39
- class="rounded-lg border border-zinc-700/60 bg-zinc-900/40 p-3 space-y-3"
39
+ class="rounded-lg border border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_60%,transparent)] bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_40%,transparent)] p-3 space-y-3"
40
40
  >
41
- <div class="flex items-center gap-2 text-[10px] uppercase tracking-widest text-zinc-400 font-semibold">
41
+ <div class="flex items-center gap-2 text-[10px] uppercase tracking-widest text-[var(--ks-fg-muted,#a1a1aa)] font-semibold">
42
42
  <span>Advanced</span>
43
43
  </div>
44
44
 
45
- <div class="rounded-lg border border-zinc-800/50 bg-zinc-950/40 p-3">
45
+ <div class="rounded-lg border border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-bg,#0a0a0a)_40%,transparent)] p-3">
46
46
  <FormField label="Tax">
47
47
  <SegmentedFilter
48
48
  options={[
@@ -75,7 +75,7 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
75
75
  : Math.round(amt * 0.12 * 100) / 100;
76
76
  const total = props.taxType === "vat_exclusive" ? sub + vat : amt;
77
77
  return (
78
- <div class="mt-2 text-xs text-zinc-500 space-y-0.5 border-t border-zinc-800/50 pt-2">
78
+ <div class="mt-2 text-xs text-[var(--ks-fg-subtle,#71717a)] space-y-0.5 border-t border-[var(--ks-border,rgba(39,39,42,0.5))] pt-2">
79
79
  <div class="flex justify-between">
80
80
  <span>VATtable Sales</span>
81
81
  <span>{formatPHP(sub)}</span>
@@ -84,7 +84,7 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
84
84
  <span>VAT (12%)</span>
85
85
  <span>{formatPHP(vat)}</span>
86
86
  </div>
87
- <div class="flex justify-between font-medium text-zinc-300">
87
+ <div class="flex justify-between font-medium text-[var(--ks-fg-muted,#a1a1aa)]">
88
88
  <span>Total</span>
89
89
  <span>{formatPHP(total)}</span>
90
90
  </div>
@@ -96,13 +96,13 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
96
96
  <Show
97
97
  when={props.category === "expense" || props.category === "payable"}
98
98
  >
99
- <div class="mt-3 border-t border-zinc-800/50 pt-3">
100
- <label class="flex items-center gap-2 text-xs text-zinc-300 cursor-pointer select-none">
99
+ <div class="mt-3 border-t border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_50%,transparent)] pt-3">
100
+ <label class="flex items-center gap-2 text-xs text-[var(--ks-fg-muted,#a1a1aa)] cursor-pointer select-none">
101
101
  <input
102
102
  type="checkbox"
103
103
  checked={props.hasEwt}
104
104
  onChange={(e) => props.setHasEwt(e.currentTarget.checked)}
105
- class="h-4 w-4 accent-amber-500 cursor-pointer"
105
+ class="h-4 w-4 accent-[var(--ks-primary,#c9a961)] cursor-pointer"
106
106
  />
107
107
  <span>Has Expanded Withholding Tax</span>
108
108
  </label>
@@ -116,7 +116,7 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
116
116
  max="100"
117
117
  value={props.ewtRate}
118
118
  onInput={(e) => props.setEwtRate(e.currentTarget.value)}
119
- class="w-full px-3 py-2 bg-zinc-900/50 border border-zinc-800 text-zinc-100 text-sm rounded-md focus:outline-none focus:border-amber-500/50"
119
+ class="w-full px-3 py-2 bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_50%,transparent)] border border-[var(--ks-border-strong,#3f3f46)] text-[var(--ks-fg,#ffffff)] text-sm rounded-md focus:outline-none focus:border-[color-mix(in_srgb,var(--ks-primary,#c9a961)_50%,transparent)]"
120
120
  placeholder="e.g. 1, 2, 5, 10, 15"
121
121
  />
122
122
  </FormField>
@@ -133,7 +133,7 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
133
133
  const rate = parseFloat(props.ewtRate);
134
134
  const ewt = Math.round(amt * rate) / 100;
135
135
  return (
136
- <div class="text-xs text-zinc-500 border-t border-zinc-800/50 pt-2">
136
+ <div class="text-xs text-[var(--ks-fg-subtle,#71717a)] border-t border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_50%,transparent)] pt-2">
137
137
  <div class="flex justify-between">
138
138
  <span>EWT ({rate}%)</span>
139
139
  <span>{formatPHP(ewt)}</span>
@@ -149,7 +149,7 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
149
149
  </div>
150
150
 
151
151
  <div
152
- class="rounded-lg border border-zinc-800/50 bg-zinc-950/40 p-4 relative"
152
+ class="rounded-lg border border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-bg,#0a0a0a)_40%,transparent)] p-4 relative"
153
153
  classList={{ "opacity-60": !props.canShare }}
154
154
  title={
155
155
  props.canShare
@@ -159,10 +159,10 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
159
159
  >
160
160
  <div class="flex items-center justify-between min-h-[44px]">
161
161
  <div class="flex items-center gap-2">
162
- <Lock size={14} class="text-zinc-500" />
162
+ <Lock size={14} class="text-[var(--ks-fg-subtle,#71717a)]" />
163
163
  <div>
164
- <span class="text-sm text-zinc-300">Private transaction</span>
165
- <p class="text-[10px] text-zinc-600">
164
+ <span class="text-sm text-[var(--ks-fg-muted,#a1a1aa)]">Private transaction</span>
165
+ <p class="text-[10px] text-[var(--ks-fg-subtle,#71717a)]">
166
166
  {props.canShare
167
167
  ? "Hidden from others unless shared"
168
168
  : "Locked — needs members.list_basic permission"}
@@ -195,15 +195,15 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
195
195
  </div>
196
196
 
197
197
  <Show when={props.isPrivate && props.canShare}>
198
- <div class="mt-4 pt-3 border-t border-zinc-800/50">
199
- <p class="text-[10px] text-zinc-500 mb-3">
198
+ <div class="mt-4 pt-3 border-t border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_50%,transparent)]">
199
+ <p class="text-[10px] text-[var(--ks-fg-subtle,#71717a)] mb-3">
200
200
  Always visible to:{" "}
201
- <span class="text-zinc-400">
201
+ <span class="text-[var(--ks-fg-muted,#a1a1aa)]">
202
202
  you (creator), org admins, superusers
203
203
  </span>
204
204
  </p>
205
205
 
206
- <span class="text-xs text-zinc-500 block mb-2">
206
+ <span class="text-xs text-[var(--ks-fg-subtle,#71717a)] block mb-2">
207
207
  Share with role
208
208
  </span>
209
209
  <div class="flex gap-2 mb-3 flex-wrap">
@@ -240,15 +240,15 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
240
240
  }}
241
241
  class="px-3 py-2 text-xs rounded-lg border cursor-pointer min-h-[36px] capitalize transition-colors active:opacity-80"
242
242
  classList={{
243
- "border-amber-500/40 bg-amber-500/10 text-amber-400":
243
+ "border-[color-mix(in_srgb,var(--ks-primary,#c9a961)_40%,transparent)] bg-[color-mix(in_srgb,var(--ks-primary,#c9a961)_10%,transparent)] text-[var(--ks-accent,#fbbf24)]":
244
244
  selected(),
245
- "border-zinc-700 bg-zinc-800/50 text-zinc-400 hover:border-zinc-600":
245
+ "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)]":
246
246
  !selected(),
247
247
  }}
248
248
  >
249
249
  All {role.label}s
250
250
  <Show when={membersInRole().length > 0}>
251
- <span class="text-zinc-600 ml-1">
251
+ <span class="text-[var(--ks-fg-subtle,#71717a)] ml-1">
252
252
  ({membersInRole().length})
253
253
  </span>
254
254
  </Show>
@@ -258,7 +258,7 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
258
258
  </For>
259
259
  </div>
260
260
 
261
- <span class="text-xs text-zinc-500 block mb-2">
261
+ <span class="text-xs text-[var(--ks-fg-subtle,#71717a)] block mb-2">
262
262
  Or select people
263
263
  </span>
264
264
  <div class="space-y-0.5 max-h-[150px] overflow-y-auto">
@@ -281,25 +281,25 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
281
281
  <label
282
282
  class="flex items-center gap-3 text-sm py-2 px-2 rounded-lg min-h-[40px] transition-colors"
283
283
  classList={{
284
- "text-zinc-300 cursor-pointer hover:bg-zinc-800/30 active:bg-zinc-800/50":
284
+ "text-[var(--ks-fg-muted,#a1a1aa)] cursor-pointer hover:bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_30%,transparent)] active:bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_50%,transparent)]":
285
285
  !coveringRole(),
286
- "text-zinc-500 cursor-not-allowed bg-zinc-900/40":
286
+ "text-[var(--ks-fg-subtle,#71717a)] cursor-not-allowed bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_40%,transparent)]":
287
287
  !!coveringRole(),
288
288
  }}
289
289
  >
290
290
  <div
291
291
  class="w-5 h-5 rounded border-2 flex items-center justify-center shrink-0 transition-colors"
292
292
  classList={{
293
- "border-amber-500 bg-amber-500":
293
+ "border-[var(--ks-primary,#c9a961)] bg-[var(--ks-primary,#c9a961)]":
294
294
  checked() && !coveringRole(),
295
- "border-amber-500/40 bg-amber-500/40":
295
+ "border-[color-mix(in_srgb,var(--ks-primary,#c9a961)_40%,transparent)] bg-[color-mix(in_srgb,var(--ks-primary,#c9a961)_40%,transparent)]":
296
296
  checked() && !!coveringRole(),
297
- "border-zinc-600 bg-transparent": !checked(),
297
+ "border-[var(--ks-border-strong,#3f3f46)] bg-transparent": !checked(),
298
298
  }}
299
299
  >
300
300
  <Show when={checked()}>
301
301
  <svg
302
- class="w-3 h-3 text-zinc-900"
302
+ class="w-3 h-3 text-[var(--ks-fg-on-primary,#0a0a0a)]"
303
303
  fill="none"
304
304
  viewBox="0 0 24 24"
305
305
  stroke="currentColor"
@@ -336,12 +336,12 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
336
336
  <Show
337
337
  when={coveringRole()}
338
338
  fallback={
339
- <span class="text-[10px] text-zinc-600 capitalize">
339
+ <span class="text-[10px] text-[var(--ks-fg-subtle,#71717a)] capitalize">
340
340
  {m.role}
341
341
  </span>
342
342
  }
343
343
  >
344
- <span class="text-[10px] text-amber-500/70">
344
+ <span class="text-[10px] text-[color-mix(in_srgb,var(--ks-primary,#c9a961)_70%,transparent)]">
345
345
  via All {coveringRole()!.label}s
346
346
  </span>
347
347
  </Show>
@@ -355,7 +355,7 @@ export default function FormAdvancedSection(props: FormAdvancedSectionProps) {
355
355
  props.orgMembers.length === 0
356
356
  }
357
357
  >
358
- <p class="text-xs text-zinc-600 py-2">Loading members...</p>
358
+ <p class="text-xs text-[var(--ks-fg-subtle,#71717a)] py-2">Loading members...</p>
359
359
  </Show>
360
360
  </div>
361
361
  </div>
@@ -223,7 +223,7 @@ const SCENARIO_CONFIGS: Record<Scenario, ScenarioConfig> = {
223
223
  },
224
224
  [SCENARIO_COMPLETED]: {
225
225
  position: "left",
226
- colorClass: "border border-zinc-600/60 text-zinc-400 hover:border-zinc-500",
226
+ colorClass: "border border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_60%,transparent)] text-[var(--ks-fg-muted,#a1a1aa)] hover:border-[var(--ks-fg-subtle,#71717a)]",
227
227
  label: "Completed",
228
228
  hidePercentage: false,
229
229
  shimmer: false,
@@ -238,8 +238,8 @@ const SCENARIO_CONFIGS: Record<Scenario, ScenarioConfig> = {
238
238
  };
239
239
 
240
240
  const COLOR_GREEN = "border border-green-600/60 text-green-400 hover:border-green-500";
241
- const COLOR_AMBER = "border border-amber-600/60 text-amber-400 hover:border-amber-500";
242
- const COLOR_RED = "border border-red-600/60 text-red-400 hover:border-red-500";
241
+ const COLOR_AMBER = "border border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_60%,transparent)] text-[var(--ks-accent,#fbbf24)] hover:border-[var(--ks-accent-hover,#fcd34d)]";
242
+ const COLOR_RED = "border border-red-600/60 text-[var(--ks-danger-fg,#f87171)] hover:border-[var(--ks-danger,#ef4444)]";
243
243
 
244
244
  const LiveTimer: Component<LiveTimerProps> = (props) => {
245
245
  const [local, others] = splitProps(props, [
@@ -288,7 +288,7 @@ function MentionHoverCard(props: { clientId: number; name: string }): JSX.Elemen
288
288
  <span class="relative inline-block" onMouseEnter={handleEnter} onMouseLeave={handleLeave}>
289
289
  <a
290
290
  href={`/clients/${props.clientId}`}
291
- class="inline-flex items-center rounded bg-zinc-800/50 px-1.5 py-0.5 text-[0.85em] text-emerald-400 hover:text-emerald-300 hover:bg-zinc-800/70 transition-colors"
291
+ class="inline-flex items-center rounded bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_50%,transparent)] px-1.5 py-0.5 text-[0.85em] text-[var(--ks-success-fg,#34d399)] hover:text-[var(--ks-success,#10b981)] hover:bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_70%,transparent)] transition-colors"
292
292
  data-testid="mention-chip"
293
293
  aria-describedby={open() && data() !== null ? hoverCardId : undefined}
294
294
  onFocus={handleFocus}
@@ -301,26 +301,26 @@ function MentionHoverCard(props: { clientId: number; name: string }): JSX.Elemen
301
301
  id={hoverCardId}
302
302
  role="tooltip"
303
303
  data-testid="mention-hover-card"
304
- class="absolute left-0 top-full z-50 mt-1 inline-block min-w-[14rem] max-w-xs rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-xs text-zinc-100 shadow-lg"
304
+ class="absolute left-0 top-full z-50 mt-1 inline-block min-w-[14rem] max-w-xs rounded-md border border-[var(--ks-border-strong,#3f3f46)] bg-[var(--ks-overlay-surface,#18181b)] px-3 py-2 text-xs text-[var(--ks-fg,#ffffff)] shadow-lg"
305
305
  onMouseEnter={() => clearTimers()}
306
306
  onMouseLeave={handleLeave}
307
307
  >
308
- <Show when={!loading() && data()} fallback={<span class="text-zinc-400">Loading…</span>}>
308
+ <Show when={!loading() && data()} fallback={<span class="text-[var(--ks-fg-muted,#a1a1aa)]">Loading…</span>}>
309
309
  {(c) => (
310
310
  <span class="block">
311
- <span class="block font-medium text-zinc-100">{c().name_raw}</span>
311
+ <span class="block font-medium text-[var(--ks-fg,#ffffff)]">{c().name_raw}</span>
312
312
  <Show when={c().email}>
313
- <span class="mt-1 block text-zinc-400" data-testid="mention-hover-email">
313
+ <span class="mt-1 block text-[var(--ks-fg-muted,#a1a1aa)]" data-testid="mention-hover-email">
314
314
  {c().email}
315
315
  </span>
316
316
  </Show>
317
317
  <Show when={c().phone}>
318
- <span class="block text-zinc-400" data-testid="mention-hover-phone">
318
+ <span class="block text-[var(--ks-fg-muted,#a1a1aa)]" data-testid="mention-hover-phone">
319
319
  {c().phone}
320
320
  </span>
321
321
  </Show>
322
322
  <Show when={!c().email && !c().phone}>
323
- <span class="mt-1 block text-zinc-500">No contact details on file.</span>
323
+ <span class="mt-1 block text-[var(--ks-fg-subtle,#71717a)]">No contact details on file.</span>
324
324
  </Show>
325
325
  </span>
326
326
  )}
@@ -339,7 +339,7 @@ function MentionChip(props: { name: string; clientId: number | null }): JSX.Elem
339
339
  when={props.clientId !== null}
340
340
  fallback={
341
341
  <span
342
- class="inline-flex items-center rounded bg-zinc-800/40 px-1.5 py-0.5 text-[0.85em] text-zinc-500"
342
+ class="inline-flex items-center rounded bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_40%,transparent)] px-1.5 py-0.5 text-[0.85em] text-[var(--ks-fg-subtle,#71717a)]"
343
343
  data-testid="mention-chip-unresolved"
344
344
  >
345
345
  @{props.name}
@@ -351,7 +351,7 @@ function MentionChip(props: { name: string; clientId: number | null }): JSX.Elem
351
351
  fallback={
352
352
  <a
353
353
  href={`/clients/${props.clientId}`}
354
- class="inline-flex items-center rounded bg-zinc-800/50 px-1.5 py-0.5 text-[0.85em] text-emerald-400 hover:text-emerald-300 hover:bg-zinc-800/70 transition-colors"
354
+ class="inline-flex items-center rounded bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_50%,transparent)] px-1.5 py-0.5 text-[0.85em] text-[var(--ks-success-fg,#34d399)] hover:text-[var(--ks-success,#10b981)] hover:bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_70%,transparent)] transition-colors"
355
355
  data-testid="mention-chip"
356
356
  >
357
357
  @{props.name}
@@ -384,7 +384,7 @@ function RenderInline(props: { tokens: InlineToken[]; searchQuery?: string }): J
384
384
  );
385
385
  if (t.kind === "code")
386
386
  return (
387
- <code class="px-1 py-0.5 rounded bg-zinc-800/60 text-[0.9em] text-zinc-200">
387
+ <code class="px-1 py-0.5 rounded bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_60%,transparent)] text-[0.9em] text-[var(--ks-fg-muted,#a1a1aa)]">
388
388
  {props.searchQuery ? highlightMatch(t.value, props.searchQuery) : t.value}
389
389
  </code>
390
390
  );
@@ -394,7 +394,7 @@ function RenderInline(props: { tokens: InlineToken[]; searchQuery?: string }): J
394
394
  href={t.href}
395
395
  target="_blank"
396
396
  rel="noopener noreferrer"
397
- class="text-emerald-400 hover:text-emerald-300 underline"
397
+ class="text-[var(--ks-success-fg,#34d399)] hover:text-[var(--ks-success,#10b981)] underline"
398
398
  >
399
399
  <RenderInline tokens={t.children} searchQuery={props.searchQuery} />
400
400
  </a>
@@ -35,9 +35,9 @@ export interface MentionTextareaProps {
35
35
  }
36
36
 
37
37
  const CHIP_BASE_CLASSES =
38
- "inline-flex items-baseline align-baseline rounded bg-amber-500/15 text-amber-300 px-1.5 py-px text-[0.9em] mx-px";
38
+ "inline-flex items-baseline align-baseline rounded bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_15%,transparent)] text-[var(--ks-accent-hover,#fcd34d)] px-1.5 py-px text-[0.9em] mx-px";
39
39
  const CHIP_UNRESOLVED_CLASSES =
40
- "inline-flex items-baseline align-baseline rounded bg-zinc-700/40 text-zinc-400 px-1.5 py-px text-[0.9em] mx-px";
40
+ "inline-flex items-baseline align-baseline rounded bg-[color-mix(in_srgb,var(--ks-input-border,#3f3f46)_40%,transparent)] text-[var(--ks-fg-muted,#a1a1aa)] px-1.5 py-px text-[0.9em] mx-px";
41
41
 
42
42
  function buildMentionChip(name: string, idStr: string): HTMLSpanElement {
43
43
  const span = document.createElement("span");
@@ -418,7 +418,7 @@ export default function MentionTextarea(props: MentionTextareaProps): JSX.Elemen
418
418
  <Show when={isEmpty() && props.placeholder}>
419
419
  <div
420
420
  aria-hidden="true"
421
- class={`${props.class ?? ""} pointer-events-none absolute inset-0 text-sm !text-zinc-500`}
421
+ class={`${props.class ?? ""} pointer-events-none absolute inset-0 text-sm !text-[var(--ks-fg-subtle,#71717a)]`}
422
422
  >
423
423
  {props.placeholder}
424
424
  </div>
@@ -430,17 +430,17 @@ export default function MentionTextarea(props: MentionTextareaProps): JSX.Elemen
430
430
  <div
431
431
  ref={popupRef}
432
432
  data-testid="mention-popup"
433
- class="z-[120] rounded-md border border-zinc-700 bg-zinc-900/95 backdrop-blur shadow-xl overflow-hidden flex flex-col"
433
+ class="z-[120] 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"
434
434
  style={popupStyle()}
435
435
  >
436
436
  <Show when={loading() && results().length === 0}>
437
- <div class="px-3 py-3 text-xs text-zinc-500 flex items-center gap-2">
437
+ <div class="px-3 py-3 text-xs text-[var(--ks-fg-subtle,#71717a)] flex items-center gap-2">
438
438
  <Loader2 size={12} class="animate-spin" />
439
439
  Searching clients…
440
440
  </div>
441
441
  </Show>
442
442
  <Show when={!loading() && results().length === 0}>
443
- <div class="px-3 py-3 text-xs text-zinc-500" data-testid="mention-popup-empty">
443
+ <div class="px-3 py-3 text-xs text-[var(--ks-fg-subtle,#71717a)]" data-testid="mention-popup-empty">
444
444
  {query() ? `No clients match "${query()}"` : "Type to search clients…"}
445
445
  </div>
446
446
  </Show>
@@ -464,14 +464,14 @@ export default function MentionTextarea(props: MentionTextareaProps): JSX.Elemen
464
464
  onMouseEnter={() => setActiveIdx(idx())}
465
465
  onClick={() => insertSelected(c)}
466
466
  class={`w-full text-left px-3 py-2 transition-colors flex items-start gap-2 cursor-pointer ${
467
- idx() === activeIdx() ? "bg-amber-500/15" : "hover:bg-amber-500/10"
467
+ idx() === activeIdx() ? "bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_15%,transparent)]" : "hover:bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_10%,transparent)]"
468
468
  }`}
469
469
  >
470
- <UserRound size={14} class="text-zinc-500 shrink-0 mt-0.5" />
470
+ <UserRound size={14} class="text-[var(--ks-fg-subtle,#71717a)] shrink-0 mt-0.5" />
471
471
  <span class="flex-1 min-w-0">
472
- <span class="block text-sm text-zinc-100 truncate">{c.name_raw}</span>
472
+ <span class="block text-sm text-[var(--ks-fg,#ffffff)] truncate">{c.name_raw}</span>
473
473
  <Show when={c.email || c.phone}>
474
- <span class="block text-[11px] text-zinc-500 truncate">
474
+ <span class="block text-[11px] text-[var(--ks-fg-subtle,#71717a)] truncate">
475
475
  {[c.email, c.phone].filter(Boolean).join(" · ")}
476
476
  </span>
477
477
  </Show>
@@ -63,10 +63,10 @@ export default function NotFound(props: NotFoundProps): JSX.Element {
63
63
  const showTitle = () => local.title !== "";
64
64
  const buttonClass =
65
65
  "inline-flex items-center justify-center gap-2 rounded-md border " +
66
- "border-amber-600/60 bg-amber-600/20 px-5 py-2.5 text-sm font-semibold " +
67
- "text-amber-400 transition-colors hover:border-amber-500 " +
68
- "hover:bg-amber-600/30 focus:outline-none focus-visible:ring-2 " +
69
- "focus-visible:ring-amber-500/60";
66
+ "border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_60%,transparent)] bg-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_20%,transparent)] px-5 py-2.5 text-sm font-semibold " +
67
+ "text-[var(--ks-accent,#fbbf24)] transition-colors hover:border-[var(--ks-accent,#fbbf24)] " +
68
+ "hover:bg-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_30%,transparent)] focus:outline-none focus-visible:ring-2 " +
69
+ "focus-visible:ring-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_60%,transparent)]";
70
70
 
71
71
  return (
72
72
  <div
@@ -83,16 +83,16 @@ export default function NotFound(props: NotFoundProps): JSX.Element {
83
83
  </Show>
84
84
 
85
85
  <Show when={showTitle()}>
86
- <h1 class="text-4xl md:text-6xl font-bold text-amber-500 mb-4">
86
+ <h1 class="text-4xl md:text-6xl font-bold text-[var(--ks-accent,#fbbf24)] mb-4">
87
87
  {local.title || "404"}
88
88
  </h1>
89
89
  </Show>
90
90
 
91
- <h2 class="text-xl md:text-2xl font-bold text-white mb-3">
91
+ <h2 class="text-xl md:text-2xl font-bold text-[var(--ks-fg,#ffffff)] mb-3">
92
92
  {local.heading || "Page Not Found"}
93
93
  </h2>
94
94
 
95
- <p class="text-sm text-zinc-400 max-w-md mb-8">
95
+ <p class="text-sm text-[var(--ks-fg-muted,#a1a1aa)] max-w-md mb-8">
96
96
  {local.message ||
97
97
  "The page you're looking for doesn't exist or has been moved."}
98
98
  </p>
@@ -197,12 +197,12 @@ export default function PaymentAccountPicker(props: PaymentAccountPickerProps):
197
197
  ? "Ask an admin to grant access to a financial account before charging."
198
198
  : undefined
199
199
  }
200
- 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"
200
+ 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-accent,#fbbf24)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_5%,transparent)] transition-colors text-sm text-left cursor-pointer disabled:cursor-not-allowed disabled:opacity-60"
201
201
  aria-haspopup="listbox"
202
202
  aria-expanded={open()}
203
203
  >
204
204
  <Show when={loading()} fallback={null}>
205
- <Loader2 size={14} class="animate-spin text-zinc-500 shrink-0" aria-hidden="true" />
205
+ <Loader2 size={14} class="animate-spin text-[var(--ks-fg-subtle,#71717a)] shrink-0" aria-hidden="true" />
206
206
  </Show>
207
207
  <Show when={!loading()}>
208
208
  <Show
@@ -212,13 +212,13 @@ export default function PaymentAccountPicker(props: PaymentAccountPickerProps):
212
212
  when={noAccounts()}
213
213
  fallback={
214
214
  <>
215
- <Banknote size={16} class="shrink-0 text-zinc-400" aria-hidden="true" />
216
- <span class="text-zinc-500 italic">Choose payment account</span>
215
+ <Banknote size={16} class="shrink-0 text-[var(--ks-fg-muted,#a1a1aa)]" aria-hidden="true" />
216
+ <span class="text-[var(--ks-fg-subtle,#71717a)] italic">Choose payment account</span>
217
217
  </>
218
218
  }
219
219
  >
220
- <Banknote size={16} class="shrink-0 text-zinc-500" aria-hidden="true" />
221
- <span class="text-zinc-500 italic">No accessible accounts</span>
220
+ <Banknote size={16} class="shrink-0 text-[var(--ks-fg-subtle,#71717a)]" aria-hidden="true" />
221
+ <span class="text-[var(--ks-fg-subtle,#71717a)] italic">No accessible accounts</span>
222
222
  </Show>
223
223
  }
224
224
  >
@@ -226,8 +226,8 @@ export default function PaymentAccountPicker(props: PaymentAccountPickerProps):
226
226
  <>
227
227
  <AccountAvatar account={acct()} size={28} />
228
228
  <span class="flex-1 min-w-0">
229
- <span class="block truncate text-zinc-100 font-medium">{acct().name}</span>
230
- <span class="block truncate text-[11px] text-zinc-500">
229
+ <span class="block truncate text-[var(--ks-fg,#ffffff)] font-medium">{acct().name}</span>
230
+ <span class="block truncate text-[11px] text-[var(--ks-fg-subtle,#71717a)]">
231
231
  {TYPE_LABELS[acct().type] ?? acct().type}
232
232
  </span>
233
233
  </span>
@@ -242,33 +242,33 @@ export default function PaymentAccountPicker(props: PaymentAccountPickerProps):
242
242
  <div
243
243
  ref={popupRef}
244
244
  data-testid="payment-account-picker-popup"
245
- class="z-[100] rounded-md border border-zinc-700 bg-zinc-900/95 backdrop-blur shadow-xl overflow-hidden flex flex-col"
245
+ 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"
246
246
  style={popupStyle()}
247
247
  >
248
- <div class="px-3 py-2 border-b border-zinc-800 flex items-center gap-2">
249
- <Banknote size={14} class="text-zinc-500 shrink-0" aria-hidden="true" />
250
- <span class="text-xs uppercase tracking-widest text-zinc-500 font-bold">
248
+ <div class="px-3 py-2 border-b border-[var(--ks-border,rgba(39,39,42,0.5))] flex items-center gap-2">
249
+ <Banknote size={14} class="text-[var(--ks-fg-subtle,#71717a)] shrink-0" aria-hidden="true" />
250
+ <span class="text-xs uppercase tracking-widest text-[var(--ks-fg-subtle,#71717a)] font-bold">
251
251
  Payment account
252
252
  </span>
253
253
  <Show when={loading()}>
254
- <Loader2 size={14} class="animate-spin text-zinc-500 ml-auto shrink-0" />
254
+ <Loader2 size={14} class="animate-spin text-[var(--ks-fg-subtle,#71717a)] ml-auto shrink-0" />
255
255
  </Show>
256
256
  </div>
257
257
  <div class="flex-1 overflow-y-auto">
258
258
  <Show when={error()}>
259
- <div role="status" class="px-3 py-2 text-xs text-red-400">
259
+ <div role="status" class="px-3 py-2 text-xs text-[var(--ks-danger-fg,#f87171)]">
260
260
  {error()}
261
261
  </div>
262
262
  </Show>
263
263
  <Show when={!loading() && !error() && accounts().length === 0}>
264
- <div role="status" class="px-3 py-4 text-xs text-zinc-500 text-center">
264
+ <div role="status" class="px-3 py-4 text-xs text-[var(--ks-fg-subtle,#71717a)] text-center">
265
265
  No accessible accounts. Ask an admin to grant access.
266
266
  </div>
267
267
  </Show>
268
268
  <For each={sections()}>
269
269
  {([type, list]) => (
270
270
  <div>
271
- <div class="px-3 pt-3 pb-1 text-[10px] uppercase tracking-widest text-zinc-600 font-semibold">
271
+ <div class="px-3 pt-3 pb-1 text-[10px] uppercase tracking-widest text-[var(--ks-fg-subtle,#71717a)] font-semibold">
272
272
  {TYPE_LABELS[type] ?? type}
273
273
  </div>
274
274
  <ul
@@ -285,14 +285,14 @@ export default function PaymentAccountPicker(props: PaymentAccountPickerProps):
285
285
  type="button"
286
286
  data-testid={`payment-account-picker-result-${a.id}`}
287
287
  onClick={() => select(a)}
288
- class="w-full text-left px-3 py-2 hover:bg-amber-500/10 transition-colors flex items-center gap-2 cursor-pointer"
288
+ class="w-full text-left px-3 py-2 hover:bg-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_10%,transparent)] transition-colors flex items-center gap-2 cursor-pointer"
289
289
  >
290
290
  <AccountAvatar account={a} size={28} />
291
- <span class="flex-1 min-w-0 text-sm text-zinc-100 truncate">
291
+ <span class="flex-1 min-w-0 text-sm text-[var(--ks-fg,#ffffff)] truncate">
292
292
  {a.name}
293
293
  </span>
294
294
  <Show when={selected()}>
295
- <span class="text-amber-400 shrink-0">✓</span>
295
+ <span class="text-[var(--ks-accent,#fbbf24)] shrink-0">✓</span>
296
296
  </Show>
297
297
  </button>
298
298
  </li>