@svadmin/ui 0.92.1 → 0.93.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.
@@ -3,7 +3,7 @@
3
3
  import { Button } from './ui/button/index.js';
4
4
  import { Checkbox } from './ui/checkbox/index.js';
5
5
  import * as Popover from './ui/popover/index.js';
6
- import { SlidersHorizontal, RotateCcw, ArrowUp, ArrowDown, Search } from '@lucide/svelte';
6
+ import { SlidersHorizontal, RotateCcw, ArrowUp, ArrowDown, Search, Lock, Pin, PinOff } from '@lucide/svelte';
7
7
  import { cn } from '../utils.js';
8
8
 
9
9
  export interface ColumnItem {
@@ -11,12 +11,21 @@
11
11
  label: string;
12
12
  visible: boolean;
13
13
  fixed?: 'left' | 'right';
14
+ /** 业务必显列。隐藏、预设和已保存偏好都不能把它关掉。 */
15
+ required?: boolean;
16
+ }
17
+
18
+ export interface ColumnPreset {
19
+ id: string;
20
+ label: string;
21
+ visibleKeys: string[];
14
22
  }
15
23
 
16
24
  interface Props {
17
25
  columns: ColumnItem[];
18
26
  onchange?: (columns: ColumnItem[]) => void;
19
27
  onreset?: () => void;
28
+ presets?: ColumnPreset[];
20
29
  storageKey?: string;
21
30
  title?: string;
22
31
  triggerVariant?: 'outline' | 'ghost' | 'secondary';
@@ -28,6 +37,7 @@
28
37
  columns = $bindable([]),
29
38
  onchange,
30
39
  onreset,
40
+ presets = [],
31
41
  storageKey,
32
42
  title,
33
43
  triggerVariant = 'outline',
@@ -41,6 +51,44 @@
41
51
 
42
52
  // Snapshot initial configuration for reset
43
53
  let initialSnapshot = $state<ColumnItem[]>(JSON.parse(JSON.stringify(columns)));
54
+ let storageHydrated = false;
55
+
56
+ // storageKey 之前只写不读;挂载后按 key 合并可见性、固定位和顺序,未知列仍按新定义保留。
57
+ $effect(() => {
58
+ if (storageHydrated || !storageKey || typeof window === 'undefined') return;
59
+ storageHydrated = true;
60
+ try {
61
+ const raw = window.localStorage.getItem(storageKey);
62
+ if (!raw) return;
63
+ const parsed: unknown = JSON.parse(raw);
64
+ if (!Array.isArray(parsed)) return;
65
+ const stored = new Map(parsed
66
+ .filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object')
67
+ .filter((entry) => typeof entry['key'] === 'string')
68
+ .map((entry) => [entry['key'] as string, entry]));
69
+ const next = columns.map((column): ColumnItem => {
70
+ const saved = stored.get(column.key);
71
+ if (!saved) return column;
72
+ const savedFixed = saved['fixed'];
73
+ const fixed = savedFixed === 'left' ? 'left' : savedFixed === 'right' ? 'right' : undefined;
74
+ const visible = column.required === true || saved['visible'] !== false;
75
+ if (fixed === undefined) return { ...column, visible };
76
+ return { ...column, visible, fixed };
77
+ });
78
+ const order = parsed
79
+ .map((entry) => (entry && typeof entry === 'object' ? (entry as Record<string, unknown>)['key'] : undefined))
80
+ .filter((key): key is string => typeof key === 'string');
81
+ next.sort((left, right) => {
82
+ const leftIndex = order.indexOf(left.key);
83
+ const rightIndex = order.indexOf(right.key);
84
+ return (leftIndex < 0 ? Number.MAX_SAFE_INTEGER : leftIndex)
85
+ - (rightIndex < 0 ? Number.MAX_SAFE_INTEGER : rightIndex);
86
+ });
87
+ columns = next;
88
+ } catch {
89
+ // 存储损坏时保持调用方传入的资源默认列。
90
+ }
91
+ });
44
92
 
45
93
  const displayTitle = $derived(title ?? i18n.t('common.columnSettings', { defaultValue: 'Columns' }));
46
94
  const resetLabel = $derived(i18n.t('common.reset', { defaultValue: 'Reset' }));
@@ -65,13 +113,48 @@
65
113
  }
