@joewinke/jatui 0.26.0 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +106 -1
- package/dist/actions/keyboardScope.d.ts +58 -0
- package/dist/actions/keyboardScope.js +110 -0
- package/dist/actions/listNav.d.ts +48 -3
- package/dist/actions/listNav.js +267 -77
- package/dist/actions/longpress.d.ts +37 -0
- package/dist/actions/longpress.js +105 -0
- package/dist/actions/stickToBottom.d.ts +66 -0
- package/dist/actions/stickToBottom.js +155 -0
- package/dist/actions/swipe.d.ts +64 -0
- package/dist/actions/swipe.js +179 -0
- package/dist/components/BottomSheet.svelte +280 -0
- package/dist/components/BottomSheet.svelte.d.ts +27 -0
- package/dist/components/ConfirmDialog.svelte +21 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +11 -1
- package/dist/components/ConfirmModal.svelte +15 -0
- package/dist/components/ConfirmModal.svelte.d.ts +5 -0
- package/dist/components/Drawer.svelte +18 -1
- package/dist/components/Drawer.svelte.d.ts +9 -1
- package/dist/components/InputDialog.svelte +21 -0
- package/dist/components/InputDialog.svelte.d.ts +11 -1
- package/dist/components/Modal.svelte +18 -1
- package/dist/components/Modal.svelte.d.ts +8 -0
- package/dist/components/basic/Empty.svelte +173 -9
- package/dist/components/basic/Empty.svelte.d.ts +50 -1
- package/dist/components/basic/ErrorState.svelte +128 -0
- package/dist/components/basic/ErrorState.svelte.d.ts +46 -0
- package/dist/components/basic/JumpToLatest.svelte +60 -0
- package/dist/components/basic/JumpToLatest.svelte.d.ts +9 -0
- package/dist/components/basic/PageHeader.svelte +216 -0
- package/dist/components/basic/PageHeader.svelte.d.ts +52 -0
- package/dist/components/basic/ProgressButton.svelte +221 -0
- package/dist/components/basic/ProgressButton.svelte.d.ts +63 -0
- package/dist/components/graph/GraphApiBridge.svelte +36 -0
- package/dist/components/graph/GraphApiBridge.svelte.d.ts +9 -0
- package/dist/components/graph/GraphCanvas.svelte +212 -27
- package/dist/components/graph/GraphCanvas.svelte.d.ts +113 -19
- package/dist/components/graph/api.d.ts +52 -0
- package/dist/components/graph/api.js +66 -0
- package/dist/components/graph/index.d.ts +2 -0
- package/dist/components/graph/index.js +1 -0
- package/dist/components/graph/theme.js +13 -0
- package/dist/components/messaging/MessageItem.svelte +5 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +7 -0
- package/dist/components/table/DataTable.svelte +452 -78
- package/dist/components/table/DataTable.svelte.d.ts +2 -2
- package/dist/components/table/ManageColumnsDropdown.svelte +466 -0
- package/dist/components/table/ManageColumnsDropdown.svelte.d.ts +26 -0
- package/dist/components/table/columnStorage.d.ts +29 -0
- package/dist/components/table/columnStorage.js +61 -0
- package/dist/components/table/groupRows.d.ts +72 -0
- package/dist/components/table/groupRows.js +118 -0
- package/dist/components/table/index.d.ts +7 -1
- package/dist/components/table/index.js +3 -0
- package/dist/components/table/types.d.ts +121 -20
- package/dist/index.d.ts +24 -2
- package/dist/index.js +36 -1
- package/dist/utils/ansiToHtml.d.ts +83 -0
- package/dist/utils/ansiToHtml.js +543 -0
- package/dist/utils/keyboardShortcutRegistry.d.ts +332 -11
- package/dist/utils/keyboardShortcutRegistry.js +1070 -76
- package/dist/utils/pollBackoff.d.ts +83 -0
- package/dist/utils/pollBackoff.js +151 -0
- package/dist/utils/snapFlick.d.ts +59 -0
- package/dist/utils/snapFlick.js +66 -0
- package/dist/video/advisors/qr.d.ts +1 -1
- package/dist/virtual/createRowWindow.d.ts +75 -0
- package/dist/virtual/createRowWindow.js +129 -0
- package/dist/virtual/index.d.ts +4 -0
- package/dist/virtual/index.js +5 -0
- package/dist/virtual/rowWindowCore.d.ts +202 -0
- package/dist/virtual/rowWindowCore.js +510 -0
- package/package.json +12 -2
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type ManageColumnsProps = {
|
|
3
|
+
/** Current column order (full ids, including hidden ones) */
|
|
4
|
+
columnOrder: string[];
|
|
5
|
+
/** Ids currently hidden */
|
|
6
|
+
hiddenColumns: Set<string>;
|
|
7
|
+
/** Resolve a column id to its display label */
|
|
8
|
+
getLabel: (id: string) => string;
|
|
9
|
+
/** Column order changed (drag-to-reorder) */
|
|
10
|
+
onReorder: (newOrder: string[]) => void;
|
|
11
|
+
/** A column's visibility was toggled */
|
|
12
|
+
onToggleVisibility: (id: string) => void;
|
|
13
|
+
/** When set, a "Reset to default" footer button is rendered */
|
|
14
|
+
onReset?: () => void;
|
|
15
|
+
/** Trigger button text */
|
|
16
|
+
label?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Render the panel with position:fixed viewport coordinates — escapes
|
|
19
|
+
* overflow-x:auto / overflow:hidden parents that would otherwise clip it
|
|
20
|
+
* (jat-hwt1h: the IDE's original was clipped by its table's scroll
|
|
21
|
+
* container). Default on; turn off for an inline absolute panel.
|
|
22
|
+
*/
|
|
23
|
+
appendToBody?: boolean;
|
|
24
|
+
};
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import { tick } from 'svelte';
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
columnOrder,
|
|
32
|
+
hiddenColumns,
|
|
33
|
+
getLabel,
|
|
34
|
+
onReorder,
|
|
35
|
+
onToggleVisibility,
|
|
36
|
+
onReset,
|
|
37
|
+
label = 'Columns',
|
|
38
|
+
appendToBody = true,
|
|
39
|
+
}: ManageColumnsProps = $props();
|
|
40
|
+
|
|
41
|
+
// Number of currently visible columns. When exactly one is visible, its
|
|
42
|
+
// toggle must be disabled so the user can't hide the last column.
|
|
43
|
+
const visibleCount = $derived(columnOrder.length - hiddenColumns.size);
|
|
44
|
+
|
|
45
|
+
// Internal open/search state
|
|
46
|
+
let open = $state(false);
|
|
47
|
+
let search = $state('');
|
|
48
|
+
let searchInput: HTMLInputElement | undefined = $state();
|
|
49
|
+
let triggerRef: HTMLButtonElement | undefined = $state();
|
|
50
|
+
let containerRef: HTMLDivElement | undefined = $state();
|
|
51
|
+
let panelStyle = $state('');
|
|
52
|
+
|
|
53
|
+
// Auto-focus the search input when opened
|
|
54
|
+
$effect(() => {
|
|
55
|
+
if (!open) return;
|
|
56
|
+
if (appendToBody) {
|
|
57
|
+
positionPanel();
|
|
58
|
+
// A position:fixed panel is anchored to the VIEWPORT, so re-anchor on
|
|
59
|
+
// every scroll (capture: any scroll container) and resize; close once
|
|
60
|
+
// the trigger leaves the view. Same contract as SearchDropdown's
|
|
61
|
+
// appendToBody mode.
|
|
62
|
+
const reanchor = (e?: Event) => {
|
|
63
|
+
if (e && e.target instanceof Node && containerRef?.contains(e.target)) return;
|
|
64
|
+
const r = triggerRef?.getBoundingClientRect();
|
|
65
|
+
if (!r || r.bottom < 0 || r.top > window.innerHeight) {
|
|
66
|
+
open = false;
|
|
67
|
+
search = '';
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
positionPanel();
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener('scroll', reanchor, { capture: true, passive: true });
|
|
73
|
+
window.addEventListener('resize', reanchor, { passive: true });
|
|
74
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
75
|
+
tick().then(() => searchInput?.focus());
|
|
76
|
+
return () => {
|
|
77
|
+
window.removeEventListener('scroll', reanchor, { capture: true });
|
|
78
|
+
window.removeEventListener('resize', reanchor);
|
|
79
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
tick().then(() => searchInput?.focus());
|
|
83
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
84
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function positionPanel() {
|
|
88
|
+
if (!triggerRef) return;
|
|
89
|
+
const rect = triggerRef.getBoundingClientRect();
|
|
90
|
+
const width = Math.max(rect.width, 200);
|
|
91
|
+
// Viewport-aware flip: grow upward when there isn't room below but there
|
|
92
|
+
// is above (rows near the bottom of a scroll area).
|
|
93
|
+
const estPanelHeight = 300;
|
|
94
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
95
|
+
const spaceAbove = rect.top;
|
|
96
|
+
const flipUp = spaceBelow < estPanelHeight && spaceAbove > spaceBelow;
|
|
97
|
+
// Clamp so a right-edge trigger doesn't push the panel off-screen.
|
|
98
|
+
const left = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8));
|
|
99
|
+
// Each branch must explicitly reset the property it isn't using — the
|
|
100
|
+
// inline panel's class rule sets top: calc(100% + 4px).
|
|
101
|
+
const vertical = flipUp
|
|
102
|
+
? `bottom: ${window.innerHeight - rect.top + 4}px; top: auto;`
|
|
103
|
+
: `top: ${rect.bottom + 4}px; bottom: auto;`;
|
|
104
|
+
panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; min-width: ${width}px; margin-top: 0;`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleClickOutside(e: MouseEvent) {
|
|
108
|
+
if (containerRef && !containerRef.contains(e.target as Node)) {
|
|
109
|
+
open = false;
|
|
110
|
+
search = '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Drag-to-reorder state (disabled while searching)
|
|
115
|
+
let draggedIndex = $state<number | null>(null);
|
|
116
|
+
let dragOverIndex = $state<number | null>(null);
|
|
117
|
+
|
|
118
|
+
function handleDragStart(index: number, e: DragEvent) {
|
|
119
|
+
draggedIndex = index;
|
|
120
|
+
if (e.dataTransfer) {
|
|
121
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
122
|
+
e.dataTransfer.setData('text/plain', String(index));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleDragOver(index: number, e: DragEvent) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
129
|
+
dragOverIndex = index;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function handleDragEnd() {
|
|
133
|
+
draggedIndex = null;
|
|
134
|
+
dragOverIndex = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function handleDrop(index: number, e: DragEvent) {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
if (draggedIndex === null || draggedIndex === index) {
|
|
140
|
+
handleDragEnd();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const newOrder = [...columnOrder];
|
|
144
|
+
const [moved] = newOrder.splice(draggedIndex, 1);
|
|
145
|
+
newOrder.splice(index, 0, moved);
|
|
146
|
+
onReorder(newOrder);
|
|
147
|
+
handleDragEnd();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function closeDropdown() {
|
|
151
|
+
open = false;
|
|
152
|
+
search = '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const filteredOrder = $derived.by(() => {
|
|
156
|
+
if (!search) return columnOrder;
|
|
157
|
+
const q = search.toLowerCase();
|
|
158
|
+
return columnOrder.filter((id) => getLabel(id).toLowerCase().includes(q));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const hasNoMatches = $derived(search.length > 0 && filteredOrder.length === 0);
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<div class="mc-wrapper" bind:this={containerRef}>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
class="mc-trigger"
|
|
168
|
+
bind:this={triggerRef}
|
|
169
|
+
aria-expanded={open}
|
|
170
|
+
onclick={() => {
|
|
171
|
+
open = !open;
|
|
172
|
+
if (!open) search = '';
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<span>{label}</span>
|
|
176
|
+
<svg class="mc-chevron" class:mc-chevron-open={open} viewBox="0 0 20 20" fill="currentColor">
|
|
177
|
+
<path
|
|
178
|
+
fill-rule="evenodd"
|
|
179
|
+
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
|
180
|
+
clip-rule="evenodd"
|
|
181
|
+
/>
|
|
182
|
+
</svg>
|
|
183
|
+
</button>
|
|
184
|
+
|
|
185
|
+
{#if open}
|
|
186
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
187
|
+
<div class="fixed inset-0 z-40" onclick={closeDropdown} onkeydown={() => {}}></div>
|
|
188
|
+
<div
|
|
189
|
+
class="mc-dropdown"
|
|
190
|
+
class:mc-dropdown-fixed={appendToBody}
|
|
191
|
+
style={appendToBody ? panelStyle : undefined}
|
|
192
|
+
>
|
|
193
|
+
<div class="mc-search">
|
|
194
|
+
<svg
|
|
195
|
+
class="mc-search-icon"
|
|
196
|
+
fill="none"
|
|
197
|
+
viewBox="0 0 24 24"
|
|
198
|
+
stroke="currentColor"
|
|
199
|
+
stroke-width="2"
|
|
200
|
+
>
|
|
201
|
+
<path
|
|
202
|
+
stroke-linecap="round"
|
|
203
|
+
stroke-linejoin="round"
|
|
204
|
+
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
|
205
|
+
/>
|
|
206
|
+
</svg>
|
|
207
|
+
<input
|
|
208
|
+
bind:this={searchInput}
|
|
209
|
+
bind:value={search}
|
|
210
|
+
type="text"
|
|
211
|
+
placeholder="Filter columns..."
|
|
212
|
+
class="mc-search-input"
|
|
213
|
+
onkeydown={(e) => {
|
|
214
|
+
if (e.key === 'Escape') {
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
closeDropdown();
|
|
217
|
+
}
|
|
218
|
+
}}
|
|
219
|
+
autocomplete="off"
|
|
220
|
+
/>
|
|
221
|
+
{#if search}
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
aria-label="Clear search"
|
|
225
|
+
class="mc-search-clear"
|
|
226
|
+
onclick={() => {
|
|
227
|
+
search = '';
|
|
228
|
+
searchInput?.focus();
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
<svg
|
|
232
|
+
class="w-3 h-3"
|
|
233
|
+
fill="none"
|
|
234
|
+
viewBox="0 0 24 24"
|
|
235
|
+
stroke="currentColor"
|
|
236
|
+
stroke-width="2"
|
|
237
|
+
>
|
|
238
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
239
|
+
</svg>
|
|
240
|
+
</button>
|
|
241
|
+
{/if}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{#each filteredOrder as colId (colId)}
|
|
245
|
+
{@const originalIndex = columnOrder.indexOf(colId)}
|
|
246
|
+
{@const isVisible = !hiddenColumns.has(colId)}
|
|
247
|
+
{@const isLastVisible = isVisible && visibleCount === 1}
|
|
248
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
249
|
+
<div
|
|
250
|
+
class="mc-row"
|
|
251
|
+
class:mc-drag-over={dragOverIndex === originalIndex && draggedIndex !== null && draggedIndex !== originalIndex}
|
|
252
|
+
draggable={!search}
|
|
253
|
+
ondragstart={(e) => !search && handleDragStart(originalIndex, e)}
|
|
254
|
+
ondragover={(e) => !search && handleDragOver(originalIndex, e)}
|
|
255
|
+
ondragend={() => !search && handleDragEnd()}
|
|
256
|
+
ondrop={(e) => !search && handleDrop(originalIndex, e)}
|
|
257
|
+
>
|
|
258
|
+
<span class="mc-grip" class:mc-grip-disabled={!!search}>⠿</span>
|
|
259
|
+
<label class="mc-label" class:mc-label-disabled={isLastVisible}>
|
|
260
|
+
<input
|
|
261
|
+
type="checkbox"
|
|
262
|
+
checked={isVisible}
|
|
263
|
+
disabled={isLastVisible}
|
|
264
|
+
title={isLastVisible ? 'At least one must stay visible' : undefined}
|
|
265
|
+
aria-label={
|
|
266
|
+
isLastVisible
|
|
267
|
+
? `${getLabel(colId)} (at least one column must stay visible)`
|
|
268
|
+
: undefined
|
|
269
|
+
}
|
|
270
|
+
onchange={() => onToggleVisibility(colId)}
|
|
271
|
+
/>
|
|
272
|
+
{getLabel(colId)}
|
|
273
|
+
</label>
|
|
274
|
+
</div>
|
|
275
|
+
{/each}
|
|
276
|
+
|
|
277
|
+
{#if hasNoMatches}
|
|
278
|
+
<div class="mc-empty">No columns match "{search}"</div>
|
|
279
|
+
{/if}
|
|
280
|
+
|
|
281
|
+
{#if onReset}
|
|
282
|
+
<div class="mc-footer">
|
|
283
|
+
<button type="button" class="mc-reset" onclick={() => onReset?.()}>
|
|
284
|
+
Reset to default
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
{/if}
|
|
288
|
+
</div>
|
|
289
|
+
{/if}
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<style>
|
|
293
|
+
.mc-wrapper {
|
|
294
|
+
position: relative;
|
|
295
|
+
}
|
|
296
|
+
.mc-trigger {
|
|
297
|
+
padding: 0.25rem 0.5rem;
|
|
298
|
+
border-radius: 0.5rem;
|
|
299
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
300
|
+
font-size: 0.8125rem;
|
|
301
|
+
display: flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
gap: 0.375rem;
|
|
304
|
+
min-height: 2rem;
|
|
305
|
+
cursor: pointer;
|
|
306
|
+
background: var(--color-base-300, oklch(0.13 0.01 250));
|
|
307
|
+
border: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
|
|
308
|
+
color: var(--color-base-content, currentColor);
|
|
309
|
+
transition:
|
|
310
|
+
background 0.15s,
|
|
311
|
+
border-color 0.15s;
|
|
312
|
+
white-space: nowrap;
|
|
313
|
+
}
|
|
314
|
+
.mc-trigger:hover {
|
|
315
|
+
background: var(--color-base-200, oklch(0.16 0.01 250));
|
|
316
|
+
border-color: var(--color-base-100, oklch(0.2 0.01 250));
|
|
317
|
+
}
|
|
318
|
+
.mc-chevron {
|
|
319
|
+
width: 14px;
|
|
320
|
+
height: 14px;
|
|
321
|
+
opacity: 0.5;
|
|
322
|
+
transition: transform 0.15s;
|
|
323
|
+
}
|
|
324
|
+
.mc-chevron-open {
|
|
325
|
+
transform: rotate(180deg);
|
|
326
|
+
}
|
|
327
|
+
.mc-dropdown {
|
|
328
|
+
position: absolute;
|
|
329
|
+
top: calc(100% + 4px);
|
|
330
|
+
right: 0;
|
|
331
|
+
z-index: 50;
|
|
332
|
+
background: var(--color-base-300, oklch(0.13 0.01 250));
|
|
333
|
+
border: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
|
|
334
|
+
border-radius: 0.375rem;
|
|
335
|
+
box-shadow: 0 8px 24px oklch(0 0 0 / 0.4);
|
|
336
|
+
padding: 0;
|
|
337
|
+
min-width: 180px;
|
|
338
|
+
max-height: 320px;
|
|
339
|
+
overflow-y: auto;
|
|
340
|
+
}
|
|
341
|
+
/* Fixed-mode overrides — positionPanel() sets the inline geometry, the class
|
|
342
|
+
only kills the inline panel's absolute defaults it would otherwise inherit. */
|
|
343
|
+
.mc-dropdown-fixed {
|
|
344
|
+
top: auto;
|
|
345
|
+
right: auto;
|
|
346
|
+
bottom: auto;
|
|
347
|
+
max-height: 320px;
|
|
348
|
+
}
|
|
349
|
+
.mc-search {
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
gap: 0.375rem;
|
|
353
|
+
padding: 0.375rem 0.625rem;
|
|
354
|
+
border-bottom: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
|
|
355
|
+
position: sticky;
|
|
356
|
+
top: 0;
|
|
357
|
+
background: var(--color-base-300, oklch(0.13 0.01 250));
|
|
358
|
+
}
|
|
359
|
+
.mc-search-icon {
|
|
360
|
+
width: 0.75rem;
|
|
361
|
+
height: 0.75rem;
|
|
362
|
+
flex-shrink: 0;
|
|
363
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 50%);
|
|
364
|
+
}
|
|
365
|
+
.mc-search-input {
|
|
366
|
+
flex: 1;
|
|
367
|
+
background: transparent;
|
|
368
|
+
border: none;
|
|
369
|
+
outline: none;
|
|
370
|
+
font-size: 0.625rem;
|
|
371
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
372
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 95%);
|
|
373
|
+
}
|
|
374
|
+
.mc-search-input::placeholder {
|
|
375
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 40%);
|
|
376
|
+
}
|
|
377
|
+
.mc-search-clear {
|
|
378
|
+
background: none;
|
|
379
|
+
border: none;
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
padding: 0;
|
|
382
|
+
display: flex;
|
|
383
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 40%);
|
|
384
|
+
}
|
|
385
|
+
.mc-search-clear:hover {
|
|
386
|
+
opacity: 0.8;
|
|
387
|
+
}
|
|
388
|
+
.mc-empty {
|
|
389
|
+
padding: 0.5rem 0.75rem;
|
|
390
|
+
font-size: 0.625rem;
|
|
391
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
392
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 50%);
|
|
393
|
+
text-align: center;
|
|
394
|
+
}
|
|
395
|
+
.mc-grip-disabled {
|
|
396
|
+
opacity: 0.2;
|
|
397
|
+
cursor: default;
|
|
398
|
+
}
|
|
399
|
+
.mc-row {
|
|
400
|
+
display: flex;
|
|
401
|
+
align-items: center;
|
|
402
|
+
gap: 0.375rem;
|
|
403
|
+
padding: 0.25rem 0.625rem;
|
|
404
|
+
cursor: grab;
|
|
405
|
+
transition: background 0.1s;
|
|
406
|
+
}
|
|
407
|
+
.mc-row:hover {
|
|
408
|
+
background: var(--color-base-100, oklch(0.2 0.01 250));
|
|
409
|
+
}
|
|
410
|
+
.mc-row.mc-drag-over {
|
|
411
|
+
border-top: 2px solid var(--color-info, oklch(0.72 0.1 240));
|
|
412
|
+
}
|
|
413
|
+
.mc-grip {
|
|
414
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 40%);
|
|
415
|
+
font-size: 0.75rem;
|
|
416
|
+
cursor: grab;
|
|
417
|
+
user-select: none;
|
|
418
|
+
}
|
|
419
|
+
.mc-label {
|
|
420
|
+
display: flex;
|
|
421
|
+
align-items: center;
|
|
422
|
+
gap: 0.375rem;
|
|
423
|
+
font-size: 0.75rem;
|
|
424
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 95%);
|
|
425
|
+
cursor: pointer;
|
|
426
|
+
user-select: none;
|
|
427
|
+
flex: 1;
|
|
428
|
+
}
|
|
429
|
+
.mc-label input[type='checkbox'] {
|
|
430
|
+
accent-color: var(--color-info, oklch(0.72 0.1 240));
|
|
431
|
+
}
|
|
432
|
+
.mc-label-disabled {
|
|
433
|
+
opacity: 0.55;
|
|
434
|
+
cursor: default;
|
|
435
|
+
}
|
|
436
|
+
.mc-label-disabled input[type='checkbox'] {
|
|
437
|
+
cursor: not-allowed;
|
|
438
|
+
}
|
|
439
|
+
.mc-footer {
|
|
440
|
+
position: sticky;
|
|
441
|
+
bottom: 0;
|
|
442
|
+
padding: 0.375rem 0.625rem;
|
|
443
|
+
border-top: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
|
|
444
|
+
background: var(--color-base-300, oklch(0.13 0.01 250));
|
|
445
|
+
}
|
|
446
|
+
.mc-reset {
|
|
447
|
+
width: 100%;
|
|
448
|
+
background: transparent;
|
|
449
|
+
border: 1px solid var(--color-base-100, oklch(0.2 0.01 250));
|
|
450
|
+
border-radius: 0.375rem;
|
|
451
|
+
padding: 0.25rem 0.5rem;
|
|
452
|
+
font-size: 0.625rem;
|
|
453
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
454
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 60%);
|
|
455
|
+
cursor: pointer;
|
|
456
|
+
transition:
|
|
457
|
+
background 0.15s,
|
|
458
|
+
border-color 0.15s,
|
|
459
|
+
color 0.15s;
|
|
460
|
+
}
|
|
461
|
+
.mc-reset:hover {
|
|
462
|
+
background: var(--color-base-200, oklch(0.16 0.01 250));
|
|
463
|
+
border-color: var(--color-base-100, oklch(0.2 0.01 250));
|
|
464
|
+
color: oklch(from var(--color-base-content, currentColor) l c h / 95%);
|
|
465
|
+
}
|
|
466
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ManageColumnsProps = {
|
|
2
|
+
/** Current column order (full ids, including hidden ones) */
|
|
3
|
+
columnOrder: string[];
|
|
4
|
+
/** Ids currently hidden */
|
|
5
|
+
hiddenColumns: Set<string>;
|
|
6
|
+
/** Resolve a column id to its display label */
|
|
7
|
+
getLabel: (id: string) => string;
|
|
8
|
+
/** Column order changed (drag-to-reorder) */
|
|
9
|
+
onReorder: (newOrder: string[]) => void;
|
|
10
|
+
/** A column's visibility was toggled */
|
|
11
|
+
onToggleVisibility: (id: string) => void;
|
|
12
|
+
/** When set, a "Reset to default" footer button is rendered */
|
|
13
|
+
onReset?: () => void;
|
|
14
|
+
/** Trigger button text */
|
|
15
|
+
label?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Render the panel with position:fixed viewport coordinates — escapes
|
|
18
|
+
* overflow-x:auto / overflow:hidden parents that would otherwise clip it
|
|
19
|
+
* (jat-hwt1h: the IDE's original was clipped by its table's scroll
|
|
20
|
+
* container). Default on; turn off for an inline absolute panel.
|
|
21
|
+
*/
|
|
22
|
+
appendToBody?: boolean;
|
|
23
|
+
};
|
|
24
|
+
declare const ManageColumnsDropdown: import("svelte").Component<ManageColumnsProps, {}, "">;
|
|
25
|
+
type ManageColumnsDropdown = ReturnType<typeof ManageColumnsDropdown>;
|
|
26
|
+
export default ManageColumnsDropdown;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column settings persistence for DataTable-based views (upstreamed from the
|
|
3
|
+
* jat IDE's `ide/src/lib/utils/columnStorage.ts` — audit jat-1alfp wave #4,
|
|
4
|
+
* U9). Pure module: no DOM beyond the guarded `localStorage` reads, no svelte
|
|
5
|
+
* imports — safe to call from any client-side component.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const saved = loadColumnSettings('my-table', allColumnIds);
|
|
9
|
+
* saveColumnSettings('my-table', { order, widths, hidden, sortField, sortDir });
|
|
10
|
+
*/
|
|
11
|
+
export interface ColumnSettings {
|
|
12
|
+
order: string[];
|
|
13
|
+
widths: Record<string, number>;
|
|
14
|
+
hidden: string[];
|
|
15
|
+
sortField?: string;
|
|
16
|
+
sortDir?: 'asc' | 'desc';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Save column settings (order, widths, hidden) to localStorage as a single JSON blob.
|
|
20
|
+
*/
|
|
21
|
+
export declare function saveColumnSettings(key: string, settings: ColumnSettings): void;
|
|
22
|
+
/**
|
|
23
|
+
* Load column settings from localStorage.
|
|
24
|
+
* Merges any new column IDs (from allColumnIds) not present in the saved order,
|
|
25
|
+
* appending them at the end — so adding a column to a view whose settings were
|
|
26
|
+
* saved earlier does not hide or lose the new column.
|
|
27
|
+
* Returns null if nothing is saved or the data cannot be parsed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadColumnSettings(key: string, allColumnIds: string[]): ColumnSettings | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column settings persistence for DataTable-based views (upstreamed from the
|
|
3
|
+
* jat IDE's `ide/src/lib/utils/columnStorage.ts` — audit jat-1alfp wave #4,
|
|
4
|
+
* U9). Pure module: no DOM beyond the guarded `localStorage` reads, no svelte
|
|
5
|
+
* imports — safe to call from any client-side component.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const saved = loadColumnSettings('my-table', allColumnIds);
|
|
9
|
+
* saveColumnSettings('my-table', { order, widths, hidden, sortField, sortDir });
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Save column settings (order, widths, hidden) to localStorage as a single JSON blob.
|
|
13
|
+
*/
|
|
14
|
+
export function saveColumnSettings(key, settings) {
|
|
15
|
+
if (typeof localStorage === 'undefined')
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
localStorage.setItem(key, JSON.stringify(settings));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore storage errors (e.g. private browsing quota) — a failed save must
|
|
22
|
+
// never break the table itself; the next save overwrites whatever partial
|
|
23
|
+
// state exists. Deliberate degradation: persistence is best-effort by design.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load column settings from localStorage.
|
|
28
|
+
* Merges any new column IDs (from allColumnIds) not present in the saved order,
|
|
29
|
+
* appending them at the end — so adding a column to a view whose settings were
|
|
30
|
+
* saved earlier does not hide or lose the new column.
|
|
31
|
+
* Returns null if nothing is saved or the data cannot be parsed.
|
|
32
|
+
*/
|
|
33
|
+
export function loadColumnSettings(key, allColumnIds) {
|
|
34
|
+
if (typeof localStorage === 'undefined')
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const raw = localStorage.getItem(key);
|
|
38
|
+
if (!raw)
|
|
39
|
+
return null;
|
|
40
|
+
const saved = JSON.parse(raw);
|
|
41
|
+
const order = (() => {
|
|
42
|
+
if (!saved.order || !Array.isArray(saved.order))
|
|
43
|
+
return allColumnIds;
|
|
44
|
+
const known = new Set(allColumnIds);
|
|
45
|
+
const validOrder = saved.order.filter((id) => known.has(id));
|
|
46
|
+
const missing = allColumnIds.filter((id) => !validOrder.includes(id));
|
|
47
|
+
return [...validOrder, ...missing];
|
|
48
|
+
})();
|
|
49
|
+
const widths = saved.widths && typeof saved.widths === 'object' ? saved.widths : {};
|
|
50
|
+
const hidden = saved.hidden && Array.isArray(saved.hidden) ? saved.hidden : [];
|
|
51
|
+
const sortField = typeof saved.sortField === 'string' ? saved.sortField : undefined;
|
|
52
|
+
const sortDir = saved.sortDir === 'asc' || saved.sortDir === 'desc' ? saved.sortDir : undefined;
|
|
53
|
+
return { order, widths, hidden, sortField, sortDir };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Corrupt or non-JSON payload: discard and let the caller fall back to its
|
|
57
|
+
// built-in column order. Returning null (rather than throwing) is the whole
|
|
58
|
+
// contract — a broken saved blob must never take the page down with it.
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* groupRows — grouped render-list flattening (jatui-u6d4e.2).
|
|
3
|
+
*
|
|
4
|
+
* Spec: jat `docs/design/datatable-virtualisation-group-rows.md` §3.2.
|
|
5
|
+
*
|
|
6
|
+
* Turns an already filtered + sorted row array into the flat list a table (or a
|
|
7
|
+
* row window) renders: group header items interleaved with row items, nested to
|
|
8
|
+
* any number of levels. Pure, DOM-free and framework-free so it runs under
|
|
9
|
+
* node:test and can feed a virtualiser directly (one item = one rendered line).
|
|
10
|
+
*
|
|
11
|
+
* - Rows inside a group keep the order the caller passed in — the page's sort
|
|
12
|
+
* comparator and tiebreakers survive untouched. Only GROUPS are reordered,
|
|
13
|
+
* by `level.compare` (default: first-seen order).
|
|
14
|
+
* - A collapsed group still emits its header (with `collapsed: true` and the
|
|
15
|
+
* full `rows`/`count`) but nothing beneath it.
|
|
16
|
+
* - `showHeader` returning false hides that header: its rows (and subgroups)
|
|
17
|
+
* still render, one visual level shallower. A hidden header cannot be
|
|
18
|
+
* toggled, so `collapsed` is ignored for it.
|
|
19
|
+
* - `depth` is the VISUAL nesting level — the number of visible headers above
|
|
20
|
+
* the item — so hidden headers never leave an indentation gap. `path` /
|
|
21
|
+
* `groupPath` always carry the full key path.
|
|
22
|
+
* - `position` (first/middle/last/only) is computed over each contiguous run of
|
|
23
|
+
* rows under the same visible parent, which is what ├ / └ tree glyphs need.
|
|
24
|
+
*/
|
|
25
|
+
export interface GroupLevel<T> {
|
|
26
|
+
/** Group key for a row at this level (e.g. task => projectOf(task)). */
|
|
27
|
+
key: (row: T) => string;
|
|
28
|
+
/** Group order. The page owns it (P0-P4, "Unassigned first", milestone
|
|
29
|
+
* sort_order, epics-first…). Default: first-seen order. */
|
|
30
|
+
compare?: (a: string, b: string) => number;
|
|
31
|
+
/** Hide the header when it would add nothing (e.g. < 2 rows and no
|
|
32
|
+
* children). The rows still render, ungrouped. Default: always shown. */
|
|
33
|
+
showHeader?: (key: string, rows: T[]) => boolean;
|
|
34
|
+
}
|
|
35
|
+
export type GroupedRowPosition = 'first' | 'middle' | 'last' | 'only';
|
|
36
|
+
export interface GroupedGroupItem<T> {
|
|
37
|
+
kind: 'group';
|
|
38
|
+
key: string;
|
|
39
|
+
/** Key path from the top level down to (and including) this group. */
|
|
40
|
+
path: string[];
|
|
41
|
+
/** Visual nesting level: number of visible group headers above this one. */
|
|
42
|
+
depth: number;
|
|
43
|
+
label: string;
|
|
44
|
+
/** Every row under this group, collapsed or not, in input order. */
|
|
45
|
+
rows: T[];
|
|
46
|
+
count: number;
|
|
47
|
+
collapsed: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface GroupedRowItem<T> {
|
|
50
|
+
kind: 'row';
|
|
51
|
+
row: T;
|
|
52
|
+
id: string;
|
|
53
|
+
/** Visual nesting level: number of visible group headers above this row. */
|
|
54
|
+
depth: number;
|
|
55
|
+
/** Key path of the innermost group containing this row ([] when flat). */
|
|
56
|
+
groupPath: string[];
|
|
57
|
+
/** Place within its contiguous run of sibling rows — drives ├ / └ glyphs. */
|
|
58
|
+
position: GroupedRowPosition;
|
|
59
|
+
}
|
|
60
|
+
export type GroupedItem<T = any> = GroupedGroupItem<T> | GroupedRowItem<T>;
|
|
61
|
+
export interface GroupRowsOptions<T> {
|
|
62
|
+
getRowId: (row: T) => string;
|
|
63
|
+
/** Collapsed groups, by path key (see `groupPathKey`), e.g. "jat" or "jat::jat-vte6e". */
|
|
64
|
+
collapsed: ReadonlySet<string>;
|
|
65
|
+
/** Header label for a group path. Default: the group's own key. */
|
|
66
|
+
label?: (path: string[]) => string;
|
|
67
|
+
}
|
|
68
|
+
/** Separator joining a group path into the key used by `collapsed`. */
|
|
69
|
+
export declare const GROUP_PATH_SEPARATOR = "::";
|
|
70
|
+
/** The `collapsed`-set key for a group path: ["jat", "jat-vte6e"] → "jat::jat-vte6e". */
|
|
71
|
+
export declare function groupPathKey(path: readonly string[]): string;
|
|
72
|
+
export declare function groupRows<T>(rows: readonly T[], levels: readonly GroupLevel<T>[], opts: GroupRowsOptions<T>): GroupedItem<T>[];
|