@kahitsan/ksui 0.39.4 → 0.39.5

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.39.4",
3
+ "version": "0.39.5",
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",
@@ -0,0 +1,54 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render, screen } from "@solidjs/testing-library";
3
+ import { Suspense } from "solid-js";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import ExistingAttachmentTile from "./ExistingAttachmentTile";
6
+
7
+ afterEach(() => {
8
+ cleanup();
9
+ vi.unstubAllGlobals();
10
+ });
11
+
12
+ describe("ExistingAttachmentTile", () => {
13
+ it("keeps its parent rendered while private attachment bytes load", async () => {
14
+ let resolveFetch!: (response: Response) => void;
15
+ vi.stubGlobal(
16
+ "fetch",
17
+ vi.fn(
18
+ () =>
19
+ new Promise<Response>((resolve) => {
20
+ resolveFetch = resolve;
21
+ }),
22
+ ),
23
+ );
24
+ vi.stubGlobal("URL", {
25
+ ...URL,
26
+ createObjectURL: vi.fn(() => "blob:attachment"),
27
+ revokeObjectURL: vi.fn(),
28
+ });
29
+
30
+ render(() => (
31
+ <Suspense fallback={<p>Modal fallback</p>}>
32
+ <div data-testid="modal-shell">
33
+ <ExistingAttachmentTile
34
+ attachment={{
35
+ id: 1,
36
+ file_name: "receipt.jpg",
37
+ mime_type: "image/jpeg",
38
+ s3_link: null,
39
+ }}
40
+ rawHref="/api/attachments/1/raw"
41
+ testId="attachment"
42
+ />
43
+ </div>
44
+ </Suspense>
45
+ ));
46
+
47
+ expect(screen.getByTestId("modal-shell")).toBeTruthy();
48
+ expect(screen.queryByText("Modal fallback")).toBeNull();
49
+ expect(screen.getByText("Loading")).toBeTruthy();
50
+
51
+ resolveFetch(new Response(new Blob(["image"]), { status: 200 }));
52
+ expect(await screen.findByAltText("receipt.jpg")).toBeTruthy();
53
+ });
54
+ });
@@ -10,14 +10,13 @@
10
10
  // route and render the resulting blob: (the proxy/blob pattern). s3_link is then
11
11
  // ignored for rendering; a spinner shows while the bytes stream.
12
12
 
13
- import { Show, createSignal, type Component } from "solid-js";
13
+ import { Show, createEffect, createSignal, onCleanup, type Component } from "solid-js";
14
14
  import Paperclip from "lucide-solid/icons/paperclip";
15
15
  import X from "lucide-solid/icons/x";
16
16
  import TriangleAlert from "lucide-solid/icons/triangle-alert";
17
17
  import Loader2 from "lucide-solid/icons/loader-2";
18
18
  import { confirm } from "../../utils/confirm";
19
19
  import { attachmentUrl, isResolvableAttachment } from "../../utils/attachments";
20
- import { createObjectUrlResource } from "../../utils/object-url-resource";
21
20
  import ImageViewer from "./ImageViewer";
22
21
 
