@kahitsan/ksui 0.39.0 → 0.39.2

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.0",
3
+ "version": "0.39.2",
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",
@@ -1,5 +1,6 @@
1
1
  import { createEffect, Show, For } from "solid-js";
2
2
  import AccountAvatar from "../base/AccountAvatar";
3
+ import Button from "../base/Button";
3
4
  import type { PaymentAccountOption } from "./PaymentAccountPicker";
4
5
 
5
6
  export interface AccountRadioPickerProps {
@@ -9,6 +10,8 @@ export interface AccountRadioPickerProps {
9
10
  ariaLabel: string;
10
11
  excludeId?: string;
11
12
  autoDefault?: boolean;
13
+ compact?: boolean;
14
+ tone?: "income" | "expense" | "transfer";
12
15
  }
13
16
 
14
17
  export default function AccountRadioPicker(props: AccountRadioPickerProps) {
@@ -82,6 +85,7 @@ export default function AccountRadioPicker(props: AccountRadioPickerProps) {
82
85
  aria-label={props.ariaLabel}
83
86
  tabIndex={-1}
84
87
  class="grid max-sm:grid-cols-2 sm:grid-cols-3 gap-2"
88
+ classList={{ "ks-transaction-account-picker": props.compact }}
85
89
  onKeyDown={onKeyDown}
86
90
  >
87
91
  <For each={visible()}>
@@ -89,17 +93,29 @@ export default function AccountRadioPicker(props: AccountRadioPickerProps) {
89
93
  const selected = () => props.value === a.id.toString();
90
94
  const isTabStop = () => selected() || (!props.value && i() === 0);
91
95
  return (
92
- <button
93
- ref={(el) => (buttonRefs[i()] = el)}
96
+ <Button
97
+ ref={(el: HTMLButtonElement) => (buttonRefs[i()] = el)}
94
98
  type="button"
95
99
  role="radio"
96
100
  aria-checked={selected()}
97
101
  tabIndex={isTabStop() ? 0 : -1}
102
+ intent="secondary"
103
+ variant="clip1"
104
+ size="sm"
105
+ noGlow
106
+ noRipple
107
+ noScanline
98
108
  onClick={() => props.onChange(a.id.toString())}
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"
109
+ class="group flex items-center gap-2 px-3 py-3 text-left text-sm transition-colors cursor-pointer"
100
110
  classList={{
101
111
  "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)]":
102
- selected(),
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",
103
119
  "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)]":
104
120
  !selected(),
105
121
  }}
@@ -114,7 +130,7 @@ export default function AccountRadioPicker(props: AccountRadioPickerProps) {
114
130
  }
115
131
  />
116
132
  <span class="truncate">{a.name}</span>
117
- </button>
133
+ </Button>
118
134
  );
119
135
  }}
120
136
  </For>
@@ -23,6 +23,7 @@ export interface TransactionAccountFieldsProps {
23
23
  transferFeeAmount: string;
24
24
  transferFeeEnabled: boolean;
25
25
  allowTransferFee: boolean;
26
+ compact?: boolean;
26
27
  }
27
28
 
28
29
  /**
@@ -43,6 +44,8 @@ export default function TransactionAccountFields(props: TransactionAccountFields
43
44
  value={
44
45
  props.category === "sale" ? props.destAccount : props.sourceAccount
45
46
  }
47
+ compact={props.compact}
48
+ tone={props.category === "sale" ? "income" : "expense"}
46
49
  onChange={(v) => {
47
50
  if (props.category === "sale") {
48
51
  props.setDestAccount(v);
@@ -299,7 +299,35 @@ export interface TransactionFormProps {
299
299
  }
300
300
 
301
301
  export default function TransactionForm(props: TransactionFormProps) {
302
- injectCSS("ksui-transaction-form-compact", `.ks-transaction-compact{min-height:0;font-family:var(--ks-font-body,ui-sans-serif,system-ui,sans-serif)}.ks-transaction-compact-body{display:flex;flex-direction:column;min-height:0;overflow:auto;padding:1rem 1.25rem 1.5rem;gap:1rem}.ks-transaction-compact-body>div{width:100%;flex:0 0 auto}.ks-transaction-compact-body input[data-testid="transactions-form-amount"]{min-height:3.5rem;text-align:center;font-size:2rem}.ks-transaction-compact-body input[data-testid="transactions-form-description"]{background:transparent;border-color:var(--ks-border)}.ks-transaction-compact-body>div:has(input[data-testid="transactions-form-amount"])>label,.ks-transaction-compact-body>div:has(input[data-testid="transactions-form-description"])>label{display:none}.ks-transaction-compact-body>div:has(button[aria-label="Today"])>label{font-size:0;text-transform:uppercase;letter-spacing:.12em}.ks-transaction-compact-body>div:has(button[aria-label="Today"])>label:after{content:"TRANSACTION DATE";font-size:10px}.ks-transaction-compact-body [role="radiogroup"]{display:flex;flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none}.ks-transaction-compact-body [role="radiogroup"]::-webkit-scrollbar{display:none}.ks-transaction-compact-body [role="radio"]{min-width:8rem}.ks-transaction-compact-footer{flex:0 0 auto}`);
302
+ injectCSS(
303
+ "ksui-transaction-form-compact",
304
+ `.ks-transaction-compact{display:flex;flex-direction:column;min-height:0;height:100%;font-family:var(--ks-font-body,"Inter", system-ui, sans-serif)}
305
+ .ks-transaction-compact form{display:flex!important;flex:1 1 auto!important;flex-direction:column!important;min-height:0!important;overflow:hidden!important}
306
+ .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
+ .ks-transaction-compact-type-switcher>button{min-height:2.25rem;border:0;border-radius:.5rem;padding:.5rem .25rem}
308
+ .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)}
309
+ .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
+ .ks-transaction-compact-body>div{width:100%!important;flex:0 0 auto!important}
311
+ .ks-transaction-compact-footer{flex:0 0 auto!important}
312
+ .ks-transaction-compact-body>.ks-transaction-compact-amount{order:2;padding:0;background:transparent}
313
+ .ks-transaction-compact-body>.ks-transaction-compact-description{order:3}
314
+ .ks-transaction-compact-body>.ks-transaction-compact-date{order:4;padding:.75rem;border:1px solid color-mix(in srgb,var(--ks-border,rgba(39,39,42,0.5)) 70%,transparent);border-radius:.75rem;background:color-mix(in srgb,var(--ks-overlay,rgba(0,0,0,0.7)) 78%,transparent)}
315
+ .ks-transaction-compact-body>.ks-transaction-compact-account{order:5}
316
+ .ks-transaction-compact-body>.ks-transaction-compact-details-toggle{order:6;display:flex;align-items:center;gap:.75rem;width:100%;padding:.25rem 0;color:var(--ks-fg-subtle,#71717a)}
317
+ .ks-transaction-compact-body>button:before,.ks-transaction-compact-body>button:after{content:"";height:1px;flex:1;background:color-mix(in srgb,var(--ks-fg,#ffffff) 8%,transparent)}
318
+ .ks-transaction-compact-body>div:has(input[data-testid="transactions-form-amount"])>label,.ks-transaction-compact-body>div:has(input[data-testid="transactions-form-description"])>label{display:none}
319
+ .ks-transaction-compact-body input,.ks-transaction-compact-body select,.ks-transaction-compact-body textarea{border-radius:.625rem}
320
+ .ks-transaction-compact-body input[data-testid="transactions-form-amount"]{height:4rem;font-family:var(--ks-font-mono,ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace);font-size:3.25rem;line-height:1;text-align:center}
321
+ .ks-transaction-compact-body input[data-testid="transactions-form-description"]{background:transparent;border-color:var(--ks-border)}
322
+ .ks-transaction-compact-body>div:has(button[aria-label="Today"])>label{font-size:10px;display:flex;align-items:center;gap:.5rem;color:color-mix(in srgb,var(--ks-fg,#ffffff) 35%,transparent);text-transform:uppercase;letter-spacing:.12em}
323
+ .ks-transaction-compact-body>div:has(button[aria-label="Today"])>label:before{content:"◷";font-size:15px;color:var(--ks-fg-muted,#a1a1aa)}
324
+ .ks-transaction-compact-body [role="radiogroup"]{display:flex;flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none}
325
+ .ks-transaction-compact-body [role="radiogroup"]::-webkit-scrollbar{display:none}
326
+ .ks-transaction-compact-body [role="radio"]{min-width:8rem}
327
+ .ks-transaction-compact:not(.ks-transaction-compact-details) .ks-transaction-compact-body>div:has([data-testid="form-payee-picker-trigger"]),.ks-transaction-compact:not(.ks-transaction-compact-details) .ks-transaction-compact-body>div:has([data-testid="subcategory-select"]),.ks-transaction-compact:not(.ks-transaction-compact-details) .ks-transaction-compact-body>div:has(textarea),.ks-transaction-compact:not(.ks-transaction-compact-details) .ks-transaction-compact-body>div:has([data-testid="mention-textarea"]),.ks-transaction-compact:not(.ks-transaction-compact-details) .ks-transaction-compact-body>div:has(button[aria-label="Add attachment"]){display:none}
328
+ .ks-transaction-compact-footer>div:last-child>button{min-height:2.75rem}
329
+ `,
330
+ );
303
331
  const compact = () => props.layout === "compact";
304
332
  const catConfig = () =>
305
333
  CATEGORY_FORM[props.category] || CATEGORY_FORM.expense;
@@ -426,7 +454,7 @@ export default function TransactionForm(props: TransactionFormProps) {
426
454
  return (
427
455
  <div
428
456
  class="relative flex flex-col flex-1 min-h-0"
429
- classList={{ "ks-transaction-compact": compact() }}
457
+ classList={{ "ks-transaction-compact": compact(), "ks-transaction-compact-details": compact() && viewMode() === "advanced" }}
430
458
  onDragEnter={handleDragEnter}
431
459
  onDragLeave={handleDragLeave}
432
460
  onDragOver={handleDragOver}
@@ -443,6 +471,39 @@ export default function TransactionForm(props: TransactionFormProps) {
443
471
  </div>
444
472
  </Show>
445
473
 
474
+ <Show when={compact() && !props.simpleMode}>
475
+ <div class="ks-transaction-compact-type-switcher" role="tablist" aria-label="Transaction type">
476
+ <For each={categoryOptions()}>
477
+ {(cat) => (
478
+ <Button
479
+ type="button"
480
+ role="tab"
481
+ aria-selected={props.category === cat}
482
+ data-testid={`transactions-form-category-${cat}`}
483
+ intent="secondary"
484
+ variant="ghost"
485
+ size="sm"
486
+ noGlow
487
+ noRipple
488
+ noScanline
489
+ class={`ks-transaction-type-button ks-transaction-type-${cat}`}
490
+ classList={{ "ks-transaction-type-active": props.category === cat }}
491
+ onClick={() => {
492
+ props.setCategory(cat);
493
+ props.setSourceAccount("");
494
+ props.setDestAccount("");
495
+ if (cat !== "business") {
496
+ props.setTransferFeeEnabled(false);
497
+ props.setTransferFeeAmount("");
498
+ }
499
+ }}
500
+ >
501
+ {CATEGORY_FORM[cat].label}
502
+ </Button>
503
+ )}
504
+ </For>
505
+ </div>
506
+ </Show>
446
507
  <Show when={cameraOpen()}>
447
508
  <CameraCapture
448
509
  onCapture={(file) => {
@@ -471,56 +532,6 @@ export default function TransactionForm(props: TransactionFormProps) {
471
532
  </div>
472
533
  </Show>
473
534
 
474
- <Show when={!props.simpleMode}>
475
- <div>
476
- <div class="text-[10px] uppercase tracking-widest text-[var(--ks-fg-subtle,#71717a)] font-semibold mb-2">
477
- Type
478
- </div>
479
- <div
480
- class="grid gap-2"
481
- classList={{
482
- "grid-cols-3": categoryOptions().length === 3,
483
- "grid-cols-4": categoryOptions().length === 4,
484
- }}
485
- >
486
- <For each={categoryOptions()}>
487
- {(cat) => {
488
- const cfg = CATEGORY_FORM[cat];
489
- const tone = CATEGORY_TONE[cat];
490
- const tc = TONE_CLASSES[tone.tone];
491
- const Ico = tone.icon;
492
- return (
493
- <button
494
- type="button"
495
- data-testid={`transactions-form-category-${cat}`}
496
- onClick={() => {
497
- props.setCategory(cat);
498
- props.setSourceAccount("");
499
- props.setDestAccount("");
500
- if (cat !== "business") {
501
- props.setTransferFeeEnabled(false);
502
- props.setTransferFeeAmount("");
503
- }
504
- }}
505
- class="flex min-h-[42px] items-center justify-center gap-2 px-3 py-2 border text-sm transition-colors ks-hud-clip-button cursor-pointer active:opacity-80"
506
- classList={{
507
- [`${tc.bg} ${tc.border} ${tc.text}`]:
508
- props.category === cat,
509
- "border-[var(--ks-border,rgba(39,39,42,0.5))] bg-transparent text-[var(--ks-fg-subtle,#71717a)] hover:border-[var(--ks-input-border,#3f3f46)] hover:text-[var(--ks-fg,#ffffff)]":
510
- props.category !== cat,
511
- }}
512
- >
513
- <Ico size={16} />
514
- <span class="font-medium">{cfg.label}</span>
515
- </button>
516
- );
517
- }}
518
- </For>
519
- </div>
520
- <p class="text-[11px] text-[var(--ks-fg-subtle,#71717a)] mt-2">{catConfig().hint}</p>
521
- </div>
522
- </Show>
523
-
524
535
  <Show when={props.category === "sale"}>
525
536
  <SalesBodyEditor
526
537
  items={props.saleItems}
@@ -537,6 +548,7 @@ export default function TransactionForm(props: TransactionFormProps) {
537
548
  <Show
538
549
  when={!(props.category === "sale" && props.saleItems.length > 0)}
539
550
  >
551
+ <div class={compact() ? "ks-transaction-compact-amount" : undefined}>
540
552
  <FormField label={compact() ? "" : "Amount *"}>
541
553
  <div class="flex items-stretch gap-2 px-4 py-3 border bg-[color-mix(in_srgb,var(--ks-overlay-surface,#18181b)_60%,transparent)] border-[color-mix(in_srgb,var(--ks-border,rgba(39,39,42,0.5))_60%,transparent)] ks-hud-clip-button focus-within:border-[color-mix(in_srgb,var(--ks-focus-ring,#c9a961)_50%,transparent)] transition-colors">
542
554
  <span class="self-center text-3xl font-bold text-[var(--ks-fg-subtle,#71717a)] tabular-nums">
@@ -567,6 +579,7 @@ export default function TransactionForm(props: TransactionFormProps) {
567
579
  </Show>
568
580
  </div>
569
581
  </FormField>
582
+ </div>
570
583
  <Show
571
584
  when={
572
585
  props.category === "business" &&
@@ -604,6 +617,7 @@ export default function TransactionForm(props: TransactionFormProps) {
604
617
  </Show>
605
618
  </Show>
606
619
 
620
+ <div class={compact() ? "ks-transaction-compact-date" : undefined}>
607
621
  <FormField label={compact() ? "TRANSACTION DATE" : "Date *"}>
608
622
  <DatePicker
609
623
  value={props.date}
@@ -616,6 +630,7 @@ export default function TransactionForm(props: TransactionFormProps) {
616
630
  </p>
617
631
  </Show>
618
632
  </FormField>
633
+ </div>
619
634
 
620
635
  <Show when={props.isBackdated}>
621
636
  <div class="rounded-lg border border-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_20%,transparent)] bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_5%,transparent)] px-3 py-2">
@@ -635,6 +650,7 @@ export default function TransactionForm(props: TransactionFormProps) {
635
650
  </div>
636
651
  </Show>
637
652
 
653
+ <div class={compact() ? "ks-transaction-compact-description" : undefined}>
638
654
  <FormField label={compact() ? "" : "Description *"}>
639
655
  <input
640
656
  type="text"
@@ -646,6 +662,7 @@ export default function TransactionForm(props: TransactionFormProps) {
646
662
  required
647
663
  />
648
664
  </FormField>
665
+ </div>
649
666
 
650
667
  <Show when={catConfig().showPayee}>
651
668
  <div class="grid grid-cols-2 gap-4">
@@ -770,6 +787,7 @@ export default function TransactionForm(props: TransactionFormProps) {
770
787
  />
771
788
  </Show>
772
789
 
790
+ <div class={compact() ? "ks-transaction-compact-account" : undefined}>
773
791
  <TransactionAccountFields
774
792
  category={props.category}
775
793
  accounts={props.accounts}
@@ -785,7 +803,9 @@ export default function TransactionForm(props: TransactionFormProps) {
785
803
  transferFeeAmount={props.transferFeeAmount}
786
804
  transferFeeEnabled={props.transferFeeEnabled}
787
805
  allowTransferFee={props.allowTransferFee}
806
+ compact={compact()}
788
807
  />
808
+ </div>
789
809
 
790
810
  <FormField label="Notes">
791
811
  <MentionTextarea
@@ -897,7 +917,7 @@ export default function TransactionForm(props: TransactionFormProps) {
897
917
  </div>
898
918
 
899
919
  <Show when={!props.simpleMode}>
900
- <div class="flex justify-center pt-2">
920
+ <div class="flex justify-center pt-2" classList={{ "ks-transaction-compact-details-toggle": compact() }}>
901
921
  <button
902
922
  type="button"
903
923
  onClick={() =>