@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahitsan/ksui",
3
- "version": "0.42.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
- ? ["sale", "expense", "business", "payable"]
405
- : ["sale", "expense", "business"];
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) {