@simplysm/solid 13.0.84 → 13.0.85

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 (91) hide show
  1. package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
  2. package/dist/components/data/sheet/DataSheet.js +6 -9
  3. package/dist/components/data/sheet/DataSheet.js.map +2 -2
  4. package/dist/components/data/sheet/hooks/createDataSheetExpansion.d.ts.map +1 -1
  5. package/dist/components/data/sheet/hooks/createDataSheetExpansion.js +15 -17
  6. package/dist/components/data/sheet/hooks/createDataSheetExpansion.js.map +1 -1
  7. package/dist/components/data/sheet/hooks/createDataSheetReorder.d.ts.map +1 -1
  8. package/dist/components/data/sheet/hooks/createDataSheetReorder.js +12 -12
  9. package/dist/components/data/sheet/hooks/createDataSheetReorder.js.map +1 -1
  10. package/dist/components/data/sheet/hooks/createDataSheetSelection.d.ts.map +1 -1
  11. package/dist/components/data/sheet/hooks/createDataSheetSelection.js +9 -3
  12. package/dist/components/data/sheet/hooks/createDataSheetSelection.js.map +1 -1
  13. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  14. package/dist/components/disclosure/Dialog.js +3 -21
  15. package/dist/components/disclosure/Dialog.js.map +2 -2
  16. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  17. package/dist/components/disclosure/Dropdown.js +1 -11
  18. package/dist/components/disclosure/Dropdown.js.map +2 -2
  19. package/dist/components/disclosure/Tabs.d.ts.map +1 -1
  20. package/dist/components/disclosure/Tabs.js +1 -3
  21. package/dist/components/disclosure/Tabs.js.map +2 -2
  22. package/dist/components/features/crud-detail/CrudDetail.js +103 -102
  23. package/dist/components/features/crud-detail/CrudDetail.js.map +2 -2
  24. package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -1
  25. package/dist/components/features/crud-sheet/CrudSheet.js +3 -5
  26. package/dist/components/features/crud-sheet/CrudSheet.js.map +1 -1
  27. package/dist/components/feedback/busy/BusyContainer.d.ts.map +1 -1
  28. package/dist/components/feedback/busy/BusyContainer.js +1 -6
  29. package/dist/components/feedback/busy/BusyContainer.js.map +2 -2
  30. package/dist/components/form-control/checkbox/SelectableBase.d.ts.map +1 -1
  31. package/dist/components/form-control/checkbox/SelectableBase.js +2 -4
  32. package/dist/components/form-control/checkbox/SelectableBase.js.map +2 -2
  33. package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
  34. package/dist/components/form-control/date-range-picker/DateRangePicker.js +1 -2
  35. package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
  36. package/dist/components/form-control/editor/RichTextEditor.d.ts.map +1 -1
  37. package/dist/components/form-control/editor/RichTextEditor.js +2 -4
  38. package/dist/components/form-control/editor/RichTextEditor.js.map +2 -2
  39. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  40. package/dist/components/form-control/field/NumberInput.js +7 -7
  41. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  42. package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
  43. package/dist/components/form-control/field/Textarea.js +1 -3
  44. package/dist/components/form-control/field/Textarea.js.map +2 -2
  45. package/dist/components/form-control/select/Select.d.ts +2 -0
  46. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  47. package/dist/components/form-control/select/Select.js +11 -10
  48. package/dist/components/form-control/select/Select.js.map +2 -2
  49. package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
  50. package/dist/components/form-control/state-preset/StatePreset.js +3 -7
  51. package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
  52. package/dist/components/layout/topbar/Topbar.js +1 -3
  53. package/dist/components/layout/topbar/Topbar.js.map +2 -2
  54. package/dist/hooks/createControllableStore.d.ts.map +1 -1
  55. package/dist/hooks/createControllableStore.js +8 -5
  56. package/dist/hooks/createControllableStore.js.map +1 -1
  57. package/dist/hooks/useLocalStorage.d.ts.map +1 -1
  58. package/dist/hooks/useLocalStorage.js +3 -2
  59. package/dist/hooks/useLocalStorage.js.map +1 -1
  60. package/dist/hooks/useSyncConfig.d.ts.map +1 -1
  61. package/dist/hooks/useSyncConfig.js +5 -4
  62. package/dist/hooks/useSyncConfig.js.map +1 -1
  63. package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
  64. package/dist/providers/shared-data/SharedDataProvider.js +0 -1
  65. package/dist/providers/shared-data/SharedDataProvider.js.map +1 -1
  66. package/package.json +5 -5
  67. package/src/components/data/sheet/DataSheet.tsx +6 -10
  68. package/src/components/data/sheet/hooks/createDataSheetExpansion.ts +17 -18
  69. package/src/components/data/sheet/hooks/createDataSheetReorder.ts +12 -13
  70. package/src/components/data/sheet/hooks/createDataSheetSelection.ts +9 -3
  71. package/src/components/disclosure/Dialog.tsx +45 -59
  72. package/src/components/disclosure/Dropdown.tsx +4 -14
  73. package/src/components/disclosure/Tabs.tsx +12 -17
  74. package/src/components/features/crud-detail/CrudDetail.tsx +4 -4
  75. package/src/components/features/crud-sheet/CrudSheet.tsx +4 -5
  76. package/src/components/feedback/busy/BusyContainer.tsx +12 -18
  77. package/src/components/form-control/checkbox/SelectableBase.tsx +10 -16
  78. package/src/components/form-control/date-range-picker/DateRangePicker.tsx +1 -4
  79. package/src/components/form-control/editor/RichTextEditor.tsx +11 -12
  80. package/src/components/form-control/field/NumberInput.tsx +6 -8
  81. package/src/components/form-control/field/Textarea.tsx +11 -10
  82. package/src/components/form-control/select/Select.tsx +23 -9
  83. package/src/components/form-control/state-preset/StatePreset.tsx +15 -22
  84. package/src/components/layout/topbar/Topbar.tsx +2 -2
  85. package/src/hooks/createControllableStore.ts +8 -4
  86. package/src/hooks/useLocalStorage.ts +3 -2
  87. package/src/hooks/useSyncConfig.ts +5 -4
  88. package/src/providers/shared-data/SharedDataProvider.tsx +0 -1
  89. package/tests/components/features/crud-detail/CrudDetail.spec.tsx +49 -0
  90. package/tests/components/form-control/select/SelectItem.spec.tsx +5 -0
  91. package/tests/providers/shared-data/SharedDataProvider.spec.tsx +0 -104
