@vobs/table 1.0.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.
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ export { KitDataTable } from './data-table'
2
+ export { KitColumnSettings } from './column-settings'
3
+ export { createColumnSettingsPersistence, createLocalColumnSettingsPersistence } from './persistence'
4
+ export type {
5
+ DataTableAlign,
6
+ DataTableChildren,
7
+ DataTableColumn,
8
+ DataTableColumnSettings,
9
+ DataTableColumnSettingsPersistence,
10
+ DataTableColumnSettingsStorage,
11
+ DataTableCommonProps,
12
+ DataTableIcons,
13
+ DataTablePage,
14
+ DataTablePagination,
15
+ DataTablePaginationContext,
16
+ DataTablePaginationMode,
17
+ DataTableResource,
18
+ DataTableQuery,
19
+ DataTableResourceData,
20
+ DataTableSort,
21
+ DataTableSortDirection,
22
+ DataTableSortType,
23
+ DataTableSortingMode,
24
+ KitColumnSettingsProps,
25
+ KitDataTableProps
26
+ } from './types'
27
+
28
+ export { createDefaultColumnSettings, normalizeColumnSettings } from './utils'
@@ -0,0 +1,99 @@
1
+ import type {
2
+ DataTableColumnSettings,
3
+ DataTableColumnSettingsPersistence,
4
+ DataTableColumnSettingsStorage
5
+ } from './types'
6
+
7
+ export function createColumnSettingsPersistence(
8
+ storage: DataTableColumnSettingsStorage,
9
+ key: string
10
+ ): DataTableColumnSettingsPersistence {
11
+ const normalizedKey = key.trim()
12
+ if (!normalizedKey) throw new Error('VOBS_TABLE001: column settings key 不能为空')
13
+
14
+ return {
15
+ load(): DataTableColumnSettings | null | undefined {
16
+ return storage.get<DataTableColumnSettings>(normalizedKey)
17
+ },
18
+ save(settings: DataTableColumnSettings): void {
19
+ storage.set(normalizedKey, settings)
20
+ },
21
+ reset(): void {
22
+ storage.remove(normalizedKey)
23
+ }
24
+ }
25
+ }
26
+
27
+ export function createLocalColumnSettingsPersistence(
28
+ key: string,
29
+ storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>
30
+ ): DataTableColumnSettingsPersistence {
31
+ const normalizedKey = key.trim()
32
+ if (!normalizedKey) throw new Error('VOBS_TABLE001: column settings key 不能为空')
33
+ const memory = new Map<string, string>()
34
+ let backend = storage ?? resolveLocalStorage()
35
+
36
+ return {
37
+ load(): DataTableColumnSettings | undefined {
38
+ const raw = read()
39
+ if (!raw) return undefined
40
+ try {
41
+ const value: unknown = JSON.parse(raw)
42
+ return isColumnSettings(value) ? value : undefined
43
+ } catch {
44
+ return undefined
45
+ }
46
+ },
47
+ save(settings: DataTableColumnSettings): void {
48
+ const raw = JSON.stringify(settings)
49
+ memory.set(normalizedKey, raw)
50
+ try {
51
+ backend?.setItem(normalizedKey, raw)
52
+ } catch {
53
+ backend = undefined
54
+ }
55
+ },
56
+ reset(): void {
57
+ memory.delete(normalizedKey)
58
+ try {
59
+ backend?.removeItem(normalizedKey)
60
+ } catch {
61
+ backend = undefined
62
+ }
63
+ }
64
+ }
65
+
66
+ function read(): string | null {
67
+ try {
68
+ return backend?.getItem(normalizedKey) ?? memory.get(normalizedKey) ?? null
69
+ } catch {
70
+ backend = undefined
71
+ return memory.get(normalizedKey) ?? null
72
+ }
73
+ }
74
+ }
75
+
76
+ function resolveLocalStorage(): Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | undefined {
77
+ if (typeof window === 'undefined') return undefined
78
+ try {
79
+ return window.localStorage
80
+ } catch {
81
+ return undefined
82
+ }
83
+ }
84
+
85
+ function isColumnSettings(value: unknown): value is DataTableColumnSettings {
86
+ if (!value || typeof value !== 'object') return false
87
+ const candidate = value as {
88
+ visibleColumnIds?: unknown
89
+ columnOrder?: unknown
90
+ columnWidths?: unknown
91
+ }
92
+ return Array.isArray(candidate.visibleColumnIds)
93
+ && candidate.visibleColumnIds.every(id => typeof id === 'string')
94
+ && Array.isArray(candidate.columnOrder)
95
+ && candidate.columnOrder.every(id => typeof id === 'string')
96
+ && Boolean(candidate.columnWidths)
97
+ && typeof candidate.columnWidths === 'object'
98
+ && !Array.isArray(candidate.columnWidths)
99
+ }
@@ -0,0 +1 @@
1
+ @import './table.css';
@@ -0,0 +1,209 @@
1
+ .vobs-data-table {
2
+ background: var(--vobs-table-surface, var(--bg-base-secondary, #fff));
3
+ border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
4
+ border-radius: var(--vobs-table-radius, 4px);
5
+ color: var(--vobs-table-text, var(--text-default, #1f2328));
6
+ overflow: hidden;
7
+ }
8
+
9
+ .vobs-data-table__toolbar,
10
+ .vobs-data-table__footer {
11
+ padding: var(--vobs-table-space, 12px);
12
+ border-color: var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
13
+ }
14
+
15
+ .vobs-data-table__toolbar { border-bottom: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de)); }
16
+ .vobs-data-table__footer { border-top: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de)); }
17
+ .vobs-data-table__viewport { width: 100%; overflow-x: auto; }
18
+ .vobs-data-table__table { width: 100%; border-collapse: collapse; table-layout: fixed; }
19
+ .vobs-data-table th,
20
+ .vobs-data-table td {
21
+ padding: var(--vobs-table-cell-padding, 10px 12px);
22
+ border-bottom: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
23
+ text-align: start;
24
+ vertical-align: middle;
25
+ }
26
+ .vobs-data-table th { color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); font-size: 0.85em; font-weight: 600; }
27
+ .vobs-data-table--sticky-header thead th {
28
+ position: sticky;
29
+ top: var(--vobs-table-sticky-top, 0px);
30
+ z-index: 2;
31
+ background: var(--vobs-table-header-surface, var(--vobs-table-surface, var(--bg-base-secondary, #fff)));
32
+ box-shadow: 0 1px 0 var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
33
+ }
34
+ .vobs-data-table tbody tr:last-child td { border-bottom: 0; }
35
+ .vobs-data-table [data-align="center"] { text-align: center; }
36
+ .vobs-data-table [data-align="end"] { text-align: end; font-variant-numeric: tabular-nums; }
37
+ .vobs-data-table tr[data-clickable="true"] { cursor: pointer; }
38
+ .vobs-data-table tr[data-clickable="true"]:hover { background: var(--vobs-table-row-hover, var(--bg-overlay-l2, rgba(0, 0, 0, .04))); }
39
+ .vobs-data-table__sort,
40
+ .vobs-data-table__page-button {
41
+ border: 0;
42
+ background: transparent;
43
+ color: inherit;
44
+ cursor: pointer;
45
+ font: inherit;
46
+ }
47
+ .vobs-data-table__sort { display: inline-flex; align-items: center; gap: 6px; padding: 0; }
48
+ .vobs-data-table__sort:hover,
49
+ .vobs-data-table__sort:focus-visible { color: var(--vobs-table-sort-active, var(--text-brand, #32F08C)); }
50
+ .vobs-data-table__sort-icon {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ min-width: 1em;
55
+ color: var(--vobs-table-muted, var(--text-tertiary, #57606a));
56
+ }
57
+ .vobs-data-table th[data-sort-direction] .vobs-data-table__sort-icon {
58
+ color: var(--vobs-table-sort-active, var(--text-brand, #32F08C));
59
+ }
60
+ .vobs-data-table__state { padding: 32px 12px; color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); text-align: center; }
61
+ .vobs-data-table__spacer { padding: 0; border: 0; }
62
+ .vobs-data-table__footer-content,
63
+ .vobs-data-table__pagination { display: flex; align-items: center; gap: 8px; }
64
+ .vobs-data-table__footer-content { justify-content: space-between; flex-wrap: wrap; }
65
+ .vobs-data-table__summary {
66
+ color: var(--vobs-table-muted, var(--text-tertiary, #57606a));
67
+ font-size: 0.85em;
68
+ font-weight: 600;
69
+ white-space: nowrap;
70
+ }
71
+ .vobs-data-table__pagination { flex-wrap: wrap; justify-content: flex-end; }
72
+ .vobs-data-table__pagination-pages { display: inline-flex; align-items: center; gap: 4px; }
73
+ .vobs-data-table__page-button {
74
+ display: inline-flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
78
+ border-radius: 2px;
79
+ min-width: 24px;
80
+ height: 24px;
81
+ padding: 0 6px;
82
+ }
83
+ .vobs-data-table__page-button.is-active {
84
+ background: var(--vobs-table-active, var(--bg-overlay-l2, rgba(0, 0, 0, .06)));
85
+ border-color: var(--vobs-table-active-border, var(--border-neutral-l2, #afb8c1));
86
+ }
87
+ .vobs-data-table__page-button:disabled { cursor: not-allowed; opacity: .45; }
88
+ .vobs-data-table__page-ellipsis { min-width: 24px; text-align: center; color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); }
89
+ .vobs-data-table__pagination-jump { display: inline-flex; align-items: center; gap: 6px; }
90
+ .vobs-data-table__page-size { display: inline-flex; align-items: center; gap: 6px; margin-left: 4px; color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); font-size: 0.85em; font-weight: 600; white-space: nowrap; }
91
+ .vobs-data-table__page-size-select {
92
+ box-sizing: border-box;
93
+ height: 24px;
94
+ padding: 0 20px 0 6px;
95
+ color: inherit;
96
+ background: var(--bg-overlay-l1, transparent);
97
+ border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
98
+ border-radius: 2px;
99
+ font: inherit;
100
+ font-weight: 400;
101
+ }
102
+ .vobs-data-table__jump-label { display: inline-flex; align-items: center; gap: 6px; color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); font-size: 0.85em; font-weight: 600; }
103
+ .vobs-data-table__jump-input {
104
+ box-sizing: border-box;
105
+ width: 52px;
106
+ height: 24px;
107
+ padding: 0 5px;
108
+ color: inherit;
109
+ background: transparent;
110
+ border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
111
+ border-radius: 2px;
112
+ font: inherit;
113
+ font-weight: 400;
114
+ text-align: center;
115
+ }
116
+ .vobs-data-table__jump-submit { min-width: 32px; }
117
+
118
+ .vobs-column-settings { position: relative; display: inline-block; }
119
+ .vobs-column-settings__trigger { cursor: pointer; list-style: none; }
120
+ .vobs-column-settings__trigger::-webkit-details-marker { display: none; }
121
+ .vobs-column-settings__panel {
122
+ position: absolute;
123
+ z-index: 10;
124
+ right: 0;
125
+ min-width: 160px;
126
+ margin-top: 4px;
127
+ padding: 8px;
128
+ background: var(--vobs-table-surface, var(--bg-base-secondary, #fff));
129
+ border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
130
+ border-radius: var(--vobs-table-radius, 4px);
131
+ box-shadow: 0 8px 24px rgba(0, 0, 0, .16);
132
+ }
133
+ .vobs-column-settings__option { display: flex; align-items: center; gap: 8px; padding: 4px; white-space: nowrap; }
134
+ .vobs-column-settings__reset {
135
+ display: block;
136
+ width: 100%;
137
+ margin-bottom: 8px;
138
+ padding: 4px 6px;
139
+ color: var(--vobs-table-muted, var(--text-tertiary, #57606a));
140
+ background: transparent;
141
+ border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
142
+ border-radius: 2px;
143
+ cursor: pointer;
144
+ font: inherit;
145
+ text-align: start;
146
+ }
147
+ .vobs-column-settings__reset:hover,
148
+ .vobs-column-settings__move:hover:not(:disabled) {
149
+ color: var(--vobs-table-text, var(--text-default, #1f2328));
150
+ background: var(--vobs-table-row-hover, var(--bg-overlay-l2, rgba(0, 0, 0, .04)));
151
+ }
152
+ .vobs-column-settings__visibility,
153
+ .vobs-column-settings__layout {
154
+ display: grid;
155
+ gap: 2px;
156
+ }
157
+ .vobs-column-settings__layout {
158
+ margin-top: 8px;
159
+ padding-top: 8px;
160
+ border-top: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
161
+ }
162
+ .vobs-column-settings__layout-option {
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: space-between;
166
+ gap: 12px;
167
+ padding: 4px;
168
+ }
169
+ .vobs-column-settings__layout-label {
170
+ min-width: 0;
171
+ overflow: hidden;
172
+ text-overflow: ellipsis;
173
+ white-space: nowrap;
174
+ }
175
+ .vobs-column-settings__layout-controls {
176
+ display: inline-flex;
177
+ align-items: center;
178
+ gap: 3px;
179
+ flex: 0 0 auto;
180
+ }
181
+ .vobs-column-settings__move {
182
+ display: inline-flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ width: 24px;
186
+ height: 24px;
187
+ padding: 0;
188
+ color: inherit;
189
+ background: transparent;
190
+ border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
191
+ border-radius: 2px;
192
+ cursor: pointer;
193
+ font: inherit;
194
+ line-height: 1;
195
+ }
196
+ .vobs-column-settings__move:disabled { cursor: not-allowed; opacity: .45; }
197
+ .vobs-column-settings__width {
198
+ box-sizing: border-box;
199
+ width: 64px;
200
+ height: 24px;
201
+ padding: 0 5px;
202
+ color: inherit;
203
+ background: transparent;
204
+ border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
205
+ border-radius: 2px;
206
+ font: inherit;
207
+ text-align: end;
208
+ }
209
+ .vobs-column-settings__width::placeholder { color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); }
package/src/types.ts ADDED
@@ -0,0 +1,183 @@
1
+ import type { VobsNode } from '@vobs/vobs'
2
+
3
+ export type DataTableAlign = 'start' | 'center' | 'end'
4
+ export type DataTableSortDirection = 'asc' | 'desc'
5
+ export type DataTableSortType = 'text' | 'number' | 'date' | 'boolean'
6
+ export type DataTableSortingMode = 'client' | 'server'
7
+ export type DataTablePaginationMode = 'simple' | 'standard' | 'all'
8
+
9
+ export interface DataTableSort {
10
+ readonly columnId: string
11
+ readonly direction: DataTableSortDirection
12
+ /** Optional backend field supplied by the column definition. */
13
+ readonly sortKey?: string
14
+ }
15
+
16
+ export interface DataTableQuery {
17
+ readonly page: number
18
+ readonly pageSize: number
19
+ readonly sort: DataTableSort | null
20
+ readonly filters: Readonly<Record<string, unknown>>
21
+ }
22
+
23
+ export interface DataTablePage<Row> {
24
+ readonly rows: readonly Row[]
25
+ readonly total?: number
26
+ }
27
+
28
+ export type DataTableResourceData<Row> = readonly Row[] | DataTablePage<Row>
29
+
30
+ export interface DataTableResource<Row> {
31
+ readonly data: { readonly value: DataTableResourceData<Row> | null }
32
+ readonly error: { readonly value: Error | null }
33
+ readonly loading: { readonly value: boolean }
34
+ refetch(): Promise<unknown>
35
+ }
36
+
37
+ export type DataTableChildren =
38
+ | VobsNode
39
+ | string
40
+ | number
41
+ | readonly DataTableChildren[]
42
+ | (() => DataTableChildren | null | undefined)
43
+
44
+ export interface DataTableColumn<Row = Record<string, unknown>> {
45
+ readonly id: string
46
+ readonly label: string
47
+ readonly key?: keyof Row
48
+ readonly render?: (row: Row, index: number) => DataTableChildren
49
+ readonly compare?: (left: Row, right: Row) => number
50
+ /** Value used by the built-in client sorter when compare is not provided. */
51
+ readonly sortValue?: (row: Row) => unknown
52
+ /** Optional value kind for deterministic client-side sorting. */
53
+ readonly sortType?: DataTableSortType
54
+ /** Backend field mapping; emitted together with the stable column id in DataTableSort. */
55
+ readonly sortKey?: string
56
+ /** Apply a filter value supplied by the page or another external filter control. */
57
+ readonly filter?: (row: Row, value: unknown) => boolean
58
+ readonly sortable?: boolean
59
+ /** Marks a column as supported by an external filter UI; no control is rendered in the table. */
60
+ readonly filterable?: boolean
61
+ readonly visible?: boolean
62
+ readonly align?: DataTableAlign
63
+ readonly width?: string | number
64
+ readonly minWidth?: string | number
65
+ }
66
+
67
+ export interface DataTableColumnSettings {
68
+ readonly visibleColumnIds: readonly string[]
69
+ readonly columnOrder: readonly string[]
70
+ readonly columnWidths: Readonly<Record<string, string | number>>
71
+ }
72
+
73
+ export interface DataTableColumnSettingsPersistence {
74
+ load(): DataTableColumnSettings | null | undefined
75
+ save(settings: DataTableColumnSettings): void
76
+ reset?(): void
77
+ }
78
+
79
+ /** Minimal storage shape accepted by createColumnSettingsPersistence. */
80
+ export interface DataTableColumnSettingsStorage {
81
+ get<T>(key: string): T | null | undefined
82
+ set<T>(key: string, value: T): void
83
+ remove(key: string): void
84
+ }
85
+
86
+ export interface DataTableIcons {
87
+ readonly ascending?: DataTableChildren
88
+ readonly descending?: DataTableChildren
89
+ readonly unsorted?: DataTableChildren
90
+ readonly previous?: DataTableChildren
91
+ readonly next?: DataTableChildren
92
+ }
93
+
94
+ export interface DataTablePaginationContext {
95
+ readonly page: number
96
+ readonly pageCount: number
97
+ readonly pageSize: number
98
+ readonly pageSizeOptions: readonly number[]
99
+ readonly total: number
100
+ readonly visibleCount: number
101
+ readonly goToPage: (page: number) => void
102
+ readonly setPageSize: (pageSize: number) => void
103
+ }
104
+
105
+ export type DataTablePagination =
106
+ | DataTableChildren
107
+ | ((context: DataTablePaginationContext) => DataTableChildren | null | undefined)
108
+
109
+ export interface DataTableCommonProps {
110
+ readonly class?: string
111
+ readonly className?: string
112
+ readonly style?: string
113
+ readonly id?: string
114
+ readonly title?: string
115
+ readonly role?: string
116
+ readonly tabIndex?: number
117
+ readonly [name: `aria-${string}`]: string | number | boolean | undefined
118
+ readonly [name: `data-${string}`]: string | number | boolean | undefined
119
+ }
120
+
121
+ export interface KitDataTableProps<Row = Record<string, unknown>> extends DataTableCommonProps {
122
+ readonly columns?: readonly DataTableColumn<Row>[]
123
+ readonly rows?: readonly Row[]
124
+ readonly resource?: DataTableResource<Row>
125
+ readonly rowKey?: (row: Row, index: number) => unknown
126
+ readonly page?: number
127
+ readonly pageSize?: number
128
+ readonly total?: number
129
+ readonly sort?: DataTableSort | null
130
+ /** Client sorts loaded rows; server leaves row order to the resource owner. */
131
+ readonly sortingMode?: DataTableSortingMode
132
+ /** Query state is owned by the caller when provided and is never edited by table-owned inputs. */
133
+ readonly filters?: Readonly<Record<string, unknown>>
134
+ readonly columnSettings?: DataTableColumnSettings
135
+ readonly visibleColumnIds?: readonly string[]
136
+ readonly virtual?: boolean
137
+ readonly stickyHeader?: boolean
138
+ readonly virtualHeight?: number
139
+ readonly rowHeight?: number
140
+ readonly overscan?: number
141
+ readonly loading?: DataTableChildren
142
+ readonly empty?: DataTableChildren
143
+ readonly error?: (error: Error, retry: () => Promise<unknown>) => DataTableChildren
144
+ readonly toolbar?: DataTableChildren
145
+ readonly footer?: DataTableChildren
146
+ readonly pagination?: DataTablePagination
147
+ readonly paginationMode?: DataTablePaginationMode
148
+ readonly pageSizeOptions?: readonly number[]
149
+ readonly pageSizeLabel?: string
150
+ readonly icons?: DataTableIcons
151
+ readonly previousLabel?: string
152
+ readonly nextLabel?: string
153
+ readonly jumpToLabel?: string
154
+ readonly jumpToPlaceholder?: string
155
+ readonly jumpToSubmitLabel?: string
156
+ /** Formats the footer summary. The last two arguments are visible row count and page size. */
157
+ readonly pageLabel?: (
158
+ page: number,
159
+ pageCount: number,
160
+ total: number,
161
+ visibleCount: number,
162
+ pageSize: number
163
+ ) => string
164
+ /** Receives table-originated sort and pagination changes for a page-level query controller. */
165
+ readonly onQueryChange?: (query: DataTableQuery) => void
166
+ readonly onRowClick?: (row: Row, index: number) => void
167
+ readonly onVisibleColumnIdsChange?: (ids: readonly string[]) => void
168
+ }
169
+
170
+ export interface KitColumnSettingsProps<Row = Record<string, unknown>> extends DataTableCommonProps {
171
+ readonly columns?: readonly DataTableColumn<Row>[]
172
+ readonly settings?: DataTableColumnSettings
173
+ readonly persistence?: DataTableColumnSettingsPersistence
174
+ readonly visibleColumnIds?: readonly string[]
175
+ readonly trigger?: DataTableChildren
176
+ readonly label?: string
177
+ readonly closeLabel?: string
178
+ readonly resetLabel?: string
179
+ readonly onChange?: (ids: readonly string[]) => void
180
+ readonly onSettingsChange?: (settings: DataTableColumnSettings) => void
181
+ readonly onReset?: () => void
182
+ readonly onPersistenceError?: (error: unknown) => void
183
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,172 @@
1
+ import { effect } from '@vobs/reactivity'
2
+ import {
3
+ createFragment,
4
+ createText,
5
+ insertBefore,
6
+ setAttribute,
7
+ setTextContent,
8
+ type VobsNode
9
+ } from '@vobs/vobs'
10
+ import type { DataTableColumn, DataTableColumnSettings } from './types'
11
+ export function readProp<T>(props: object, name: string, fallback: T): T {
12
+ const value = Reflect.get(props, name)
13
+ return (value === undefined ? fallback : value) as T
14
+ }
15
+
16
+ export function hasProp(props: object, name: string): boolean {
17
+ return name in props
18
+ }
19
+
20
+ export function bindClassList(root: Element, props: object, classes: () => readonly (string | undefined)[]): void {
21
+ effect(() => {
22
+ const userClasses = [readString(props, 'class'), readString(props, 'className')]
23
+ setAttribute(root, 'class', [...classes(), ...userClasses].filter(Boolean).join(' '))
24
+ })
25
+ }
26
+
27
+ export function bindCommonAttributes(root: Element, props: object, skip: readonly string[]): void {
28
+ const ignored = new Set([...skip, 'class', 'className', 'style', 'children'])
29
+ effect(() => {
30
+ for (const name of Object.keys(props)) {
31
+ if (ignored.has(name) || name.startsWith('on')) continue
32
+ if (name !== 'id' && name !== 'title' && name !== 'role' && name !== 'tabIndex'
33
+ && !name.startsWith('aria-') && !name.startsWith('data-')) continue
34
+ const value = Reflect.get(props, name)
35
+ if (value === undefined || value === null || value === false) removeAttribute(root, normalizeAttributeName(name))
36
+ else setAttribute(root, normalizeAttributeName(name), String(value))
37
+ }
38
+ })
39
+ }
40
+
41
+ export function bindStyle(root: Element, props: object, internal: () => string = () => ''): void {
42
+ effect(() => {
43
+ const value = [internal(), readString(props, 'style')].filter(Boolean).join('; ')
44
+ if (value) setAttribute(root, 'style', value)
45
+ else removeAttribute(root, 'style')
46
+ })
47
+ }
48
+
49
+ export function bindText(node: Text, read: () => unknown): void {
50
+ effect(() => setTextContent(node, String(read() ?? '')))
51
+ }
52
+
53
+ export function resolveSlot(value: unknown): VobsNode | null {
54
+ const resolved = typeof value === 'function' ? value() : value
55
+ if (resolved === undefined || resolved === null) return null
56
+ if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved))
57
+ if (Array.isArray(resolved)) {
58
+ return createFragment((parent, anchor) => {
59
+ for (const child of resolved) {
60
+ const node = resolveSlot(child)
61
+ if (node) insertBefore(parent, node, anchor)
62
+ }
63
+ })
64
+ }
65
+ return resolved as VobsNode
66
+ }
67
+
68
+ export function setOptionalAttribute(node: Element, name: string, value: unknown): void {
69
+ if (value === undefined || value === null || value === false || value === '') removeAttribute(node, name)
70
+ else setAttribute(node, name, String(value))
71
+ }
72
+
73
+ export function normalizePixels(value: string | number | undefined): string | undefined {
74
+ if (value === undefined) return undefined
75
+ return typeof value === 'number' ? `${value}px` : value
76
+ }
77
+
78
+ export function createDefaultColumnSettings<Row>(
79
+ columns: readonly DataTableColumn<Row>[],
80
+ visibleColumnIds?: readonly string[]
81
+ ): DataTableColumnSettings {
82
+ const columnOrder = columns.map(column => column.id)
83
+ const requested = new Set(visibleColumnIds ?? columns
84
+ .filter(column => column.visible !== false)
85
+ .map(column => column.id))
86
+ const visible = columnOrder.filter(id => requested.has(id)
87
+ && columns.some(column => column.id === id && column.visible !== false))
88
+ if (visible.length === 0) {
89
+ const fallback = columns.find(column => column.visible !== false)
90
+ if (fallback) visible.push(fallback.id)
91
+ }
92
+ return { visibleColumnIds: visible, columnOrder, columnWidths: {} }
93
+ }
94
+
95
+ export function normalizeColumnSettings<Row>(
96
+ columns: readonly DataTableColumn<Row>[],
97
+ settings?: Partial<DataTableColumnSettings> | null,
98
+ visibleColumnIds?: readonly string[]
99
+ ): DataTableColumnSettings {
100
+ const fallback = createDefaultColumnSettings(columns, visibleColumnIds)
101
+ const knownIds = new Set(fallback.columnOrder)
102
+ const configuredOrder = Array.isArray(settings?.columnOrder) ? settings.columnOrder : undefined
103
+ const order = uniqueKnownIds(configuredOrder, knownIds)
104
+ for (const id of fallback.columnOrder) {
105
+ if (!order.includes(id)) order.push(id)
106
+ }
107
+
108
+ const configuredVisible = Array.isArray(settings?.visibleColumnIds) ? settings.visibleColumnIds : undefined
109
+ const requestedVisible = new Set(configuredVisible ?? fallback.visibleColumnIds)
110
+ const visible = order.filter(id => requestedVisible.has(id)
111
+ && columns.some(column => column.id === id && column.visible !== false))
112
+ if (visible.length === 0) {
113
+ const fallbackVisible = order.find(id => columns.some(column => column.id === id && column.visible !== false))
114
+ if (fallbackVisible) visible.push(fallbackVisible)
115
+ }
116
+
117
+ const columnWidths: Record<string, string | number> = {}
118
+ const configuredWidths = settings?.columnWidths && typeof settings.columnWidths === 'object'
119
+ && !Array.isArray(settings.columnWidths) ? settings.columnWidths : {}
120
+ for (const [id, width] of Object.entries(configuredWidths)) {
121
+ if (knownIds.has(id) && isColumnWidthValue(width)) columnWidths[id] = width
122
+ }
123
+ return { visibleColumnIds: visible, columnOrder: order, columnWidths }
124
+ }
125
+
126
+ export function orderColumns<Row>(
127
+ columns: readonly DataTableColumn<Row>[],
128
+ columnOrder?: readonly string[]
129
+ ): readonly DataTableColumn<Row>[] {
130
+ if (!columnOrder || columnOrder.length === 0) return columns
131
+ const byId = new Map(columns.map(column => [column.id, column]))
132
+ const ordered: DataTableColumn<Row>[] = []
133
+ const seen = new Set<string>()
134
+ for (const id of columnOrder) {
135
+ const column = byId.get(id)
136
+ if (column && !seen.has(id)) {
137
+ ordered.push(column)
138
+ seen.add(id)
139
+ }
140
+ }
141
+ for (const column of columns) {
142
+ if (!seen.has(column.id)) ordered.push(column)
143
+ }
144
+ return ordered
145
+ }
146
+
147
+ export function isColumnWidthValue(value: unknown): value is string | number {
148
+ return typeof value === 'number'
149
+ ? Number.isFinite(value) && value > 0
150
+ : typeof value === 'string' && value.trim().length > 0 && !/[;{}]/u.test(value)
151
+ }
152
+
153
+ function uniqueKnownIds(values: readonly string[] | undefined, knownIds: ReadonlySet<string>): string[] {
154
+ const result: string[] = []
155
+ for (const id of values ?? []) {
156
+ if (knownIds.has(id) && !result.includes(id)) result.push(id)
157
+ }
158
+ return result
159
+ }
160
+
161
+ function readString(props: object, name: string): string | undefined {
162
+ const value = Reflect.get(props, name)
163
+ return typeof value === 'string' ? value : undefined
164
+ }
165
+
166
+ function normalizeAttributeName(name: string): string {
167
+ return name === 'tabIndex' ? 'tabindex' : name
168
+ }
169
+
170
+ function removeAttribute(node: Element, name: string): void {
171
+ node.removeAttribute(name)
172
+ }