23
22
  export interface ExistingAttachment {
@@ -40,17 +39,53 @@ interface Props {
40
39
  }
41
40
 
42
41
  export default function ExistingAttachmentTile(props: Props) {
43
- // Always call the hook (Solid rule); a null href no-ops when not in blob mode.
44
- const blob = createObjectUrlResource(
45
- () => props.rawHref ?? null,
46
- { init: props.rawInit },
47
- );
42
+ // Keep attachment loading local. A pending createResource promise can suspend
43
+ // the nearest boundary and briefly unmount the transaction modal.
44
+ const [blob, setBlob] = createSignal<string | null>(null);
45
+ const [blobLoading, setBlobLoading] = createSignal(false);
46
+ let requestId = 0;
47
+ let currentUrl: string | null = null;
48
+ const revokeUrl = () => {
49
+ if (currentUrl) URL.revokeObjectURL(currentUrl);
50
+ currentUrl = null;
51
+ };
52
+ createEffect(() => {
53
+ const href = props.rawHref ?? null;
54
+ const id = ++requestId;
55
+ revokeUrl();
56
+ setBlob(null);
57
+ if (!href) {
58
+ setBlobLoading(false);
59
+ return;
60
+ }
61
+ setBlobLoading(true);
62
+ void fetch(href, { credentials: "include", ...props.rawInit })
63
+ .then(async (res) => (res.ok ? URL.createObjectURL(await res.blob()) : null))
64
+ .then((url) => {
65
+ if (id !== requestId) {
66
+ if (url) URL.revokeObjectURL(url);
67
+ return;
68
+ }
69
+ currentUrl = url;
70
+ setBlob(url);
71
+ })
72
+ .catch(() => {
73
+ if (id === requestId) setBlob(null);
74
+ })
75
+ .finally(() => {
76
+ if (id === requestId) setBlobLoading(false);
77
+ });
78
+ });
79
+ onCleanup(() => {
80
+ requestId++;
81
+ revokeUrl();
82
+ });
48
83
  const isBlobMode = () => props.rawHref != null;
49
84
  const url = (): string | undefined =>
50
85
  isBlobMode() ? (blob() ?? undefined) : attachmentUrl(props.attachment.s3_link);
51
86
  const resolvable = () =>
52
87
  isBlobMode() ? blob() != null : isResolvableAttachment(props.attachment.s3_link);
53
- const loading = () => (isBlobMode() ? blob.loading : false);
88
+ const loading = () => (isBlobMode() ? blobLoading() : false);
54
89
  const [viewerOpen, setViewerOpen] = createSignal(false);
55
90
 
56
91
  const FallbackIcon = () => {
@@ -0,0 +1,25 @@
1
+ import { cleanup, fireEvent, render } from "@solidjs/testing-library";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import ImageViewer from "./ImageViewer";
4
+
5
+ describe("ImageViewer", () => {
6
+ afterEach(() => cleanup());
7
+
8
+ it("reserves viewer space before the image loads", () => {
9
+ const showModal = vi.fn();
10
+ Object.defineProperty(HTMLDialogElement.prototype, "showModal", { configurable: true, value: showModal });
11
+ Object.defineProperty(HTMLDialogElement.prototype, "close", { configurable: true, value: vi.fn() });
12
+ const view = render(() => <ImageViewer src="blob:test" alt="Receipt" onClose={vi.fn()} />);
13
+ const frame = view.container.querySelector<HTMLElement>(".ksui-imgviewer-frame");
14
+ const image = view.container.querySelector<HTMLImageElement>("img");
15
+
16
+ expect(frame).toBeTruthy();
17
+ expect(frame?.className).toContain("ksui-imgviewer-frame");
18
+ expect(image?.style.opacity).toBe("0");
19
+ expect(showModal).toHaveBeenCalledTimes(1);
20
+
21
+ fireEvent.load(image!);
22
+ expect(image?.style.opacity).toBe("1");
23
+
24
+ });
25
+ });
@@ -13,7 +13,7 @@
13
13
  // with unscoped `ksui-imgviewer-*` class names. Mount === open, unmount === closed
14
14
  // (wrap in `<Show when={…}>`).
15
15
 
16
- import { onCleanup, onMount } from "solid-js";
16
+ import { createSignal, onCleanup, onMount } from "solid-js";
17
17
  import X from "lucide-solid/icons/x";
18
18
  import { lockPullToRefresh, unlockPullToRefresh } from "../../utils/dom";
19
19
  import { injectCSS } from "../../utils/inject-css";
@@ -26,9 +26,10 @@ const STYLE_CSS = `
26
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;}
27
27
  .ksui-imgviewer[open]{display:flex;align-items:center;justify-content:center;}
28
28
  .ksui-imgviewer::backdrop{background:var(--ks-overlay, rgba(0,0,0,0.7));backdrop-filter:blur(2px);}
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);}
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;}
31
- .ksui-imgviewer-close:hover{background:rgba(255,255,255,0.22);}
29
+ .ksui-imgviewer-frame{display:grid;place-items:center;width:96vw;height:96vh;}
30
+ .ksui-imgviewer-img{display:block;max-width:100%;max-height:100%;object-fit:contain;border-radius:0.25rem;box-shadow:0 25px 50px -12px color-mix(in srgb,var(--ks-overlay,rgba(0,0,0,0.7)) 80%,transparent);}
31
+ .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:color-mix(in srgb,var(--ks-fg,#ffffff) 12%,transparent);color:var(--ks-fg,#ffffff);border:0;cursor:pointer;}
32
+ .ksui-imgviewer-close:hover{background:color-mix(in srgb,var(--ks-fg,#ffffff) 22%,transparent);}
32
33
  `;
33
34
 
34
35
  interface Props {
@@ -43,6 +44,7 @@ interface Props {
43
44
  export default function ImageViewer(props: Props) {
44
45
  injectCSS(STYLE_ID, STYLE_CSS);
45
46
  let dialogEl: HTMLDialogElement | undefined;
47
+ const [loaded, setLoaded] = createSignal(false);
46
48
 
47
49
  lockPullToRefresh();
48
50
  onCleanup(unlockPullToRefresh);
@@ -89,7 +91,15 @@ export default function ImageViewer(props: Props) {
89
91
  >
90
92
  <X size={22} />
91
93
  </button>
92
- <img src={props.src} alt={props.alt || ""} class="ksui-imgviewer-img" />
94
+ <div class="ksui-imgviewer-frame" aria-busy={!loaded()}>
95
+ <img
96
+ src={props.src}
97
+ alt={props.alt || ""}
98
+ class="ksui-imgviewer-img"
99
+ style={{ opacity: loaded() ? 1 : 0 }}
100
+ onLoad={() => setLoaded(true)}
101
+ />
102
+ </div>
93
103
  </dialog>
94
104
  );
95
105
  }
@@ -1,6 +1,5 @@
1
1
  import { createEffect, Show, For } from "solid-js";
2
2
  import AccountAvatar from "../base/AccountAvatar";
3
- import Button from "../base/Button";
4
3
  import type { PaymentAccountOption } from "./PaymentAccountPicker";
5
4
 
6
5
  export interface AccountRadioPickerProps {
@@ -10,8 +9,6 @@ export interface AccountRadioPickerProps {
10
9
  ariaLabel: string;
11
10
  excludeId?: string;
12
11
  autoDefault?: boolean;
13
- compact?: boolean;
14
- tone?: "income" | "expense" | "transfer";
15
12
  }
16
13
 
17
14
  export default function AccountRadioPicker(props: AccountRadioPickerProps) {
@@ -85,37 +82,24 @@ export default function AccountRadioPicker(props: AccountRadioPickerProps) {
85
82
  aria-label={props.ariaLabel}
86
83
  tabIndex={-1}
87
84
  class="grid max-sm:grid-cols-2 sm:grid-cols-3 gap-2"
88
- classList={{ "ks-transaction-account-picker": props.compact }}
89
85
  onKeyDown={onKeyDown}
90
86
  >
91
87
  <For each={visible()}>
92
88
  {(a, i) => {
93
89
  const selected = () => props.value === a.id.toString();
94
- const isTabStop = () => selected() || (!props.value && i() === 0);
90
+ const isTabStop = () => selected() || currentIndex() === i();
95
91
  return (
96
- <Button
97
- ref={(el: HTMLButtonElement) => (buttonRefs[i()] = el)}
92
+ <button
93
+ ref={(el) => (buttonRefs[i()] = el)}
98
94
  type="button"
99
95
  role="radio"
100
96
  aria-checked={selected()}
101
97
  tabIndex={isTabStop() ? 0 : -1}
102
- intent="secondary"
103
- variant="clip1"
104
- size="sm"
105
- noGlow
106
- noRipple
107
- noScanline
108
98
  onClick={() => props.onChange(a.id.toString())}
109
- class="group flex items-center gap-2 px-3 py-3 text-left text-sm transition-colors cursor-pointer"
99
+ class="group flex items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm transition-colors cursor-pointer ks-hud-clip-top-left-bottom-right"
110
100
  classList={{
111
101
  "border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_10%,transparent)] text-[var(--ks-accent-hover,#fcd34d)]":
112
- selected() && (!props.compact || !props.tone),
113
- "border-[color-mix(in_srgb,var(--ks-success,#10b981)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-success,#10b981)_10%,transparent)] text-[var(--ks-success-fg,#34d399)]":
114
- selected() && props.compact && props.tone === "income",
115
- "border-[color-mix(in_srgb,var(--ks-danger,#ef4444)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-danger,#ef4444)_10%,transparent)] text-[var(--ks-danger-fg,#f87171)]":
116
- selected() && props.compact && props.tone === "expense",
117
- "border-[color-mix(in_srgb,var(--ks-info,#38bdf8)_50%,transparent)] bg-[color-mix(in_srgb,var(--ks-info,#38bdf8)_10%,transparent)] text-[var(--ks-info,#38bdf8)]":
118
- selected() && props.compact && props.tone === "transfer",
102
+ selected(),
119
103
  "border-[var(--ks-border-strong,#3f3f46)] bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_50%,transparent)] text-[var(--ks-fg-muted,#a1a1aa)] hover:border-[var(--ks-border-strong,#3f3f46)] hover:bg-[var(--ks-surface-raised,#1a1a1a)]":
120
104
  !selected(),
121
105
  }}
@@ -130,7 +114,7 @@ export default function AccountRadioPicker(props: AccountRadioPickerProps) {
130
114
  }
131
115
  />
132
116
  <span class="truncate">{a.name}</span>
133
- </Button>
117
+ </button>
134
118
  );
135
119
  }}
136
120
  </For>
@@ -23,7 +23,6 @@ export interface TransactionAccountFieldsProps {
23
23
  transferFeeAmount: string;
24
24
  transferFeeEnabled: boolean;
25
25
  allowTransferFee: boolean;
26
- compact?: boolean;
27
26
  }
28
27
 
29
28
  /**
@@ -44,8 +43,6 @@ export default function TransactionAccountFields(props: TransactionAccountFields
44
43
  value={
45
44
  props.category === "sale" ? props.destAccount : props.sourceAccount
46
45
  }
47
- compact={props.compact}
48
- tone={props.category === "sale" ? "income" : "expense"}
49
46
  onChange={(v) => {
50
47
  if (props.category === "sale") {
51
48
  props.setDestAccount(v);
@@ -7,13 +7,7 @@
7
7
  // advanced-fields toggle entirely, for a caller that locks `category` to one
8
8
  // value and never needs EWT/sharing (e.g. a "record my own expense" surface).
9
9
 
10
- import {
11
- createEffect,
12
- createResource,
13
- createSignal,
14
- Show,
15
- For,
16
- } from "solid-js";
10
+ import { createEffect, createSignal, Show, For } from "solid-js";
17
11
  import X from "lucide-solid/icons/x";
18
12
  import Upload from "lucide-solid/icons/upload";
19
13
  import FileIcon from "lucide-solid/icons/file";
@@ -306,6 +300,9 @@ export default function TransactionForm(props: TransactionFormProps) {
306
300
  .ks-transaction-compact-type-switcher{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.375rem;margin:0 1.25rem .25rem;padding:.375rem;border-radius:.75rem;background:color-mix(in srgb,var(--ks-fg,#ffffff) 5%,transparent)}
307
301
  .ks-transaction-compact-type-switcher>button{min-height:2.25rem;border:0;border-radius:.5rem;padding:.5rem .25rem}
308
302
  .ks-transaction-compact-type-switcher>button.ks-transaction-type-active{background:color-mix(in srgb,var(--ks-primary,#c9a961) 16%,transparent);color:var(--ks-primary,#c9a961)}
303
+ .ks-transaction-compact-type-switcher>button.ks-transaction-type-sale.ks-transaction-type-active{background:color-mix(in srgb,var(--ks-success,#10b981) 16%,transparent);color:var(--ks-success-fg,#34d399)}
304
+ .ks-transaction-compact-type-switcher>button.ks-transaction-type-expense.ks-transaction-type-active{background:color-mix(in srgb,var(--ks-danger,#ef4444) 16%,transparent);color:var(--ks-danger-fg,#f87171)}
305
+ .ks-transaction-compact-type-switcher>button.ks-transaction-type-business.ks-transaction-type-active{background:color-mix(in srgb,var(--ks-info,#38bdf8) 16%,transparent);color:var(--ks-info,#38bdf8)}
309
306
  .ks-transaction-compact-body{display:flex!important;flex:1 1 auto!important;flex-direction:column!important;width:100%!important;min-height:0!important;overflow-x:hidden!important;overflow-y:auto!important;padding:1rem 1.25rem 1.5rem!important;gap:1rem}
310
307
  .ks-transaction-compact-body>div{width:100%!important;flex:0 0 auto!important}
311
308
  .ks-transaction-compact-footer{flex:0 0 auto!important}
@@ -360,23 +357,43 @@ export default function TransactionForm(props: TransactionFormProps) {
360
357
  return "expense";
361
358
  return null;
362
359
  };
363
- const [subcategoryOptions] = createResource(
364
- subcategoryAppliesTo,
365
- async (appliesTo) => {
366
- if (!appliesTo) return [] as { id: number; name: string }[];
367
- const res = await fetch(
368
- `/api/transactions/subcategories?applies_to=${appliesTo}`,
369
- {
370
- credentials: "include",
371
- }
372
- );
373
- if (!res.ok) return [] as { id: number; name: string }[];
374
- const data = (await res.json()) as {
375
- subcategories: { id: number; name: string }[];
376
- };
377
- return data.subcategories;
360
+ const [subcategoryOptions, setSubcategoryOptions] = createSignal<
361
+ { id: number; name: string }[] | undefined
362
+ >(undefined);
363
+ const subcategoryCache = new Map<
364
+ "income" | "expense",
365
+ { id: number; name: string }[]
366
+ >();
367
+ let subcategoryRequestId = 0;
368
+ createEffect(() => {
369
+ const appliesTo = subcategoryAppliesTo();
370
+ const requestId = ++subcategoryRequestId;
371
+ if (!appliesTo) {
372
+ setSubcategoryOptions(undefined);
373
+ return;
378
374
  }
379
- );
375
+ const cached = subcategoryCache.get(appliesTo);
376
+ if (cached) {
377
+ setSubcategoryOptions(cached);
378
+ return;
379
+ }
380
+ setSubcategoryOptions(undefined);
381
+ void fetch(`/api/transactions/subcategories?applies_to=${appliesTo}`, {
382
+ credentials: "include",
383
+ })
384
+ .then(async (res) => {
385
+ if (!res.ok) return [] as { id: number; name: string }[];
386
+ const data = (await res.json()) as {
387
+ subcategories: { id: number; name: string }[];
388
+ };
389
+ return data.subcategories;
390
+ })
391
+ .catch(() => [] as { id: number; name: string }[])
392
+ .then((options) => {
393
+ subcategoryCache.set(appliesTo, options);
394
+ if (requestId === subcategoryRequestId) setSubcategoryOptions(options);
395
+ });
396
+ });
380
397
 
381
398
  // True once the async resource has resolved at least once. Gates the
382
399
  // SearchableSelect mount so the loading-state placeholder shows while the
@@ -471,7 +488,7 @@ export default function TransactionForm(props: TransactionFormProps) {
471
488
  </div>
472
489
  </Show>
473
490
 
474
- <Show when={compact() && !props.simpleMode}>
491
+ <Show when={!props.simpleMode}>
475
492
  <div class="ks-transaction-compact-type-switcher" role="tablist" aria-label="Transaction type">
476
493
  <For each={categoryOptions()}>
477
494
  {(cat) => (
@@ -726,48 +743,36 @@ export default function TransactionForm(props: TransactionFormProps) {
726
743
 
727
744
  <Show when={subcategoryAppliesTo() !== null}>
728
745
  <FormField label="Category">
729
- <Show
730
- when={subcategoryOptionsReady()}
731
- fallback={
732
- <select
733
- disabled
734
- data-testid="subcategory-select-loading"
735
- class="w-full bg-[color-mix(in_srgb,var(--ks-input-bg,#18181b)_60%,transparent)] border border-[var(--ks-border,rgba(39,39,42,0.5))] px-3 py-3 text-sm text-[var(--ks-fg-subtle,#71717a)] ks-hud-clip-button focus:outline-none"
736
- >
737
- <option>Loading…</option>
738
- </select>
739
- }
740
- >
741
- <SearchableSelect
742
- triggerTestId="subcategory-select"
743
- wrapperClass="relative w-full"
744
- value={props.subcategory}
745
- options={(() => {
746
- const list = (subcategoryOptions() || []).map((opt) => ({
747
- value: opt.name,
748
- label: opt.name,
749
- }));
750
- list.unshift({ value: "", label: "— Uncategorised —" });
751
- if (
752
- props.subcategory &&
753
- !list.some((o) => o.value === props.subcategory)
754
- ) {
755
- list.push({
756
- value: props.subcategory,
757
- label: props.subcategory,
758
- });
759
- }
760
- return list;
761
- })()}
762
- onChange={(opt) =>
763
- props.setSubcategory(opt ? String(opt.value) : "")
746
+ <SearchableSelect
747
+ triggerTestId="subcategory-select"
748
+ wrapperClass="relative w-full"
749
+ value={props.subcategory}
750
+ loading={!subcategoryOptionsReady()}
751
+ options={(() => {
752
+ const list = (subcategoryOptions() || []).map((opt) => ({
753
+ value: opt.name,
754
+ label: opt.name,
755
+ }));
756
+ list.unshift({ value: "", label: "— Uncategorised —" });
757
+ if (
758
+ props.subcategory &&
759
+ !list.some((o) => o.value === props.subcategory)
760
+ ) {
761
+ list.push({
762
+ value: props.subcategory,
763
+ label: props.subcategory,
764
+ });
764
765
  }
765
- placeholder="— Uncategorised —"
766
- searchPlaceholder="Search categories…"
767
- triggerClass="w-full bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_60%,transparent)] border border-[color-mix(in_srgb,var(--ks-border,rgba(39,39,42,0.5))_60%,transparent)] px-3 py-3 text-sm text-[var(--ks-fg,#ffffff)] ks-hud-clip-button cursor-pointer focus:outline-none focus:border-[color-mix(in_srgb,var(--ks-focus-ring,#c9a961)_50%,transparent)] flex items-center justify-between gap-2"
768
- triggerLabelClass="truncate text-left flex-1 min-w-0"
769
- />
770
- </Show>
766
+ return list;
767
+ })()}
768
+ onChange={(opt) =>
769
+ props.setSubcategory(opt ? String(opt.value) : "")
770
+ }
771
+ placeholder="— Uncategorised —"
772
+ searchPlaceholder="Search categories…"
773
+ triggerClass="w-full bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_60%,transparent)] border border-[color-mix(in_srgb,var(--ks-border,rgba(39,39,42,0.5))_60%,transparent)] px-3 py-3 text-sm text-[var(--ks-fg,#ffffff)] ks-hud-clip-button cursor-pointer focus:outline-none focus:border-[color-mix(in_srgb,var(--ks-focus-ring,#c9a961)_50%,transparent)] flex items-center justify-between gap-2"
774
+ triggerLabelClass="truncate text-left flex-1 min-w-0"
775
+ />
771
776
  <p class="text-[10px] text-[var(--ks-fg-subtle,#71717a)] mt-0.5">
772
777
  Optional. Used for tax-prep classification.
773
778
  </p>
@@ -803,7 +808,6 @@ export default function TransactionForm(props: TransactionFormProps) {
803
808
  transferFeeAmount={props.transferFeeAmount}
804
809
  transferFeeEnabled={props.transferFeeEnabled}
805
810
  allowTransferFee={props.allowTransferFee}
806
- compact={compact()}
807
811
  />
808
812
  </div>
809
813