@lobb-js/studio 0.42.0 → 0.44.0

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 (38) hide show
  1. package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
  2. package/dist/components/dataTable/dataTable.svelte +49 -17
  3. package/dist/components/dataTable/filter.svelte +26 -23
  4. package/dist/components/dataTable/header.svelte +17 -13
  5. package/dist/components/dataTable/header.svelte.d.ts +1 -0
  6. package/dist/components/dataTable/listViewChildren.svelte +60 -77
  7. package/dist/components/dataTable/listViewChildren.svelte.d.ts +1 -1
  8. package/dist/components/dataTable/table.svelte +20 -61
  9. package/dist/components/dataTable/table.svelte.d.ts +2 -2
  10. package/dist/components/detailView/changeTreeUtils.d.ts +7 -0
  11. package/dist/components/detailView/changeTreeUtils.js +47 -0
  12. package/dist/components/detailView/create/createDetailView.svelte +17 -13
  13. package/dist/components/detailView/detailView.svelte +7 -2
  14. package/dist/components/detailView/detailView.svelte.d.ts +1 -0
  15. package/dist/components/detailView/fieldInput.svelte +10 -9
  16. package/dist/components/detailView/fieldInput.svelte.d.ts +1 -0
  17. package/dist/components/detailView/update/updateDetailView.svelte +22 -18
  18. package/dist/components/drawer.svelte +15 -2
  19. package/dist/components/foreingKeyInput.svelte +32 -60
  20. package/dist/components/foreingKeyInput.svelte.d.ts +1 -1
  21. package/dist/components/polymorphicInput.svelte +42 -66
  22. package/dist/components/polymorphicInput.svelte.d.ts +1 -0
  23. package/package.json +2 -2
  24. package/src/app.css +2 -2
  25. package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
  26. package/src/lib/components/dataTable/dataTable.svelte +49 -17
  27. package/src/lib/components/dataTable/filter.svelte +26 -23
  28. package/src/lib/components/dataTable/header.svelte +17 -13
  29. package/src/lib/components/dataTable/listViewChildren.svelte +60 -77
  30. package/src/lib/components/dataTable/table.svelte +20 -61
  31. package/src/lib/components/detailView/changeTreeUtils.ts +39 -0
  32. package/src/lib/components/detailView/create/createDetailView.svelte +17 -13
  33. package/src/lib/components/detailView/detailView.svelte +7 -2
  34. package/src/lib/components/detailView/fieldInput.svelte +10 -9
  35. package/src/lib/components/detailView/update/updateDetailView.svelte +22 -18
  36. package/src/lib/components/drawer.svelte +15 -2
  37. package/src/lib/components/foreingKeyInput.svelte +32 -60
  38. package/src/lib/components/polymorphicInput.svelte +42 -66
@@ -17,7 +17,7 @@
17
17
  <AlertDialog.Content>
18
18
  <AlertDialog.Header>
19
19
  <AlertDialog.Title>{title}</AlertDialog.Title>
20
- <AlertDialog.Description class="whitespace-pre-line">
20
+ <AlertDialog.Description class="whitespace-pre-wrap font-mono text-xs">
21
21
  {description}
22
22
  </AlertDialog.Description>
23
23
  </AlertDialog.Header>
@@ -14,8 +14,10 @@
14
14
  import Table, { type TableProps } from "./table.svelte";
15
15
  import { getCollectionColumns, getCollectionParamsFields } from "./utils";
16
16
  import CanAccess from "../canAccess.svelte";
17
- import { Pencil, Trash, Unlink, RotateCcw } from "lucide-svelte";
17
+ import { Pencil, Trash, Unlink, RotateCcw, Network } from "lucide-svelte";
18
18
  import ListViewChildren from "./listViewChildren.svelte";
19
+ import Drawer from "../drawer.svelte";
20
+ import { ArrowLeft } from "lucide-svelte";
19
21
  import FieldCell from "./fieldCell.svelte";
20
22
  import Skeleton from "../ui/skeleton/skeleton.svelte";
21
23
  import Button from "../ui/button/button.svelte";
