@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
@@ -42,6 +42,7 @@ import {
42
42
  normalizeDate,
43
43
  type ParsedDate,
44
44
  } from "../../utils/parse-date";
45
+ import { injectCSS } from "../../utils/inject-css";
45
46
 
46
47
  // ---------------------------------------------------------------------------
47
48
  // Injected CSS
@@ -52,107 +53,101 @@ import {
52
53
  // retint container restyles both. Picker-only tones use `--ksui-dp-*`. Fallback
53
54
  // after each `var(...)` = the host DatePicker's exact Tailwind value.
54
55
  //
55
- // --ksui-dt-control-bg popover + trigger + input bg (zinc-900, #18181b)
56
- // --ksui-dt-border borders / dividers (zinc-800/50, rgba(39,39,42,0.5))
57
- // --ksui-dt-fg primary white-ish text (white, #ffffff)
58
- // --ksui-dt-text secondary text (zinc-400, #a1a1aa)
59
- // --ksui-dt-muted placeholder / day-header / hint (zinc-500, #71717a)
60
- // --ksui-dt-faint "to" separator (zinc-600, #52525b)
61
- // --ksui-dt-accent active accent text (amber-400, #fbbf24)
62
- // --ksui-dt-accent-border focus ring (amber-500/50, rgba(245,158,11,0.5))
63
- // --ksui-dp-trigger-active-bg active trigger bg (amber-600/10, rgba(217,119,6,0.1))
64
- // --ksui-dp-trigger-active-border active trigger border (amber-500/40, rgba(245,158,11,0.4))
65
- // --ksui-dp-trigger-active-text active trigger text (amber-400, #fbbf24)
66
- // --ksui-dp-row-hover day-cell / nav hover bg (zinc-800/50, rgba(39,39,42,0.5))
67
- // --ksui-dp-cell-text in-month day text (zinc-300, #d4d4d8)
68
- // --ksui-dp-cell-out out-of-month day text (zinc-700, #3f3f46)
69
- // --ksui-dp-sel-bg selected single-day bg (amber-600/30, rgba(217,119,6,0.3))
70
- // --ksui-dp-range-end-bg range start/end bg (amber-600/40, rgba(217,119,6,0.4))
71
- // --ksui-dp-range-mid-bg in-range bg (amber-500/15, rgba(245,158,11,0.15))
72
- // --ksui-dp-preview-bg preview/hover bg (amber-500/10, rgba(245,158,11,0.1))
73
- // --ksui-dp-accent-soft range/in-range/preview text (amber-300, #fcd34d)
74
- // --ksui-dp-input-bg popover text-input bg (zinc-800/50, rgba(39,39,42,0.5))
75
- // --ksui-dp-input-border popover text-input border (zinc-700/50, rgba(63,63,70,0.5))
76
- // --ksui-dp-toggle-off off switch bg (zinc-700, #3f3f46)
77
- // --ksui-dp-toggle-on on switch bg (amber-500/80, rgba(245,158,11,0.8))
78
- // --ksui-dp-danger clear-hover / time-error text (red-400, #f87171)
79
- // --ksui-dp-danger-border time-input invalid border (red-500/50, rgba(239,68,68,0.5))
56
+ // Every `--ksui-*` fallback below now resolves through a `--ks-*` design
57
+ // token (THEME-SPEC §1.2) rather than a bare Tailwind literal, so a themed
58
+ // host restyles the picker without any ksui code change.
59
+ //
60
+ // --ksui-dt-control-bg popover + trigger + input bg -> --ks-input-bg
61
+ // --ksui-dt-border borders / dividers -> --ks-border
62
+ // --ksui-dt-fg primary text -> --ks-fg
63
+ // --ksui-dt-text secondary text -> --ks-fg-muted
64
+ // --ksui-dt-muted placeholder / day-header / hint -> --ks-fg-subtle
65
+ // --ksui-dt-faint "to" separator -> --ks-fg-subtle
66
+ // --ksui-dt-accent active accent text -> --ks-accent
67
+ // --ksui-dt-accent-border focus ring -> --ks-focus-ring (color-mix)
68
+ // --ksui-dp-trigger-active-bg / -border / -text -> --ks-primary / --ks-accent
69
+ // --ksui-dp-row-hover day-cell / nav hover bg -> --ks-border
70
+ // --ksui-dp-cell-text in-month day text -> --ks-fg
71
+ // --ksui-dp-cell-out out-of-month day text -> --ks-border-strong
72
+ // --ksui-dp-sel-bg / -range-end-bg / -range-mid-bg / -preview-bg -> --ks-primary (color-mix)
73
+ // --ksui-dp-accent-soft range/in-range/preview text -> --ks-accent-hover
74
+ // --ksui-dp-input-bg popover text-input bg -> --ks-border (color-mix)
75
+ // --ksui-dp-input-border popover text-input border -> --ks-border-strong (color-mix)
76
+ // --ksui-dp-toggle-off off switch bg -> --ks-border-strong
77
+ // --ksui-dp-toggle-on on switch bg -> --ks-primary (color-mix)
78
+ // --ksui-dp-danger clear-hover / time-error text -> --ks-danger-fg
79
+ // --ksui-dp-danger-border time-input invalid border -> --ks-danger (color-mix)
80
80
  //
81
81
  const STYLE_ID = "ksui-datepicker-style";
82
82
 
83
83
  const DATEPICKER_CSS = `
84
84
  .ksui-datepicker{position:relative;display:flex;align-items:center;gap:0.25rem;}
85
- .ksui-datepicker-trigger{display:inline-flex;cursor:pointer;align-items:center;gap:0.5rem;border-radius:0.5rem;border:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));background:var(--ksui-dt-control-bg,#18181b);padding:0.5rem 0.75rem;font-size:0.75rem;line-height:1rem;color:var(--ksui-dt-text,#a1a1aa);transition:color 0.15s ease,background-color 0.15s ease,border-color 0.15s ease;}
86
- .ksui-datepicker-trigger:hover:not(:disabled){color:var(--ksui-dt-fg,#ffffff);}
85
+ .ksui-datepicker-trigger{display:inline-flex;cursor:pointer;align-items:center;gap:0.5rem;border-radius:0.5rem;border:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));background:var(--ksui-dt-control-bg,var(--ks-input-bg,#18181b));padding:0.5rem 0.75rem;font-size:0.75rem;line-height:1rem;color:var(--ksui-dt-text,var(--ks-fg-muted,#a1a1aa));transition:color 0.15s ease,background-color 0.15s ease,border-color 0.15s ease;}
86
+ .ksui-datepicker-trigger:hover:not(:disabled){color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));}
87
87
  .ksui-datepicker-trigger:disabled{cursor:not-allowed;opacity:0.5;}
88
- .ksui-datepicker-trigger-active{border-color:var(--ksui-dp-trigger-active-border,rgba(245,158,11,0.4));background:var(--ksui-dp-trigger-active-bg,rgba(217,119,6,0.1));color:var(--ksui-dp-trigger-active-text,#fbbf24);}
89
- .ksui-datepicker-clear-btn{border-radius:0.25rem;padding:0.25rem;color:var(--ksui-dt-muted,#71717a);background:transparent;border:0;cursor:pointer;display:inline-flex;transition:color 0.15s ease,background-color 0.15s ease;}
90
- .ksui-datepicker-clear-btn:hover{background:var(--ksui-dp-row-hover,rgba(39,39,42,0.5));color:var(--ksui-dt-fg,#ffffff);}
91
- .ksui-datepicker-popover{position:fixed;z-index:60;overflow-y:auto;border-radius:0.75rem;border:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));background:var(--ksui-dt-control-bg,#18181b);box-shadow:0 25px 50px -12px rgba(0,0,0,0.5);}
88
+ .ksui-datepicker-trigger-active{border-color:var(--ksui-dp-trigger-active-border,color-mix(in srgb, var(--ks-primary, #c9a961) 40%, transparent));background:var(--ksui-dp-trigger-active-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 10%, transparent));color:var(--ksui-dp-trigger-active-text,var(--ks-accent,#fbbf24));}
89
+ .ksui-datepicker-clear-btn{border-radius:0.25rem;padding:0.25rem;color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));background:transparent;border:0;cursor:pointer;display:inline-flex;transition:color 0.15s ease,background-color 0.15s ease;}
90
+ .ksui-datepicker-clear-btn:hover{background:var(--ksui-dp-row-hover,var(--ks-border,rgba(39,39,42,0.5)));color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));}
91
+ .ksui-datepicker-popover{position:fixed;z-index:60;overflow-y:auto;border-radius:0.75rem;border:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));background:var(--ksui-dt-control-bg,var(--ks-input-bg,#18181b));box-shadow:0 25px 50px -12px rgba(0,0,0,0.5);}
92
92
  .ksui-datepicker-section{padding:0.75rem;}
93
- .ksui-datepicker-section-bordered{border-bottom:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));padding:0.75rem;}
94
- .ksui-datepicker-input{width:100%;border-radius:0.5rem;border:1px solid var(--ksui-dp-input-border,rgba(63,63,70,0.5));background:var(--ksui-dp-input-bg,rgba(39,39,42,0.5));padding:0.5rem 0.75rem;font-size:0.875rem;line-height:1.25rem;color:var(--ksui-dt-fg,#ffffff);outline:none;transition:border-color 0.15s ease;}
95
- .ksui-datepicker-input::placeholder{color:var(--ksui-dt-muted,#71717a);}
96
- .ksui-datepicker-input:focus{border-color:var(--ksui-dt-accent-border,rgba(245,158,11,0.5));}
93
+ .ksui-datepicker-section-bordered{border-bottom:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));padding:0.75rem;}
94
+ .ksui-datepicker-input{width:100%;border-radius:0.5rem;border:1px solid var(--ksui-dp-input-border,color-mix(in srgb, var(--ks-border-strong, #3f3f46) 50%, transparent));background:var(--ksui-dp-input-bg,var(--ks-border,rgba(39,39,42,0.5)));padding:0.5rem 0.75rem;font-size:0.875rem;line-height:1.25rem;color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));outline:none;transition:border-color 0.15s ease;}
95
+ .ksui-datepicker-input::placeholder{color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
96
+ .ksui-datepicker-input:focus{border-color:var(--ksui-dt-accent-border,color-mix(in srgb, var(--ks-focus-ring, #c9a961) 50%, transparent));}
97
97
  .ksui-datepicker-range-row{display:flex;align-items:center;gap:0.5rem;}
98
98
  .ksui-datepicker-range-input{flex:1 1 0%;}
99
- .ksui-datepicker-range-input-active{border-color:var(--ksui-dt-accent-border,rgba(245,158,11,0.5));}
100
- .ksui-datepicker-range-sep{font-size:0.75rem;color:var(--ksui-dt-faint,#52525b);}
99
+ .ksui-datepicker-range-input-active{border-color:var(--ksui-dt-accent-border,color-mix(in srgb, var(--ks-focus-ring, #c9a961) 50%, transparent));}
100
+ .ksui-datepicker-range-sep{font-size:0.75rem;color:var(--ksui-dt-faint,var(--ks-fg-subtle,#71717a));}
101
101
  .ksui-datepicker-preview{margin-top:0.5rem;display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;}
102
- .ksui-datepicker-preview-chip{border-radius:0.25rem;background:var(--ksui-dp-preview-bg,rgba(245,158,11,0.1));padding:0.125rem 0.5rem;color:var(--ksui-dt-accent,#fbbf24);}
103
- .ksui-datepicker-preview-hint{color:var(--ksui-dt-muted,#71717a);}
104
- .ksui-datepicker-quick{display:flex;gap:0.375rem;border-bottom:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));padding:0.5rem 0.75rem;}
105
- .ksui-datepicker-quick-btn{border-radius:0.375rem;padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--ksui-dt-text,#a1a1aa);background:transparent;border:0;cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease;}
106
- .ksui-datepicker-quick-btn:hover{background:var(--ksui-dp-row-hover,rgba(39,39,42,0.5));color:var(--ksui-dt-fg,#ffffff);}
107
- .ksui-datepicker-quick-btn-active{background:var(--ksui-dp-range-mid-bg,rgba(245,158,11,0.2));color:var(--ksui-dt-accent,#fbbf24);}
108
- .ksui-datepicker-quick-btn-active:hover{background:var(--ksui-dp-range-mid-bg,rgba(245,158,11,0.2));color:var(--ksui-dt-accent,#fbbf24);}
102
+ .ksui-datepicker-preview-chip{border-radius:0.25rem;background:var(--ksui-dp-preview-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 10%, transparent));padding:0.125rem 0.5rem;color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
103
+ .ksui-datepicker-preview-hint{color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
104
+ .ksui-datepicker-quick{display:flex;gap:0.375rem;border-bottom:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));padding:0.5rem 0.75rem;}
105
+ .ksui-datepicker-quick-btn{border-radius:0.375rem;padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--ksui-dt-text,var(--ks-fg-muted,#a1a1aa));background:transparent;border:0;cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease;}
106
+ .ksui-datepicker-quick-btn:hover{background:var(--ksui-dp-row-hover,var(--ks-border,rgba(39,39,42,0.5)));color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));}
107
+ .ksui-datepicker-quick-btn-active{background:var(--ksui-dp-range-mid-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 20%, transparent));color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
108
+ .ksui-datepicker-quick-btn-active:hover{background:var(--ksui-dp-range-mid-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 20%, transparent));color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
109
109
  .ksui-datepicker-nav{margin-bottom:0.5rem;display:flex;align-items:center;justify-content:space-between;}
110
- .ksui-datepicker-nav-btn{border-radius:0.25rem;padding:0.25rem;color:var(--ksui-dt-text,#a1a1aa);background:transparent;border:0;cursor:pointer;display:inline-flex;transition:background-color 0.15s ease,color 0.15s ease;}
111
- .ksui-datepicker-nav-btn:hover{background:var(--ksui-dp-row-hover,rgba(39,39,42,0.5));color:var(--ksui-dt-fg,#ffffff);}
112
- .ksui-datepicker-month-label{font-size:0.875rem;font-weight:500;color:var(--ksui-dt-fg,#e4e4e7);}
113
- .ksui-datepicker-dow{margin-bottom:0.25rem;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));text-align:center;font-size:0.75rem;color:var(--ksui-dt-muted,#71717a);}
110
+ .ksui-datepicker-nav-btn{border-radius:0.25rem;padding:0.25rem;color:var(--ksui-dt-text,var(--ks-fg-muted,#a1a1aa));background:transparent;border:0;cursor:pointer;display:inline-flex;transition:background-color 0.15s ease,color 0.15s ease;}
111
+ .ksui-datepicker-nav-btn:hover{background:var(--ksui-dp-row-hover,var(--ks-border,rgba(39,39,42,0.5)));color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));}
112
+ .ksui-datepicker-month-label{font-size:0.875rem;font-weight:500;color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));}
113
+ .ksui-datepicker-dow{margin-bottom:0.25rem;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));text-align:center;font-size:0.75rem;color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
114
114
  .ksui-datepicker-dow span{padding:0.25rem 0;}
115
115
  .ksui-datepicker-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:1px;}
116
- .ksui-datepicker-cell{border-radius:0.25rem;padding:0.375rem 0;text-align:center;font-size:0.75rem;color:var(--ksui-dp-cell-text,#d4d4d8);background:transparent;border:0;cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease;}
117
- .ksui-datepicker-cell:hover{background:var(--ksui-dp-row-hover,rgba(39,39,42,0.5));color:var(--ksui-dt-fg,#ffffff);}
118
- .ksui-datepicker-cell-out{color:var(--ksui-dp-cell-out,#3f3f46);}
119
- .ksui-datepicker-cell-out:hover{background:var(--ksui-dp-preview-bg,rgba(39,39,42,0.5));color:var(--ksui-dt-muted,#71717a);}
120
- .ksui-datepicker-cell-today{font-weight:500;color:var(--ksui-dt-accent,#fbbf24);}
121
- .ksui-datepicker-cell-selected{background:var(--ksui-dp-sel-bg,rgba(217,119,6,0.3));font-weight:500;color:var(--ksui-dt-accent,#fbbf24);}
122
- .ksui-datepicker-cell-selected:hover{background:var(--ksui-dp-sel-bg,rgba(217,119,6,0.3));color:var(--ksui-dt-accent,#fbbf24);}
123
- .ksui-datepicker-cell-preview{background:var(--ksui-dp-preview-bg,rgba(245,158,11,0.1));font-weight:500;color:var(--ksui-dp-accent-soft,#fcd34d);}
124
- .ksui-datepicker-cell-range-end{background:var(--ksui-dp-range-end-bg,rgba(217,119,6,0.4));font-weight:500;color:var(--ksui-dp-accent-soft,#fcd34d);}
125
- .ksui-datepicker-cell-range-end:hover{background:var(--ksui-dp-range-end-bg,rgba(217,119,6,0.4));color:var(--ksui-dp-accent-soft,#fcd34d);}
126
- .ksui-datepicker-cell-in-range{background:var(--ksui-dp-range-mid-bg,rgba(245,158,11,0.15));color:var(--ksui-dp-accent-soft,#fcd34d);}
127
- .ksui-datepicker-cell-in-range:hover{background:var(--ksui-dp-range-mid-bg,rgba(245,158,11,0.15));color:var(--ksui-dp-accent-soft,#fcd34d);}
128
- .ksui-datepicker-cell-range-preview{background:var(--ksui-dp-preview-bg,rgba(245,158,11,0.1));color:var(--ksui-dp-accent-soft,#fcd34d);}
129
- .ksui-datepicker-toggle-row{display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));padding:0.5rem 0.75rem;font-size:0.75rem;color:var(--ksui-dt-fg,#d4d4d8);}
130
- .ksui-datepicker-switch{position:relative;height:1rem;width:1.75rem;border-radius:9999px;padding:0.125rem;border:0;cursor:pointer;background:var(--ksui-dp-toggle-off,#3f3f46);transition:background-color 0.15s ease;}
131
- .ksui-datepicker-switch-on{background:var(--ksui-dp-toggle-on,rgba(245,158,11,0.8));}
116
+ .ksui-datepicker-cell{border-radius:0.25rem;padding:0.375rem 0;text-align:center;font-size:0.75rem;color:var(--ksui-dp-cell-text,var(--ks-fg,#ffffff));background:transparent;border:0;cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease;}
117
+ .ksui-datepicker-cell:hover{background:var(--ksui-dp-row-hover,var(--ks-border,rgba(39,39,42,0.5)));color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));}
118
+ .ksui-datepicker-cell-out{color:var(--ksui-dp-cell-out,var(--ks-border-strong,#3f3f46));}
119
+ .ksui-datepicker-cell-out:hover{background:var(--ksui-dp-preview-bg,var(--ks-border,rgba(39,39,42,0.5)));color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
120
+ .ksui-datepicker-cell-today{font-weight:500;color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
121
+ .ksui-datepicker-cell-selected{background:var(--ksui-dp-sel-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 30%, transparent));font-weight:500;color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
122
+ .ksui-datepicker-cell-selected:hover{background:var(--ksui-dp-sel-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 30%, transparent));color:var(--ksui-dt-accent,var(--ks-accent,#fbbf24));}
123
+ .ksui-datepicker-cell-preview{background:var(--ksui-dp-preview-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 10%, transparent));font-weight:500;color:var(--ksui-dp-accent-soft,var(--ks-accent-hover,#fcd34d));}
124
+ .ksui-datepicker-cell-range-end{background:var(--ksui-dp-range-end-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 40%, transparent));font-weight:500;color:var(--ksui-dp-accent-soft,var(--ks-accent-hover,#fcd34d));}
125
+ .ksui-datepicker-cell-range-end:hover{background:var(--ksui-dp-range-end-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 40%, transparent));color:var(--ksui-dp-accent-soft,var(--ks-accent-hover,#fcd34d));}
126
+ .ksui-datepicker-cell-in-range{background:var(--ksui-dp-range-mid-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 15%, transparent));color:var(--ksui-dp-accent-soft,var(--ks-accent-hover,#fcd34d));}
127
+ .ksui-datepicker-cell-in-range:hover{background:var(--ksui-dp-range-mid-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 15%, transparent));color:var(--ksui-dp-accent-soft,var(--ks-accent-hover,#fcd34d));}
128
+ .ksui-datepicker-cell-range-preview{background:var(--ksui-dp-preview-bg,color-mix(in srgb, var(--ks-primary, #c9a961) 10%, transparent));color:var(--ksui-dp-accent-soft,var(--ks-accent-hover,#fcd34d));}
129
+ .ksui-datepicker-toggle-row{display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));padding:0.5rem 0.75rem;font-size:0.75rem;color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));}
130
+ .ksui-datepicker-switch{position:relative;height:1rem;width:1.75rem;border-radius:9999px;padding:0.125rem;border:0;cursor:pointer;background:var(--ksui-dp-toggle-off,var(--ks-border-strong,#3f3f46));transition:background-color 0.15s ease;}
131
+ .ksui-datepicker-switch-on{background:var(--ksui-dp-toggle-on,color-mix(in srgb, var(--ks-primary, #c9a961) 80%, transparent));}
132
132
  .ksui-datepicker-switch-knob{display:block;height:0.75rem;width:0.75rem;border-radius:9999px;background:#ffffff;transition:transform 0.15s ease;transform:translateX(0);}
133
133
  .ksui-datepicker-switch-on .ksui-datepicker-switch-knob{transform:translateX(0.75rem);}
134
- .ksui-datepicker-time-row{border-top:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));padding:0.5rem 0.75rem;}
134
+ .ksui-datepicker-time-row{border-top:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));padding:0.5rem 0.75rem;}
135
135
  .ksui-datepicker-time-inner{display:flex;align-items:center;gap:0.5rem;}
136
- .ksui-datepicker-time-icon{color:var(--ksui-dt-muted,#71717a);display:inline-flex;}
137
- .ksui-datepicker-time-input{flex:1 1 0%;border-radius:0.25rem;border:1px solid var(--ksui-dp-input-border,rgba(63,63,70,0.5));background:var(--ksui-dp-input-bg,rgba(39,39,42,0.5));padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--ksui-dt-fg,#ffffff);outline:none;transition:border-color 0.15s ease;}
138
- .ksui-datepicker-time-input::placeholder{color:var(--ksui-dt-muted,#71717a);}
139
- .ksui-datepicker-time-input:focus{border-color:var(--ksui-dt-accent-border,rgba(245,158,11,0.5));}
140
- .ksui-datepicker-time-input-invalid{border-color:var(--ksui-dp-danger-border,rgba(239,68,68,0.5));}
141
- .ksui-datepicker-time-clear{border-radius:0.25rem;padding:0.125rem;color:var(--ksui-dt-muted,#71717a);background:transparent;border:0;cursor:pointer;display:inline-flex;}
142
- .ksui-datepicker-time-clear:hover{color:var(--ksui-dt-fg,#ffffff);}
143
- .ksui-datepicker-time-error{margin-top:0.25rem;font-size:0.75rem;color:var(--ksui-dp-danger,#f87171);}
144
- .ksui-datepicker-footer{display:flex;align-items:center;justify-content:flex-end;border-top:1px solid var(--ksui-dt-border,rgba(39,39,42,0.5));padding:0.5rem 0.75rem;}
145
- .ksui-datepicker-footer-clear{font-size:0.75rem;color:var(--ksui-dt-muted,#71717a);background:transparent;border:0;cursor:pointer;transition:color 0.15s ease;}
146
- .ksui-datepicker-footer-clear:hover{color:var(--ksui-dp-danger,#f87171);}
136
+ .ksui-datepicker-time-icon{color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));display:inline-flex;}
137
+ .ksui-datepicker-time-input{flex:1 1 0%;border-radius:0.25rem;border:1px solid var(--ksui-dp-input-border,color-mix(in srgb, var(--ks-border-strong, #3f3f46) 50%, transparent));background:var(--ksui-dp-input-bg,var(--ks-border,rgba(39,39,42,0.5)));padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));outline:none;transition:border-color 0.15s ease;}
138
+ .ksui-datepicker-time-input::placeholder{color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));}
139
+ .ksui-datepicker-time-input:focus{border-color:var(--ksui-dt-accent-border,color-mix(in srgb, var(--ks-focus-ring, #c9a961) 50%, transparent));}
140
+ .ksui-datepicker-time-input-invalid{border-color:var(--ksui-dp-danger-border,color-mix(in srgb, var(--ks-danger, #ef4444) 50%, transparent));}
141
+ .ksui-datepicker-time-clear{border-radius:0.25rem;padding:0.125rem;color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));background:transparent;border:0;cursor:pointer;display:inline-flex;}
142
+ .ksui-datepicker-time-clear:hover{color:var(--ksui-dt-fg,var(--ks-fg,#ffffff));}
143
+ .ksui-datepicker-time-error{margin-top:0.25rem;font-size:0.75rem;color:var(--ksui-dp-danger,var(--ks-danger-fg,#f87171));}
144
+ .ksui-datepicker-footer{display:flex;align-items:center;justify-content:flex-end;border-top:1px solid var(--ksui-dt-border,var(--ks-border,rgba(39,39,42,0.5)));padding:0.5rem 0.75rem;}
145
+ .ksui-datepicker-footer-clear{font-size:0.75rem;color:var(--ksui-dt-muted,var(--ks-fg-subtle,#71717a));background:transparent;border:0;cursor:pointer;transition:color 0.15s ease;}
146
+ .ksui-datepicker-footer-clear:hover{color:var(--ksui-dp-danger,var(--ks-danger-fg,#f87171));}
147
147
  `;
148
148
 
149
149
  function ensureDatePickerStyle(): void {
150
- if (typeof document === "undefined") return;
151
- if (document.getElementById(STYLE_ID)) return;
152
- const el = document.createElement("style");
153
- el.id = STYLE_ID;
154
- el.textContent = DATEPICKER_CSS;
155
- document.head.appendChild(el);
150
+ injectCSS(STYLE_ID, DATEPICKER_CSS);
156
151
  }
157
152
 
158
153
  // ---------------------------------------------------------------------------
@@ -47,13 +47,13 @@ export default function DateTile(props: DateTileProps): JSX.Element {
47
47
  style={{ width: "60px", height: "68px", flex: "0 0 auto" }}
48
48
  class={`${interactive() ? "cursor-pointer" : ""} flex flex-col rounded-md overflow-hidden border transition-colors`}
49
49
  classList={{
50
- "border-amber-500/25 bg-zinc-800/40": on(),
51
- "border-zinc-800/60 bg-zinc-900/40 opacity-50 hover:opacity-80": !on(),
50
+ "border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_25%,transparent)] bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_40%,transparent)]": on(),
51
+ "border-[color-mix(in_srgb,var(--ks-border,rgba(39,39,42,0.5))_60%,transparent)] bg-[color-mix(in_srgb,var(--ks-surface,#0f0f0f)_40%,transparent)] opacity-50 hover:opacity-80": !on(),
52
52
  }}
53
53
  >
54
54
  <div
55
55
  style={{ height: "18px" }}
56
- class="flex items-center justify-center text-[9px] font-semibold uppercase tracking-[0.15em] bg-zinc-800/70 text-zinc-500"
56
+ class="flex items-center justify-center text-[9px] font-semibold uppercase tracking-[0.15em] bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_70%,transparent)] text-[var(--ks-fg-subtle,#71717a)]"
57
57
  >
58
58
  {props.topLabel ?? ""}
59
59
  </div>
@@ -61,8 +61,8 @@ export default function DateTile(props: DateTileProps): JSX.Element {
61
61
  <div
62
62
  class="text-xl font-semibold leading-none tabular-nums"
63
63
  classList={{
64
- "text-amber-400/90": on(),
65
- "text-zinc-500": !on(),
64
+ "text-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_90%,transparent)]": on(),
65
+ "text-[var(--ks-fg-subtle,#71717a)]": !on(),
66
66
  }}
67
67
  >
68
68
  {props.value}
@@ -71,8 +71,8 @@ export default function DateTile(props: DateTileProps): JSX.Element {
71
71
  <div
72
72
  class="text-[9px] mt-0.5 tabular-nums"
73
73
  classList={{
74
- "text-zinc-500": on(),
75
- "text-zinc-700": !on(),
74
+ "text-[var(--ks-fg-subtle,#71717a)]": on(),
75
+ "text-[var(--ks-border-strong,#3f3f46)]": !on(),
76
76
  }}
77
77
  >
78
78
  {props.subLabel}
@@ -10,8 +10,8 @@
10
10
  export default function DetailRow(props: { label: string; value: string | null | undefined }) {
11
11
  return (
12
12
  <div>
13
- <span class="text-xs text-zinc-500 block">{props.label}</span>
14
- <span class="text-sm text-zinc-200">{props.value || "—"}</span>
13
+ <span class="text-xs text-[var(--ks-fg-subtle,#71717a)] block">{props.label}</span>
14
+ <span class="text-sm text-[var(--ks-fg,#ffffff)]">{props.value || "—"}</span>
15
15
  </div>
16
16
  );
17
17
  }
@@ -53,11 +53,11 @@ export interface DropdownProps {
53
53
  function statusColor(status?: DropdownItemStatus): string {
54
54
  switch (status) {
55
55
  case "open":
56
- return "#10B981";
56
+ return "var(--ks-success, #10b981)";
57
57
  case "closed":
58
- return "#6B7280";
58
+ return "var(--ks-fg-muted, #a1a1aa)";
59
59
  default:
60
- return "#6B7280";
60
+ return "var(--ks-fg-muted, #a1a1aa)";
61
61
  }
62
62
  }
63
63
 
@@ -96,34 +96,34 @@ export default function Dropdown(props: DropdownProps): JSX.Element {
96
96
  <button
97
97
  onClick={() => !props.disabled && setIsOpen(!isOpen())}
98
98
  disabled={props.disabled}
99
- class={`flex items-center gap-3 px-4 py-3 bg-zinc-900/50 border border-zinc-800/50 rounded-lg hover:bg-zinc-900/70 transition-all backdrop-blur-sm select-none ${
99
+ class={`flex items-center gap-3 px-4 py-3 bg-[color-mix(in_srgb,var(--ks-surface,#0f0f0f)_50%,transparent)] border border-[var(--ks-border,rgba(39,39,42,0.5))] rounded-lg hover:bg-[color-mix(in_srgb,var(--ks-surface,#0f0f0f)_70%,transparent)] transition-all backdrop-blur-sm select-none ${
100
100
  props.disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
101
101
  }`}
102
102
  style={{ "min-width": props.minWidth ?? "280px" }}
103
103
  >
104
104
  <Show when={props.icon}>
105
- {(Icon) => <Dynamic component={Icon()} size={18} class="text-amber-500" />}
105
+ {(Icon) => <Dynamic component={Icon()} size={18} class="text-[var(--ks-accent,#fbbf24)]" />}
106
106
  </Show>
107
107
  <div class="flex-1 text-left">
108
- <div class="font-semibold text-white">
108
+ <div class="font-semibold text-[var(--ks-fg,#ffffff)]">
109
109
  {currentSelection()?.label ?? props.placeholder ?? "Select an option"}
110
110
  </div>
111
111
  <Show when={currentSelection()?.description}>
112
- <div class="text-xs text-zinc-400">{currentSelection()?.description}</div>
112
+ <div class="text-xs text-[var(--ks-fg-muted,#a1a1aa)]">{currentSelection()?.description}</div>
113
113
  </Show>
114
114
  </div>
115
- <ChevronDown size={18} class={`text-zinc-400 transition-transform ${isOpen() ? "rotate-180" : ""}`} />
115
+ <ChevronDown size={18} class={`text-[var(--ks-fg-muted,#a1a1aa)] transition-transform ${isOpen() ? "rotate-180" : ""}`} />
116
116
  </button>
117
117
 
118
118
  <Show when={isOpen()}>
119
- <div class="absolute top-full left-0 mt-2 w-full bg-zinc-900/95 border border-zinc-800/50 rounded-lg backdrop-blur-xl z-50 shadow-xl select-none">
119
+ <div class="absolute top-full left-0 mt-2 w-full bg-[color-mix(in_srgb,var(--ks-surface,#0f0f0f)_95%,transparent)] border border-[var(--ks-border,rgba(39,39,42,0.5))] rounded-lg backdrop-blur-xl z-50 shadow-xl select-none">
120
120
  <For each={props.items}>
121
121
  {(item) => (
122
122
  <button
123
123
  onClick={() => handleSelect(item)}
124
124
  disabled={item.disabled}
125
- class={`w-full px-4 py-3 flex items-center gap-3 hover:bg-zinc-800/50 transition-all first:rounded-t-lg last:rounded-b-lg select-none ${
126
- currentSelection()?.id === item.id ? "bg-zinc-800/30" : ""
125
+ class={`w-full px-4 py-3 flex items-center gap-3 hover:bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_50%,transparent)] transition-all first:rounded-t-lg last:rounded-b-lg select-none ${
126
+ currentSelection()?.id === item.id ? "bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_30%,transparent)]" : ""
127
127
  } ${item.disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
128
128
  >
129
129
  <div class="flex items-center gap-2 flex-shrink-0">
@@ -138,20 +138,20 @@ export default function Dropdown(props: DropdownProps): JSX.Element {
138
138
  </Show>
139
139
  }
140
140
  >
141
- {(Icon) => <Dynamic component={Icon()} size={16} class="text-amber-500" />}
141
+ {(Icon) => <Dynamic component={Icon()} size={16} class="text-[var(--ks-accent,#fbbf24)]" />}
142
142
  </Show>
143
143
  </div>
144
144
  <div class="flex-1 text-left min-w-0">
145
- <div class="font-medium text-white flex items-center gap-2">
145
+ <div class="font-medium text-[var(--ks-fg,#ffffff)] flex items-center gap-2">
146
146
  {item.label}
147
147
  <Show when={item.badge}>
148
- <span class="px-2 py-0.5 text-xs font-medium bg-zinc-700/50 text-zinc-400 rounded flex-shrink-0">
148
+ <span class="px-2 py-0.5 text-xs font-medium bg-[color-mix(in_srgb,var(--ks-surface-sunken,#141414)_50%,transparent)] text-[var(--ks-fg-muted,#a1a1aa)] rounded flex-shrink-0">
149
149
  {item.badge}
150
150
  </span>
151
151
  </Show>
152
152
  </div>
153
153
  <Show when={item.description}>
154
- <div class="text-xs text-zinc-400 truncate">{item.description}</div>
154
+ <div class="text-xs text-[var(--ks-fg-muted,#a1a1aa)] truncate">{item.description}</div>
155
155
  </Show>
156
156
  </div>
157
157
  </button>
@@ -64,7 +64,7 @@ export default function ExistingAttachmentTile(props: Props) {
64
64
  when={resolvable()}
65
65
  fallback={
66
66
  <div
67
- class="flex w-24 h-24 flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-zinc-700 bg-zinc-900/40 px-2 text-center text-zinc-500"
67
+ class="flex w-24 h-24 flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_40%,transparent)] px-2 text-center text-[var(--ks-fg-subtle,#71717a)]"
68
68
  title={
69
69
  loading()
70
70
  ? `${props.attachment.file_name} (loading)`
@@ -73,9 +73,9 @@ export default function ExistingAttachmentTile(props: Props) {
73
73
  >
74
74
  <Show
75
75
  when={loading()}
76
- fallback={<TriangleAlert size={18} class="text-amber-500/70" />}
76
+ fallback={<TriangleAlert size={18} class="text-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_70%,transparent)]" />}
77
77
  >
78
- <Loader2 size={18} class="animate-spin text-zinc-500" />
78
+ <Loader2 size={18} class="animate-spin text-[var(--ks-fg-subtle,#71717a)]" />
79
79
  </Show>
80
80
  <span class="truncate max-w-full text-[10px]">{props.attachment.file_name}</span>
81
81
  <span class="text-[9px] uppercase tracking-wider">
@@ -90,7 +90,7 @@ export default function ExistingAttachmentTile(props: Props) {
90
90
  <a
91
91
  href={url()}
92
92
  download={props.attachment.file_name}
93
- class="flex w-24 h-24 flex-col items-center justify-center gap-1 rounded-lg border border-zinc-700 bg-zinc-800/50 px-2 text-xs text-zinc-300 hover:border-amber-500/30"
93
+ class="flex w-24 h-24 flex-col items-center justify-center gap-1 rounded-lg border border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_50%,transparent)] px-2 text-xs text-[var(--ks-fg-muted,#a1a1aa)] hover:border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_30%,transparent)]"
94
94
  >
95
95
  <FallbackIcon />
96
96
  <span class="truncate max-w-full text-[10px]">{props.attachment.file_name}</span>
@@ -100,7 +100,7 @@ export default function ExistingAttachmentTile(props: Props) {
100
100
  <button
101
101
  type="button"
102
102
  onClick={() => setViewerOpen(true)}
103
- class="block rounded-lg border border-zinc-700 overflow-hidden hover:border-amber-500/30 cursor-pointer"
103
+ class="block rounded-lg border border-[var(--ks-border-strong,#3f3f46)] overflow-hidden hover:border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_30%,transparent)] cursor-pointer"
104
104
  >
105
105
  <img src={url()} alt={props.attachment.file_name} class="w-24 h-24 object-cover" />
106
106
  </button>
@@ -119,7 +119,7 @@ export default function ExistingAttachmentTile(props: Props) {
119
119
  });
120
120
  if (ok) await props.onDelete!(props.attachment.id);
121
121
  }}
122
- class="absolute -top-2 -right-2 flex w-7 h-7 items-center justify-center rounded-full bg-red-600/90 border border-red-400/60 text-white cursor-pointer hover:bg-red-500 active:bg-red-700 shadow-lg"
122
+ class="absolute -top-2 -right-2 flex w-7 h-7 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_90%,transparent)] border border-[color-mix(in_srgb,var(--ks-danger,#ef4444)_60%,transparent)] text-[var(--ks-fg,#ffffff)] cursor-pointer hover:bg-[var(--ks-danger,#ef4444)] active:bg-[var(--ks-danger,#ef4444)] shadow-lg"
123
123
  >
124
124
  <X size={12} />
125
125
  </button>
@@ -16,29 +16,29 @@ interface ToneClass {
16
16
  // the badge reads as a quiet accent rather than a loud chip.
17
17
  const TONE_CLASS: Record<EyebrowTone, ToneClass> = {
18
18
  amber: {
19
- text: "text-amber-400",
20
- bg: "bg-amber-500/10",
21
- border: "border-amber-500",
19
+ text: "text-[var(--ks-accent,#fbbf24)]",
20
+ bg: "bg-[var(--ks-warning-bg,rgba(245,158,11,0.12))]",
21
+ border: "border-[var(--ks-warning,#f59e0b)]",
22
22
  },
23
23
  blue: {
24
- text: "text-blue-400",
25
- bg: "bg-blue-500/10",
26
- border: "border-blue-500",
24
+ text: "text-[var(--ks-info-fg,#7dd3fc)]",
25
+ bg: "bg-[var(--ks-info-bg,rgba(56,189,248,0.12))]",
26
+ border: "border-[var(--ks-info,#38bdf8)]",
27
27
  },
28
28
  emerald: {
29
- text: "text-emerald-400",
30
- bg: "bg-emerald-500/10",
31
- border: "border-emerald-500",
29
+ text: "text-[var(--ks-success-fg,#34d399)]",
30
+ bg: "bg-[var(--ks-success-bg,rgba(16,185,129,0.12))]",
31
+ border: "border-[var(--ks-success,#10b981)]",
32
32
  },
33
33
  red: {
34
- text: "text-red-400",
35
- bg: "bg-red-500/10",
36
- border: "border-red-500",
34
+ text: "text-[var(--ks-danger-fg,#f87171)]",
35
+ bg: "bg-[var(--ks-danger-bg,rgba(239,68,68,0.12))]",
36
+ border: "border-[var(--ks-danger,#ef4444)]",
37
37
  },
38
38
  zinc: {
39
- text: "text-zinc-500",
40
- bg: "bg-zinc-800/50",
41
- border: "border-zinc-600",
39
+ text: "text-[var(--ks-fg-subtle,#71717a)]",
40
+ bg: "bg-[var(--ks-border,rgba(39,39,42,0.5))]",
41
+ border: "border-[var(--ks-border-strong,#3f3f46)]",
42
42
  },
43
43
  };
44
44
 
@@ -18,7 +18,7 @@ export default function FormErrorBanner(props: FormErrorBannerProps): JSX.Elemen
18
18
  <Show when={props.message}>
19
19
  <div
20
20
  role="alert"
21
- class={`rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300 ${props.class ?? ""}`}
21
+ class={`rounded-md border border-[var(--ks-danger,#ef4444)]/30 bg-[var(--ks-danger-bg,rgba(239,68,68,0.12))] px-3 py-2 text-xs text-[var(--ks-danger-fg,#f87171)] ${props.class ?? ""}`}
22
22
  >
23
23
  {props.message}
24
24
  </div>
@@ -9,10 +9,10 @@ import type { JSX } from "solid-js";
9
9
  export default function FormField(props: { label: string; children: JSX.Element; hint?: string }) {
10
10
  return (
11
11
  <div>
12
- <label class="block text-xs text-zinc-500 mb-1">{props.label}</label>
12
+ <label class="block text-xs text-[var(--ks-fg-subtle,#71717a)] mb-1">{props.label}</label>
13
13
  {props.children}
14
14
  <Show when={props.hint}>
15
- <p class="text-[10px] text-zinc-600 mt-1">{props.hint}</p>
15
+ <p class="text-[10px] text-[var(--ks-fg-subtle,#71717a)] mt-1">{props.hint}</p>
16
16
  </Show>
17
17
  </div>
18
18
  );
@@ -193,11 +193,11 @@ export default function ImageCropper(props: ImageCropperProps) {
193
193
  return (
194
194
  <Modal onClose={props.onCancel} size="md">
195
195
  <div class="flex items-center justify-between mb-4">
196
- <h2 class="text-lg font-semibold text-zinc-100">{props.title ?? "Crop logo"}</h2>
196
+ <h2 class="text-lg font-semibold text-[var(--ks-fg,#ffffff)]">{props.title ?? "Crop logo"}</h2>
197
197
  <button
198
198
  type="button"
199
199
  onClick={props.onCancel}
200
- class="text-zinc-500 hover:text-zinc-300 cursor-pointer"
200
+ class="text-[var(--ks-fg-subtle,#71717a)] hover:text-[var(--ks-fg-muted,#a1a1aa)] cursor-pointer"
201
201
  disabled={props.busy}
202
202
  aria-label="Close cropper"
203
203
  >
@@ -206,12 +206,12 @@ export default function ImageCropper(props: ImageCropperProps) {
206
206
  </div>
207
207
 
208
208
  <Show when={loadError()}>
209
- <div class="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-400 mb-3">
209
+ <div class="rounded-lg border border-[var(--ks-danger,#ef4444)]/30 bg-[var(--ks-danger-bg,rgba(239,68,68,0.12))] px-3 py-2 text-sm text-[var(--ks-danger-fg,#f87171)] mb-3">
210
210
  {loadError()}
211
211
  </div>
212
212
  </Show>
213
213
 
214
- <p class="text-xs text-zinc-500 mb-3">
214
+ <p class="text-xs text-[var(--ks-fg-subtle,#71717a)] mb-3">
215
215
  Drag inside the box to reposition. Drag a corner to resize. The result is saved as a 1:1 square.
216
216
  </p>
217
217
 
@@ -219,43 +219,43 @@ export default function ImageCropper(props: ImageCropperProps) {
219
219
  <Show when={image()}>
220
220
  {(img) => (
221
221
  <div
222
- class="relative select-none touch-none bg-zinc-900 rounded-lg overflow-hidden"
222
+ class="relative select-none touch-none bg-[var(--ks-surface,#0f0f0f)] rounded-lg overflow-hidden"
223
223
  style={{ width: `${img().displayedWidth}px`, height: `${img().displayedHeight}px` }}
224
224
  onPointerMove={handlePointerMove}
225
225
  onPointerUp={handlePointerUp}
226
226
  onPointerCancel={handlePointerUp}
227
227
  >
228
228
  <img src={imageUrl()} alt="" draggable={false} class="absolute inset-0 w-full h-full pointer-events-none" />
229
- <div class="absolute bg-black/60 pointer-events-none" style={maskTop()} />
230
- <div class="absolute bg-black/60 pointer-events-none" style={maskBottom()} />
231
- <div class="absolute bg-black/60 pointer-events-none" style={maskLeft()} />
232
- <div class="absolute bg-black/60 pointer-events-none" style={maskRight()} />
233
- <div class="absolute border-2 border-amber-400 cursor-move" style={selectionStyle()} onPointerDown={handlePointerDown("move")}>
234
- <span class="absolute inset-0 ring-1 ring-amber-400/30 pointer-events-none" />
235
- <span class="absolute -top-1.5 -left-1.5 w-3 h-3 bg-amber-400 rounded-sm cursor-nwse-resize" onPointerDown={handlePointerDown("tl")} />
236
- <span class="absolute -top-1.5 -right-1.5 w-3 h-3 bg-amber-400 rounded-sm cursor-nesw-resize" onPointerDown={handlePointerDown("tr")} />
237
- <span class="absolute -bottom-1.5 -left-1.5 w-3 h-3 bg-amber-400 rounded-sm cursor-nesw-resize" onPointerDown={handlePointerDown("bl")} />
238
- <span class="absolute -bottom-1.5 -right-1.5 w-3 h-3 bg-amber-400 rounded-sm cursor-nwse-resize" onPointerDown={handlePointerDown("br")} />
229
+ <div class="absolute bg-[color-mix(in_srgb,var(--ks-overlay,rgba(0,0,0,0.7))_60%,transparent)] pointer-events-none" style={maskTop()} />
230
+ <div class="absolute bg-[color-mix(in_srgb,var(--ks-overlay,rgba(0,0,0,0.7))_60%,transparent)] pointer-events-none" style={maskBottom()} />
231
+ <div class="absolute bg-[color-mix(in_srgb,var(--ks-overlay,rgba(0,0,0,0.7))_60%,transparent)] pointer-events-none" style={maskLeft()} />
232
+ <div class="absolute bg-[color-mix(in_srgb,var(--ks-overlay,rgba(0,0,0,0.7))_60%,transparent)] pointer-events-none" style={maskRight()} />
233
+ <div class="absolute border-2 border-[var(--ks-accent,#fbbf24)] cursor-move" style={selectionStyle()} onPointerDown={handlePointerDown("move")}>
234
+ <span class="absolute inset-0 ring-1 ring-[var(--ks-accent,#fbbf24)]/30 pointer-events-none" />
235
+ <span class="absolute -top-1.5 -left-1.5 w-3 h-3 bg-[var(--ks-accent,#fbbf24)] rounded-sm cursor-nwse-resize" onPointerDown={handlePointerDown("tl")} />
236
+ <span class="absolute -top-1.5 -right-1.5 w-3 h-3 bg-[var(--ks-accent,#fbbf24)] rounded-sm cursor-nesw-resize" onPointerDown={handlePointerDown("tr")} />
237
+ <span class="absolute -bottom-1.5 -left-1.5 w-3 h-3 bg-[var(--ks-accent,#fbbf24)] rounded-sm cursor-nesw-resize" onPointerDown={handlePointerDown("bl")} />
238
+ <span class="absolute -bottom-1.5 -right-1.5 w-3 h-3 bg-[var(--ks-accent,#fbbf24)] rounded-sm cursor-nwse-resize" onPointerDown={handlePointerDown("br")} />
239
239
  </div>
240
240
  </div>
241
241
  )}
242
242
  </Show>
243
243
  <Show when={!image() && !loadError()}>
244
- <div class="w-[320px] h-[240px] flex items-center justify-center text-xs text-zinc-500 border border-zinc-700 rounded-lg">
244
+ <div class="w-[320px] h-[240px] flex items-center justify-center text-xs text-[var(--ks-fg-subtle,#71717a)] border border-[var(--ks-border-strong,#3f3f46)] rounded-lg">
245
245
  Loading…
246
246
  </div>
247
247
  </Show>
248
248
  </div>
249
249
 
250
- <div class="flex items-center justify-between text-xs text-zinc-500 mb-4">
250
+ <div class="flex items-center justify-between text-xs text-[var(--ks-fg-subtle,#71717a)] mb-4">
251
251
  <span>
252
- Source crop: <span class="tabular-nums text-zinc-300">{sourceCropSizePx()}</span> px ·
252
+ Source crop: <span class="tabular-nums text-[var(--ks-fg-muted,#a1a1aa)]">{sourceCropSizePx()}</span> px ·
253
253
  output {outputSize()}×{outputSize()}
254
254
  </span>
255
255
  <button
256
256
  type="button"
257
257
  onClick={recenter}
258
- class="text-amber-400 hover:text-amber-300 transition-colors cursor-pointer disabled:opacity-50"
258
+ class="text-[var(--ks-accent,#fbbf24)] hover:text-[var(--ks-accent-hover,#fcd34d)] transition-colors cursor-pointer disabled:opacity-50"
259
259
  disabled={!image() || props.busy}
260
260
  >
261
261
  Reset selection
@@ -16,24 +16,20 @@
16
16
  import { onCleanup, onMount } from "solid-js";
17
17
  import X from "lucide-solid/icons/x";
18
18
  import { lockPullToRefresh, unlockPullToRefresh } from "../../utils/dom";
19
+ import { injectCSS } from "../../utils/inject-css";
19
20
 
20
21
  const STYLE_ID = "ksui-image-viewer-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
- style.textContent = `
22
+ // The backdrop scrim maps to --ks-overlay (§6a); the img/close-button chrome
23
+ // stays a fixed dark treatment since it sits on top of the photo itself, not
24
+ // a themeable app surface.
25
+ const STYLE_CSS = `
28
26
  .ksui-imgviewer{position:fixed;inset:0;z-index:60;background:transparent;padding:0;margin:0;max-width:none;max-height:none;width:100vw;height:100vh;border:0;}
29
27
  .ksui-imgviewer[open]{display:flex;align-items:center;justify-content:center;}
30
- .ksui-imgviewer::backdrop{background:rgba(0,0,0,0.92);backdrop-filter:blur(2px);}
28
+ .ksui-imgviewer::backdrop{background:var(--ks-overlay, rgba(0,0,0,0.7));backdrop-filter:blur(2px);}
31
29
  .ksui-imgviewer-img{max-width:96vw;max-height:96vh;object-fit:contain;border-radius:0.25rem;box-shadow:0 25px 50px -12px rgba(0,0,0,0.8);}
32
30
  .ksui-imgviewer-close{position:absolute;top:1rem;right:1rem;display:flex;width:2.5rem;height:2.5rem;align-items:center;justify-content:center;border-radius:9999px;background:rgba(255,255,255,0.12);color:#fff;border:0;cursor:pointer;}
33
31
  .ksui-imgviewer-close:hover{background:rgba(255,255,255,0.22);}
34
32
  `;
35
- document.head.appendChild(style);
36
- }
37
33
 
38
34
  interface Props {
39
35
  /** Image URL to display (typically a blob: object URL or a public link). */
@@ -45,7 +41,7 @@ interface Props {
45
41
  }
46
42
 
47
43
  export default function ImageViewer(props: Props) {
48
- ensureStyle();
44
+ injectCSS(STYLE_ID, STYLE_CSS);
49
45
  let dialogEl: HTMLDialogElement | undefined;
50
46
 
51
47
  lockPullToRefresh();