@shotleybuilder/svelte-gridlite-kit 0.1.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 (43) hide show
  1. package/README.md +260 -0
  2. package/dist/GridLite.svelte +1361 -0
  3. package/dist/GridLite.svelte.d.ts +42 -0
  4. package/dist/components/CellContextMenu.svelte +209 -0
  5. package/dist/components/CellContextMenu.svelte.d.ts +28 -0
  6. package/dist/components/ColumnMenu.svelte +234 -0
  7. package/dist/components/ColumnMenu.svelte.d.ts +29 -0
  8. package/dist/components/ColumnPicker.svelte +403 -0
  9. package/dist/components/ColumnPicker.svelte.d.ts +29 -0
  10. package/dist/components/FilterBar.svelte +390 -0
  11. package/dist/components/FilterBar.svelte.d.ts +38 -0
  12. package/dist/components/FilterCondition.svelte +643 -0
  13. package/dist/components/FilterCondition.svelte.d.ts +35 -0
  14. package/dist/components/GroupBar.svelte +463 -0
  15. package/dist/components/GroupBar.svelte.d.ts +33 -0
  16. package/dist/components/RowDetailModal.svelte +213 -0
  17. package/dist/components/RowDetailModal.svelte.d.ts +25 -0
  18. package/dist/components/SortBar.svelte +232 -0
  19. package/dist/components/SortBar.svelte.d.ts +30 -0
  20. package/dist/components/SortCondition.svelte +129 -0
  21. package/dist/components/SortCondition.svelte.d.ts +30 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.js +29 -0
  24. package/dist/query/builder.d.ts +160 -0
  25. package/dist/query/builder.js +432 -0
  26. package/dist/query/live.d.ts +50 -0
  27. package/dist/query/live.js +118 -0
  28. package/dist/query/schema.d.ts +30 -0
  29. package/dist/query/schema.js +75 -0
  30. package/dist/state/migrations.d.ts +29 -0
  31. package/dist/state/migrations.js +113 -0
  32. package/dist/state/views.d.ts +54 -0
  33. package/dist/state/views.js +130 -0
  34. package/dist/styles/gridlite.css +966 -0
  35. package/dist/types.d.ts +164 -0
  36. package/dist/types.js +2 -0
  37. package/dist/utils/filters.d.ts +14 -0
  38. package/dist/utils/filters.js +49 -0
  39. package/dist/utils/formatters.d.ts +16 -0
  40. package/dist/utils/formatters.js +39 -0
  41. package/dist/utils/fuzzy.d.ts +47 -0
  42. package/dist/utils/fuzzy.js +142 -0
  43. package/package.json +76 -0