@@ -73,6 +75,7 @@
73
75
  }: Props = $props();
74
76
 
75
77
  const isRecordingMode = onChanges !== undefined;
78
+ const isSelectMode = $derived(tableProps?.select != null);
76
79
  let localChanges = $state<ChildrenChanges>(
77
80
  untrack(() => changes) ?? { created: [], updated: [], deleted: [], linked: [], unlinked: [] }
78
81
  );
@@ -135,17 +138,17 @@
135
138
  const state = entry._recordingState;
136
139
  const border = cellIndex === 0 ? {
137
140
  deleted: 'border-l-2 border-l-red-500',
138
- unlinked: 'border-l-2 border-l-orange-500',
141
+ unlinked: 'border-l-2 border-l-slate-500',
139
142
  created: 'border-l-2 border-l-green-500',
140
143
  linked: 'border-l-2 border-l-blue-500',
141
- updated: 'border-l-2 border-l-amber-500',
144
+ updated: 'border-l-2 border-l-orange-500',
142
145
  }[state as string] ?? '' : '';
143
146
  const bg: Record<string, string> = {
144
- deleted: '!bg-red-500/5 opacity-50',
145
- unlinked: '!bg-orange-500/5 opacity-50',
147
+ deleted: '!bg-red-500/5',
148
+ unlinked: '!bg-slate-500/5',
146
149
  created: '!bg-green-500/5',
147
150
  linked: '!bg-blue-500/5',
148
- updated: '!bg-amber-500/5',
151
+ updated: '!bg-orange-500/5',
149
152
  };
150
153
  return `${bg[state as string] ?? ''} ${border}`.trim();
151
154
  }
@@ -220,6 +223,10 @@
220
223
  (ctx.meta.collections[collectionName]?.children ?? [])
