@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.
- package/package.json +4 -1
- package/src/components/base/AccountAvatar.tsx +1 -1
- package/src/components/base/AddAttachmentTile.tsx +4 -4
- package/src/components/base/BadgeSelect.tsx +10 -10
- package/src/components/base/Button.tsx +70 -38
- package/src/components/base/CameraCapture.tsx +8 -8
- package/src/components/base/ChartLegend.tsx +3 -3
- package/src/components/base/CopyButton.tsx +1 -1
- package/src/components/base/DataTable.tsx +68 -72
- package/src/components/base/DatePicker.tsx +77 -82
- package/src/components/base/DateTile.tsx +7 -7
- package/src/components/base/DetailRow.tsx +2 -2
- package/src/components/base/Dropdown.tsx +15 -15
- package/src/components/base/ExistingAttachmentTile.tsx +6 -6
- package/src/components/base/EyebrowBadge.tsx +15 -15
- package/src/components/base/FormErrorBanner.tsx +1 -1
- package/src/components/base/FormField.tsx +2 -2
- package/src/components/base/ImageCropper.tsx +19 -19
- package/src/components/base/ImageViewer.tsx +7 -11
- package/src/components/base/KpiCard.tsx +7 -7
- package/src/components/base/Modal.tsx +10 -14
- package/src/components/base/ProgressBar.tsx +63 -59
- package/src/components/base/RadioCardGroup.tsx +13 -6
- package/src/components/base/SectionHeading.tsx +4 -4
- package/src/components/base/SegmentedFilter.tsx +60 -4
- package/src/components/base/SocialLinksBar.tsx +3 -15
- package/src/components/base/StatusIndicator.tsx +11 -11
- package/src/components/base/StatusPill.tsx +15 -15
- package/src/components/base/TagPill.tsx +1 -1
- package/src/components/base/ThemeToggle.tsx +12 -20
- package/src/components/base/Tooltip.tsx +2 -2
- package/src/components/composite/AccountRadioPicker.tsx +8 -4
- package/src/components/composite/ComboBox.tsx +51 -44
- package/src/components/composite/CustomRenderer.tsx +6 -13
- package/src/components/composite/FileField.tsx +10 -17
- package/src/components/composite/FlowGraph.tsx +29 -36
- package/src/components/composite/FlowRunner.tsx +11 -18
- package/src/components/composite/FormAdvancedSection.tsx +31 -31
- package/src/components/composite/LiveTimer.tsx +3 -3
- package/src/components/composite/MarkdownNotes.tsx +11 -11
- package/src/components/composite/MentionTextarea.tsx +10 -10
- package/src/components/composite/NotFound.tsx +7 -7
- package/src/components/composite/PaymentAccountPicker.tsx +19 -19
- package/src/components/composite/SalesBodyEditor.tsx +26 -26
- package/src/components/composite/SearchableSelect.tsx +14 -14
- package/src/components/composite/SecretReveal.tsx +5 -5
- package/src/components/composite/TransactionAccountFields.tsx +78 -0
- package/src/components/composite/TransactionForm.tsx +83 -169
- package/src/components/composite/TransactionPayableFields.tsx +97 -0
- package/src/components/composite/TransferAccountsPicker.tsx +20 -20
- package/src/components/composite/TransferFeeChip.tsx +2 -2
- package/src/components/composite/VoucherPicker.tsx +25 -25
- package/src/components/composite/resource/ResourceForm.tsx +1 -1
- package/src/components/composite/resource/ResourcePage.tsx +8 -8
- package/src/components/composite/resource/cells.tsx +6 -3
- package/src/components/composite/resource/form-spec.ts +141 -0
- package/src/components/composite/resource/route-adapter.ts +63 -0
- package/src/components/composite/resource/route-spec.test.ts +125 -0
- package/src/components/composite/resource/route-spec.ts +158 -0
- package/src/index.ts +8 -0
- package/src/utils/INPUT_CLASS.ts +6 -3
- package/src/utils/account-icons.ts +29 -7
- package/src/utils/highlight.tsx +5 -6
- package/src/utils/inject-css.ts +13 -0
- 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-
|
|
13
|
-
danger: { text: "text-
|
|
14
|
-
info: { text: "text-
|
|
15
|
-
warning: { text: "text-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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: "
|
|
71
|
-
indicator: "#
|
|
72
|
-
stripe: "
|
|
73
|
-
overflow: "
|
|
74
|
-
shimmer: "
|
|
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: "
|
|
78
|
-
indicator: "#
|
|
79
|
-
stripe: "
|
|
80
|
-
overflow: "
|
|
81
|
-
shimmer: "
|
|
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: "
|
|
85
|
-
indicator: "#
|
|
86
|
-
stripe: "
|
|
87
|
-
overflow: "
|
|
88
|
-
shimmer: "
|
|
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: "
|
|
92
|
-
indicator: "#
|
|
93
|
-
stripe: "
|
|
94
|
-
overflow: "
|
|
95
|
-
shimmer: "
|
|
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: "
|
|
99
|
-
indicator: "#
|
|
100
|
-
stripe: "
|
|
101
|
-
overflow: "
|
|
102
|
-
shimmer: "
|
|
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: "
|
|
106
|
-
indicator: "#
|
|
107
|
-
stripe: "
|
|
108
|
-
overflow: "
|
|
109
|
-
shimmer: "
|
|
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: "
|
|
113
|
-
indicator: "#
|
|
114
|
-
stripe: "
|
|
115
|
-
overflow: "
|
|
116
|
-
shimmer: "
|
|
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: "
|
|
120
|
-
indicator: "#
|
|
121
|
-
stripe: "
|
|
122
|
-
overflow: "
|
|
123
|
-
shimmer: "
|
|
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: "
|
|
127
|
-
indicator: "#
|
|
128
|
-
stripe: "
|
|
129
|
-
overflow: "
|
|
130
|
-
shimmer: "
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
104
|
-
"bg-
|
|
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
|
-
"
|
|
135
|
-
"border-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
40
|
-
"text-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
20
|
-
text: "text-
|
|
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-
|
|
25
|
-
text: "text-
|
|
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-
|
|
30
|
-
text: "text-
|
|
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-
|
|
35
|
-
text: "text-
|
|
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-
|
|
40
|
-
text: "text-
|
|
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-
|
|
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-
|
|
22
|
-
bg: "bg-
|
|
23
|
-
bgSolid: "bg-
|
|
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-
|
|
27
|
-
bg: "bg-
|
|
28
|
-
bgSolid: "bg-
|
|
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-
|
|
32
|
-
bg: "bg-
|
|
33
|
-
bgSolid: "bg-
|
|
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-
|
|
37
|
-
bg: "bg-
|
|
38
|
-
bgSolid: "bg-
|
|
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-
|
|
42
|
-
bg: "bg-
|
|
43
|
-
bgSolid: "bg-
|
|
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-
|
|
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
|
>
|