@warkypublic/svelix 0.1.47 → 0.2.1
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/dist/components/Boxer/Boxer.svelte +5 -1
- package/dist/components/CardGrid/CardGrid.svelte +50 -4
- package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
- package/dist/components/CardGrid/DefaultCard.svelte +7 -2
- package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
- package/dist/components/CardGrid/cardGrid.d.ts +24 -0
- package/dist/components/CardGrid/cardGrid.js +20 -0
- package/dist/components/Former/FormerDrawer.svelte +10 -22
- package/dist/components/Former/FormerModal.svelte +10 -22
- package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
- package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
- package/dist/components/Former/index.d.ts +1 -0
- package/dist/components/Former/index.js +1 -0
- package/dist/components/FormerControllers/ButtonCtrl.svelte +11 -16
- package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
- package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +6 -13
- package/dist/components/FormerControllers/IconButtonCtrl.svelte +11 -16
- package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -13
- package/dist/components/FormerControllers/NumberInputCtrl.svelte +18 -14
- package/dist/components/FormerControllers/PasswordInputCtrl.svelte +7 -13
- package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -3
- package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -13
- package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -13
- package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
- package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
- package/dist/components/FormerControllers/index.d.ts +2 -0
- package/dist/components/FormerControllers/index.js +2 -0
- package/dist/components/FormerControllers/utils.d.ts +1 -0
- package/dist/components/FormerControllers/utils.js +3 -0
- package/dist/components/Gridler/components/Gridler.svelte +10 -4
- package/dist/components/Gridler/components/GridlerFull.svelte +2 -1
- package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
- package/dist/components/Gridler/utils/serverPaging.js +7 -0
- package/dist/components/VTree/VTree.svelte +107 -2
- package/dist/components/VTree/VTree.utils.d.ts +23 -0
- package/dist/components/VTree/VTree.utils.js +73 -0
- package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
- package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
- package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
- package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
- package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeRow.svelte +71 -57
- package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
- package/dist/components/VTree/VTreeVirtualViewport.svelte +20 -40
- package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
- package/dist/components/VTree/types.d.ts +39 -1
- package/llm/COMPONENT_GUIDE.md +25 -0
- package/package.json +45 -45
|
@@ -218,9 +218,13 @@ $effect(() => {
|
|
|
218
218
|
});
|
|
219
219
|
});
|
|
220
220
|
// Select first
|
|
221
|
+
// `value` is read via untrack() — this effect should react to boxerData becoming
|
|
222
|
+
// ready, not to its own write (onOptionSubmit assigns `value`), which would
|
|
223
|
+
// otherwise cause a spurious second fire whenever the first option's value is
|
|
224
|
+
// falsy (0/"").
|
|
221
225
|
$effect(() => {
|
|
222
226
|
const boxerData = storeBoxerData;
|
|
223
|
-
if (selectFirst && boxerData.length > 0 && !multiSelect && !value) {
|
|
227
|
+
if (selectFirst && boxerData.length > 0 && !multiSelect && !untrack(() => value)) {
|
|
224
228
|
onOptionSubmit(0);
|
|
225
229
|
}
|
|
226
230
|
});
|
|
@@ -9,7 +9,12 @@ import { applyLocalSort, sortOrderToSortOptions } from "../Gridler/utils/sort.js
|
|
|
9
9
|
import CardGridFilterPanel from "./CardGridFilterPanel.svelte";
|
|
10
10
|
import DefaultCard from "./DefaultCard.svelte";
|
|
11
11
|
import { toggleItemInSelection, rangeSelect, isItemSelected, computeColumnsInRow } from "./cardGrid.js";
|
|
12
|
-
let { columns, displayColumns, cardMinWidth = 280, cardGap = 16, cardMaxFrontColumns, card, cardBack, empty, skeleton, dataSource = "local", dataSourceOptions, data, pageSize = 20, uniqueID: uniqueIDProp, searchValue, onSearchValueChange, serverSideSearch = true, searchColumns, searchPlaceholder = "Search…", sortOrder, onSortOrderChange, sortOptions, filters, onFilterChange, selectedItems: selectedItemsProp, onSelectedItemsChange, multiSelect = true, onCardClick, onCardDblClick, onCardContextMenu, menuItems, total = $bindable(0), loading = $bindable(false), onLoadError, class: cls, toolbarEnd } = $props();
|
|
12
|
+
let { columns, displayColumns, cardMinWidth = 280, cardGap = 16, cardMaxFrontColumns, card, cardBack, empty, skeleton, dataSource = "local", dataSourceOptions, data, pageSize = 20, uniqueID: uniqueIDProp, searchValue, onSearchValueChange, serverSideSearch = true, searchColumns, searchPlaceholder = "Search…", sortOrder, onSortOrderChange, sortOptions, filters, onFilterChange, selectedItems: selectedItemsProp, onSelectedItemsChange, multiSelect = true, onCardClick, onCardDblClick, onCardContextMenu, menuItems, total = $bindable(0), loading = $bindable(false), onLoadError, class: cls, toolbarEnd, animate: animateProp, density: densityProp, expand: expandProp } = $props();
|
|
13
|
+
// ── Animation defaults ──────────────────────────────────────────────
|
|
14
|
+
const animateEnabled = $derived(animateProp ?? true);
|
|
15
|
+
// ── Density/expand defaults ─────────────────────────────────────────────
|
|
16
|
+
const densityMode = $derived(densityProp ?? "default");
|
|
17
|
+
const expandMode = $derived(expandProp ?? "none");
|
|
13
18
|
// ── Derived config ─────────────────────────────────────────────────────────
|
|
14
19
|
const resolvedUniqueID = $derived(uniqueIDProp ?? dataSourceOptions?.uniqueID ?? "id");
|
|
15
20
|
const resolvedColumns = $derived(displayColumns?.length ? columns.filter((c) => displayColumns?.includes(c.id)) : columns);
|
|
@@ -41,6 +46,11 @@ function handleSortOrderChange(order) {
|
|
|
41
46
|
onSortOrderChange?.(order);
|
|
42
47
|
}
|
|
43
48
|
const resolvedSort = $derived(Object.values(resolvedSortOrder).some((d) => d !== "none") ? sortOrderToSortOptions(resolvedSortOrder) : []);
|
|
49
|
+
// ── Density/expand resolution ───────────────────────────────────────────
|
|
50
|
+
const densityMinWidth = $derived(densityMode === "compact" ? 200 : densityMode === "spacious" ? 400 : densityMode === "comfortable" ? 340 : cardMinWidth);
|
|
51
|
+
const densityGap = $derived(densityMode === "compact" ? 8 : densityMode === "spacious" ? 32 : densityMode === "comfortable" ? 24 : cardGap);
|
|
52
|
+
// Expand multiplies the effective min-width/gap per-card
|
|
53
|
+
const expandFactor = $derived(expandMode === "none" ? 1 : expandMode === "medium" ? 1.5 : 2);
|
|
44
54
|
// ── Controlled/uncontrolled selection ─────────────────────────────────────
|
|
45
55
|
let internalSelectedItems = $state([]);
|
|
46
56
|
const resolvedSelectedItems = $derived(selectedItemsProp ?? internalSelectedItems);
|
|
@@ -208,7 +218,7 @@ function toggleFlip(e, item) {
|
|
|
208
218
|
$effect(() => {
|
|
209
219
|
if (!containerEl) return;
|
|
210
220
|
const obs = new ResizeObserver((entries) => {
|
|
211
|
-
columnsInRow = computeColumnsInRow(entries[0]?.contentRect.width ?? 0,
|
|
221
|
+
columnsInRow = computeColumnsInRow(entries[0]?.contentRect.width ?? 0, densityMinWidth * expandFactor, densityGap * expandFactor);
|
|
212
222
|
});
|
|
213
223
|
obs.observe(containerEl);
|
|
214
224
|
return () => obs.disconnect();
|
|
@@ -392,7 +402,13 @@ function scrollToCard(index) {
|
|
|
392
402
|
});
|
|
393
403
|
}
|
|
394
404
|
}
|
|
395
|
-
|
|
405
|
+
export function selectItem(uniqueIDValue) {
|
|
406
|
+
const idx = displayData.findIndex((item) => item[resolvedUniqueID] === uniqueIDValue);
|
|
407
|
+
if (idx < 0) return false;
|
|
408
|
+
focusedIndex = idx;
|
|
409
|
+
scrollToCard(idx);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
396
412
|
export function refresh() {
|
|
397
413
|
if (isServerMode) {
|
|
398
414
|
refreshTrigger++;
|
|
@@ -407,6 +423,19 @@ export function refresh() {
|
|
|
407
423
|
});
|
|
408
424
|
}
|
|
409
425
|
}
|
|
426
|
+
// Reconcile selection across refresh() — drop items no longer in displayData.
|
|
427
|
+
// `internalSelectedItems` is read via untrack() so this effect doesn't depend on
|
|
428
|
+
// its own write target — .filter() always returns a new array reference, which
|
|
429
|
+
// would otherwise re-trigger this same effect forever (effect_update_depth_exceeded).
|
|
430
|
+
$effect(() => {
|
|
431
|
+
if (displayData.length === 0) return;
|
|
432
|
+
const currentIds = new Set(displayData.map((item) => item[resolvedUniqueID]));
|
|
433
|
+
const current = untrack(() => internalSelectedItems);
|
|
434
|
+
const next = current.filter((item) => currentIds.has(item[resolvedUniqueID]));
|
|
435
|
+
if (next.length !== current.length) {
|
|
436
|
+
internalSelectedItems = next;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
410
439
|
// ── Loading states ─────────────────────────────────────────────────────────
|
|
411
440
|
const isFirstLoad = $derived(isServerMode && serverLoading && serverData.length === 0 || !isServerMode && localLoading && localData.length === 0);
|
|
412
441
|
const isLoadingMore = $derived(isServerMode && serverLoading && serverData.length > 0);
|
|
@@ -567,12 +596,13 @@ $effect(() => {
|
|
|
567
596
|
<div
|
|
568
597
|
bind:this={containerEl}
|
|
569
598
|
class="cg-grid"
|
|
599
|
+
class:cg-animate={animateEnabled}
|
|
570
600
|
role="listbox"
|
|
571
601
|
aria-multiselectable={multiSelect}
|
|
572
602
|
tabindex="0"
|
|
573
603
|
onkeydown={handleKeyDown}
|
|
574
604
|
onclick={handleContainerClick}
|
|
575
|
-
style="--cg-min-width:{
|
|
605
|
+
style="--cg-min-width:{densityMinWidth * expandFactor}px;--cg-gap:{densityGap * expandFactor}px;--cg-cols:{columnsInRow};"
|
|
576
606
|
>
|
|
577
607
|
{#if isFirstLoad}
|
|
578
608
|
<!-- ── Skeleton cards ─────────────────────────────────── -->
|
|
@@ -947,6 +977,22 @@ $effect(() => {
|
|
|
947
977
|
outline-offset: 2px;
|
|
948
978
|
}
|
|
949
979
|
|
|
980
|
+
/* ── Card entry animation (enabled via .cg-animate on the container) ── */
|
|
981
|
+
|
|
982
|
+
@keyframes cg-card-in {
|
|
983
|
+
from { opacity: 0; transform: translateY(6px) scale(0.97); }
|
|
984
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.cg-animate .cg-card {
|
|
988
|
+
animation: cg-card-in 180ms ease both;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/* Selected cards override the entry animation's transform */
|
|
992
|
+
.cg-animate .cg-card-selected {
|
|
993
|
+
animation: none;
|
|
994
|
+
}
|
|
995
|
+
|
|
950
996
|
/* ── Skeleton cards ───────────────────────────────────────────────────── */
|
|
951
997
|
|
|
952
998
|
@keyframes cg-shimmer {
|
|
@@ -53,8 +53,12 @@ interface Props {
|
|
|
53
53
|
onLoadError?: (error: string) => void;
|
|
54
54
|
class?: string;
|
|
55
55
|
toolbarEnd?: Snippet;
|
|
56
|
+
animate?: boolean;
|
|
57
|
+
density?: 'compact' | 'default' | 'comfortable' | 'spacious';
|
|
58
|
+
expand?: 'none' | 'medium' | 'large';
|
|
56
59
|
}
|
|
57
60
|
declare const CardGrid: import("svelte").Component<Props, {
|
|
61
|
+
selectItem: (uniqueIDValue: string) => boolean;
|
|
58
62
|
refresh: () => void;
|
|
59
63
|
}, "loading" | "total">;
|
|
60
64
|
type CardGrid = ReturnType<typeof CardGrid>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">const { record, columns, selected, focused } = $props();
|
|
1
|
+
<script lang="ts">const { record, columns, selected, focused, expanded } = $props();
|
|
2
2
|
// First column → title, second → subtitle, rest → detail rows
|
|
3
3
|
const titleCol = $derived(columns[0]);
|
|
4
4
|
const subtitleCol = $derived(columns.length > 1 ? columns[1] : undefined);
|
|
@@ -84,7 +84,7 @@ function statusVariant(val) {
|
|
|
84
84
|
export {};
|
|
85
85
|
</script>
|
|
86
86
|
|
|
87
|
-
<article class="dc-card" class:dc-selected={selected} class:dc-focused={focused}>
|
|
87
|
+
<article class="dc-card" class:dc-selected={selected} class:dc-focused={focused} class:dc-expanded={expanded}>
|
|
88
88
|
<!-- Header -->
|
|
89
89
|
{#if titleCol}
|
|
90
90
|
{@const titleKey = titleCol.dataKey ?? titleCol.id}
|
|
@@ -132,6 +132,11 @@ export {};
|
|
|
132
132
|
flex-direction: column;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/* Expanded mode — merges front + back columns */
|
|
136
|
+
.dc-expanded {
|
|
137
|
+
--cg-colspan: span 2;
|
|
138
|
+
}
|
|
139
|
+
|
|
135
140
|
/* ── Header ───────────────────────────────────────────────────────── */
|
|
136
141
|
|
|
137
142
|
.dc-header {
|
|
@@ -31,3 +31,27 @@ export declare function rangeSelect(allItems: Record<string, unknown>[], anchorI
|
|
|
31
31
|
export declare function isItemSelected(selectedItems: Record<string, unknown>[], item: Record<string, unknown>, uniqueID: string): boolean;
|
|
32
32
|
/** Compute how many cards fit in a row given container width, min card width, and gap. */
|
|
33
33
|
export declare function computeColumnsInRow(containerWidth: number, cardMinWidth: number, gap: number): number;
|
|
34
|
+
/** Density mode for the card grid. */
|
|
35
|
+
export type CardGridDensity = 'compact' | 'default' | 'comfortable';
|
|
36
|
+
/** Expand mode for the card grid. */
|
|
37
|
+
export type CardGridExpand = 'none' | 'medium' | 'large';
|
|
38
|
+
/** Extended props interface including animation, density and expand controls. */
|
|
39
|
+
export interface CardGridAnimationProps {
|
|
40
|
+
/** Enable staggered entry/exit animations on cards (default true). */
|
|
41
|
+
animate?: boolean;
|
|
42
|
+
/** Duration of card entrance transitions in ms (default 200). */
|
|
43
|
+
animateDuration?: number;
|
|
44
|
+
}
|
|
45
|
+
/** Extended props interface including animation, density and expand controls. */
|
|
46
|
+
export interface CardGridDensityProps {
|
|
47
|
+
/** Density mode: compact(default), comfortable. Overrides cardMinWidth/gap when set. */
|
|
48
|
+
density?: CardGridDensity;
|
|
49
|
+
/** Expand mode: none, medium, large. Controls how much space each card occupies. */
|
|
50
|
+
expand?: CardGridExpand;
|
|
51
|
+
}
|
|
52
|
+
/** Build virtual rows from flat displayData, packing columnsInRow items per row. */
|
|
53
|
+
export declare function buildVirtualRows(displayData: Record<string, unknown>[], columnsInRow: number): Record<string, unknown>[][];
|
|
54
|
+
/** Match a search string against any hotfield column value. */
|
|
55
|
+
export declare function typeaheadMatch(searchValue: string, item: Record<string, unknown>, hotfields?: string[]): boolean;
|
|
56
|
+
export interface ExtendedCardGridProps extends CardGridAnimationProps, CardGridDensityProps {
|
|
57
|
+
}
|
|
@@ -19,3 +19,23 @@ export function isItemSelected(selectedItems, item, uniqueID) {
|
|
|
19
19
|
export function computeColumnsInRow(containerWidth, cardMinWidth, gap) {
|
|
20
20
|
return Math.max(1, Math.floor((containerWidth + gap) / (cardMinWidth + gap)));
|
|
21
21
|
}
|
|
22
|
+
/** Build virtual rows from flat displayData, packing columnsInRow items per row. */
|
|
23
|
+
export function buildVirtualRows(displayData, columnsInRow) {
|
|
24
|
+
const cols = Math.max(1, columnsInRow);
|
|
25
|
+
const rows = [];
|
|
26
|
+
for (let i = 0; i < displayData.length; i += cols) {
|
|
27
|
+
rows.push(displayData.slice(i, i + cols));
|
|
28
|
+
}
|
|
29
|
+
return rows;
|
|
30
|
+
}
|
|
31
|
+
/** Match a search string against any hotfield column value. */
|
|
32
|
+
export function typeaheadMatch(searchValue, item, hotfields) {
|
|
33
|
+
if (!searchValue)
|
|
34
|
+
return true;
|
|
35
|
+
const lower = searchValue.toLowerCase();
|
|
36
|
+
if (hotfields && hotfields.length > 0) {
|
|
37
|
+
return Object.entries(item).some(([k, v]) => hotfields.some((f) => k === f || k.endsWith(`.${f}`)) &&
|
|
38
|
+
String(v ?? '').toLowerCase().includes(lower));
|
|
39
|
+
}
|
|
40
|
+
return Object.values(item).some((v) => String(v ?? '').toLowerCase().includes(lower));
|
|
41
|
+
}
|
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
<script lang="ts">/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { Dialog, Portal } from "@skeletonlabs/skeleton-svelte";
|
|
3
|
-
import { registerOverlay } from "../OverlayStack";
|
|
4
3
|
import Former from "./Former.svelte";
|
|
4
|
+
import { createOverlayZIndex, deriveAutoTitle } from "./formerOverlayHelpers.svelte";
|
|
5
5
|
let { afterGet, afterSave, beforeSave, children, id, keepOpen = $bindable(false), layout, onAPICall, onChange, onClose, onConfirmDelete, onError, onOpen, opened = $bindable(false), primeData, request = $bindable("insert"), title, uniqueKeyField = "id", values = $bindable(undefined), width = "32rem" } = $props();
|
|
6
|
-
const autoTitle = $derived(
|
|
6
|
+
const autoTitle = $derived(deriveAutoTitle({
|
|
7
|
+
request,
|
|
8
|
+
title,
|
|
9
|
+
uniqueKeyField,
|
|
10
|
+
values
|
|
11
|
+
}));
|
|
7
12
|
const animBackdrop = "transition transition-discrete opacity-0 starting:data-[state=open]:opacity-0 data-[state=open]:opacity-100";
|
|
8
13
|
const animContent = "transition transition-discrete opacity-0 translate-x-full starting:data-[state=open]:opacity-0 starting:data-[state=open]:translate-x-full data-[state=open]:opacity-100 data-[state=open]:translate-x-0";
|
|
9
|
-
|
|
10
|
-
let contentZ = $state(1010);
|
|
11
|
-
$effect(() => {
|
|
12
|
-
if (!opened) {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
const overlay = registerOverlay({
|
|
16
|
-
kind: "drawer",
|
|
17
|
-
mount: "portal"
|
|
18
|
-
});
|
|
19
|
-
backdropZ = overlay.layer.backdrop;
|
|
20
|
-
contentZ = overlay.layer.content;
|
|
21
|
-
return () => {
|
|
22
|
-
overlay.unregister();
|
|
23
|
-
backdropZ = 1e3;
|
|
24
|
-
contentZ = 1010;
|
|
25
|
-
};
|
|
26
|
-
});
|
|
14
|
+
const overlayZ = createOverlayZIndex("drawer", () => opened);
|
|
27
15
|
</script>
|
|
28
16
|
|
|
29
17
|
<Dialog
|
|
@@ -39,11 +27,11 @@ $effect(() => {
|
|
|
39
27
|
<Portal>
|
|
40
28
|
<Dialog.Backdrop
|
|
41
29
|
class="fixed inset-0 bg-black/50 backdrop-blur-sm {animBackdrop}"
|
|
42
|
-
style={`z-index:${backdropZ};`}
|
|
30
|
+
style={`z-index:${overlayZ.backdropZ};`}
|
|
43
31
|
/>
|
|
44
32
|
<Dialog.Positioner
|
|
45
33
|
class="fixed inset-0 flex justify-end"
|
|
46
|
-
style={`z-index:${contentZ};`}
|
|
34
|
+
style={`z-index:${overlayZ.contentZ};`}
|
|
47
35
|
>
|
|
48
36
|
<Dialog.Content
|
|
49
37
|
class="card bg-surface-100-900 shadow-2xl h-full flex flex-col overflow-hidden rounded-none rounded-l-container-token {animContent}"
|
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
<script lang="ts">/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { Dialog, Portal } from "@skeletonlabs/skeleton-svelte";
|
|
3
|
-
import { registerOverlay } from "../OverlayStack";
|
|
4
3
|
import Former from "./Former.svelte";
|
|
4
|
+
import { createOverlayZIndex, deriveAutoTitle } from "./formerOverlayHelpers.svelte";
|
|
5
5
|
let { afterGet, afterSave, beforeSave, children, id, keepOpen = $bindable(false), layout, onAPICall, onChange, onClose, onConfirmDelete, onError, onOpen, opened = $bindable(false), primeData, request = $bindable("insert"), title, uniqueKeyField = "id", values = $bindable(undefined) } = $props();
|
|
6
|
-
const autoTitle = $derived(
|
|
6
|
+
const autoTitle = $derived(deriveAutoTitle({
|
|
7
|
+
request,
|
|
8
|
+
title,
|
|
9
|
+
uniqueKeyField,
|
|
10
|
+
values
|
|
11
|
+
}));
|
|
7
12
|
const animBackdrop = "transition transition-discrete opacity-0 starting:data-[state=open]:opacity-0 data-[state=open]:opacity-100";
|
|
8
13
|
const animContent = "transition transition-discrete opacity-0 scale-95 starting:data-[state=open]:opacity-0 starting:data-[state=open]:scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100";
|
|
9
|
-
|
|
10
|
-
let contentZ = $state(1010);
|
|
11
|
-
$effect(() => {
|
|
12
|
-
if (!opened) {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
const overlay = registerOverlay({
|
|
16
|
-
kind: "dialog",
|
|
17
|
-
mount: "portal"
|
|
18
|
-
});
|
|
19
|
-
backdropZ = overlay.layer.backdrop;
|
|
20
|
-
contentZ = overlay.layer.content;
|
|
21
|
-
return () => {
|
|
22
|
-
overlay.unregister();
|
|
23
|
-
backdropZ = 1e3;
|
|
24
|
-
contentZ = 1010;
|
|
25
|
-
};
|
|
26
|
-
});
|
|
14
|
+
const overlayZ = createOverlayZIndex("dialog", () => opened);
|
|
27
15
|
</script>
|
|
28
16
|
|
|
29
17
|
<Dialog
|
|
@@ -39,9 +27,9 @@ $effect(() => {
|
|
|
39
27
|
<Portal>
|
|
40
28
|
<Dialog.Backdrop
|
|
41
29
|
class="fixed inset-0 bg-black/50 backdrop-blur-sm {animBackdrop}"
|
|
42
|
-
style={`z-index:${backdropZ};`}
|
|
30
|
+
style={`z-index:${overlayZ.backdropZ};`}
|
|
43
31
|
/>
|
|
44
|
-
<Dialog.Positioner class="fixed inset-0 flex items-center justify-center p-4" style={`z-index:${contentZ};`}>
|
|
32
|
+
<Dialog.Positioner class="fixed inset-0 flex items-center justify-center p-4" style={`z-index:${overlayZ.contentZ};`}>
|
|
45
33
|
<Dialog.Content
|
|
46
34
|
class="card bg-surface-100-900 shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col overflow-hidden {animContent}"
|
|
47
35
|
>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FormRequestType } from './types';
|
|
2
|
+
export declare function deriveAutoTitle(params: {
|
|
3
|
+
request?: FormRequestType;
|
|
4
|
+
title?: string;
|
|
5
|
+
uniqueKeyField: string;
|
|
6
|
+
values?: unknown;
|
|
7
|
+
}): string;
|
|
8
|
+
export declare function createOverlayZIndex(kind: 'dialog' | 'drawer', getOpened: () => boolean): {
|
|
9
|
+
readonly backdropZ: number;
|
|
10
|
+
readonly contentZ: number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { registerOverlay } from '../OverlayStack';
|
|
3
|
+
export function deriveAutoTitle(params) {
|
|
4
|
+
const { request, title, uniqueKeyField, values } = params;
|
|
5
|
+
if (title)
|
|
6
|
+
return title;
|
|
7
|
+
const keyValue = values?.[uniqueKeyField];
|
|
8
|
+
if (request === 'delete')
|
|
9
|
+
return `Delete Record${keyValue ? ` — ${keyValue}` : ''}`;
|
|
10
|
+
if (request === 'insert')
|
|
11
|
+
return 'New Record';
|
|
12
|
+
return `Edit Record${keyValue ? ` — ${keyValue}` : ''}`;
|
|
13
|
+
}
|
|
14
|
+
export function createOverlayZIndex(kind, getOpened) {
|
|
15
|
+
let backdropZ = $state(1000);
|
|
16
|
+
let contentZ = $state(1010);
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (!getOpened()) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const overlay = registerOverlay({ kind, mount: 'portal' });
|
|
22
|
+
backdropZ = overlay.layer.backdrop;
|
|
23
|
+
contentZ = overlay.layer.content;
|
|
24
|
+
return () => {
|
|
25
|
+
overlay.unregister();
|
|
26
|
+
backdropZ = 1000;
|
|
27
|
+
contentZ = 1010;
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
get backdropZ() {
|
|
32
|
+
return backdropZ;
|
|
33
|
+
},
|
|
34
|
+
get contentZ() {
|
|
35
|
+
return contentZ;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -3,6 +3,7 @@ export { default as FormerButtonArea } from './FormerButtonArea.svelte';
|
|
|
3
3
|
export { default as FormerDrawer } from './FormerDrawer.svelte';
|
|
4
4
|
export { default as FormerModal } from './FormerModal.svelte';
|
|
5
5
|
export { createFormerState } from './formerState.svelte';
|
|
6
|
+
export { createOverlayZIndex, deriveAutoTitle } from './formerOverlayHelpers.svelte';
|
|
6
7
|
export { FormerResolveSpecAPI } from './FormerResolveSpecAPI';
|
|
7
8
|
export { FormerRestHeadSpecAPI } from './FormerRestHeadSpecAPI';
|
|
8
9
|
export * from './types';
|
|
@@ -3,6 +3,7 @@ export { default as FormerButtonArea } from './FormerButtonArea.svelte';
|
|
|
3
3
|
export { default as FormerDrawer } from './FormerDrawer.svelte';
|
|
4
4
|
export { default as FormerModal } from './FormerModal.svelte';
|
|
5
5
|
export { createFormerState } from './formerState.svelte';
|
|
6
|
+
export { createOverlayZIndex, deriveAutoTitle } from './formerOverlayHelpers.svelte';
|
|
6
7
|
export { FormerResolveSpecAPI } from './FormerResolveSpecAPI';
|
|
7
8
|
export { FormerRestHeadSpecAPI } from './FormerRestHeadSpecAPI';
|
|
8
9
|
export * from './types';
|
|
@@ -1,29 +1,24 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
const
|
|
1
|
+
<script lang="ts">import { createClickLoading, createClickPulse } from "./ctrlLoading.svelte";
|
|
2
|
+
const { children, disabled, loading: externalLoading = false, onclick, sid, title, tooltip, variant = "filled" } = $props();
|
|
3
|
+
const clickLoading = createClickLoading(() => externalLoading);
|
|
4
4
|
const variantClass = $derived(variant === "outlined" ? "preset-outlined-primary-500" : variant === "ghost" ? "preset-tonal-primary" : "preset-filled-primary-500");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
await onclick();
|
|
10
|
-
} finally {
|
|
11
|
-
internalLoading = false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
5
|
+
const clickPulse = createClickPulse();
|
|
6
|
+
function handleClick() {
|
|
7
|
+
clickPulse.trigger();
|
|
8
|
+
clickLoading.run(onclick);
|
|
14
9
|
}
|
|
15
|
-
export {};
|
|
16
10
|
</script>
|
|
17
11
|
|
|
18
12
|
<button
|
|
19
|
-
class="btn {variantClass}"
|
|
13
|
+
class="btn {variantClass} transition-transform duration-150"
|
|
14
|
+
class:scale-90={clickPulse.pressed}
|
|
20
15
|
data-sid={sid}
|
|
21
|
-
disabled={disabled || isLoading}
|
|
16
|
+
disabled={disabled || clickLoading.isLoading}
|
|
22
17
|
title={tooltip}
|
|
23
18
|
type="button"
|
|
24
19
|
onclick={handleClick}
|
|
25
20
|
>
|
|
26
|
-
{#if isLoading}
|
|
21
|
+
{#if clickLoading.isLoading}
|
|
27
22
|
<span class="animate-spin mr-1">⟳</span>
|
|
28
23
|
{/if}
|
|
29
24
|
{#if children}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">const { children, error, id, label, required, tooltip } = $props();
|
|
2
|
+
export {};
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<div class="flex flex-col gap-1" title={tooltip}>
|
|
6
|
+
{#if label}
|
|
7
|
+
<label class="label text-sm font-medium" for={id}>
|
|
8
|
+
{label}{#if required}<span class="text-error-500">*</span>{/if}
|
|
9
|
+
</label>
|
|
10
|
+
{/if}
|
|
11
|
+
{@render children()}
|
|
12
|
+
{#if error}
|
|
13
|
+
<p class="text-error-500 text-xs">{error}</p>
|
|
14
|
+
{/if}
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export interface Props {
|
|
3
|
+
children: Snippet;
|
|
4
|
+
error?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
tooltip?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const CtrlFieldShell: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type CtrlFieldShell = ReturnType<typeof CtrlFieldShell>;
|
|
12
|
+
export default CtrlFieldShell;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script lang="ts">import { tick } from "svelte";
|
|
2
2
|
import Portal from "../../Portal/Portal.svelte";
|
|
3
3
|
import { registerOverlay } from "../../OverlayStack";
|
|
4
|
+
import CtrlFieldShell from "../CtrlFieldShell.svelte";
|
|
5
|
+
import { makeFieldId } from "../utils";
|
|
4
6
|
import DateTimeCtrlPickerPanel from "./DateTimeCtrlPickerPanel.svelte";
|
|
5
7
|
import { formatDateTimeByFormat, getCalendarCells, getNowDateTimeParts, getYearMonthLabel, isDateSelectable, normalizePartsForMode, parseDateTimeByFormat, parsePartialDateTimeByFormat, parseNativeDate, resolveDateTimeFormat, shiftYearMonth, toNativeDate, validateDateTimeParts } from "./DateTimeCtrl.utils";
|
|
6
8
|
let { amLabel, closeOnPick = true, clearable = true, disableAutocomplete = false, disablePortal = false, disabled, error, hoursStep, inputFormat = "ISO", label, maxDate, maxTime, minDate, minTime, minutesStep, mode = "datetime", name, onblur, onchange, outputFormat = "ISO", pmLabel, popoverProps, placeholder, required, sid, tooltip, secondsStep, timePickerProps, timePresets, use12Hour, value = $bindable(null), withPicker = true, withTimeDropdown } = $props();
|
|
7
|
-
const id = $derived(
|
|
9
|
+
const id = $derived(makeFieldId(name, sid));
|
|
8
10
|
const pickerDialogId = $derived(`${id}_picker_dialog`);
|
|
9
11
|
const resolvedInputFormat = $derived(resolveDateTimeFormat(inputFormat, mode));
|
|
10
12
|
const resolvedOutputFormat = $derived(resolveDateTimeFormat(outputFormat, mode));
|
|
@@ -280,18 +282,13 @@ $effect(() => {
|
|
|
280
282
|
});
|
|
281
283
|
</script>
|
|
282
284
|
|
|
283
|
-
<
|
|
284
|
-
{#if label}
|
|
285
|
-
<label class="label text-sm font-medium" for={id}>
|
|
286
|
-
{label}{#if required}<span class="text-error-500">*</span>{/if}
|
|
287
|
-
</label>
|
|
288
|
-
{/if}
|
|
289
|
-
|
|
285
|
+
<CtrlFieldShell error={effectiveError} {id} {label} {required} {tooltip}>
|
|
290
286
|
<div class="relative" bind:this={anchorRef}>
|
|
291
287
|
<div class="relative">
|
|
292
288
|
<input
|
|
293
289
|
{id}
|
|
294
290
|
{disabled}
|
|
291
|
+
{name}
|
|
295
292
|
{required}
|
|
296
293
|
class={`input ${inputRightPaddingClass}`}
|
|
297
294
|
class:input-error={!!effectiveError}
|
|
@@ -479,8 +476,4 @@ $effect(() => {
|
|
|
479
476
|
{/if}
|
|
480
477
|
</Portal>
|
|
481
478
|
</div>
|
|
482
|
-
|
|
483
|
-
{#if effectiveError}
|
|
484
|
-
<p class="text-xs text-error-500">{effectiveError}</p>
|
|
485
|
-
{/if}
|
|
486
|
-
</div>
|
|
479
|
+
</CtrlFieldShell>
|
|
@@ -1,30 +1,25 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
const
|
|
1
|
+
<script lang="ts">import { createClickLoading, createClickPulse } from "./ctrlLoading.svelte";
|
|
2
|
+
const { children, disabled, loading: externalLoading = false, onclick, sid, tooltip, variant = "ghost" } = $props();
|
|
3
|
+
const clickLoading = createClickLoading(() => externalLoading);
|
|
4
4
|
const variantClass = $derived(variant === "outlined" ? "preset-outlined-primary-500" : variant === "filled" ? "preset-filled-primary-500" : "preset-tonal-primary");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
await onclick();
|
|
10
|
-
} finally {
|
|
11
|
-
internalLoading = false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
5
|
+
const clickPulse = createClickPulse();
|
|
6
|
+
function handleClick() {
|
|
7
|
+
clickPulse.trigger();
|
|
8
|
+
clickLoading.run(onclick);
|
|
14
9
|
}
|
|
15
|
-
export {};
|
|
16
10
|
</script>
|
|
17
11
|
|
|
18
12
|
<button
|
|
19
13
|
aria-label={tooltip ?? sid ?? 'Action'}
|
|
20
|
-
class="btn-icon btn-sm {variantClass}"
|
|
14
|
+
class="btn-icon btn-sm {variantClass} transition-transform duration-150"
|
|
15
|
+
class:scale-90={clickPulse.pressed}
|
|
21
16
|
data-sid={sid}
|
|
22
|
-
disabled={disabled || isLoading}
|
|
17
|
+
disabled={disabled || clickLoading.isLoading}
|
|
23
18
|
title={tooltip}
|
|
24
19
|
type="button"
|
|
25
20
|
onclick={handleClick}
|
|
26
21
|
>
|
|
27
|
-
{#if isLoading}
|
|
22
|
+
{#if clickLoading.isLoading}
|
|
28
23
|
<span class="animate-spin">⟳</span>
|
|
29
24
|
{:else if children}
|
|
30
25
|
{@render children()}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<script lang="ts">import CtrlFieldShell from "./CtrlFieldShell.svelte";
|
|
2
|
+
import { makeFieldId } from "./utils";
|
|
3
|
+
let { disabled, error, label, name, onchange, options = [], placeholder, required, sid, tooltip, value = $bindable("") } = $props();
|
|
4
|
+
const id = $derived(makeFieldId(name, sid));
|
|
4
5
|
</script>
|
|
5
6
|
|
|
6
|
-
<
|
|
7
|
-
{#if label}
|
|
8
|
-
<label class="label text-sm font-medium" for={id}>
|
|
9
|
-
{label}{#if required}<span class="text-error-500">*</span>{/if}
|
|
10
|
-
</label>
|
|
11
|
-
{/if}
|
|
7
|
+
<CtrlFieldShell {error} {id} {label} {required} {tooltip}>
|
|
12
8
|
<select
|
|
13
9
|
{id}
|
|
14
10
|
{disabled}
|
|
11
|
+
{name}
|
|
15
12
|
{required}
|
|
16
13
|
bind:value
|
|
17
14
|
class="select"
|
|
@@ -29,7 +26,4 @@ export {};
|
|
|
29
26
|
{/if}
|
|
30
27
|
{/each}
|
|
31
28
|
</select>
|
|
32
|
-
|
|
33
|
-
<p class="text-error-500 text-xs">{error}</p>
|
|
34
|
-
{/if}
|
|
35
|
-
</div>
|
|
29
|
+
</CtrlFieldShell>
|
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
1
|
+
<script lang="ts">import CtrlFieldShell from "./CtrlFieldShell.svelte";
|
|
2
|
+
import { makeFieldId } from "./utils";
|
|
3
|
+
let { disabled, error, label, max, min, name, onchange, placeholder, required, sid, step, tooltip, value = $bindable(undefined) } = $props();
|
|
4
|
+
const id = $derived(makeFieldId(name, sid));
|
|
3
5
|
function handleInput(e) {
|
|
4
6
|
const target = e.target;
|
|
5
|
-
const
|
|
7
|
+
const raw = target.value;
|
|
8
|
+
if (raw === "") {
|
|
9
|
+
value = undefined;
|
|
10
|
+
onchange?.(undefined);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const parsed = Number(raw);
|
|
14
|
+
// Intermediate typed states (e.g. "-", "1e") parse to NaN; ignore until valid.
|
|
15
|
+
if (Number.isNaN(parsed)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
6
18
|
value = parsed;
|
|
7
19
|
onchange?.(parsed);
|
|
8
20
|
}
|
|
9
|
-
export {};
|
|
10
21
|
</script>
|
|
11
22
|
|
|
12
|
-
<
|
|
13
|
-
{#if label}
|
|
14
|
-
<label class="label text-sm font-medium" for={id}>
|
|
15
|
-
{label}{#if required}<span class="text-error-500">*</span>{/if}
|
|
16
|
-
</label>
|
|
17
|
-
{/if}
|
|
23
|
+
<CtrlFieldShell {error} {id} {label} {required} {tooltip}>
|
|
18
24
|
<input
|
|
19
25
|
{id}
|
|
20
26
|
{disabled}
|
|
21
27
|
{max}
|
|
22
28
|
{min}
|
|
29
|
+
{name}
|
|
23
30
|
{placeholder}
|
|
24
31
|
{required}
|
|
25
32
|
{step}
|
|
@@ -29,7 +36,4 @@ export {};
|
|
|
29
36
|
value={value ?? ''}
|
|
30
37
|
oninput={handleInput}
|
|
31
38
|
/>
|
|
32
|
-
|
|
33
|
-
<p class="text-error-500 text-xs">{error}</p>
|
|
34
|
-
{/if}
|
|
35
|
-
</div>
|
|
39
|
+
</CtrlFieldShell>
|