@@ -368,7 +368,6 @@ const DataSheetInner = <TItem,>(props: DataSheetProps<TItem>) => {
368
368
  );
369
369
 
370
370
  // #region Display
371
- const displayItems = flatItems;
372
371
 
373
372
  // #region Selection
374
373
  const {
@@ -387,7 +386,7 @@ const DataSheetInner = <TItem,>(props: DataSheetProps<TItem>) => {
387
386
  get onSelectionChange() { return local.onSelectionChange; },
388
387
  get isItemSelectable() { return local.isItemSelectable; },
389
388
  },
390
- displayItems,
389
+ flatItems,
391
390
  );
392
391
 
393
392
  // #region AutoSelect
@@ -408,7 +407,7 @@ const DataSheetInner = <TItem,>(props: DataSheetProps<TItem>) => {
408
407
  get onItemsReorder() { return local.onItemsReorder; },
409
408
  get itemChildren() { return local.itemChildren; },
410
409
  },
411
- displayItems,
410
+ flatItems,
412
411
  );
413
412
 
414
413
  // #region Keyboard Navigation (move rows with Enter/Shift+Enter)
@@ -428,11 +427,8 @@ const DataSheetInner = <TItem,>(props: DataSheetProps<TItem>) => {
428
427
  if (!tbody) return;
429
428
 
430
429
  const rows = tbody.rows;
431
- const rowIndex = Array.from(rows).indexOf(tr);
432
- if (rowIndex < 0) return;
433
-
434
- const cellIndex = Array.from(tr.cells).indexOf(td);
435
- if (cellIndex < 0) return;
430
+ const rowIndex = tr.sectionRowIndex;
431
+ const cellIndex = td.cellIndex;
436
432
 
437
433
  const targetRowIndex = e.shiftKey ? rowIndex - 1 : rowIndex + 1;
438
434
  if (targetRowIndex < 0 || targetRowIndex >= rows.length) return;
@@ -784,7 +780,7 @@ const DataSheetInner = <TItem,>(props: DataSheetProps<TItem>) => {
784
780
  <div class={featureCellClickableClass} onClick={() => toggleSelectAll()}>
785
781
  <Checkbox
786
782
  checked={(() => {
787
- const selectableItems = displayItems()
783
+ const selectableItems = flatItems()
788
784
  .map((flat) => flat.item)
789
785
  .filter((item) => getItemSelectable(item) === true);
790
786
  return (
@@ -829,7 +825,7 @@ const DataSheetInner = <TItem,>(props: DataSheetProps<TItem>) => {
829
825
  {renderSummaryRow()}
830
826
  </thead>
831
827
  <tbody>
832
- <For each={displayItems()}>
828
+ <For each={flatItems()}>
833
829
  {(flat) => (
834
830
  <tr
835
831
  data-selected={selection().includes(flat.item) ? "" : undefined}
@@ -37,20 +37,6 @@ export function createDataSheetExpansion<TItem>(
37
37
  }
38
38
  }
39
39
 
40
- function toggleExpandAll(): void {
41
- if (!props.itemChildren) return;
42
- const indexMap = originalIndexMap();
43
- const allExpandable = collectAllExpandable(
44
- pagedItems(),
45
- props.itemChildren,
46
- (item) => indexMap.get(item) ?? -1,
47
- );
48
- const isAllCurrentlyExpanded = allExpandable.every((item) =>
49
- expandedItems().includes(item),
50
- );
51
- setExpandedItems(isAllCurrentlyExpanded ? [] : allExpandable);
52
- }
53
-
54
40
  const flatItems = createMemo((): FlatItem<TItem>[] => {
55
41
  const indexMap = originalIndexMap();
56
42
  return flattenTree(
@@ -61,16 +47,29 @@ export function createDataSheetExpansion<TItem>(
61
47
  );
62
48
  });
63
49
 
64
- const isAllExpanded = createMemo(() => {
65
- if (!props.itemChildren) return false;
50
+ const allExpandable = createMemo(() => {
51
+ if (!props.itemChildren) return [];
66
52
  const indexMap = originalIndexMap();
67
- const allExpandable = collectAllExpandable(
53
+ return collectAllExpandable(
68
54
  pagedItems(),
69
55
  props.itemChildren,
70
56
  (item) => indexMap.get(item) ?? -1,
71
57
  );
58
+ });
59
+
60
+ function toggleExpandAll(): void {
61
+ const items = allExpandable();
62
+ if (items.length === 0) return;
63
+ const isAllCurrentlyExpanded = items.every((item) =>
64
+ expandedItems().includes(item),
65
+ );
66
+ setExpandedItems(isAllCurrentlyExpanded ? [] : items);
67
+ }
68
+
69
+ const isAllExpanded = createMemo(() => {
70
+ const items = allExpandable();
72
71
  return (
73
- allExpandable.length > 0 && allExpandable.every((item) => expandedItems().includes(item))
72
+ items.length > 0 && items.every((item) => expandedItems().includes(item))
74
73
  );
75
74
  });
76
75
 
@@ -37,11 +37,13 @@ export function createDataSheetReorder<TItem>(
37
37
  const tableEl = target.closest("table")!;
38
38
  const tbody = tableEl.querySelector("tbody")!;
39
39
  const rows = Array.from(tbody.rows);
40
+ const scrollContainer = tableEl.closest<HTMLElement>("[data-sheet-scroll]");
40
41
 
41
42
  setDragState({ draggingItem: item, targetItem: null, position: null });
42
43
 
43
44
  startPointerDrag(target, e.pointerId, {
44
45
  onMove(ev) {
46
+ const items = displayItems();
45
47
  let foundTarget: TItem | null = null;
46
48
  let foundPosition: "before" | "after" | "inside" | null = null;
47
49
 
@@ -50,8 +52,8 @@ export function createDataSheetReorder<TItem>(
50
52
  const rect = row.getBoundingClientRect();
51
53
  if (ev.clientY < rect.top || ev.clientY > rect.bottom) continue;
52
54
 
53
- if (i >= displayItems().length) break;
54
- const flat = displayItems()[i];
55
+ if (i >= items.length) break;
56
+ const flat = items[i];
55
57
  if (flat.item === item) break;
56
58
 
57
59
  // Cannot drop to child items of self
@@ -82,8 +84,8 @@ export function createDataSheetReorder<TItem>(
82
84
  rows[i].removeAttribute("data-dragging");
83
85
  rows[i].removeAttribute("data-drag-over");
84
86
 
85
- if (i < displayItems().length) {
86
- const flat = displayItems()[i];
87
+ if (i < items.length) {
88
+ const flat = items[i];
87
89
  if (flat.item === item) {
88
90
  rows[i].setAttribute("data-dragging", "");
89
91
  }
@@ -94,22 +96,20 @@ export function createDataSheetReorder<TItem>(
94
96
  }
95
97
 
96
98
  // before/after indicator
97
- const indicatorEl = tableEl
98
- .closest("[data-sheet-scroll]")
99
+ const indicatorEl = scrollContainer
99
100
  ?.querySelector("[data-reorder-indicator]") as HTMLElement | null;
100
101
  if (indicatorEl) {
101
102
  if (foundTarget != null && foundPosition != null && foundPosition !== "inside") {
102
- const targetIdx = displayItems().findIndex((f) => f.item === foundTarget);
103
+ const targetIdx = items.findIndex((f) => f.item === foundTarget);
103
104
  if (targetIdx >= 0) {
104
105
  const targetRow = rows[targetIdx];
105
- const containerRect = tableEl.closest("[data-sheet-scroll]")!.getBoundingClientRect();
106
+ const containerRect = scrollContainer!.getBoundingClientRect();
106
107
  const rowRect = targetRow.getBoundingClientRect();
107
- const scrollEl = tableEl.closest("[data-sheet-scroll]") as HTMLElement;
108
108
 
109
109
  const top =
110
110
  foundPosition === "before"
111
- ? rowRect.top - containerRect.top + scrollEl.scrollTop
112
- : rowRect.bottom - containerRect.top + scrollEl.scrollTop;
111
+ ? rowRect.top - containerRect.top + scrollContainer!.scrollTop
112
+ : rowRect.bottom - containerRect.top + scrollContainer!.scrollTop;
113
113
 
114
114
  indicatorEl.style.display = "block";
115
115
  indicatorEl.style.top = `${top}px`;
@@ -134,8 +134,7 @@ export function createDataSheetReorder<TItem>(
134
134
  row.removeAttribute("data-dragging");
135
135
  row.removeAttribute("data-drag-over");
136
136
  }
137
- const indicatorEl = tableEl
138
- .closest("[data-sheet-scroll]")
137
+ const indicatorEl = scrollContainer
139
138
  ?.querySelector("[data-reorder-indicator]") as HTMLElement | null;
140
139
  if (indicatorEl) {
141
140
  indicatorEl.style.display = "none";
@@ -57,7 +57,8 @@ export function createDataSheetSelection<TItem>(
57
57
  const selectableItems = displayItems()
58
58
  .map((flat) => flat.item)
59
59
  .filter((item) => getItemSelectable(item) === true);
60
- const isAllSelected = selectableItems.every((item) => selection().includes(item));
60
+ const selectionSet = new Set(selection());
61
+ const isAllSelected = selectableItems.every((item) => selectionSet.has(item));
61
62
  setSelection(isAllSelected ? [] : selectableItems);
62
63
  }
63
64
 
@@ -74,13 +75,18 @@ export function createDataSheetSelection<TItem>(
74
75
  .filter((item) => getItemSelectable(item) === true);
75
76
 
76
77
  if (lastClickAction() === "select") {
78
+ const selectionSet = new Set(selection());
77
79
  const newItems = [...selection()];
78
80
  for (const item of rangeItems) {
79
- if (!newItems.includes(item)) newItems.push(item);
81
+ if (!selectionSet.has(item)) {
82
+ newItems.push(item);
83
+ selectionSet.add(item);
84
+ }
80
85
  }
81
86
  setSelection(newItems);
82
87
  } else {
83
- setSelection(selection().filter((item) => !rangeItems.includes(item)));
88
+ const rangeSet = new Set(rangeItems);
89
+ setSelection(selection().filter((item) => !rangeSet.has(item)));
84
90
  }
85
91
  }
86
92
 
@@ -478,70 +478,38 @@ const DialogInner: ParentComponent<DialogProps> = (props) => {
478
478
  return style;
479
479
  };
480
480
 
481
- // Animation class
482
- const animationClass = () => {
483
- const base = clsx("transition-[opacity,transform]", "duration-200", "ease-out");
484
- if (animating()) {
485
- return clsx(base, "translate-y-0 opacity-100");
486
- } else {
487
- return clsx(base, "-translate-y-4 opacity-0");
488
- }
489
- };
490
-
491
- // Wrapper class
492
- const wrapperClass = () =>
493
- // eslint-disable-next-line tailwindcss/enforces-shorthand -- inset-0 not supported in Chrome 84
494
- clsx(
495
- "fixed bottom-0 left-0 right-0 top-0",
496
- "flex flex-col items-center",
497
- local.mode !== "fill" && "pt-[calc(3rem+0.5rem)]",
498
- local.mode === "float" && "pointer-events-none",
499
- );
500
-
501
- // Backdrop class
502
- const backdropClass = () =>
503
- // eslint-disable-next-line tailwindcss/enforces-shorthand -- inset-0 not supported in Chrome 84
504
- clsx(
505
- "absolute bottom-0 left-0 right-0 top-0",
506
- "bg-black/30",
507
- "dark:bg-black/50",
508
- "transition-opacity",
509
- "duration-200",
510
- "ease-out",
511
- animating() ? "opacity-100" : "opacity-0",
512
- );
513
-
514
- // Dialog class
515
- const dialogBaseClass = () =>
516
- clsx(
517
- "relative",
518
- "mx-auto",
519
- "w-fit min-w-[200px]",
520
- bg.surface,
521
- local.mode === "float"
522
- ? clsx("shadow-md dark:shadow-black/30", "border", border.subtle)
523
- : "shadow-2xl dark:shadow-black/40",
524
- local.mode === "fill" ? "rounded-none border-none" : "rounded-lg",
525
- "overflow-hidden",
526
- "flex flex-col",
527
- "focus:outline-none",
528
- local.mode === "float" && "pointer-events-auto",
529
- animationClass(),
530
- );
531
-
532
- // Header class
533
- const headerClass = () =>
534
- clsx("flex items-center gap-2", "px-3 py-1", "select-none", "border-b", border.subtle);
535
-
536
481
  return (
537
482
  <Show when={mounted()}>
538
483
  <Portal>
539
484
  <HeaderProvider>
540
485
  <ActionProvider>
541
- <div ref={setWrapperRef} data-dialog class={wrapperClass()}>
486
+ <div
487
+ ref={setWrapperRef}
488
+ data-dialog
489
+ // eslint-disable-next-line tailwindcss/enforces-shorthand -- inset-0 not supported in Chrome 84
490
+ class={clsx(
491
+ "fixed bottom-0 left-0 right-0 top-0",
492
+ "flex flex-col items-center",
493
+ local.mode !== "fill" && "pt-[calc(3rem+0.5rem)]",
494
+ local.mode === "float" && "pointer-events-none",
495
+ )}
496
+ >
542
497
  {/* Backdrop */}
543
498
  <Show when={local.mode !== "float"}>
544
- <div data-dialog-backdrop class={backdropClass()} onClick={handleBackdropClick} />
499
+ <div
500
+ data-dialog-backdrop
501
+ // eslint-disable-next-line tailwindcss/enforces-shorthand -- inset-0 not supported in Chrome 84
502
+ class={clsx(
503
+ "absolute bottom-0 left-0 right-0 top-0",
504
+ "bg-black/30",
505
+ "dark:bg-black/50",
506
+ "transition-opacity",
507
+ "duration-200",
508
+ "ease-out",
509
+ animating() ? "opacity-100" : "opacity-0",
510
+ )}
511
+ onClick={handleBackdropClick}
512
+ />
545
513
  </Show>
546
514
 
547
515
  {/* Dialog */}
@@ -554,7 +522,25 @@ const DialogInner: ParentComponent<DialogProps> = (props) => {
554
522
  aria-modal={local.mode === "float" ? undefined : true}
555
523
  aria-labelledby={hasHeader() ? headerId : undefined}
556
524
  tabIndex={0}
557
- class={twMerge(dialogBaseClass(), local.class)}
525
+ class={twMerge(
526
+ clsx(
527
+ "relative",
528
+ "mx-auto",
529
+ "w-fit min-w-[200px]",
530
+ bg.surface,
531
+ local.mode === "float"
532
+ ? clsx("shadow-md dark:shadow-black/30", "border", border.subtle)
533
+ : "shadow-2xl dark:shadow-black/40",
534
+ local.mode === "fill" ? "rounded-none border-none" : "rounded-lg",
535
+ "overflow-hidden",
536
+ "flex flex-col",
537
+ "focus:outline-none",
538
+ local.mode === "float" && "pointer-events-auto",
539
+ "transition-[opacity,transform] duration-200 ease-out",
540
+ animating() ? "translate-y-0 opacity-100" : "-translate-y-4 opacity-0",
541
+ ),
542
+ local.class,
543
+ )}
558
544
  style={dialogStyle()}
559
545
  onFocus={handleDialogFocus}
560
546
  onTransitionEnd={handleTransitionEnd}
@@ -563,7 +549,7 @@ const DialogInner: ParentComponent<DialogProps> = (props) => {
563
549
  <Show when={hasHeader()}>
564
550
  <div
565
551
  data-dialog-header
566
- class={clsx(headerClass(), "touch-none")}
552
+ class={clsx("flex items-center gap-2", "px-3 py-1", "select-none", "border-b", border.subtle, "touch-none")}
567
553
  style={
568
554
  typeof local.headerStyle === "string"
569
555
  ? mergeStyles(local.headerStyle)
@@ -402,19 +402,6 @@ const DropdownInner: ParentComponent<DropdownProps> = (props: DropdownProps) =>
402
402
 
403
403
  const maxHeight = () => local.maxHeight ?? 300;
404
404
 
405
- // Animation class (only transition opacity and transform, not position properties)
406
- const animationClass = () => {
407
- const base = "transition-[opacity,transform] duration-150 ease-out";
408
- const visible = animating();
409
- const dir = direction();
410
-
411
- if (visible) {
412
- return clsx(base, "translate-y-0 opacity-100");
413
- } else {
414
- return clsx(base, "opacity-0", dir === "down" ? "-translate-y-1" : "translate-y-1");
415
- }
416
- };
417
-
418
405
  return (
419
406
  <TriggerProvider>
420
407
  <ContentProvider>
@@ -452,7 +439,10 @@ const DropdownInner: ParentComponent<DropdownProps> = (props: DropdownProps) =>
452
439
  "shadow-lg dark:shadow-black/30",
453
440
  "rounded-md",
454
441
  "overflow-y-auto",
455
- animationClass(),
442
+ "transition-[opacity,transform] duration-150 ease-out",
443
+ animating()
444
+ ? "translate-y-0 opacity-100"
445
+ : clsx("opacity-0", direction() === "down" ? "-translate-y-1" : "translate-y-1"),
456
446
  ),
457
447
  local.class,
458
448
  )}
@@ -39,21 +39,6 @@ function TabsTabInner(props: TabsTabProps) {
39
39
  xl: "px-5 py-3 text-lg",
40
40
  };
41
41
 
42
- const sizeClasses = () => tabSizeClasses[ctx.size() ?? "md"];
43
-
44
- const stateClass = () =>
45
- isSelected()
46
- ? clsx(
47
- "border-b-2 border-primary-500 dark:border-primary-400",
48
- themeTokens.primary.text,
49
- )
50
- : clsx(
51
- "border-b-2 border-transparent",
52
- text.muted,
53
- "hover:border-base-300 hover:text-base-700",
54
- "dark:hover:border-base-600 dark:hover:text-base-200",
55
- );
56
-
57
42
  return (
58
43
  <button
59
44
  type="button"
@@ -63,8 +48,18 @@ function TabsTabInner(props: TabsTabProps) {
63
48
  tabIndex={props.disabled ? -1 : 0}
64
49
  class={twMerge(
65
50
  "relative cursor-pointer select-none font-medium transition-colors -mb-px",
66
- sizeClasses(),
67
- stateClass(),
51
+ tabSizeClasses[ctx.size() ?? "md"],
52
+ isSelected()
53
+ ? clsx(
54
+ "border-b-2 border-primary-500 dark:border-primary-400",
55
+ themeTokens.primary.text,
56
+ )
57
+ : clsx(
58
+ "border-b-2 border-transparent",
59
+ text.muted,
60
+ "hover:border-base-300 hover:text-base-700",
61
+ "dark:hover:border-base-600 dark:hover:text-base-200",
62
+ ),
68
63
  props.disabled && "opacity-50 pointer-events-none",
69
64
  props.class,
70
65
  )}
@@ -201,7 +201,6 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
201
201
  <>
202
202
  <Show when={showSave()}>
203
203
  <Button
204
- size="lg"
205
204
  variant="ghost"
206
205
  theme="primary"
207
206
  onClick={() => formRef?.requestSubmit()}
@@ -213,7 +212,6 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
213
212
  <Show when={showDelete()}>
214
213
  {(_) => (
215
214
  <Button
216
- size="lg"
217
215
  variant="ghost"
218
216
  theme="danger"
219
217
  onClick={() => void handleToggleDelete()}
@@ -223,7 +221,7 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
223
221
  </Button>
224
222
  )}
225
223
  </Show>
226
- <Button size="lg" variant="ghost" theme="info" onClick={() => void handleRefresh()}>
224
+ <Button variant="ghost" theme="info" onClick={() => void handleRefresh()}>
227
225
  <Icon icon={IconRefresh} class="mr-1" />
228
226
  {i18n.t("crudDetail.refresh")}
229
227
  </Button>
@@ -259,8 +257,9 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
259
257
  <BusyContainer
260
258
  ready={ready()}
261
259
  busy={busyCount() > 0}
262
- class={clsx("flex h-full flex-col gap-2", local.class)}
260
+ class={clsx("flex h-full flex-col", local.class)}
263
261
  >
262
+ <div class="flex min-h-0 flex-1 flex-col gap-2">
264
263
  {/* Toolbar */}
265
264
  <Show when={(!isInDialog && !topbarCtx) || tools()}>
266
265
  <div class="flex gap-2 pb-0">
@@ -318,6 +317,7 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
318
317
 
319
318
  {/* After (outside form) */}
320
319
  <Show when={after()}>{(afterSlot) => afterSlot().children}</Show>
320
+ </div>
321
321
 
322
322
  {/* Dialog mode: bottom bar */}
323
323
  <Show when={isInDialog && canEdit()}>
@@ -369,13 +369,13 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, unknown>>(
369
369
 
370
370
  // -- Keyboard Shortcuts --
371
371
  createEventListener(document, "keydown", async (e: KeyboardEvent) => {
372
- if (!isActiveCrud(crudId)) return;
373
- if (e.ctrlKey && e.key === "s" && !isSelectMode()) {
372
+ if (!e.ctrlKey || !isActiveCrud(crudId)) return;
373
+ if (e.key === "s" && !isSelectMode()) {
374
374
  e.preventDefault();
375
375
  e.stopImmediatePropagation();
376
376
  formRef?.requestSubmit();
377
377
  }
378
- if (e.ctrlKey && e.altKey && e.key === "l") {
378
+ if (e.altKey && e.key === "l") {
379
379
  e.preventDefault();
380
380
  e.stopImmediatePropagation();
381
381
  if (!checkIgnoreChanges()) return;
@@ -402,7 +402,6 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, unknown>>(
402
402
  <>
403
403
  <Show when={canEdit() && local.inlineEdit}>
404
404
  <Button
405
- size="lg"
406
405
  variant="ghost"
407
406
  theme="primary"
408
407
  onClick={() => formRef?.requestSubmit()}
@@ -411,7 +410,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, unknown>>(
411
410
  {i18n.t("crudSheet.save")}
412
411
  </Button>
413
412
  </Show>
414
- <Button size="lg" variant="ghost" theme="info" onClick={handleRefresh}>
413
+ <Button variant="ghost" theme="info" onClick={handleRefresh}>
415
414
  <Icon icon={IconRefresh} class="mr-1" />
416
415
  {i18n.t("crudSheet.refresh")}
417
416
  </Button>
@@ -72,27 +72,21 @@ export const BusyContainer: ParentComponent<BusyContainerProps> = (props) => {
72
72
  );
73
73
  });
74
74
 
75
-
76
- const screenClass = () =>
77
- clsx(
78
- "absolute inset-0 z-busy bg-white/70 transition-opacity duration-150 dark:bg-base-900/70",
79
- animating() ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0",
80
- );
81
-
82
- // spinner: slide down animation
83
- const rectClass = () => {
84
- if (currVariant() !== "spinner") return "";
85
- return clsx(
86
- "transition-transform duration-100",
87
- animating() ? "translate-y-0 ease-out" : "-translate-y-full ease-in",
88
- );
89
- };
90
-
91
75
  return (
92
76
  <div ref={containerRef} class={twMerge("relative size-full min-h-[70px] min-w-[70px] overflow-auto", local.class)} {...rest}>
93
77
  <Show when={mounted()}>
94
- <div class={screenClass()} onTransitionEnd={handleTransitionEnd}>
95
- <div class={rectClass()}>
78
+ <div
79
+ class={clsx(
80
+ "absolute inset-0 z-busy bg-white/70 transition-opacity duration-150 dark:bg-base-900/70",
81
+ animating() ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0",
82
+ )}
83
+ onTransitionEnd={handleTransitionEnd}
84
+ >
85
+ <div
86
+ class={currVariant() === "spinner"
87
+ ? clsx("transition-transform duration-100", animating() ? "translate-y-0 ease-out" : "-translate-y-full ease-in")
88
+ : ""}
89
+ >
96
90
  <Show when={currVariant() === "spinner"}>
97
91
  <div class="mx-auto mt-5 size-8 animate-spin rounded-full border-[6px] border-base-200 border-b-primary-500 shadow-md dark:border-base-700 dark:border-b-primary-400" />
98
92
  </Show>
@@ -76,20 +76,6 @@ export const SelectableBase: ParentComponent<SelectableBaseProps & { config: Sel
76
76
  }
77
77
  };
78
78
 
79
- const getWrapperClass = () =>
80
- twMerge(
81
- checkboxBaseClass,
82
- checkboxSizeClasses[local.size ?? "md"],
83
- local.inset && checkboxInsetClass,
84
- local.inset && checkboxInsetSizeHeightClasses[local.size ?? "md"],
85
- local.inline && checkboxInlineClass,
86
- local.disabled && checkboxDisabledClass,
87
- local.class,
88
- );
89
-
90
- const getIndicatorClass = () =>
91
- twMerge(indicatorBaseClass, local.config.indicatorShape, checked() && checkedClass);
92
-
93
79
  const errorMsg = createMemo(() => {
94
80
  const v = local.checked ?? false;
95
81
  if (local.required && !v) return i18n.t("validation.requiredSelection");
@@ -104,12 +90,20 @@ export const SelectableBase: ParentComponent<SelectableBaseProps & { config: Sel
104
90
  role={local.config.role}
105
91
  aria-checked={checked()}
106
92
  tabIndex={local.disabled ? -1 : 0}
107
- class={getWrapperClass()}
93
+ class={twMerge(
94
+ checkboxBaseClass,
95
+ checkboxSizeClasses[local.size ?? "md"],
96
+ local.inset && checkboxInsetClass,
97
+ local.inset && checkboxInsetSizeHeightClasses[local.size ?? "md"],
98
+ local.inline && checkboxInlineClass,
99
+ local.disabled && checkboxDisabledClass,
100
+ local.class,
101
+ )}
108
102
  style={local.style}
109
103
  onClick={handleClick}
110
104
  onKeyDown={handleKeyDown}
111
105
  >
112
- <div class={getIndicatorClass()}>
106
+ <div class={twMerge(indicatorBaseClass, local.config.indicatorShape, checked() && checkedClass)}>
113
107
  <Show when={checked()}>
114
108
  {local.config.indicatorContent}
115
109
  </Show>
@@ -153,11 +153,8 @@ export const DateRangePicker: Component<DateRangePickerProps> = (props) => {
153
153
  setTo(newTo);
154
154
  };
155
155
 
156
- // Wrapper CSS class
157
- const getWrapperClass = () => twMerge(clsx("inline-flex items-center", gap.md), local.class);
158
-
159
156
  return (
160
- <div {...rest} data-date-range-picker class={getWrapperClass()} style={local.style}>
157
+ <div {...rest} data-date-range-picker class={twMerge(clsx("inline-flex items-center", gap.md), local.class)} style={local.style}>
161
158
  <Select
162
159
  value={periodType()}
163
160
  onValueChange={handlePeriodTypeChange}
@@ -146,18 +146,17 @@ export const RichTextEditor: Component<RichTextEditorProps> = (props) => {
146
146
  editor()?.destroy();
147
147
  });
148
148
 
149
- const getWrapperClass = () =>
150
- twMerge(
151
- clsx("flex flex-col bg-primary-50 dark:bg-primary-950/30", text.default, "border", border.default, "rounded focus-within:border-primary-500"),
152
- local.disabled && clsx(bg.muted, text.muted),
153
- local.class,
154
- );
155
-
156
- const getContentClass = () =>
157
- twMerge("outline-none prose prose-sm max-w-none dark:prose-invert", editorContentSizeClasses[local.size ?? "md"]);
158
-
159
149
  return (
160
- <div {...rest} data-rich-text-editor class={getWrapperClass()} style={local.style}>
150
+ <div
151
+ {...rest}
152
+ data-rich-text-editor
153
+ class={twMerge(
154
+ clsx("flex flex-col bg-primary-50 dark:bg-primary-950/30", text.default, "border", border.default, "rounded focus-within:border-primary-500"),
155
+ local.disabled && clsx(bg.muted, text.muted),
156
+ local.class,
157
+ )}
158
+ style={local.style}
159
+ >
161
160
  <Show when={editor()}>
162
161
  {(instance) => (
163
162
  <Show when={!local.disabled}>
@@ -165,7 +164,7 @@ export const RichTextEditor: Component<RichTextEditorProps> = (props) => {
165
164
  </Show>
166
165
  )}
167
166
  </Show>
168
- <div ref={editorRef} class={getContentClass()} />
167
+ <div ref={editorRef} class={twMerge("outline-none prose prose-sm max-w-none dark:prose-invert", editorContentSizeClasses[local.size ?? "md"])} />
169
168
  </div>
170
169
  );
171
170
  };
@@ -19,13 +19,6 @@ import { useI18n } from "../../../providers/i18n/I18nProvider";
19
19
  import { FieldShell } from "./FieldShell";
20
20
  import clsx from "clsx";
21
21
 
22
- // NumberInput-specific input style (right-aligned + spinner hidden)
23
- const numberInputClass = clsx(
24
- fieldInputClass,
25
- "text-right",
26
- "[&::-webkit-outer-spin-button]:appearance-none",
27
- "[&::-webkit-inner-spin-button]:appearance-none",
28
- );
29
22
 
30
23
  const [NumberInputPrefixSlot, createNumberInputPrefixAccessor] = createSlot<{ children: JSX.Element }>();
31
24
  export const NumberInputPrefix = NumberInputPrefixSlot;
@@ -331,7 +324,12 @@ const NumberInputInner = (props: NumberInputProps): JSX.Element => {
331
324
  <input
332
325
  type="text"
333
326
  inputmode="numeric"
334
- class={numberInputClass}
327
+ class={clsx(
328
+ fieldInputClass,
329
+ "text-right",
330
+ "[&::-webkit-outer-spin-button]:appearance-none",
331
+ "[&::-webkit-inner-spin-button]:appearance-none",
332
+ )}
335
333
  value={displayValue()}
336
334
  placeholder={local.placeholder}
337
335
  title={local.title}