@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
@@ -9,10 +9,10 @@ export type KpiTone = "success" | "danger" | "info" | "warning";
9
9
  // Domain-free tone record. Each tone supplies a Tailwind text class for the
10
10
  // label icon + value and a literal SVG stroke color for the sparkline.
11
11
  const TONE: Record<KpiTone, { text: string; stroke: string }> = {
12
- success: { text: "text-emerald-400", stroke: "#34d399" },
13
- danger: { text: "text-red-400", stroke: "#f87171" },
14
- info: { text: "text-blue-400", stroke: "#60a5fa" },
15
- warning: { text: "text-amber-400", stroke: "#fbbf24" },
12
+ success: { text: "text-[var(--ks-success-fg,#34d399)]", stroke: "var(--ks-success-fg, #34d399)" },
13
+ danger: { text: "text-[var(--ks-danger-fg,#f87171)]", stroke: "var(--ks-danger-fg, #f87171)" },
14
+ info: { text: "text-[var(--ks-info-fg,#7dd3fc)]", stroke: "var(--ks-info-fg, #7dd3fc)" },
15
+ warning: { text: "text-[var(--ks-warning-fg,#fbbf24)]", stroke: "var(--ks-warning-fg, #fbbf24)" },
16
16
  };
17
17
 
18
18
  // Module-private counter so each card's sparkline gradient gets a unique id,