@@ -0,0 +1,463 @@
1
+ <script>export let columns;
2
+ export let columnConfigs = [];
3
+ export let grouping = [];
4
+ export let onGroupingChange;
5
+ export let isExpanded = false;
6
+ export let onExpandedChange = void 0;
7
+ const MAX_LEVELS = 3;
8
+ const aggregateFunctions = [
9
+ { value: "count", label: "Count" },
10
+ { value: "sum", label: "Sum" },
11
+ { value: "avg", label: "Average" },
12
+ { value: "min", label: "Min" },
13
+ { value: "max", label: "Max" }
14
+ ];
15
+ function getColumnLabel(col) {
16
+ const cfg = columnConfigs.find((c) => c.name === col.name);
17
+ return cfg?.label ?? col.name;
18
+ }
19
+ function setExpanded(value) {
20
+ isExpanded = value;
21
+ if (onExpandedChange) {
22
+ onExpandedChange(value);
23
+ }
24
+ }
25
+ function addGroup() {
26
+ if (grouping.length >= MAX_LEVELS) return;
27
+ onGroupingChange([...grouping, { column: "" }]);
28
+ setExpanded(true);
29
+ }
30
+ function updateGroupColumn(index, column) {
31
+ const newGrouping = [...grouping];
32
+ newGrouping[index] = { ...newGrouping[index], column };
33
+ onGroupingChange(newGrouping);
34
+ }
35
+ function removeGroup(index) {
36
+ const newGrouping = grouping.filter((_, i) => i !== index);
37
+ onGroupingChange(newGrouping);
38
+ if (newGrouping.length === 0) {
39
+ setExpanded(false);
40
+ }
41
+ }
42
+ function clearAllGroups() {
43
+ onGroupingChange([]);
44
+ setExpanded(false);
45
+ }
46
+ function addAggregation(groupIndex) {
47
+ const newGrouping = [...grouping];
48
+ const group = { ...newGrouping[groupIndex] };
49
+ const agg = { column: "", function: "count" };
50
+ group.aggregations = [...group.aggregations ?? [], agg];
51
+ newGrouping[groupIndex] = group;
52
+ onGroupingChange(newGrouping);
53
+ }
54
+ function updateAggregation(groupIndex, aggIndex, updates) {
55
+ const newGrouping = [...grouping];
56
+ const group = { ...newGrouping[groupIndex] };
57
+ const aggs = [...group.aggregations ?? []];
58
+ aggs[aggIndex] = { ...aggs[aggIndex], ...updates };
59
+ group.aggregations = aggs;
60
+ newGrouping[groupIndex] = group;
61
+ onGroupingChange(newGrouping);
62
+ }
63
+ function removeAggregation(groupIndex, aggIndex) {
64
+ const newGrouping = [...grouping];
65
+ const group = { ...newGrouping[groupIndex] };
66
+ group.aggregations = (group.aggregations ?? []).filter((_, i) => i !== aggIndex);
67
+ newGrouping[groupIndex] = group;
68
+ onGroupingChange(newGrouping);
69
+ }
70
+ function handleAggFunctionChange(groupIndex, aggIndex, value) {
71
+ const updates = {};
72
+ updates.function = value;
73
+ updateAggregation(groupIndex, aggIndex, updates);
74
+ }
75
+ $: numericColumns = columns.filter((col) => {
76
+ const cfg = columnConfigs.find((c) => c.name === col.name);
77
+ const dt = cfg?.dataType ?? col.dataType;
78
+ return dt === "number";
79
+ });
80
+ $: hasGroups = grouping.length > 0;
81
+ $: validGroupCount = grouping.filter((g) => g.column !== "").length;
82
+ $: canAddMore = grouping.length < MAX_LEVELS;
83
+ </script>
84
+
85
+ <div class="group-bar">
86
+ <button class="group-toggle-btn" on:click={() => setExpanded(!isExpanded)}>
87
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
88
+ <path
89
+ stroke-linecap="round"
90
+ stroke-linejoin="round"
91
+ stroke-width="2"
92
+ d="M4 6h16M4 10h16M4 14h16M4 18h16"
93
+ />
94
+ </svg>
95
+ Group
96
+ {#if validGroupCount > 0}
97
+ <span class="group-badge">{validGroupCount}</span>
98
+ {/if}
99
+ <svg
100
+ class="chevron"
101
+ class:expanded={isExpanded}
102
+ fill="none"
103
+ stroke="currentColor"
104
+ viewBox="0 0 24 24"
105
+ >
106
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
107
+ </svg>
108
+ </button>
109
+
110
+ {#if isExpanded}
111
+ <div class="group-panel">
112
+ {#if hasGroups}
113
+ <div class="group-header">
114
+ <span class="group-label">Group by</span>
115
+ <button class="clear-all-btn" on:click={clearAllGroups}> Clear all </button>
116
+ </div>
117
+
118
+ <div class="group-levels">
119
+ {#each grouping as group, index (index)}
120
+ <div class="group-level">
121
+ <div class="group-level-row">
122
+ <select
123
+ class="field-select"
124
+ value={group.column}
125
+ on:change={(e) => updateGroupColumn(index, e.currentTarget.value)}
126
+ >
127
+ <option value="">Select field...</option>
128
+ {#each columns as column}
129
+ <option value={column.name}>
130
+ {getColumnLabel(column)}
131
+ </option>
132
+ {/each}
133
+ </select>
134
+
135
+ <button class="remove-btn" on:click={() => removeGroup(index)} title="Remove group" type="button">
136
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137
+ <path
138
+ stroke-linecap="round"
139
+ stroke-linejoin="round"
140
+ stroke-width="2"
141
+ d="M6 18L18 6M6 6l12 12"
142
+ />
143
+ </svg>
144
+ </button>
145
+ </div>
146
+
147
+ {#if group.column}
148
+ <!-- Aggregations for this group level -->
149
+ {#if group.aggregations && group.aggregations.length > 0}
150
+ <div class="aggregations">
151
+ {#each group.aggregations as agg, aggIndex}
152
+ <div class="aggregation-row">
153
+ <select
154
+ class="agg-function-select"
155
+ value={agg.function}
156
+ on:change={(e) => handleAggFunctionChange(index, aggIndex, e.currentTarget.value)}
157
+ >
158
+ {#each aggregateFunctions as fn}
159
+ <option value={fn.value}>{fn.label}</option>
160
+ {/each}
161
+ </select>
162
+
163
+ <select
164
+ class="agg-column-select"
165
+ value={agg.column}
166
+ on:change={(e) =>
167
+ updateAggregation(index, aggIndex, {
168
+ column: e.currentTarget.value
169
+ })}
170
+ >
171
+ <option value="">Select column...</option>
172
+ {#if agg.function === 'count'}
173
+ <option value="*">All rows (*)</option>
174
+ {/if}
175
+ {#each agg.function === 'sum' || agg.function === 'avg' ? numericColumns : columns as col}
176
+ <option value={col.name}>{getColumnLabel(col)}</option>
177
+ {/each}
178
+ </select>
179
+
180
+ <button
181
+ class="remove-btn small"
182
+ on:click={() => removeAggregation(index, aggIndex)}
183
+ title="Remove aggregation"
184
+ type="button"
185
+ >
186
+ <svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
187
+ <path
188
+ stroke-linecap="round"
189
+ stroke-linejoin="round"
190
+ stroke-width="2"
191
+ d="M6 18L18 6M6 6l12 12"
192
+ />
193
+ </svg>
194
+ </button>
195
+ </div>
196
+ {/each}
197
+ </div>
198
+ {/if}
199
+
200
+ <button
201
+ class="add-agg-btn"
202
+ on:click={() => addAggregation(index)}
203
+ type="button"
204
+ >
205
+ + Add aggregation
206
+ </button>
207
+ {/if}
208
+ </div>
209
+ {/each}
210
+ </div>
211
+ {/if}
212
+
213
+ {#if canAddMore}
214
+ <button class="add-group-btn" on:click={addGroup}>
215
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
216
+ <path
217
+ stroke-linecap="round"
218
+ stroke-linejoin="round"
219
+ stroke-width="2"
220
+ d="M12 6v6m0 0v6m0-6h6m-6 0H6"
221
+ />
222
+ </svg>
223
+ {hasGroups ? 'Add subgroup' : 'Add group'}
224
+ </button>
225
+ {/if}
226
+ </div>
227
+ {/if}
228
+ </div>
229
+
230
+ <style>
231
+ .group-bar {
232
+ position: relative;
233
+ }
234
+
235
+ .group-toggle-btn {
236
+ display: inline-flex;
237
+ align-items: center;
238
+ gap: 0.5rem;
239
+ padding: 0.5rem 1rem;
240
+ font-size: 0.875rem;
241
+ font-weight: 500;
242
+ color: #374151;
243
+ background: white;
244
+ border: 1px solid #d1d5db;
245
+ border-radius: 0.375rem;
246
+ cursor: pointer;
247
+ transition: all 0.2s;
248
+ }
249
+
250
+ .group-toggle-btn:hover {
251
+ background: #f9fafb;
252
+ border-color: #9ca3af;
253
+ }
254
+
255
+ .group-badge {
256
+ display: inline-flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ min-width: 1.25rem;
260
+ height: 1.25rem;
261
+ padding: 0 0.375rem;
262
+ font-size: 0.75rem;
263
+ font-weight: 600;
264
+ color: white;
265
+ background: #059669;
266
+ border-radius: 0.75rem;
267
+ }
268
+
269
+ .chevron {
270
+ width: 1rem;
271
+ height: 1rem;
272
+ transition: transform 0.2s;
273
+ }
274
+
275
+ .chevron.expanded {
276
+ transform: rotate(180deg);
277
+ }
278
+
279
+ .group-panel {
280
+ position: absolute;
281
+ top: calc(100% + 0.5rem);
282
+ left: 0;
283
+ z-index: 20;
284
+ min-width: 450px;
285
+ padding: 1rem;
286
+ background: white;
287
+ border: 1px solid #e5e7eb;
288
+ border-radius: 0.5rem;
289
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
290
+ }
291
+
292
+ .group-header {
293
+ display: flex;
294
+ justify-content: space-between;
295
+ align-items: center;
296
+ margin-bottom: 0.75rem;
297
+ }
298
+
299
+ .group-label {
300
+ font-size: 0.875rem;
301
+ font-weight: 600;
302
+ color: #374151;
303
+ }
304
+
305
+ .clear-all-btn {
306
+ font-size: 0.75rem;
307
+ color: #6b7280;
308
+ background: none;
309
+ border: none;
310
+ cursor: pointer;
311
+ padding: 0.25rem 0.5rem;
312
+ border-radius: 0.25rem;
313
+ transition: all 0.2s;
314
+ }
315
+
316
+ .clear-all-btn:hover {
317
+ color: #dc2626;
318
+ background: #fee2e2;
319
+ }
320
+
321
+ .group-levels {
322
+ display: flex;
323
+ flex-direction: column;
324
+ gap: 0.75rem;
325
+ margin-bottom: 0.75rem;
326
+ }
327
+
328
+ .group-level {
329
+ padding: 0.5rem;
330
+ background: #f9fafb;
331
+ border-radius: 0.375rem;
332
+ }
333
+
334
+ .group-level-row {
335
+ display: flex;
336
+ align-items: center;
337
+ gap: 0.5rem;
338
+ }
339
+
340
+ .field-select {
341
+ flex: 1;
342
+ padding: 0.375rem 0.75rem;
343
+ font-size: 0.875rem;
344
+ border: 1px solid #d1d5db;
345
+ border-radius: 0.375rem;
346
+ background: white;
347
+ }
348
+
349
+ .field-select:focus {
350
+ outline: none;
351
+ border-color: #059669;
352
+ box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.1);
353
+ }
354
+
355
+ /* Aggregation rows */
356
+ .aggregations {
357
+ display: flex;
358
+ flex-direction: column;
359
+ gap: 0.375rem;
360
+ margin-top: 0.5rem;
361
+ padding-left: 1rem;
362
+ }
363
+
364
+ .aggregation-row {
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 0.375rem;
368
+ }
369
+
370
+ .agg-function-select,
371
+ .agg-column-select {
372
+ padding: 0.25rem 0.5rem;
373
+ font-size: 0.8125rem;
374
+ border: 1px solid #d1d5db;
375
+ border-radius: 0.25rem;
376
+ background: white;
377
+ }
378
+
379
+ .agg-function-select {
380
+ min-width: 90px;
381
+ }
382
+
383
+ .agg-column-select {
384
+ flex: 1;
385
+ min-width: 120px;
386
+ }
387
+
388
+ .agg-function-select:focus,
389
+ .agg-column-select:focus {
390
+ outline: none;
391
+ border-color: #059669;
392
+ box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.1);
393
+ }
394
+
395
+ .add-agg-btn {
396
+ display: inline-block;
397
+ margin-top: 0.375rem;
398
+ margin-left: 1rem;
399
+ padding: 0.25rem 0.5rem;
400
+ font-size: 0.75rem;
401
+ color: #6b7280;
402
+ background: none;
403
+ border: none;
404
+ cursor: pointer;
405
+ border-radius: 0.25rem;
406
+ transition: all 0.15s;
407
+ }
408
+
409
+ .add-agg-btn:hover {
410
+ color: #059669;
411
+ background: #d1fae5;
412
+ }
413
+
414
+ .remove-btn {
415
+ flex-shrink: 0;
416
+ padding: 0.375rem;
417
+ background: none;
418
+ border: none;
419
+ color: #6b7280;
420
+ cursor: pointer;
421
+ border-radius: 0.25rem;
422
+ transition: all 0.2s;
423
+ }
424
+
425
+ .remove-btn:hover {
426
+ background: #fee2e2;
427
+ color: #dc2626;
428
+ }
429
+
430
+ .remove-btn.small {
431
+ padding: 0.25rem;
432
+ }
433
+
434
+ .add-group-btn {
435
+ display: inline-flex;
436
+ align-items: center;
437
+ gap: 0.5rem;
438
+ padding: 0.5rem 0.75rem;
439
+ font-size: 0.875rem;
440
+ font-weight: 500;
441
+ color: #059669;
442
+ background: white;
443
+ border: 1px dashed #059669;
444
+ border-radius: 0.375rem;
445
+ cursor: pointer;
446
+ transition: all 0.2s;
447
+ }
448
+
449
+ .add-group-btn:hover {
450
+ background: #d1fae5;
451
+ border-style: solid;
452
+ }
453
+
454
+ .icon {
455
+ width: 1rem;
456
+ height: 1rem;
457
+ }
458
+
459
+ .icon-sm {
460
+ width: 0.875rem;
461
+ height: 0.875rem;
462
+ }
463
+ </style>
@@ -0,0 +1,33 @@
1
+ import { SvelteComponent } from "svelte";
2
+ /**
3
+ * GroupBar — Multi-level grouping controls with aggregation support.
4
+ *
5
+ * Emits GroupConfig[] which maps to GROUP BY with SUM/AVG/COUNT/MIN/MAX
6
+ * in the query builder. Up to 3 nested group levels.
7
+ *
8
+ * PGLite-native: uses GroupConfig with AggregationConfig for SQL aggregates,
9
+ * unlike svelte-table-kit which used simple string[] grouping.
10
+ */
11
+ import type { GroupConfig, ColumnMetadata, ColumnConfig } from '../types.js';
12
+ declare const __propDef: {
13
+ props: {
14
+ columns: ColumnMetadata[];
15
+ columnConfigs?: ColumnConfig[];
16
+ grouping?: GroupConfig[];
17
+ onGroupingChange: (grouping: GroupConfig[]) => void;
18
+ isExpanded?: boolean;
19
+ onExpandedChange?: ((expanded: boolean) => void) | undefined;
20
+ };
21
+ events: {
22
+ [evt: string]: CustomEvent<any>;
23
+ };
24
+ slots: {};
25
+ exports?: {} | undefined;
26
+ bindings?: string | undefined;
27
+ };
28
+ export type GroupBarProps = typeof __propDef.props;
29
+ export type GroupBarEvents = typeof __propDef.events;
30
+ export type GroupBarSlots = typeof __propDef.slots;
31
+ export default class GroupBar extends SvelteComponent<GroupBarProps, GroupBarEvents, GroupBarSlots> {
32
+ }
33
+ export {};
@@ -0,0 +1,213 @@
1
+ <script>import { onMount, onDestroy } from "svelte";
2
+ export let isOpen = false;
3
+ export let hasPrev = false;
4
+ export let hasNext = false;
5
+ export let onClose;
6
+ export let onPrev = void 0;
7
+ export let onNext = void 0;
8
+ let modalElement = null;
9
+ let previouslyFocused = null;
10
+ let mounted = false;
11
+ function handleClose() {
12
+ onClose();
13
+ }
14
+ function handlePrev() {
15
+ if (hasPrev && onPrev) onPrev();
16
+ }
17
+ function handleNext() {
18
+ if (hasNext && onNext) onNext();
19
+ }
20
+ function handleKeydown(event) {
21
+ if (event.key === "Escape") {
22
+ onClose();
23
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
24
+ event.preventDefault();
25
+ if (hasPrev && onPrev) onPrev();
26
+ } else if (event.key === "ArrowRight" || event.key === "ArrowDown") {
27
+ event.preventDefault();
28
+ if (hasNext && onNext) onNext();
29
+ }
30
+ }
31
+ function handleBackdropClick(event) {
32
+ if (event.target === event.currentTarget) {
33
+ onClose();
34
+ }
35
+ }
36
+ $: if (mounted) {
37
+ if (isOpen) {
38
+ previouslyFocused = document.activeElement;
39
+ document.addEventListener("keydown", handleKeydown);
40
+ document.body.style.overflow = "hidden";
41
+ requestAnimationFrame(() => {
42
+ modalElement?.focus();
43
+ });
44
+ } else {
45
+ document.removeEventListener("keydown", handleKeydown);
46
+ document.body.style.overflow = "";
47
+ if (previouslyFocused) {
48
+ previouslyFocused.focus();
49
+ previouslyFocused = null;
50
+ }
51
+ }
52
+ }
53
+ onMount(() => {
54
+ mounted = true;
55
+ });
56
+ onDestroy(() => {
57
+ if (mounted) {
58
+ document.removeEventListener("keydown", handleKeydown);
59
+ document.body.style.overflow = "";
60
+ }
61
+ });
62
+ </script>
63
+
64
+ {#if isOpen}
65
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
66
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
67
+ <div class="row-detail-backdrop" on:click={handleBackdropClick}>
68
+ <div
69
+ class="row-detail-modal"
70
+ bind:this={modalElement}
71
+ role="dialog"
72
+ aria-modal="true"
73
+ tabindex="-1"
74
+ >
75
+ <div class="row-detail-header">
76
+ <div class="row-detail-nav">
77
+ <button
78
+ class="nav-btn"
79
+ disabled={!hasPrev}
80
+ on:click={handlePrev}
81
+ aria-label="Previous row"
82
+ title="Previous row"
83
+ >
84
+ <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
+ <path
86
+ stroke-linecap="round"
87
+ stroke-linejoin="round"
88
+ stroke-width="2"
89
+ d="M15 19l-7-7 7-7"
90
+ />
91
+ </svg>
92
+ </button>
93
+ <button
94
+ class="nav-btn"
95
+ disabled={!hasNext}
96
+ on:click={handleNext}
97
+ aria-label="Next row"
98
+ title="Next row"
99
+ >
100
+ <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
101
+ <path
102
+ stroke-linecap="round"
103
+ stroke-linejoin="round"
104
+ stroke-width="2"
105
+ d="M9 5l7 7-7 7"
106
+ />
107
+ </svg>
108
+ </button>
109
+ </div>
110
+ <button class="close-btn" on:click={handleClose} aria-label="Close" title="Close">
111
+ <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
112
+ <path
113
+ stroke-linecap="round"
114
+ stroke-linejoin="round"
115
+ stroke-width="2"
116
+ d="M6 18L18 6M6 6l12 12"
117
+ />
118
+ </svg>
119
+ </button>
120
+ </div>
121
+
122
+ <div class="row-detail-body">
123
+ <slot />
124
+ </div>
125
+ </div>
126
+ </div>
127
+ {/if}
128
+
129
+ <style>
130
+ .row-detail-backdrop {
131
+ position: fixed;
132
+ inset: 0;
133
+ z-index: 1000;
134
+ background: rgba(0, 0, 0, 0.5);
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ }
139
+
140
+ .row-detail-modal {
141
+ position: relative;
142
+ background: white;
143
+ border-radius: 0.75rem;
144
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
145
+ max-width: 48rem;
146
+ width: calc(100% - 2rem);
147
+ max-height: 80vh;
148
+ display: flex;
149
+ flex-direction: column;
150
+ outline: none;
151
+ }
152
+
153
+ .row-detail-header {
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: space-between;
157
+ padding: 0.75rem 1rem;
158
+ border-bottom: 1px solid #e5e7eb;
159
+ flex-shrink: 0;
160
+ }
161
+
162
+ .row-detail-nav {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: 0.25rem;
166
+ }
167
+
168
+ .nav-btn {
169
+ display: inline-flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ padding: 0.375rem;
173
+ background: white;
174
+ border: 1px solid #d1d5db;
175
+ border-radius: 0.375rem;
176
+ cursor: pointer;
177
+ color: #374151;
178
+ transition: background 0.15s;
179
+ }
180
+
181
+ .nav-btn:hover:not(:disabled) {
182
+ background: #f9fafb;
183
+ }
184
+
185
+ .nav-btn:disabled {
186
+ opacity: 0.4;
187
+ cursor: not-allowed;
188
+ }
189
+
190
+ .close-btn {
191
+ display: inline-flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ padding: 0.375rem;
195
+ background: none;
196
+ border: none;
197
+ border-radius: 0.375rem;
198
+ cursor: pointer;
199
+ color: #6b7280;
200
+ transition: all 0.15s;
201
+ }
202
+
203
+ .close-btn:hover {
204
+ background: #f3f4f6;
205
+ color: #374151;
206
+ }
207
+
208
+ .row-detail-body {
209
+ padding: 1.5rem;
210
+ overflow-y: auto;
211
+ flex: 1;
212
+ }
213
+ </style>