66
114
 
67
115
  function toggleColumn(key: string) {
116
+ const target = columns.find((column) => column.key === key);
117
+ if (!target || target.required) return;
118
+ if (target.visible && columns.filter((column) => column.visible).length <= 1) return;
68
119
  columns = columns.map((c) => (c.key === key ? { ...c, visible: !c.visible } : c));
69
120
  savePreference(columns);
70
121
  onchange?.(columns);
71
122
  }
72
123
 
73
124
  function toggleAll(checked: boolean) {
74
- columns = columns.map((c) => ({ ...c, visible: checked }));
125
+ const hasRequired = columns.some((column) => column.required);
126
+ if (!checked && !hasRequired) {
127
+ // 至少保留一列,避免空表头无法自愈;保留第一列作为最小可见集。
128
+ columns = columns.map((column, index) => ({ ...column, visible: index === 0 }));
129
+ } else {
130
+ columns = columns.map((c) => (c.required ? { ...c, visible: true } : { ...c, visible: checked }));
131
+ }
132
+ savePreference(columns);
133
+ onchange?.(columns);
134
+ }
135
+
136
+ function toggleFixed(key: string) {
137
+ columns = columns.map((column) => {
138
+ if (column.key !== key) return column;
139
+ if (column.fixed) {
140
+ const { fixed: _fixed, ...rest } = column;
141
+ return rest;
142
+ }
143
+ return { ...column, fixed: 'left' as const };
144
+ });
145
+ savePreference(columns);
146
+ onchange?.(columns);
147
+ }
148
+
149
+ function applyPreset(preset: ColumnPreset) {
150
+ const visibleKeys = new Set(preset.visibleKeys);
151
+ columns = columns.map((column) => ({
152
+ ...column,
153
+ visible: column.required === true || visibleKeys.has(column.key),
154
+ }));
155
+ if (!columns.some((column) => column.visible) && columns[0]) {
156
+ columns = columns.map((column, index) => ({ ...column, visible: index === 0 }));
157
+ }
75
158
  savePreference(columns);
76
159
  onchange?.(columns);
77
160
  }
@@ -155,6 +238,20 @@
155
238
  </div>
156
239
  {/if}
157
240
 
