@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.
Files changed (52) hide show
  1. package/dist/components/Boxer/Boxer.svelte +5 -1
  2. package/dist/components/CardGrid/CardGrid.svelte +50 -4
  3. package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
  4. package/dist/components/CardGrid/DefaultCard.svelte +7 -2
  5. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
  6. package/dist/components/CardGrid/cardGrid.d.ts +24 -0
  7. package/dist/components/CardGrid/cardGrid.js +20 -0
  8. package/dist/components/Former/FormerDrawer.svelte +10 -22
  9. package/dist/components/Former/FormerModal.svelte +10 -22
  10. package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
  11. package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
  12. package/dist/components/Former/index.d.ts +1 -0
  13. package/dist/components/Former/index.js +1 -0
  14. package/dist/components/FormerControllers/ButtonCtrl.svelte +11 -16
  15. package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
  16. package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
  17. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +6 -13
  18. package/dist/components/FormerControllers/IconButtonCtrl.svelte +11 -16
  19. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -13
  20. package/dist/components/FormerControllers/NumberInputCtrl.svelte +18 -14
  21. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +7 -13
  22. package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -3
  23. package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -13
  24. package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -13
  25. package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
  26. package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
  27. package/dist/components/FormerControllers/index.d.ts +2 -0
  28. package/dist/components/FormerControllers/index.js +2 -0
  29. package/dist/components/FormerControllers/utils.d.ts +1 -0
  30. package/dist/components/FormerControllers/utils.js +3 -0
  31. package/dist/components/Gridler/components/Gridler.svelte +10 -4
  32. package/dist/components/Gridler/components/GridlerFull.svelte +2 -1
  33. package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
  34. package/dist/components/Gridler/utils/serverPaging.js +7 -0
  35. package/dist/components/VTree/VTree.svelte +107 -2
  36. package/dist/components/VTree/VTree.utils.d.ts +23 -0
  37. package/dist/components/VTree/VTree.utils.js +73 -0
  38. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
  39. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
  40. package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
  41. package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
  42. package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
  43. package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
  44. package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
  45. package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
  46. package/dist/components/VTree/VTreeRow.svelte +71 -57
  47. package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
  48. package/dist/components/VTree/VTreeVirtualViewport.svelte +20 -40
  49. package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
  50. package/dist/components/VTree/types.d.ts +39 -1
  51. package/llm/COMPONENT_GUIDE.md +25 -0
  52. 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, cardMinWidth, cardGap);
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
- // ── Public API ─────────────────────────────────────────────────────────────
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:{cardMinWidth}px;--cg-gap:{cardGap}px;--cg-cols:{columnsInRow};"
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 {
@@ -4,6 +4,7 @@ interface Props {
4
4
  columns: CardGridColumn[];
5
5
  selected: boolean;
6
6
  focused: boolean;
7
+ expanded?: boolean;
7
8
  }
8
9
  declare const DefaultCard: import("svelte").Component<Props, {}, "">;
9
10
  type DefaultCard = ReturnType<typeof DefaultCard>;
@@ -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(title ?? (request === "delete" ? `Delete Record${values?.[uniqueKeyField] ? ` — ${values[uniqueKeyField]}` : ""}` : request === "insert" ? "New Record" : `Edit Record${values?.[uniqueKeyField] ? ` — ${values[uniqueKeyField]}` : ""}`));
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
- let backdropZ = $state(1e3);
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(title ?? (request === "delete" ? `Delete Record${values?.[uniqueKeyField] ? ` — ${values[uniqueKeyField]}` : ""}` : request === "insert" ? "New Record" : `Edit Record${values?.[uniqueKeyField] ? ` — ${values[uniqueKeyField]}` : ""}`));
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
- let backdropZ = $state(1e3);
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">const { children, disabled, loading: externalLoading = false, onclick, sid, title, tooltip, variant = "filled" } = $props();
2
- let internalLoading = $state(false);
3
- const isLoading = $derived(internalLoading || externalLoading);
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
- async function handleClick() {
6
- if (onclick && !isLoading) {
7
- internalLoading = true;
8
- try {
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(`field_${name ?? ""}_${sid ?? ""}`);
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
- <div class="flex flex-col gap-1" title={tooltip}>
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">const { children, disabled, loading: externalLoading = false, onclick, sid, tooltip, variant = "ghost" } = $props();
2
- let internalLoading = $state(false);
3
- const isLoading = $derived(internalLoading || externalLoading);
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
- async function handleClick() {
6
- if (onclick && !isLoading) {
7
- internalLoading = true;
8
- try {
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">let { disabled, error, label, name, onchange, options = [], placeholder, required, sid, tooltip, value = $bindable("") } = $props();
2
- const id = $derived(`field_${name ?? ""}_${sid ?? ""}`);
3
- export {};
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
- <div class="flex flex-col gap-1" title={tooltip}>
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
- {#if error}
33
- <p class="text-error-500 text-xs">{error}</p>
34
- {/if}
35
- </div>
29
+ </CtrlFieldShell>
@@ -1,25 +1,32 @@
1
- <script lang="ts">let { disabled, error, label, max, min, name, onchange, placeholder, required, sid, step, tooltip, value = $bindable(undefined) } = $props();
2
- const id = $derived(`field_${name ?? ""}_${sid ?? ""}`);
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 parsed = target.value !== "" ? Number(target.value) : undefined;
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
- <div class="flex flex-col gap-1" title={tooltip}>
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
- {#if error}
33
- <p class="text-error-500 text-xs">{error}</p>
34
- {/if}
35
- </div>
39
+ </CtrlFieldShell>