@kahitsan/ksui 0.38.1 → 0.38.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.38.
|
|
3
|
+
"version": "0.38.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",
|
|
@@ -112,10 +112,11 @@ describe("VoucherPicker dialog", () => {
|
|
|
112
112
|
await waitFor(() => expect(getByTestId("voucher-picker-result-1")).toBeTruthy());
|
|
113
113
|
expect(getByTestId("voucher-picker-popup").closest("dialog")).not.toBeNull();
|
|
114
114
|
|
|
115
|
-
// Staging a row must not reach the consumer yet
|
|
115
|
+
// Staging a row must not reach the consumer yet; the footer no longer
|
|
116
|
+
// repeats the code (it's in the listing), so the confirm enabling is the signal.
|
|
116
117
|
fireEvent.click(getByTestId("voucher-picker-result-1"));
|
|
117
118
|
expect(onChange).not.toHaveBeenCalled();
|
|
118
|
-
expect(getByTestId("voucher-picker-
|
|
119
|
+
expect(getByTestId("voucher-picker-confirm").hasAttribute("disabled")).toBe(false);
|
|
119
120
|
|
|
120
121
|
fireEvent.click(getByTestId("voucher-picker-confirm"));
|
|
121
122
|
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ id: 1, code: "SAVE20" }));
|
|
@@ -147,9 +148,10 @@ describe("VoucherPicker dialog", () => {
|
|
|
147
148
|
await waitFor(() => expect(getByTestId("voucher-picker-result-1")).toBeTruthy());
|
|
148
149
|
|
|
149
150
|
fireEvent.click(getByTestId("voucher-picker-result-1"));
|
|
150
|
-
expect(getByTestId("voucher-picker-
|
|
151
|
+
expect(getByTestId("voucher-picker-confirm").hasAttribute("disabled")).toBe(false);
|
|
151
152
|
|
|
152
153
|
fireEvent.click(getByTestId("voucher-picker-result-1"));
|
|
154
|
+
expect(getByTestId("voucher-picker-confirm").hasAttribute("disabled")).toBe(true);
|
|
153
155
|
expect(getByTestId("voucher-picker-draft-summary").textContent).toContain("No voucher selected");
|
|
154
156
|
});
|
|
155
157
|
|
|
@@ -175,7 +177,7 @@ describe("VoucherPicker dialog", () => {
|
|
|
175
177
|
expect(mark!.textContent?.toLowerCase()).toBe("partner");
|
|
176
178
|
});
|
|
177
179
|
|
|
178
|
-
it("shows the voucher description in the row and the
|
|
180
|
+
it("shows the voucher description in the row and the footer draft", async () => {
|
|
179
181
|
mockPagedFetch([
|
|
180
182
|
voucher({ id: 50, code: "PARTNER_ACES", notes: "BISCAST ACES outreach discount" }),
|
|
181
183
|
]);
|
|
@@ -190,7 +192,8 @@ describe("VoucherPicker dialog", () => {
|
|
|
190
192
|
);
|
|
191
193
|
|
|
192
194
|
fireEvent.click(getByTestId("voucher-picker-result-50"));
|
|
193
|
-
|
|
195
|
+
// The footer shows the description under the summary, beside the buttons.
|
|
196
|
+
expect(getByTestId("voucher-picker-draft-description").textContent).toContain(
|
|
194
197
|
"BISCAST ACES outreach discount",
|
|
195
198
|
);
|
|
196
199
|
});
|
|
@@ -185,6 +185,10 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
185
185
|
const [total, setTotal] = createSignal(0);
|
|
186
186
|
// Staged choice. `props.selected` only changes on Confirm.
|
|
187
187
|
const [draft, setDraft] = createSignal<VoucherOption | null>(null);
|
|
188
|
+
// Footer description popup: clamped to two lines, "More" only when it overflows.
|
|
189
|
+
const [descOpen, setDescOpen] = createSignal(false);
|
|
190
|
+
const [descOverflowing, setDescOverflowing] = createSignal(false);
|
|
191
|
+
let descEl: HTMLSpanElement | undefined;
|
|
188
192
|
|
|
189
193
|
// Signal, not a plain ref: the sentinel mounts only once the first page
|
|
190
194
|
// reveals there are more, which is after the observer effect first runs.
|
|
@@ -370,7 +374,7 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
370
374
|
});
|
|
371
375
|
|
|
372
376
|
const metaLine = (v: VoucherOption): string =>
|
|
373
|
-
[
|
|
377
|
+
[formatExpiry(v.valid_until, today()), formatUsage(v)]
|
|
374
378
|
.filter(Boolean)
|
|
375
379
|
.join(" · ");
|
|
376
380
|
|
|
@@ -381,6 +385,18 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
381
385
|
return n ? n : null;
|
|
382
386
|
});
|
|
383
387
|
|
|
388
|
+
// The footer clamps the description to two lines; offer "More" only when the
|
|
389
|
+
// text actually overflowed. Re-measured whenever the staged description changes.
|
|
390
|
+
createEffect(() => {
|
|
391
|
+
const notes = draftNotes();
|
|
392
|
+
const el = descEl;
|
|
393
|
+
if (!notes || !el) {
|
|
394
|
+
setDescOverflowing(false);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
setDescOverflowing(el.scrollHeight > el.clientHeight);
|
|
398
|
+
});
|
|
399
|
+
|
|
384
400
|
const expiresSoon = (v: VoucherOption): boolean => {
|
|
385
401
|
if (!v.valid_until) return false;
|
|
386
402
|
const days = daysUntil(toDay(v.valid_until), today());
|
|
@@ -514,7 +530,12 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
514
530
|
selected(),
|
|
515
531
|
}}
|
|
516
532
|
>
|
|
517
|
-
<
|
|
533
|
+
<span class="flex flex-col items-center gap-1 shrink-0">
|
|
534
|
+
<Ticket size={18} class="text-[var(--ks-success-fg,#34d399)]" aria-hidden="true" />
|
|
535
|
+
<span class="text-[11px] leading-tight text-center text-[var(--ks-success-fg,#34d399)]">
|
|
536
|
+
{formatVoucherDescription(v)}
|
|
537
|
+
</span>
|
|
538
|
+
</span>
|
|
518
539
|
<span class="flex-1 min-w-0">
|
|
519
540
|
<span class="block text-sm font-medium text-[var(--ks-fg,#ffffff)] truncate">
|
|
520
541
|
{highlightMatch(v.code, debouncedQuery())}
|
|
@@ -562,7 +583,12 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
562
583
|
class="w-full text-left px-3 py-3 mb-1 rounded-lg border border-transparent flex items-center gap-3 opacity-60 cursor-not-allowed"
|
|
563
584
|
aria-disabled="true"
|
|
564
585
|
>
|
|
565
|
-
<
|
|
586
|
+
<span class="flex flex-col items-center gap-1 shrink-0">
|
|
587
|
+
<Ticket size={18} class="text-[var(--ks-fg-subtle,#71717a)]" aria-hidden="true" />
|
|
588
|
+
<span class="text-[11px] leading-tight text-center text-[var(--ks-fg-subtle,#71717a)]">
|
|
589
|
+
{formatVoucherDescription(entry.voucher)}
|
|
590
|
+
</span>
|
|
591
|
+
</span>
|
|
566
592
|
<span class="flex-1 min-w-0">
|
|
567
593
|
<span class="block text-sm text-[var(--ks-fg,#ffffff)] truncate">
|
|
568
594
|
{highlightMatch(entry.voucher.code, debouncedQuery())}
|
|
@@ -605,20 +631,42 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
605
631
|
{/* Bled to the card edges so the rule spans the full width, matching
|
|
606
632
|
the header. */}
|
|
607
633
|
<div class="-mx-6 -mb-6 mt-4 px-5 sm:px-6 py-3 border-t border-[color-mix(in_srgb,var(--ks-border,rgba(39,39,42,0.5))_60%,transparent)] flex items-center justify-between gap-3 shrink-0">
|
|
608
|
-
<
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
634
|
+
<div class="min-w-0 flex flex-col gap-1">
|
|
635
|
+
<Show
|
|
636
|
+
when={draft()}
|
|
637
|
+
fallback={
|
|
638
|
+
<span
|
|
639
|
+
data-testid="voucher-picker-draft-summary"
|
|
640
|
+
class="text-xs text-[var(--ks-fg-subtle,#71717a)]"
|
|
641
|
+
>
|
|
642
|
+
No voucher selected
|
|
643
|
+
</span>
|
|
644
|
+
}
|
|
645
|
+
>
|
|
614
646
|
<Show when={draftNotes()}>
|
|
615
|
-
|
|
616
|
-
|
|
647
|
+
<span class="flex flex-col items-start gap-0.5 min-w-0">
|
|
648
|
+
<span
|
|
649
|
+
ref={descEl}
|
|
650
|
+
data-testid="voucher-picker-draft-description"
|
|
651
|
+
class="min-w-0 text-xs text-[var(--ks-fg-muted,#a1a1aa)] whitespace-pre-line line-clamp-2"
|
|
652
|
+
>
|
|
653
|
+
{draftNotes()}
|
|
654
|
+
</span>
|
|
655
|
+
<Show when={descOverflowing()}>
|
|
656
|
+
<button
|
|
657
|
+
type="button"
|
|
658
|
+
data-testid="voucher-picker-expand-description"
|
|
659
|
+
onClick={() => setDescOpen(true)}
|
|
660
|
+
class="text-xs leading-tight text-[var(--ks-primary,#c9a961)] hover:underline transition-colors cursor-pointer"
|
|
661
|
+
aria-label="Show full description"
|
|
662
|
+
>
|
|
663
|
+
See more
|
|
664
|
+
</button>
|
|
665
|
+
</Show>
|
|
666
|
+
</span>
|
|
617
667
|
</Show>
|
|
618
|
-
{" · "}
|
|
619
|
-
{discountLabel(draft()!).replace("−", "")} off
|
|
620
668
|
</Show>
|
|
621
|
-
</
|
|
669
|
+
</div>
|
|
622
670
|
<div class="flex items-center gap-2 shrink-0">
|
|
623
671
|
<Show when={props.selected && draft()}>
|
|
624
672
|
<button
|
|
@@ -656,6 +704,43 @@ export default function VoucherPicker(props: VoucherPickerProps): JSX.Element {
|
|
|
656
704
|
</div>
|
|
657
705
|
</Modal>
|
|
658
706
|
</Show>
|
|
707
|
+
|
|
708
|
+
{/* Full-description popup: "More" in the footer only appears when the
|
|
709
|
+
clamped text overflowed, but the popup itself is reachable whenever a
|
|
710
|
+
staged description exists. */}
|
|
711
|
+
<Show when={descOpen() && draftNotes()}>
|
|
712
|
+
<Modal onClose={() => setDescOpen(false)} size="md" ariaLabel="Voucher description">
|
|
713
|
+
<div data-testid="voucher-picker-description-popup" class="flex flex-col gap-4">
|
|
714
|
+
<div class="flex items-center justify-between gap-3">
|
|
715
|
+
<h2 class="m-0 text-base font-semibold text-[var(--ks-fg,#ffffff)]">
|
|
716
|
+
Voucher description
|
|
717
|
+
</h2>
|
|
718
|
+
<button
|
|
719
|
+
type="button"
|
|
720
|
+
data-testid="voucher-picker-close-description"
|
|
721
|
+
onClick={() => setDescOpen(false)}
|
|
722
|
+
class="w-8 h-8 flex items-center justify-center rounded text-[var(--ks-fg-muted,#a1a1aa)] hover:text-[var(--ks-fg,#ffffff)] hover:bg-[color-mix(in_srgb,var(--ks-surface-raised,#1a1a1a)_50%,transparent)] transition-colors cursor-pointer shrink-0"
|
|
723
|
+
aria-label="Close"
|
|
724
|
+
>
|
|
725
|
+
<X size={16} />
|
|
726
|
+
</button>
|
|
727
|
+
</div>
|
|
728
|
+
<p class="m-0 text-sm text-[var(--ks-fg,#ffffff)] whitespace-pre-line">
|
|
729
|
+
{draftNotes()}
|
|
730
|
+
</p>
|
|
731
|
+
<div class="flex justify-end">
|
|
732
|
+
<button
|
|
733
|
+
type="button"
|
|
734
|
+
data-testid="voucher-picker-description-close"
|
|
735
|
+
onClick={() => setDescOpen(false)}
|
|
736
|
+
class="px-4 py-2 rounded-lg text-sm font-medium bg-[var(--ks-primary,#c9a961)] text-[var(--ks-fg-on-accent,#0a0a0a)] hover:opacity-90 transition-opacity cursor-pointer"
|
|
737
|
+
>
|
|
738
|
+
Close
|
|
739
|
+
</button>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
</Modal>
|
|
743
|
+
</Show>
|
|
659
744
|
</>
|
|
660
745
|
);
|
|
661
746
|
}
|