221
224
  .some((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic")
222
225
  );
226
+ // Select-mode drawer has no checkbox, edit, delete, or unlink — the
227
+ // left-sticky tools column would render empty, so drop it entirely.
228
+ const showLeftTools = $derived(!isSelectMode);
229
+ let childrenDrawerEntry = $state<Record<string, any> | null>(null);
223
230
 
224
231
  // requests the data from the server when the params is changed
225
232
  $effect(() => {
@@ -404,6 +411,7 @@
404
411
  bind:selectedRecords
405
412
  {showImport}
406
413
  {showFilter}
414
+ showCreate={!isSelectMode}
407
415
  {loading}
408
416
  {parentContext}
409
417
  onLink={isRecordingMode ? handleLink : undefined}
@@ -453,7 +461,6 @@
453
461
  <Table
454
462
  {data}
455
463
  {columns}
456
- showCollapsible={doesCollectionHasChildren}
457
464
  selectByColumn="id"
458
465
  showLastRowBorder={true}
459
466
  showLastColumnBorder={true}
@@ -461,11 +468,25 @@
461
468
  bind:selectedRecords
462
469
  bind:tableWidth={dataTableWidth}
463
470
  {...tableProps}
471
+ {showLeftTools}
464
472
  rowActions={hasRowActions ? rowActionsSnippet : undefined}
465
473
  onCellClass={isRecordingMode ? onCellClass : undefined}>
474
+ {#snippet preTools(entry)}
475
+ {#if doesCollectionHasChildren}
476
+ <Button
477
+ class="h-6 w-6 text-muted-foreground hover:bg-transparent"
478
+ variant="ghost"
479
+ size="icon"
480
+ onclick={() => { childrenDrawerEntry = entry; }}
481
+ Icon={Network}
482
+ title="Show children"
483
+ aria-label={`Show children of record ${entry.id}`}
484
+ ></Button>
485
+ {/if}
486
+ {/snippet}
466
487
  {#snippet tools(entry)}
467
488
  {#if entry._recordingState !== 'deleted' && entry._recordingState !== 'unlinked'}
468
- {#if showEdit}
489
+ {#if showEdit && !isSelectMode}
469
490
  {@const isPending = entry._recordingState === 'created'}
470
491
  <CanAccess collection={collectionName} action="update">
471
492
  <UpdateDetailViewButton
@@ -524,15 +545,6 @@
524
545
  refresh={() => { params = { ...params }; }}
525
546
  />
526
547
  {/snippet}
527
- {#snippet collapsible(entry)}
528
- <ListViewChildren
529
- {collectionName}
530
- recordId={entry.id}
531
- width={dataTableWidth > dataTableContainerWidth
532
- ? dataTableContainerWidth
533
- : dataTableWidth}
534
- />
535
- {/snippet}
536
548
  </Table>
537
549
  {/if}
538
550
  </div>
@@ -546,3 +558,23 @@
546
558
  />
547
559
  {/if}
548
560
  </div>
561
+
562
+ {#if childrenDrawerEntry}
563
+ <Drawer position="bottom" onHide={async () => { childrenDrawerEntry = null; }}>
564
+ <div class="flex h-12 items-center gap-4 border-b px-4 shrink-0">
565
+ <Button
566
+ variant="outline"
567
+ onclick={() => { childrenDrawerEntry = null; }}
568
+ class="h-8 w-8 rounded-full text-xs font-normal"
569
+ Icon={ArrowLeft}
570
+ ></Button>
571
+ <div class="flex items-center gap-2 text-sm">
572
+ <span>Children of</span>
573
+ <span class="rounded-md border bg-muted px-2 py-0.5">{collectionName} #{childrenDrawerEntry.id}</span>
574
+ </div>
575
+ </div>
576
+ <div class="flex-1 overflow-y-auto">
577
+ <ListViewChildren {collectionName} recordId={String(childrenDrawerEntry.id)} />
578
+ </div>
579
+ </Drawer>
580
+ {/if}
@@ -20,11 +20,13 @@
20
20
  // • anything else → explicit $and/$or arrays.
21
21
 
22
22
  import * as Select from "../ui/select/index.js";
23
+ import * as Popover from "../ui/popover/index.js";
23
24
  import Button from "../ui/button/button.svelte";
24
- import { Plus, X, Boxes, Settings2 } from "lucide-svelte";
25
+ import { ChevronDown, Plus, X, Boxes, Settings2 } from "lucide-svelte";
25
26
  import { getStudioContext } from "../../context";
26
27
  import { getFieldIcon } from "./utils";
27
28
  import { getFieldRelationTarget } from "../../relations";
29
+ import FieldPicker from "./fieldPicker.svelte";
28
30
 
29
31
  const { ctx } = getStudioContext();
30
32
 
@@ -43,6 +45,9 @@
43
45
  isEmpty = $bindable(true),
44
46
  }: Props = $props();
45
47
 
48
+ // Per-rule open state for the field-picker popovers.
49
+ let fieldPickerOpen = $state<Record<string, boolean>>({});
50
+
46
51
  type OperatorDef = {
47
52
  value: string;
48
53
  label: string;
@@ -471,30 +476,28 @@
471
476
  {@const currentOp = ops.find((o) => o.value === rule.operator) ?? ops[0]}
472
477
  {@const kind = getFieldKind(rule.field)}
473
478
  {@const FieldIcon = getFieldIcon(ctx, rule.field, collectionName)}
474
- <!-- Field picker -->
475
- <Select.Root
476
- type="single"
477
- value={rule.field}
478
- onValueChange={(v) => v && patchRuleInPlace(rule.id, { field: v })}
479
- >
480
- <Select.Trigger class="bg-muted h-7 w-36 text-xs">
481
- <div class="inline-flex items-center gap-1.5">
479
+ <!-- Field picker — typeahead popover, same widget Sort uses -->
480
+ <Popover.Root bind:open={fieldPickerOpen[rule.id]}>
481
+ <Popover.Trigger
482
+ class="inline-flex h-7 w-36 items-center justify-between gap-1.5 rounded-md border bg-muted px-2 text-xs"
483
+ >
484
+ <div class="inline-flex items-center gap-1.5 truncate">
482
485
  <FieldIcon size="13" />
483
- {rule.field}
486
+ <span class="truncate">{rule.field}</span>
484
487
  </div>
485
- </Select.Trigger>
486
- <Select.Content>
487
- {#each allFieldNames as fname}
488
- {@const OptionIcon = getFieldIcon(ctx, fname, collectionName)}
489
- <Select.Item value={fname}>
490
- <div class="inline-flex items-center gap-1.5">
491
- <OptionIcon size="13" />
492
- {fname}
493
- </div>
494
- </Select.Item>
495
- {/each}
496
- </Select.Content>
497
- </Select.Root>
488
+ <ChevronDown size="13" class="text-muted-foreground shrink-0" />
489
+ </Popover.Trigger>
490
+ <Popover.Content class="w-64 p-2">
491
+ <FieldPicker
492
+ {collectionName}
493
+ placeholder="Pick a field…"
494
+ onPick={(fname: string) => {
495
+ patchRuleInPlace(rule.id, { field: fname });
496
+ fieldPickerOpen[rule.id] = false;
497
+ }}
498
+ />
499
+ </Popover.Content>
500
+ </Popover.Root>
498
501
 
499
502
  <!-- Operator picker -->
500
503
  <Select.Root
@@ -26,6 +26,7 @@
26
26
  onCreate?: (changes: Changes) => void;
27
27
  showImport?: boolean;
28
28
  showFilter?: boolean;
29
+ showCreate?: boolean;
29
30
  loading?: boolean;
30
31
  left?: Snippet<[]>;
31
32
  excludeIds?: (string | number)[];
@@ -40,6 +41,7 @@
40
41
  onCreate,
41
42
  showImport = true,
42
43
  showFilter = true,
44
+ showCreate = true,
43
45
  loading = false,
44
46
  left,
45
47
  excludeIds = [],
@@ -154,7 +156,7 @@
154
156
  {headerIsSmall ? "" : "Refresh"}
155
157
  {/if}
156
158
  </Button>
157
- {#if showImport}
159
+ {#if showImport && showCreate}
158
160
  <CanAccess collection={collectionName} action="create">
159
161
  <ImportButton
160
162
  {collectionName}
@@ -183,17 +185,19 @@
183
185
  {headerIsSmall ? "" : "Link"}
184
186
  </SelectRecord>
185
187
  {/if}
186
- <CanAccess collection={collectionName} action="create">
187
- <CreateDetailViewButton
188
- {collectionName}
189
- variant="default"
190
- size="sm"
191
- Icon={Plus}
192
- onChanges={onCreate ? handleCreate : undefined}
193
- onSuccessfullSave={onCreate ? undefined : handleCreate}
194
- >
195
- {headerIsSmall ? "" : "Create"}
196
- </CreateDetailViewButton>
197
- </CanAccess>
188
+ {#if showCreate}
189
+ <CanAccess collection={collectionName} action="create">
190
+ <CreateDetailViewButton
191
+ {collectionName}
192
+ variant="default"
193
+ size="sm"
194
+ Icon={Plus}
195
+ onChanges={onCreate ? handleCreate : undefined}
196
+ onSuccessfullSave={onCreate ? undefined : handleCreate}
197
+ >
198
+ {headerIsSmall ? "" : "Create"}
199
+ </CreateDetailViewButton>
200
+ </CanAccess>
201
+ {/if}
198
202
  </div>
199
203
  </div>
@@ -10,6 +10,7 @@ interface Props {
10
10
  onCreate?: (changes: Changes) => void;
11
11
  showImport?: boolean;
12
12
  showFilter?: boolean;
13
+ showCreate?: boolean;
13
14
  loading?: boolean;
14
15
  left?: Snippet<[]>;
15
16
  excludeIds?: (string | number)[];
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getStudioContext } from "../../context";
3
- import { ChevronRight, Table, Plus, Link, ArrowLeftRight, GitFork } from "lucide-svelte";
3
+ import { Table, Plus } from "lucide-svelte";
4
4
  import DataTable from "./dataTable.svelte";
5
5
  import CreateDetailViewButton from "../detailView/create/createDetailViewButton.svelte";
6
6
  import ExtensionsComponents from "../extensionsComponents.svelte";
@@ -11,93 +11,76 @@
11
11
  interface Props {
12
12
  collectionName: string;
13
13
  recordId: string;
14
- width: number;
14
+ width?: number;
15
15
  }
16
16
 
17
- let { collectionName, recordId, width }: Props = $props();
17
+ let { collectionName, recordId, width = 0 }: Props = $props();
18
18
 
19
19
  const children = (ctx.meta.collections[collectionName]?.children ?? [])
20
20
  .filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
21
21
 
22
- let expandedRows: boolean[] = $state(new Array(children.length).fill(false));
23
- let refreshDataTable = $state(true);
24
- let tableHeaderWidth = $state(0);
22
+ let activeTab = $state(children[0]?.collection ?? '');
23
+ let refreshKey = $state(0);
24
+ let counts = $state<Record<string, number>>({});
25
+
26
+ const activeChild = $derived(children.find((c: any) => c.collection === activeTab));
25
27
  </script>
26
28
 
27
- <div class="flex" style="width: {width}px;">
28
- <div
29
- class="flex justify-center border-r bg-background"
30
- style="width: 40px"
31
- ></div>
32
- <div class="flex-1 flex flex-col">
33
- {#each children as child, index}
34
- {@const lastRow = children.length - 1 === index}
35
- <div class="overflow-hidden bg-background">
36
- <div
37
- bind:clientWidth={tableHeaderWidth}
38
- class="flex justify-between items-center gap-2 text-sm h-10 {expandedRows[index] || !lastRow ? 'border-b' : ''}"
29
+ <div class="flex flex-col h-full overflow-hidden">
30
+ <!-- Tab bar -->
31
+ <div class="flex border-b shrink-0 overflow-x-auto">
32
+ {#each children as child}
33
+ <button
34
+ class="flex items-center gap-1.5 px-4 h-10 text-xs font-medium whitespace-nowrap border-b-2 transition-colors
35
+ {activeTab === child.collection
36
+ ? 'border-foreground text-foreground'
37
+ : 'border-transparent text-muted-foreground hover:text-foreground'}"
38
+ onclick={() => { activeTab = child.collection; }}
39
+ >
40
+ <Table size="11" class="opacity-50" />
41
+ {child.collection}
42
+ {#if counts[child.collection] !== undefined}
43
+ <span class="rounded-full bg-muted px-1.5 py-0.5 text-[0.65rem] text-muted-foreground">{counts[child.collection]}</span>
44
+ {/if}
45
+ </button>
46
+ {/each}
47
+ <!-- Create button for FK children -->
48
+ {#if activeChild?.type === "fk"}
49
+ <div class="ml-auto flex items-center px-2">
50
+ <CreateDetailViewButton
51
+ collectionName={activeChild.collection}
52
+ variant="ghost"
53
+ size="sm"
54
+ Icon={Plus}
55
+ values={{ [activeChild.field]: recordId }}
56
+ onSuccessfullSave={async () => { refreshKey++; }}
39
57
  >
40
- <button
41
- class="flex gap-2 px-2 flex-1 h-full items-center"
42
- onclick={() => { expandedRows[index] = !expandedRows[index]; }}
58
+ Create
59
+ </CreateDetailViewButton>
60
+ </div>
61
+ {/if}
62
+ </div>
63
+
64
+ <!-- Tab content — render all but hide inactive so counts load for all tabs -->
65
+ <div class="flex-1 overflow-hidden relative">
66
+ {#each children as child}
67
+ <div class="absolute inset-0 {activeTab === child.collection ? '' : 'invisible pointer-events-none'}">
68
+ {#key refreshKey}
69
+ <ExtensionsComponents
70
+ name="listView.entry.children.{child.collection}"
71
+ collectionName={child.collection}
72
+ searchParams={{ children_of: collectionName, parent_id: recordId }}
73
+ utils={getExtensionUtils(lobb, ctx)}
43
74
  >
44
- <ChevronRight
45
- size="17.5"
46
- class="text-muted-foreground transition-transform"
47
- style={expandedRows[index] ? "transform: rotate(90deg);" : "transform: rotate(0deg);"}
75
+ <DataTable
76
+ collectionName={child.collection}
77
+ searchParams={{ children_of: collectionName, parent_id: recordId }}
78
+ showDelete={child.type === "fk" || child.type === "m2m"}
79
+ tableProps={{ showLastRowBorder: true, showLastColumnBorder: true }}
80
+ onDataLoad={(total) => { counts[child.collection] = total; }}
48
81
  />
49
- <Table size="17.5" class="text-muted-foreground" />
50
- <div class="text-muted-foreground">{child.collection}</div>
51
- {#if child.type === "fk"}
52
- <span title="Direct (FK)"><Link size="13" class="text-muted-foreground/50" /></span>
53
- {:else if child.type === "m2m"}
54
- <span title="Many to Many"><ArrowLeftRight size="13" class="text-muted-foreground/50" /></span>
55
- {:else if child.type === "polymorphic"}
56
- <span title="Polymorphic"><GitFork size="13" class="text-muted-foreground/50" /></span>
57
- {/if}
58
- </button>
59
- {#if child.type === "fk"}
60
- <div class="flex items-center px-2">
61
- <CreateDetailViewButton
62
- collectionName={child.collection}
63
- variant="ghost"
64
- size="sm"
65
- Icon={Plus}
66
- values={{ [child.field]: recordId }}
67
- onSuccessfullSave={async () => { refreshDataTable = !refreshDataTable; }}
68
- >
69
- Create
70
- </CreateDetailViewButton>
71
- </div>
72
- {/if}
73
- </div>
74
- {#if expandedRows[index]}
75
- <div class="flex max-h-96 overflow-auto {lastRow ? '' : 'border-b'}">
76
- <div
77
- class="border-r"
78
- style="width: 100vw; max-width: 40px"
79
- ></div>
80
- <div class="flex-1" style="width: {tableHeaderWidth - 40}px;">
81
- {#key refreshDataTable}
82
- <ExtensionsComponents
83
- name="listView.entry.children.{child.collection}"
84
- collectionName={child.collection}
85
- searchParams={{ children_of: collectionName, parent_id: recordId }}
86
- utils={getExtensionUtils(lobb, ctx)}
87
- >
88
- <DataTable
89
- collectionName={child.collection}
90
- searchParams={{ children_of: collectionName, parent_id: recordId }}
91
- showHeader={false}
92
- showFooter={false}
93
- showDelete={child.type === "fk"}
94
- tableProps={{ showLastRowBorder: false, showLastColumnBorder: false, showCheckboxes: false }}
95
- />
96
- </ExtensionsComponents>
97
- {/key}
98
- </div>
99
- </div>
100
- {/if}
82
+ </ExtensionsComponents>
83
+ {/key}
101
84
  </div>
102
85
  {/each}
103
86
  </div>
@@ -1,7 +1,7 @@
1
1
  interface Props {
2
2
  collectionName: string;
3
3
  recordId: string;
4
- width: number;
4
+ width?: number;
5
5
  }
6
6
  declare const ListViewChildren: import("svelte").Component<Props, {}, "">;
7
7
  type ListViewChildren = ReturnType<typeof ListViewChildren>;
@@ -15,8 +15,6 @@
15
15
  export interface TableProps {
16
16
  data: Entry[];
17
17
  columns?: Column[];
18
- showCollapsible?: boolean;
19
-
20
18
  // sorting
21
19
  sort?: Record<string, "asc" | "desc">;
22
20
  localSorting?: boolean;
@@ -33,15 +31,19 @@
33
31
 
34
32
  // snippets
35
33
  overrideCell?: Snippet<[any, Column, Entry]>;
34
+ preTools?: Snippet<[Entry, number]>;
36
35
  tools?: Snippet<[Entry, number]>;
37
36
  rowActions?: Snippet<[Entry, number]>;
38
- collapsible?: Snippet<[Entry, number]>;
39
-
40
37
  // other
41
38
  parentWidth?: number;
42
39
  select?: Select;
43
40
  tableWidth?: number;
44
41
 
42
+ // Hide the left sticky tools column entirely when the caller knows
43
+ // nothing inside it would render (e.g. select-mode drawer with no
44
+ // checkboxes, no edit/delete buttons, and no children-network icon).
45
+ showLeftTools?: boolean;
46
+
45
47
  // recording mode row visuals — cellIndex 0 = tools cell, 1+ = data/action cells
46
48
  onCellClass?: (entry: Entry, cellIndex: number) => string;
47
49
  }
@@ -51,7 +53,6 @@
51
53
  import {
52
54
  ArrowDownNarrowWide,
53
55
  ArrowUpWideNarrow,
54
- ChevronRight,
55
56
  CircleOff,
56
57
  } from "lucide-svelte";
57
58
  import Checkbox from "../ui/checkbox/checkbox.svelte";
@@ -66,7 +67,6 @@
66
67
  id: key,
67
68
  };
68
69
  }),
69
- showCollapsible = false,
70
70
  sort = $bindable({}),
71
71
  localSorting = false,
72
72
  selectedRecords = $bindable(),
@@ -77,20 +77,19 @@
77
77
  headerBorderTop = false,
78
78
  parentWidth,
79
79
  overrideCell,
80
+ preTools,
80
81
  tools,
81
82
  rowActions,
82
- collapsible,
83
83
  select,
84
84
  tableWidth = $bindable(),
85
+ showLeftTools = true,
85
86
  onCellClass,
86
87
  }: TableProps = $props();
87
88
 
88
- let expandedRows: boolean[] = $state(new Array(data.length).fill(false));
89
-
90
89
  // calculate columns count
91
- const toolsExists = selectedRecords || tools ? 1 : 0;
90
+ const toolsExists = $derived(showLeftTools && (selectedRecords || tools || preTools) ? 1 : 0);
92
91
  const rowActionsExists = $derived(rowActions ? 1 : 0);
93
- const columnsLength = columns.length + toolsExists;
92
+ const columnsLength = $derived(columns.length + toolsExists);
94
93
 
95
94
  // set table width
96
95
  let columnsWidths: number[] = $state([]);
@@ -173,13 +172,14 @@
173
172
  </script>
174
173
 
175
174
  <div
175
+ class="min-w-max"
176
176
  style="
177
177
  display: grid;
178
- grid-template-columns: minmax(auto, 10rem) repeat({columnsLength - 1}, minmax(auto, 15rem)){rowActionsExists ? ' minmax(auto, 7.5rem)' : ''};
178
+ grid-template-columns: fit-content(10rem) repeat({columnsLength - 1}, fit-content(15rem)){rowActionsExists ? ' fit-content(7.5rem)' : ''};
179
179
  grid-template-rows: 2.5rem;
180
180
  "
181
181
  >
182
- {#if selectedRecords || tools}
182
+ {#if showLeftTools && (selectedRecords || tools || preTools)}
183
183
  <div
184
184
  bind:clientWidth={columnsWidths[0]}
185
185
  class="
@@ -187,13 +187,9 @@
187
187
  flex items-center p-2.5 text-xs h-10
188
188
  border-r border-b gap-2
189
189
  {headerBorderTop ? 'border-t' : ''}
190
- bg-muted/50
190
+ bg-muted
191
191
  "
192
192
  >
193
- <!-- collapsable toggle -->
194
- {#if showCollapsible}
195
- <div class="w-[20px]"></div>
196
- {/if}
197
193
  {#if selectedRecords && showCheckboxes}
198
194
  <Checkbox
199
195
  class="border-muted-foreground hover:border-foreground"
@@ -212,7 +208,7 @@
212
208
  class="
213
209
  sticky top-0 z-10
214
210
  flex items-center p-2.5 text-xs h-10
215
- bg-muted/50
211
+ bg-muted
216
212
  {lastColumn && !showLastColumnBorder ? '' : 'border-r'}
217
213
  border-b gap-2
218
214
  {headerBorderTop ? 'border-t' : ''}
@@ -239,7 +235,7 @@
239
235
  class="
240
236
  sticky top-0 right-0 z-20
241
237
  flex items-center p-2.5 h-10
242
- bg-muted/50
238
+ bg-muted
243
239
  border-l border-b
244
240
  {headerBorderTop ? 'border-t' : ''}
245
241
  "
@@ -249,25 +245,12 @@
249
245
  {#each data as entry, index}
250
246
  {@const isDisabled = Boolean(entry.__disabled)}
251
247
  {@const lastRow = data.length - 1 === index}
252
- {#if selectedRecords || tools}
248
+ {#if showLeftTools && (selectedRecords || tools || preTools)}
253
249
  <div
254
250
  class="sticky left-0 flex items-center p-2.5 text-xs h-10 bg-card border-r gap-2 {onCellClass?.(entry, 0) ?? ''}"
255
251
  >
256
- <!-- collapsable toggle -->
257
- {#if showCollapsible}
258
- <Button
259
- variant="ghost"
260
- class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent transition-transform"
261
- style={expandedRows[index]
262
- ? "transform: rotate(90deg);"
263
- : "transform: rotate(0deg);"}
264
- Icon={ChevronRight}
265
- onclick={() => {
266
- expandedRows[index] = !expandedRows[index];
267
- expandedRows = [...expandedRows];
268
- }}
269
- disabled={isDisabled}
270
- ></Button>
252
+ {#if preTools && !isDisabled}
253
+ {@render preTools(entry, index)}
271
254
  {/if}
272
255
  {#if selectedRecords && showCheckboxes}
273
256
  <Checkbox
@@ -309,31 +292,7 @@
309
292
  {@render rowActions?.(entry, index)}
310
293
  </div>
311
294
  {/if}
312
- <!-- nested data -->
313
- <div
314
- style="grid-column: span {columnsLength + rowActionsExists};"
315
- class="
316
- {!showLastColumnBorder ? '' : 'border-r'}
317
- {lastRow && !showLastRowBorder ? '' : 'border-b'}
318
- "
319
- >
320
- <div
321
- style="
322
- {parentWidth ? `width: ${parentWidth}px` : ''};
323
- max-width: 100vw;
324
- {expandedRows[index] ? '' : 'height: 0px;'}
325
- "
326
- class="
327
- sticky left-0 top-0 overflow-auto bg-muted
328
-
329
- {expandedRows[index] ? 'border-t' : ''}
330
- "
331
- >
332
- {#if collapsible && expandedRows[index]}
333
- {@render collapsible(entry, index)}
334
- {/if}
335
- </div>
336
- </div>
295
+ <div style="grid-column: span {columnsLength + rowActionsExists};" class="{!showLastColumnBorder ? '' : 'border-r'} {lastRow && !showLastRowBorder ? '' : 'border-b'}"></div>
337
296
  {/each}
338
297
  {/if}
339
298
  </div>
@@ -11,7 +11,6 @@ interface Select {
11
11
  export interface TableProps {
12
12
  data: Entry[];
13
13
  columns?: Column[];
14
- showCollapsible?: boolean;
15
14
  sort?: Record<string, "asc" | "desc">;
16
15
  localSorting?: boolean;
17
16
  selectedRecords?: Array<any>;
@@ -21,12 +20,13 @@ export interface TableProps {
21
20
  showLastColumnBorder?: boolean;
22
21
  headerBorderTop?: boolean;
23
22
  overrideCell?: Snippet<[any, Column, Entry]>;
23
+ preTools?: Snippet<[Entry, number]>;
24
24
  tools?: Snippet<[Entry, number]>;
25
25
  rowActions?: Snippet<[Entry, number]>;
26
- collapsible?: Snippet<[Entry, number]>;
27
26
  parentWidth?: number;
28
27
  select?: Select;
29
28
  tableWidth?: number;
29
+ showLeftTools?: boolean;
30
30
  onCellClass?: (entry: Entry, cellIndex: number) => string;
31
31
  }
32
32
  import type { Snippet } from "svelte";