@@ -71,12 +71,12 @@ export default function KpiCard(props: KpiCardProps): JSX.Element {
71
71
 
72
72
  return (
73
73
  <div
74
- class={`rounded-lg border border-zinc-800/50 bg-zinc-900/50 p-4 relative overflow-hidden ${
74
+ class={`rounded-lg border border-[var(--ks-border,rgba(39,39,42,0.5))] bg-[var(--ks-surface,#0f0f0f)]/50 p-4 relative overflow-hidden ${
75
75
  props.clipClass ?? ""
76
76
  } ${props.class ?? ""}`}
77
77
  >
78
78
  <div class="flex items-start justify-between mb-3">
79
- <span class="text-[10px] text-zinc-500 uppercase tracking-[0.25em] font-semibold">
79
+ <span class="text-[10px] text-[var(--ks-fg-subtle,#71717a)] uppercase tracking-[0.25em] font-semibold">
80
80
  {props.label}
81
81
  </span>
82
82
  <div class={`flex items-center justify-center ${t().text}`}>
@@ -91,7 +91,7 @@ export default function KpiCard(props: KpiCardProps): JSX.Element {
91
91
  {props.value}
92
92
  </div>
93
93
  <Show when={props.hint}>
94
- <div class="text-[11px] text-zinc-600 mt-1">{props.hint}</div>
94
+ <div class="text-[11px] text-[var(--ks-fg-subtle,#71717a)] mt-1">{props.hint}</div>
95
95
  </Show>
96
96
  </div>
97
97
  <Show when={sparkPath()}>
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { JSX, onCleanup, onMount, splitProps } from "solid-js";
19
19
  import { autoFocusOnMount, lockPullToRefresh, unlockPullToRefresh, useFocusTrap } from "../../utils/dom";
20
+ import { injectCSS } from "../../utils/inject-css";
20
21
 
21
22
  export type ModalSize = "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "5xl" | "7xl";
22
23
  export type ModalTone = "default" | "danger";
@@ -33,27 +34,22 @@ const SIZE_MAX_WIDTH: Record<ModalSize, string> = {
33
34
  };
34
35
 
35
36
  const STYLE_ID = "ksui-modal-style";
36
-
37
- function ensureModalStyle(): void {
38
- if (typeof document === "undefined") return;
39
- if (document.getElementById(STYLE_ID)) return;
40
- const style = document.createElement("style");
41
- style.id = STYLE_ID;
42
- style.textContent = `
37
+ // Backdrops read --ks-overlay (§6a named exception); card surface/border keep
38
+ // the old --ksui-modal-* name as the live read-point, per §1.4's direction,
39
+ // now falling through to the new token before the dark literal.
40
+ const STYLE_CSS = `
43
41
  .ksui-modal-dialog{position:fixed;inset:0;z-index:50;background:transparent;padding:0;margin:0;max-width:none;max-height:none;width:100vw;height:100vh;border:0;}
44
42
  .ksui-modal-dialog[open]{display:flex;align-items:center;justify-content:center;padding:1rem;}
45
- .ksui-modal-dialog::backdrop{background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);}
43
+ .ksui-modal-dialog::backdrop{background:var(--ks-overlay, rgba(0,0,0,0.7));backdrop-filter:blur(4px);}
46
44
  .ksui-modal-sheet-overlay{position:fixed;inset:0;z-index:50;display:flex;align-items:flex-end;justify-content:center;}
47
45
  @media (min-width:640px){.ksui-modal-sheet-overlay{align-items:center;padding:1rem;}}
48
- .ksui-modal-sheet-backdrop{position:absolute;inset:0;background:rgba(0,0,0,0.7);backdrop-filter:blur(6px);}
49
- .ksui-modal-card{position:relative;z-index:10;width:100%;background:var(--ksui-modal-bg,#18181b);color:var(--ksui-modal-fg,inherit);border:1px solid var(--ksui-modal-border,rgba(245,158,11,0.3));border-radius:0.75rem;padding:1.5rem;box-shadow:0 25px 50px -12px rgba(0,0,0,0.6);max-height:90vh;overflow-x:hidden;overflow-y:auto;}
46
+ .ksui-modal-sheet-backdrop{position:absolute;inset:0;background:var(--ks-overlay, rgba(0,0,0,0.7));backdrop-filter:blur(6px);}
47
+ .ksui-modal-card{position:relative;z-index:10;width:100%;background:var(--ksui-modal-bg,var(--ks-overlay-surface,#18181b));color:var(--ksui-modal-fg,inherit);border:1px solid var(--ksui-modal-border,rgba(245,158,11,0.3));border-radius:0.75rem;padding:1.5rem;box-shadow:0 25px 50px -12px rgba(0,0,0,0.6);max-height:90vh;overflow-x:hidden;overflow-y:auto;}
50
48
  .ksui-modal-card.danger{border-color:var(--ksui-modal-border-danger,rgba(239,68,68,0.3));}
51
- .ksui-modal-sheet-card{position:relative;z-index:10;width:100%;background:var(--ksui-modal-bg,#18181b);color:var(--ksui-modal-fg,inherit);border:1px solid var(--ksui-modal-border,rgba(245,158,11,0.3));box-shadow:0 25px 50px -12px rgba(0,0,0,0.6);max-height:92vh;overflow:hidden;overscroll-behavior:contain;}
49
+ .ksui-modal-sheet-card{position:relative;z-index:10;width:100%;background:var(--ksui-modal-bg,var(--ks-overlay-surface,#18181b));color:var(--ksui-modal-fg,inherit);border:1px solid var(--ksui-modal-border,rgba(245,158,11,0.3));box-shadow:0 25px 50px -12px rgba(0,0,0,0.6);max-height:92vh;overflow:hidden;overscroll-behavior:contain;}
52
50
  .ksui-modal-sheet-card.danger{border-color:var(--ksui-modal-border-danger,rgba(239,68,68,0.3));}
53
51
  @media (min-width:640px){.ksui-modal-sheet-card{width:auto;border-radius:0.75rem;max-height:88vh;}}
54
52
  `;
55
- document.head.appendChild(style);
56
- }
57
53
 
58
54
  export interface ModalProps {
59
55
  /** Fired on Escape, backdrop click, or any other dismissal trigger. */
@@ -74,7 +70,7 @@ export interface ModalProps {
74
70
  type LocalProps = Omit<ModalProps, "variant">;
75
71
 
76
72
  export function Modal(props: ModalProps): JSX.Element {
77
- ensureModalStyle();
73
+ injectCSS(STYLE_ID, STYLE_CSS);
78
74
  const [local] = splitProps(props, [
79
75
  "onClose",
80
76
  "dismissable",
@@ -6,6 +6,7 @@
6
6
  import type { Component, JSX } from "solid-js";
7
7
  import { createMemo, splitProps, Show } from "solid-js";
8
8
  import { Dynamic } from "solid-js/web";
9
+ import { injectCSS } from "../../utils/inject-css";
9
10
 
10
11
  // The monolith ships these as a CSS module (ProgressBar.module.css). The
11
12
  // plugin remote builds to a single IIFE that the host serves as one script —
@@ -14,22 +15,16 @@ import { Dynamic } from "solid-js/web";
14
15
  // the keyframes + helper classes once per page via a <style> tag and reference
15
16
  // them with plain (unscoped) class names so the bundle stays self-contained.
16
17
  const PROGRESS_STYLE_ID = "ks-progress-bar-inline-style";
18
+ // Shimmer stops per THEME-SPEC §6a named exception: primary through
19
+ // surface-raised reads as a brand-tinted sheen instead of a flat white one.
17
20
  const PROGRESS_CSS = `
18
21
  .ks-progress-fill { position: relative; border-radius: 0; transition: width 1s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; }
19
22
  .ks-progress-indicator { border-radius: 0; box-shadow: 0 0 2px currentColor; }
20
23
  .ks-progress-overflow { position: relative; }
21
24
  @keyframes ksLiveTimerShimmer { 0% { transform: translateX(-100%) skewX(-25deg); } 100% { transform: translateX(100%) skewX(-25deg); } }
22
- .ks-progress-shimmer { animation: ksLiveTimerShimmer 2s infinite; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); background-size: 200% 100%; height: 100%; width: 100%; position: absolute; top: 0; left: 0; opacity: 0.7; }
25
+ .ks-progress-shimmer { animation: ksLiveTimerShimmer 2s infinite; background: linear-gradient(90deg, transparent, var(--ks-primary, #c9a961), transparent); background-size: 200% 100%; height: 100%; width: 100%; position: absolute; top: 0; left: 0; opacity: 0.7; }
23
26
  @media (prefers-reduced-motion: reduce) { .ks-progress-fill { transition: width 0.3s ease; } }
24
27
  `;
25
- function ensureProgressStyle() {
26
- if (typeof document === "undefined") return;
27
- if (document.getElementById(PROGRESS_STYLE_ID)) return;
28
- const el = document.createElement("style");
29
- el.id = PROGRESS_STYLE_ID;
30
- el.textContent = PROGRESS_CSS;
31
- document.head.appendChild(el);
32
- }
33
28
  const styles: Record<string, string> = {
34
29
  "ks-progress-fill": "ks-progress-fill",
35
30
  "ks-progress-indicator": "ks-progress-indicator",
@@ -65,69 +60,78 @@ interface ColorInfo {
65
60
  shimmer: string;
66
61
  }
67
62
 
63
+ // Caller picks a color by name (e.g. class="text-red-400"); each name maps to
64
+ // the closest §1.2 token by role — the 4 semantic tokens for their matching
65
+ // hue, the neutral fg-muted token for the 3 near-identical grays, and the
66
+ // nearest chart-series hue for the two colors with no semantic/neutral token
67
+ // (orange, purple) since ksui has no dedicated token for those hues. Every
68
+ // effect string below is written out literally (not built from a template at
69
+ // runtime) because the theming rollout's check-token-fallbacks.mjs gate
70
+ // statically greps source text for the color-mix CSS function with a literal
71
+ // --ks-* var name.
68
72
  const COLOR_MAP: Record<string, ColorInfo> = {
69
73
  red: {
70
- fill: "rgba(255, 68, 68, 0.2)",
71
- indicator: "#FF4444",
72
- stripe: "rgba(255, 68, 68, 0.4)",
73
- overflow: "rgba(255, 68, 68, 0.4)",
74
- shimmer: "rgba(255, 68, 68, 0.3)",
74
+ fill: "color-mix(in srgb, var(--ks-danger, #ef4444) 20%, transparent)",
75
+ indicator: "var(--ks-danger, #ef4444)",
76
+ stripe: "color-mix(in srgb, var(--ks-danger, #ef4444) 40%, transparent)",
77
+ overflow: "color-mix(in srgb, var(--ks-danger, #ef4444) 40%, transparent)",
78
+ shimmer: "color-mix(in srgb, var(--ks-danger, #ef4444) 30%, transparent)",
75
79
  },
76
80
  green: {
77
- fill: "rgba(0, 204, 136, 0.2)",
78
- indicator: "#00CC88",
79
- stripe: "rgba(0, 204, 136, 0.4)",
80
- overflow: "rgba(0, 204, 136, 0.4)",
81
- shimmer: "rgba(0, 204, 136, 0.3)",
81
+ fill: "color-mix(in srgb, var(--ks-success, #10b981) 20%, transparent)",
82
+ indicator: "var(--ks-success, #10b981)",
83
+ stripe: "color-mix(in srgb, var(--ks-success, #10b981) 40%, transparent)",
84
+ overflow: "color-mix(in srgb, var(--ks-success, #10b981) 40%, transparent)",
85
+ shimmer: "color-mix(in srgb, var(--ks-success, #10b981) 30%, transparent)",
82
86
  },
83
87
  blue: {
84
- fill: "rgba(74, 158, 255, 0.2)",
85
- indicator: "#4A9EFF",
86
- stripe: "rgba(74, 158, 255, 0.4)",
87
- overflow: "rgba(74, 158, 255, 0.4)",
88
- shimmer: "rgba(74, 158, 255, 0.3)",
88
+ fill: "color-mix(in srgb, var(--ks-info, #38bdf8) 20%, transparent)",
89
+ indicator: "var(--ks-info, #38bdf8)",
90
+ stripe: "color-mix(in srgb, var(--ks-info, #38bdf8) 40%, transparent)",
91
+ overflow: "color-mix(in srgb, var(--ks-info, #38bdf8) 40%, transparent)",
92
+ shimmer: "color-mix(in srgb, var(--ks-info, #38bdf8) 30%, transparent)",
89
93
  },
90
94
  amber: {
91
- fill: "rgba(245, 158, 11, 0.2)",
92
- indicator: "#F59E0B",
93
- stripe: "rgba(245, 158, 11, 0.4)",
94
- overflow: "rgba(245, 158, 11, 0.4)",
95
- shimmer: "rgba(245, 158, 11, 0.3)",
95
+ fill: "color-mix(in srgb, var(--ks-warning, #f59e0b) 20%, transparent)",
96
+ indicator: "var(--ks-warning, #f59e0b)",
97
+ stripe: "color-mix(in srgb, var(--ks-warning, #f59e0b) 40%, transparent)",
98
+ overflow: "color-mix(in srgb, var(--ks-warning, #f59e0b) 40%, transparent)",
99
+ shimmer: "color-mix(in srgb, var(--ks-warning, #f59e0b) 30%, transparent)",
96
100
  },
97
101
  orange: {
98
- fill: "rgba(255, 136, 51, 0.2)",
99
- indicator: "#FF8833",
100
- stripe: "rgba(255, 136, 51, 0.4)",
101
- overflow: "rgba(255, 136, 51, 0.4)",
102
- shimmer: "rgba(255, 136, 51, 0.3)",
102
+ fill: "color-mix(in srgb, var(--ks-chart-4, #f59e0b) 20%, transparent)",
103
+ indicator: "var(--ks-chart-4, #f59e0b)",
104
+ stripe: "color-mix(in srgb, var(--ks-chart-4, #f59e0b) 40%, transparent)",
105
+ overflow: "color-mix(in srgb, var(--ks-chart-4, #f59e0b) 40%, transparent)",
106
+ shimmer: "color-mix(in srgb, var(--ks-chart-4, #f59e0b) 30%, transparent)",
103
107
  },
104
108
  purple: {
105
- fill: "rgba(168, 85, 247, 0.2)",
106
- indicator: "#A855F7",
107
- stripe: "rgba(168, 85, 247, 0.4)",
108
- overflow: "rgba(168, 85, 247, 0.4)",
109
- shimmer: "rgba(168, 85, 247, 0.3)",
109
+ fill: "color-mix(in srgb, var(--ks-chart-6, #a78bfa) 20%, transparent)",
110
+ indicator: "var(--ks-chart-6, #a78bfa)",
111
+ stripe: "color-mix(in srgb, var(--ks-chart-6, #a78bfa) 40%, transparent)",
112
+ overflow: "color-mix(in srgb, var(--ks-chart-6, #a78bfa) 40%, transparent)",
113
+ shimmer: "color-mix(in srgb, var(--ks-chart-6, #a78bfa) 30%, transparent)",
110
114
  },
111
115
  slate: {
112
- fill: "rgba(148, 163, 184, 0.2)",
113
- indicator: "#94A3B8",
114
- stripe: "rgba(148, 163, 184, 0.4)",
115
- overflow: "rgba(148, 163, 184, 0.4)",
116
- shimmer: "rgba(148, 163, 184, 0.3)",
116
+ fill: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 20%, transparent)",
117
+ indicator: "var(--ks-fg-muted, #a1a1aa)",
118
+ stripe: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 40%, transparent)",
119
+ overflow: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 40%, transparent)",
120
+ shimmer: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 30%, transparent)",
117
121
  },
118
122
  gray: {
119
- fill: "rgba(156, 163, 175, 0.2)",
120
- indicator: "#9CA3AF",
121
- stripe: "rgba(156, 163, 175, 0.4)",
122
- overflow: "rgba(156, 163, 175, 0.4)",
123
- shimmer: "rgba(156, 163, 175, 0.3)",
123
+ fill: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 20%, transparent)",
124
+ indicator: "var(--ks-fg-muted, #a1a1aa)",
125
+ stripe: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 40%, transparent)",
126
+ overflow: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 40%, transparent)",
127
+ shimmer: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 30%, transparent)",
124
128
  },
125
129
  zinc: {
126
- fill: "rgba(161, 161, 170, 0.2)",
127
- indicator: "#A1A1AA",
128
- stripe: "rgba(161, 161, 170, 0.4)",
129
- overflow: "rgba(161, 161, 170, 0.4)",
130
- shimmer: "rgba(161, 161, 170, 0.3)",
130
+ fill: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 20%, transparent)",
131
+ indicator: "var(--ks-fg-muted, #a1a1aa)",
132
+ stripe: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 40%, transparent)",
133
+ overflow: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 40%, transparent)",
134
+ shimmer: "color-mix(in srgb, var(--ks-fg-muted, #a1a1aa) 30%, transparent)",
131
135
  },