241
+ {#if presets.length > 0}
242
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-359090c2d529">
243
+ {#each presets as preset (preset.id)}
244
+ <Button
245
+ type="button"
246
+ variant="outline"
247
+ size="sm"
248
+ data-column-preset={preset.id}
249
+ onclick={() => applyPreset(preset)}
250
+ >{preset.label}</Button>
251
+ {/each}
252
+ </div>
253
+ {/if}
254
+
158
255
  <!-- Column list with ordering -->
159
256
  <div class="svadmin-u-50d0d216a2f8 svadmin-u-67d7e383dca6 svadmin-u-92bf82f493b1 svadmin-u-da7c36cd8867 svadmin-u-eda955402ba6">
160
257
  {#each filteredColumns as col (col.key)}
@@ -163,14 +260,32 @@
163
260
  <label class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-34516836730d svadmin-u-7f6912283f11 svadmin-u-f283ea9bea0e svadmin-u-36e579c0b41c svadmin-u-aa2c13a5e1b4">
164
261
  <Checkbox
165
262
  checked={col.visible}
263
+ disabled={col.required}
264
+ aria-disabled={col.required ? 'true' : undefined}
265
+ data-column-required={col.required ? col.key : undefined}
166
266
  onCheckedChange={() => toggleColumn(col.key)}
167
267
  />
168
268
  <span class="svadmin-u-f283ea9bea0e svadmin-u-d4108abe6359 {col.visible ? '' : 'svadmin-u-bfa603190748 line-through svadmin-u-f2868c227fcd'}">
169
269
  {col.label}
170
270
  </span>
271
+ {#if col.required}
272
+ <Lock class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" aria-label="Required column" data-column-required={col.key} />
273
+ {/if}
171
274
  </label>
172
275
 
173
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-a3899220f90e svadmin-u-7065497e1ca0 svadmin-u-181f3d6c9821 svadmin-u-67d6184a0024">
276
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-a3899220f90e">
277
+ <Button
278
+ variant="ghost"
279
+ size="icon"
280
+ class="svadmin-u-cd0d9c512cdc svadmin-u-72470489ff4e svadmin-u-bfa603190748 svadmin-u-ea7b2e9e070e"
281
+ aria-label={col.fixed ? 'Cancel fixed column' : 'Fix column left'}
282
+ aria-pressed={Boolean(col.fixed)}
283
+ data-column-fixed={col.fixed ?? 'none'}
284
+ onclick={() => toggleFixed(col.key)}
285
+ >
286
+ {#if col.fixed}<PinOff class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />{:else}<Pin class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />{/if}
287
+ </Button>
288
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-a3899220f90e svadmin-u-7065497e1ca0 svadmin-u-181f3d6c9821 svadmin-u-67d6184a0024">
174
289
  <Button
175
290
  variant="ghost"
176
291
  size="icon"
@@ -191,6 +306,7 @@
191
306
  >
192
307
  <ArrowDown class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
193
308
  </Button>
309
+ </div>
194
310
  </div>
195
311
  </div>
196
312
  {/each}
@@ -3,11 +3,19 @@ export interface ColumnItem {
3
3
  label: string;
4
4
  visible: boolean;
5
5
  fixed?: 'left' | 'right';
6
+ /** 业务必显列。隐藏、预设和已保存偏好都不能把它关掉。 */
7
+ required?: boolean;
8
+ }
9
+ export interface ColumnPreset {
10
+ id: string;
11
+ label: string;
12
+ visibleKeys: string[];
6
13
  }
7
14
  interface Props {
8
15
  columns: ColumnItem[];
9
16
  onchange?: (columns: ColumnItem[]) => void;
10
17
  onreset?: () => void;
18
+ presets?: ColumnPreset[];
11
19
  storageKey?: string;
12
20
  title?: string;
13
21
  triggerVariant?: 'outline' | 'ghost' | 'secondary';
package/dist/index.d.ts CHANGED
@@ -92,7 +92,7 @@ export { default as FilterBuilder } from './components/FilterBuilder.svelte';
92
92
  export type { FilterRuleItem } from './components/FilterBuilder.svelte';
93
93
  export { default as Watermark } from './components/Watermark.svelte';
94
94
  export { default as ColumnSettings } from './components/ColumnSettings.svelte';
95
- export type { ColumnItem } from './components/ColumnSettings.svelte';
95
+ export type { ColumnItem, ColumnPreset } from './components/ColumnSettings.svelte';
96
96
  export { default as UnsavedChangesPrompt } from './components/UnsavedChangesPrompt.svelte';
97
97
  export { default as ImportWizard } from './components/ImportWizard.svelte';
98
98
  export type { ImportPreviewFailure, ImportPreviewResult } from '@svadmin/core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.92.1",
3
+ "version": "0.93.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -88,7 +88,7 @@
88
88
  "@codemirror/lang-json": "^6.0.0",
89
89
  "@codemirror/lang-markdown": "^6.0.0",
90
90
  "@codemirror/lang-sql": "^6.0.0",
91
- "@svadmin/core": "^0.66.1",
91
+ "@svadmin/core": "^0.66.2",
92
92
  "@tanstack/svelte-query": "^6.1.48",
93
93
  "codemirror": "^6.0.0",
94
94
  "isomorphic-dompurify": "^3.23.0",
@@ -139,7 +139,7 @@
139
139
  }
140
140
  },
141
141
  "dependencies": {
142
- "@svadmin/devtools": "^0.3.8",
142
+ "@svadmin/devtools": "^0.3.9",
143
143
  "@svadmin/devtools-contract": "^0.3.1",
144
144
  "@vibeunion/devtools-devframe": "^0.1.0",
145
145
  "typebox": "^1.3.34",
@@ -177,7 +177,7 @@
177
177
  "@codemirror/lang-markdown": "^6.5.2",
178
178
  "@codemirror/lang-sql": "^6.10.0",
179
179
  "@happy-dom/global-registrator": "^20.11.12",
180
- "@svadmin/ai-elements": "^0.12.5",
180
+ "@svadmin/ai-elements": "^0.12.6",
181
181
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
182
182
  "@tailwindcss/cli": "4.3.3",
183
183
  "@testing-library/dom": "^10.4.1",