@kahitsan/ksui 0.42.0 → 0.44.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 +1 -1
- package/src/components/base/DatePicker.test.tsx +6 -0
- package/src/components/base/DatePicker.tsx +5 -2
- package/src/components/base/SegmentedFilter.test.tsx +6 -0
- package/src/components/base/SegmentedFilter.tsx +4 -2
- package/src/components/composite/TransactionForm.tsx +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahitsan/ksui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "ksui is a standalone set of SolidJS UI components for KahitSan/Hilinga and any SolidJS app. Published to the public npm registry and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with only solid-js externalized; it depends on nothing but solid-js + lucide-solid and injects its own CSS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -43,6 +43,12 @@ describe("DatePicker", () => {
|
|
|
43
43
|
expect(onChange).toHaveBeenCalledWith("2026-06-20");
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
it("labels the trigger and calendar navigation", async () => {
|
|
47
|
+
render(() => <DatePicker ariaLabel="Paid date" value="2026-06-15" onChange={() => {}} />);
|
|
48
|
+
await fireEvent.click(screen.getByRole("button", { name: "Paid date" }));
|
|
49
|
+
expect(screen.getByRole("button", { name: /Previous month from/ })).toBeTruthy();
|
|
50
|
+
expect(screen.getByRole("button", { name: /Next month from/ })).toBeTruthy();
|
|
51
|
+
});
|
|
46
52
|
it("keeps the trigger and clear action keyboard reachable", () => {
|
|
47
53
|
render(() => <DatePicker value="2026-06-15" onChange={() => {}} />);
|
|
48
54
|
const trigger = screen.getByText("Jun 15").closest("button")!;
|
|
@@ -165,6 +165,8 @@ interface DatePickerSharedProps {
|
|
|
165
165
|
placeholder?: string;
|
|
166
166
|
withTime?: boolean;
|
|
167
167
|
disabled?: boolean;
|
|
168
|
+
/** Accessible purpose label for the trigger, for example "Paid date". */
|
|
169
|
+
ariaLabel?: string;
|
|
168
170
|
/** Override the trigger button class entirely (escape hatch for custom triggers). */
|
|
169
171
|
triggerClass?: string;
|
|
170
172
|
}
|
|
@@ -724,6 +726,7 @@ export default function DatePicker(props: DatePickerProps) {
|
|
|
724
726
|
ref={triggerRef}
|
|
725
727
|
onClick={openPicker}
|
|
726
728
|
type="button"
|
|
729
|
+
aria-label={props.ariaLabel}
|
|
727
730
|
aria-haspopup="dialog"
|
|
728
731
|
aria-expanded={open()}
|
|
729
732
|
disabled={props.disabled}
|
|
@@ -869,11 +872,11 @@ export default function DatePicker(props: DatePickerProps) {
|
|
|
869
872
|
<div class="ksui-datepicker-section">
|
|
870
873
|
{/* Month nav */}
|
|
871
874
|
<div class="ksui-datepicker-nav">
|
|
872
|
-
<button type="button" onClick={prevMonth} class="ksui-datepicker-nav-btn">
|
|
875
|
+
<button type="button" aria-label={`Previous month from ${monthLabel()}`} onClick={prevMonth} class="ksui-datepicker-nav-btn">
|
|
873
876
|
<ChevronLeft size={16} />
|
|
874
877
|
</button>
|
|
875
878
|
<span class="ksui-datepicker-month-label">{monthLabel()}</span>
|
|
876
|
-
<button type="button" onClick={nextMonth} class="ksui-datepicker-nav-btn">
|
|
879
|
+
<button type="button" aria-label={`Next month from ${monthLabel()}`} onClick={nextMonth} class="ksui-datepicker-nav-btn">
|
|
877
880
|
<ChevronRight size={16} />
|
|
878
881
|
</button>
|
|
879
882
|
</div>
|
|
@@ -27,6 +27,12 @@ describe("SegmentedFilter", () => {
|
|
|
27
27
|
expect(radios[0].getAttribute("aria-checked")).toBe("true");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it("supports a mobile-safe scrolling row", () => {
|
|
31
|
+
const { container } = render(() => (
|
|
32
|
+
<SegmentedFilter options={STRING_OPTIONS} value="today" onChange={() => {}} scrollable />
|
|
33
|
+
));
|
|
34
|
+
expect(container.querySelector('[role="radiogroup"]')?.className).toContain("overflow-x-auto");
|
|
35
|
+
});
|
|
30
36
|
it("renders object options with an explicit label", () => {
|
|
31
37
|
const { getAllByRole } = render(() => (
|
|
32
38
|
<SegmentedFilter options={OBJECT_OPTIONS} value="table" onChange={() => {}} />
|
|
@@ -23,6 +23,8 @@ interface SegmentedFilterProps {
|
|
|
23
23
|
class?: string;
|
|
24
24
|
/** Accessible label for the radiogroup wrapper (WAI-ARIA radiogroup pattern). */
|
|
25
25
|
ariaLabel?: string;
|
|
26
|
+
/** Keep long rows reachable on narrow screens with horizontal scrolling. */
|
|
27
|
+
scrollable?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
// A rounded bordered row of segment buttons with one active at a time.
|
|
@@ -85,7 +87,7 @@ export default function SegmentedFilter(props: SegmentedFilterProps): JSX.Elemen
|
|
|
85
87
|
<div
|
|
86
88
|
role="radiogroup"
|
|
87
89
|
aria-label={props.ariaLabel}
|
|
88
|
-
class={`flex rounded-lg border border-[var(--ks-border,rgba(39,39,42,0.5))] overflow-hidden ${props.class ?? ""}`}
|
|
90
|
+
class={`flex rounded-lg border border-[var(--ks-border,rgba(39,39,42,0.5))] ${props.scrollable ? "max-w-full overflow-x-auto" : "overflow-hidden"} ${props.class ?? ""}`}
|
|
89
91
|
onKeyDown={onKeyDown}
|
|
90
92
|
>
|
|
91
93
|
<For each={props.options}>
|
|
@@ -115,7 +117,7 @@ export default function SegmentedFilter(props: SegmentedFilterProps): JSX.Elemen
|
|
|
115
117
|
if (opt.disabled) return;
|
|
116
118
|
props.onChange(opt.value);
|
|
117
119
|
}}
|
|
118
|
-
class="px-3 py-1.5 text-xs transition-colors"
|
|
120
|
+
class="shrink-0 px-3 py-1.5 text-xs transition-colors"
|
|
119
121
|
classList={{
|
|
120
122
|
capitalize: opt.capitalize,
|
|
121
123
|
"cursor-not-allowed": opt.disabled,
|
|
@@ -264,6 +264,9 @@ export interface TransactionFormProps {
|
|
|
264
264
|
transferFeeAmount: string;
|
|
265
265
|
setTransferFeeAmount: (v: string) => void;
|
|
266
266
|
allowTransferFee: boolean;
|
|
267
|
+
// Editing a legacy payable row keeps the payable pane; create flows opt out
|
|
268
|
+
// via allowPayable={false} now that payables live in the Payables module.
|
|
269
|
+
allowPayable?: boolean;
|
|
267
270
|
pendingFiles: PendingFile[];
|
|
268
271
|
setPendingFiles: (v: PendingFile[]) => void;
|
|
269
272
|
existingAttachments?: TransactionAttachment[];
|
|
@@ -399,10 +402,11 @@ export default function TransactionForm(props: TransactionFormProps) {
|
|
|
399
402
|
// SearchableSelect mount so the loading-state placeholder shows while the
|
|
400
403
|
// per-tenant options are still in flight.
|
|
401
404
|
const subcategoryOptionsReady = () => subcategoryOptions() !== undefined;
|
|
402
|
-
const categoryOptions = () =>
|
|
403
|
-
props.category === "payable"
|
|
404
|
-
|
|
405
|
-
|
|
405
|
+
const categoryOptions = () => {
|
|
406
|
+
if (props.category === "payable") return ["sale", "expense", "business", "payable"];
|
|
407
|
+
const base = ["sale", "expense", "business"];
|
|
408
|
+
return props.allowPayable === false ? base : [...base, "payable"];
|
|
409
|
+
};
|
|
406
410
|
|
|
407
411
|
createEffect(() => {
|
|
408
412
|
if (props.category !== "business" && props.transferFeeEnabled) {
|