132
136
  };
133
137
 
@@ -149,7 +153,7 @@ function extractTextSize(className: string): number {
149
153
  }
150
154
 
151
155
  const ProgressBar: Component<ProgressBarProps> = (props) => {
152
- ensureProgressStyle();
156
+ injectCSS(PROGRESS_STYLE_ID, PROGRESS_CSS);
153
157
  const [local, others] = splitProps(props, [
154
158
  "progress",
155
159
  "icon",
@@ -185,7 +189,7 @@ const ProgressBar: Component<ProgressBarProps> = (props) => {
185
189
 
186
190
  const containerClasses = createMemo(() =>
187
191
  cn(
188
- "select-none relative overflow-hidden rounded bg-black/20 border border-zinc-700/40",
192
+ "select-none relative overflow-hidden rounded bg-[var(--ks-surface,#0f0f0f)]/20 border border-[var(--ks-border-strong,#3f3f46)]/40",
189
193
  "h-8",
190
194
  !classProp()?.includes("w-") ? "w-full" : "",
191
195
  classProp(),
@@ -302,9 +306,9 @@ const ProgressBar: Component<ProgressBarProps> = (props) => {
302
306
  )}
303
307
  {label() && (
304
308
  <>
305
- {(Icon() || statusLabel()) && <span class="text-zinc-500 mx-1">·</span>}
309
+ {(Icon() || statusLabel()) && <span class="text-[var(--ks-fg-subtle,#71717a)] mx-1">·</span>}
306
310
  <span class="flex-1 min-w-0">
307
- <span class="text-zinc-400 block overflow-hidden text-ellipsis whitespace-nowrap">
311
+ <span class="text-[var(--ks-fg-muted,#a1a1aa)] block overflow-hidden text-ellipsis whitespace-nowrap">
308
312
  {label()}
309
313
  </span>
310
314
  </span>
@@ -317,7 +321,7 @@ const ProgressBar: Component<ProgressBarProps> = (props) => {
317
321
  </div>
318
322
  ) : !hidePercentage() ? (
319
323
  <div class="flex-shrink-0 ml-2 text-right">
320
- <span class="font-mono font-medium text-zinc-400">{Math.round(progressVal())}%</span>
324
+ <span class="font-mono font-medium text-[var(--ks-fg-muted,#a1a1aa)]">{Math.round(progressVal())}%</span>
321
325
  </div>
322
326
  ) : null}
323
327
  </div>
@@ -34,6 +34,8 @@ interface RadioCardGroupProps<T> {
34
34
  * default amber-active styling per call site.
35
35
  */
36
36
  itemClass?: string;
37
+ /** Disables the whole group: no tab stop, clicks suppressed, aria-disabled. */
38
+ disabled?: boolean;
37
39
  }
38
40
 
39
41
  // A controlled, keyboard-navigable group of radio cards. The reusable part is
@@ -57,6 +59,7 @@ export default function RadioCardGroup<T>(props: RadioCardGroupProps<T>): JSX.El
57
59
  };
58
60
 
59
61
  const selectByIndex = (idx: number) => {
62
+ if (props.disabled) return;
60
63
  const list = props.options;
61
64
  if (list.length === 0) return;
62
65
  const wrapped = ((idx % list.length) + list.length) % list.length;
@@ -100,8 +103,8 @@ export default function RadioCardGroup<T>(props: RadioCardGroupProps<T>): JSX.El
100
103
  <span
101
104
  class="h-2 w-2 rounded-full shrink-0"
102
105
  classList={{
103
- "bg-amber-400": selected,
104
- "bg-zinc-600 group-hover:bg-zinc-500": !selected,
106
+ "bg-[var(--ks-accent,#fbbf24)]": selected,
107
+ "bg-[var(--ks-fg-subtle,#71717a)] group-hover:bg-[var(--ks-fg-muted,#a1a1aa)]": !selected,
105
108
  }}
106
109
  />
107
110
  <span class="truncate">{label(option)}</span>
@@ -112,6 +115,7 @@ export default function RadioCardGroup<T>(props: RadioCardGroupProps<T>): JSX.El
112
115
  <div
113
116
  role="radiogroup"
114
117
  aria-label={props.ariaLabel}
118
+ aria-disabled={props.disabled}
115
119
  tabIndex={-1}
116
120
  class={`${gridClass()} ${props.class ?? ""}`}
117
121
  onKeyDown={onKeyDown}
@@ -120,19 +124,22 @@ export default function RadioCardGroup<T>(props: RadioCardGroupProps<T>): JSX.El
120
124
  {(option, i) => {
121
125
  const k = keyAt(option);
122
126
  const selected = () => props.value === k;
123
- const isTabStop = () => selected() || (!props.value && i() === 0);
127
+ const isTabStop = () => !props.disabled && (selected() || (!props.value && i() === 0));
124
128
  return (
125
129
  <button
126
130
  ref={(el) => (buttonRefs[i()] = el)}
127
131
  type="button"
128
132
  role="radio"
129
133
  aria-checked={selected()}
134
+ aria-disabled={props.disabled}
130
135
  tabIndex={isTabStop() ? 0 : -1}
131
- onClick={() => props.onChange(k)}
136
+ onClick={() => !props.disabled && props.onChange(k)}
132
137
  class={`group flex items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm transition-colors cursor-pointer ${props.itemClass ?? ""}`}
133
138
  classList={{
134
- "border-amber-500/50 bg-amber-600/10 text-amber-300": selected(),
135
- "border-zinc-700 bg-zinc-800/50 text-zinc-300 hover:border-zinc-600 hover:bg-zinc-800":
139
+ "opacity-50 pointer-events-none cursor-not-allowed": !!props.disabled,
140
+ "border-[var(--ks-accent,#fbbf24)]/50 bg-[var(--ks-accent,#fbbf24)]/10 text-[var(--ks-accent,#fbbf24)]":
141
+ selected(),
142
+ "border-[var(--ks-border-strong,#3f3f46)] bg-[var(--ks-surface-raised,#1a1a1a)] text-[var(--ks-fg-muted,#a1a1aa)] hover:border-[var(--ks-fg-subtle,#71717a)] hover:bg-[var(--ks-surface-sunken,#141414)]":
136
143
  !selected(),
137
144
  }}
138
145
  >
@@ -75,23 +75,23 @@ export default function SectionHeading(props: SectionHeadingProps): JSX.Element
75
75
  <Show when={props.kicker}>
76
76
  <div
77
77
  class={`text-xs font-bold tracking-[0.3em] uppercase mb-2 ${
78
- props.kickerClass ?? "text-amber-500"
78
+ props.kickerClass ?? "text-[var(--ks-accent,#fbbf24)]"
79
79
  }`}
80
80
  >
81
81
  {props.kicker}
82
82
  </div>
83
83
  </Show>
84
84
 
85
- <Title class={`${TITLE_SIZE[level()]} ${props.titleClass ?? "text-white"}`}>
85
+ <Title class={`${TITLE_SIZE[level()]} ${props.titleClass ?? "text-[var(--ks-fg,#ffffff)]"}`}>
86
86
  {props.title}
87
87
  </Title>
88
88
 
89
89
  <Show when={props.accent}>
90
- <div class={`w-20 h-1 mt-4 rounded-full ${props.accentClass ?? "bg-amber-500"}`} />
90
+ <div class={`w-20 h-1 mt-4 rounded-full ${props.accentClass ?? "bg-[var(--ks-accent,#fbbf24)]"}`} />
91
91
  </Show>
92
92
 
93
93
  <Show when={props.subtitle}>
94
- <p class={`mt-4 text-base md:text-lg max-w-2xl ${props.subtitleClass ?? "text-zinc-400"}`}>
94
+ <p class={`mt-4 text-base md:text-lg max-w-2xl ${props.subtitleClass ?? "text-[var(--ks-fg-muted,#a1a1aa)]"}`}>
95
95
  {props.subtitle}
96
96
  </p>
97
97
  </Show>
@@ -16,28 +16,84 @@ interface SegmentedFilterProps {
16
16
  testIdPrefix?: string;
17
17
  /** Extra classes on the outer bordered row. */
18
18
  class?: string;
19
+ /** Accessible label for the radiogroup wrapper (WAI-ARIA radiogroup pattern). */
20
+ ariaLabel?: string;
19
21
  }
20
22
 
21
23
  // A rounded bordered row of segment buttons with one active at a time.
22
24
  // Presentational only and domain free: the caller passes the segment values
23
25
  // and the active value, so there are no status literals baked in here.
26
+ // Semantics follow the WAI-ARIA radiogroup pattern (role="radiogroup" +
27
+ // role="radio"/aria-checked per segment + roving tabindex + arrow-key nav) so
28
+ // this reads as a single control, not a strip of unrelated buttons.
24
29
  export default function SegmentedFilter(props: SegmentedFilterProps): JSX.Element {
30
+ const buttonRefs: (HTMLButtonElement | undefined)[] = [];
25
31
  const optionOf = (o: SegmentedFilterOption) =>
26
32
  typeof o === "string" ? { value: o, label: o, capitalize: true } : { ...o, capitalize: false };
33
+
34
+ const currentIndex = () => {
35
+ const i = props.options.findIndex((o) => optionOf(o).value === props.value);
36
+ return i >= 0 ? i : 0;
37
+ };
38
+
39
+ const selectByIndex = (idx: number) => {
40
+ const list = props.options;
41
+ if (list.length === 0) return;
42
+ const wrapped = ((idx % list.length) + list.length) % list.length;
43
+ props.onChange(optionOf(list[wrapped]).value);
44
+ buttonRefs[wrapped]?.focus();
45
+ };
46
+
47
+ const onKeyDown = (e: KeyboardEvent) => {
48
+ switch (e.key) {
49
+ case "ArrowRight":
50
+ case "ArrowDown":
51
+ e.preventDefault();
52
+ selectByIndex(currentIndex() + 1);
53
+ break;
54
+ case "ArrowLeft":
55
+ case "ArrowUp":
56
+ e.preventDefault();
57
+ selectByIndex(currentIndex() - 1);
58
+ break;
59
+ case "Home":
60
+ e.preventDefault();
61
+ selectByIndex(0);
62
+ break;
63
+ case "End":
64
+ e.preventDefault();
65
+ selectByIndex(props.options.length - 1);
66
+ break;
67
+ }
68
+ };
69
+
27
70
  return (
28
- <div class={`flex rounded-lg border border-zinc-800/50 overflow-hidden ${props.class ?? ""}`}>
71
+ <div
72
+ role="radiogroup"
73
+ aria-label={props.ariaLabel}
74
+ class={`flex rounded-lg border border-[var(--ks-border,rgba(39,39,42,0.5))] overflow-hidden ${props.class ?? ""}`}
75
+ onKeyDown={onKeyDown}
76
+ >
29
77
  <For each={props.options}>
30
- {(o) => {
78
+ {(o, i) => {
31
79
  const opt = optionOf(o);
80
+ const selected = () => props.value === opt.value;
81
+ const isTabStop = () => selected() || (!props.value && i() === 0);
32
82
  return (
33
83
  <button
84
+ ref={(el) => (buttonRefs[i()] = el)}
85
+ type="button"
86
+ role="radio"
87
+ aria-checked={selected()}
88
+ tabIndex={isTabStop() ? 0 : -1}
34
89
  data-testid={props.testIdPrefix ? `${props.testIdPrefix}-${opt.value}` : undefined}
35
90
  onClick={() => props.onChange(opt.value)}
36
91
  class="px-3 py-1.5 text-xs transition-colors cursor-pointer"
37
92
  classList={{
38
93
  capitalize: opt.capitalize,
39
- "bg-amber-500/20 text-amber-400": props.value === opt.value,
40
- "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50": props.value !== opt.value,
94
+ "bg-[var(--ks-accent,#fbbf24)]/20 text-[var(--ks-accent,#fbbf24)]": selected(),
95
+ "text-[var(--ks-fg-muted,#a1a1aa)] hover:text-[var(--ks-fg,#ffffff)] hover:bg-[var(--ks-surface-raised,#1a1a1a)]":
96
+ !selected(),
41
97
  }}
42
98
  >
43
99
  {opt.label}
@@ -1,4 +1,5 @@
1
1
  import { For, type Component, type JSX } from "solid-js";
2
+ import { injectCSS } from "../../utils/inject-css";
2
3
 
3
4
  /** Icon component shape: any component that accepts a numeric `size`
4
5
  * (lucide-solid icons satisfy this, as does any custom SVG component). */
@@ -38,19 +39,6 @@ export interface SocialLinksBarProps {
38
39
  const CLIP_STYLE_ID = "ksui-social-clip-style";
39
40
  const CLIP_STYLE = `.ksui-social-clip{clip-path:polygon(0 0,calc(100% - 8px) 0,100% 8px,100% 100%,0 100%);}`;
40
41
 
41
- // Inject the clip-path rule once per document, matching the SSR-safe
42
- // getElementById-dedupe pattern used by Button/ThemeToggle. A module-level
43
- // boolean would be non-reentrant across SSR requests (the worker reuses the
44
- // module), so the style must be keyed off the document, not module state.
45
- function ensureSocialClipStyle(): void {
46
- if (typeof document === "undefined") return;
47
- if (document.getElementById(CLIP_STYLE_ID)) return;
48
- const el = document.createElement("style");
49
- el.id = CLIP_STYLE_ID;
50
- el.textContent = CLIP_STYLE;
51
- document.head.appendChild(el);
52
- }
53
-
54
42
  /**
55
43
  * A horizontal row of accessible, round (or clip-cornered) icon buttons that
56
44
  * link out to external profiles. Each button opens its href in a new tab with
@@ -59,7 +47,7 @@ function ensureSocialClipStyle(): void {
59
47
  * Domain-free: the specific URLs, icons, and labels all come from `links`.
60
48
  */
61
49
  export default function SocialLinksBar(props: SocialLinksBarProps): JSX.Element {
62
- ensureSocialClipStyle();
50
+ injectCSS(CLIP_STYLE_ID, CLIP_STYLE);
63
51
  const shape = () => props.shape ?? "round";
64
52
  const btn = () => props.buttonSize ?? (shape() === "clip" ? 48 : 40);
65
53
  const icon = () => props.iconSize ?? Math.round(btn() * 0.45);
@@ -80,7 +68,7 @@ export default function SocialLinksBar(props: SocialLinksBarProps): JSX.Element
80
68
  rel="noopener noreferrer"
81
69
  title={link.label}
82
70
  aria-label={link.label}
83
- class={`flex items-center justify-center bg-zinc-800 text-zinc-400 transition-all hover:bg-amber-500 hover:text-black ${shapeClass()}`}
71
+ class={`flex items-center justify-center bg-[var(--ks-surface-raised,#1a1a1a)] text-[var(--ks-fg-muted,#a1a1aa)] transition-all hover:bg-[var(--ks-accent,#fbbf24)] hover:text-[var(--ks-fg-on-accent,#0a0a0a)] ${shapeClass()}`}
84
72
  style={{ width: `${btn()}px`, height: `${btn()}px` }}
85
73
  >
86
74
  {link.icon({ size: icon() })}
@@ -16,28 +16,28 @@ interface ToneClass {
16
16
  // passes a plain label; nothing domain-specific leaks into this atom.
17
17
  const TONE_CLASS: Record<StatusIndicatorTone, ToneClass> = {
18
18
  success: {
19
- dot: "bg-emerald-400",
20
- text: "text-white",
19
+ dot: "bg-[var(--ks-success,#10b981)]",
20
+ text: "text-[var(--ks-fg,#ffffff)]",
21
21
  glow: "shadow-[0_0_10px_rgba(52,211,153,0.6)]",
22
22
  },
23
23
  neutral: {
24
- dot: "bg-zinc-400",
25
- text: "text-zinc-400",
24
+ dot: "bg-[var(--ks-fg-muted,#a1a1aa)]",
25
+ text: "text-[var(--ks-fg-muted,#a1a1aa)]",
26
26
  glow: "shadow-[0_0_10px_rgba(161,161,170,0.5)]",
27
27
  },
28
28
  warning: {
29
- dot: "bg-amber-400",
30
- text: "text-amber-300",
29
+ dot: "bg-[var(--ks-warning,#f59e0b)]",
30
+ text: "text-[var(--ks-warning-fg,#fbbf24)]",
31
31
  glow: "shadow-[0_0_10px_rgba(251,191,36,0.6)]",
32
32
  },
33
33
  danger: {
34
- dot: "bg-red-400",
35
- text: "text-red-300",
34
+ dot: "bg-[var(--ks-danger,#ef4444)]",
35
+ text: "text-[var(--ks-danger-fg,#f87171)]",
36
36
  glow: "shadow-[0_0_10px_rgba(248,113,113,0.6)]",
37
37
  },
38
38
  info: {
39
- dot: "bg-blue-400",
40
- text: "text-blue-300",
39
+ dot: "bg-[var(--ks-info,#38bdf8)]",
40
+ text: "text-[var(--ks-info-fg,#7dd3fc)]",
41
41
  glow: "shadow-[0_0_10px_rgba(96,165,250,0.6)]",
42
42
  },
43
43
  };
@@ -89,7 +89,7 @@ export default function StatusIndicator(props: StatusIndicatorProps): JSX.Elemen
89
89
  <Show when={props.caption}>
90
90
  <div
91
91
  class={`text-xs font-bold uppercase tracking-widest ${
92
- props.captionClass ?? "text-amber-400"
92
+ props.captionClass ?? "text-[var(--ks-accent,#fbbf24)]"
93
93
  }`}
94
94
  >
95
95
  {props.caption}
@@ -18,29 +18,29 @@ interface ToneClass {
18
18
  // compact sizes tables now use than the previous bordered pill did.
19
19
  const TONE_CLASS: Record<StatusTone, ToneClass> = {
20
20
  success: {
21
- text: "text-emerald-400",
22
- bg: "bg-emerald-500/10",
23
- bgSolid: "bg-emerald-500/20",
21
+ text: "text-[var(--ks-success-fg,#34d399)]",
22
+ bg: "bg-[var(--ks-success-bg,rgba(16,185,129,0.12))]",
23
+ bgSolid: "bg-[var(--ks-success,#10b981)]/20",
24
24
  },
25
25
  neutral: {
26
- text: "text-zinc-400",
27
- bg: "bg-zinc-800/60",
28
- bgSolid: "bg-zinc-700/40",
26
+ text: "text-[var(--ks-fg-muted,#a1a1aa)]",
27
+ bg: "bg-[var(--ks-surface-raised,#1a1a1a)]",
28
+ bgSolid: "bg-[var(--ks-surface-sunken,#141414)]",
29
29
  },
30
30
  warning: {
31
- text: "text-amber-400",
32
- bg: "bg-amber-500/10",
33
- bgSolid: "bg-amber-500/20",
31
+ text: "text-[var(--ks-warning-fg,#fbbf24)]",
32
+ bg: "bg-[var(--ks-warning-bg,rgba(245,158,11,0.12))]",
33
+ bgSolid: "bg-[var(--ks-warning,#f59e0b)]/20",
34
34
  },
35
35
  danger: {
36
- text: "text-red-400",
37
- bg: "bg-red-500/10",
38
- bgSolid: "bg-red-500/20",
36
+ text: "text-[var(--ks-danger-fg,#f87171)]",
37
+ bg: "bg-[var(--ks-danger-bg,rgba(239,68,68,0.12))]",
38
+ bgSolid: "bg-[var(--ks-danger,#ef4444)]/20",
39
39
  },
40
40
  info: {
41
- text: "text-blue-400",
42
- bg: "bg-blue-500/10",
43
- bgSolid: "bg-blue-500/20",
41
+ text: "text-[var(--ks-info-fg,#7dd3fc)]",
42
+ bg: "bg-[var(--ks-info-bg,rgba(56,189,248,0.12))]",
43
+ bgSolid: "bg-[var(--ks-info,#38bdf8)]/20",
44
44
  },
45
45
  };
46
46
 
@@ -14,7 +14,7 @@ interface TagPillProps {
14
14
  export default function TagPill(props: TagPillProps): JSX.Element {
15
15
  return (
16
16
  <span
17
- class={`inline-block rounded-full border border-zinc-700 bg-zinc-800/50 px-2 py-0.5 text-[10px] text-zinc-400 ${
17
+ class={`inline-block rounded-full border border-[var(--ks-border-strong,#3f3f46)] bg-[var(--ks-surface-raised,#1a1a1a)] px-2 py-0.5 text-[10px] text-[var(--ks-fg-muted,#a1a1aa)] ${
18
18
  props.class ?? ""
19
19
  }`}
20
20
  >