intable 0.0.6 → 0.0.7

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 (169) hide show
  1. package/README.md +16 -263
  2. package/docs/index-BaMALNy6.css +1 -0
  3. package/docs/index-CDN48t9E.js +3 -0
  4. package/docs/index-Cc4RNkLY.css +1 -0
  5. package/docs/index-MRnbkYmU.js +3 -0
  6. package/docs/index.html +15 -0
  7. package/docs/vite.svg +1 -0
  8. package/index.html +14 -0
  9. package/package.json +30 -38
  10. package/packages/intable/README.md +379 -0
  11. package/packages/intable/package.json +51 -0
  12. package/packages/intable/src/assets/ClearFormat.svg +3 -0
  13. package/packages/intable/src/assets/Forms.svg +4 -0
  14. package/packages/intable/src/assets/MergeCell.svg +4 -0
  15. package/packages/intable/src/assets/SplitCell.svg +4 -0
  16. package/packages/intable/src/assets/gap.svg +3 -0
  17. package/packages/intable/src/assets/loading.svg +12 -0
  18. package/packages/intable/src/assets/paint.svg +9 -0
  19. package/packages/intable/src/assets/solid.svg +1 -0
  20. package/packages/intable/src/components/Columns.tsx +86 -0
  21. package/packages/intable/src/components/DocTree.tsx +36 -0
  22. package/packages/intable/src/components/Menu.tsx +109 -0
  23. package/packages/intable/src/components/Popover.tsx +55 -0
  24. package/packages/intable/src/components/RecycleList.tsx +99 -0
  25. package/packages/intable/src/components/Render.tsx +26 -0
  26. package/packages/intable/src/components/Split.tsx +56 -0
  27. package/packages/intable/src/components/Tree.tsx +115 -0
  28. package/packages/intable/src/components/utils.tsx +12 -0
  29. package/packages/intable/src/hooks/index.ts +200 -0
  30. package/packages/intable/src/hooks/useDir.ts +78 -0
  31. package/packages/intable/src/hooks/useSelector.ts +91 -0
  32. package/packages/intable/src/hooks/useSort.tsx +118 -0
  33. package/packages/intable/src/hooks/useVirtualizer.ts +180 -0
  34. package/packages/intable/src/index.tsx +481 -0
  35. package/packages/intable/src/plugins/CellChangeHighlightPlugin.tsx +5 -0
  36. package/packages/intable/src/plugins/CellMergePlugin.tsx +153 -0
  37. package/packages/intable/src/plugins/CellSelectionPlugin.tsx +175 -0
  38. package/packages/intable/src/plugins/CommandPlugin.tsx +74 -0
  39. package/packages/intable/src/plugins/CopyPastePlugin.tsx +63 -0
  40. package/packages/intable/src/plugins/DiffPlugin.tsx +107 -0
  41. package/packages/intable/src/plugins/DragPlugin.tsx +81 -0
  42. package/packages/intable/src/plugins/EditablePlugin.tsx +252 -0
  43. package/packages/intable/src/plugins/ExpandPlugin.tsx +80 -0
  44. package/packages/intable/src/plugins/HeaderGroup.tsx +289 -0
  45. package/packages/intable/src/plugins/HistoryPlugin.tsx +49 -0
  46. package/packages/intable/src/plugins/MenuPlugin.tsx +195 -0
  47. package/packages/intable/src/plugins/RenderPlugin/components.tsx +51 -0
  48. package/packages/intable/src/plugins/RenderPlugin/index.tsx +81 -0
  49. package/packages/intable/src/plugins/ResizePlugin.tsx +122 -0
  50. package/packages/intable/src/plugins/RowGroupPlugin.tsx +122 -0
  51. package/packages/intable/src/plugins/RowSelectionPlugin.tsx +65 -0
  52. package/packages/intable/src/plugins/TreePlugin.tsx +212 -0
  53. package/packages/intable/src/plugins/VirtualScrollPlugin.tsx +190 -0
  54. package/packages/intable/src/plugins/ZodValidatorPlugin.tsx +61 -0
  55. package/packages/intable/src/style.scss +244 -0
  56. package/{dist → packages/intable/src}/theme/antd.scss +14 -5
  57. package/{dist → packages/intable/src}/theme/element-plus.scss +6 -5
  58. package/packages/intable/src/tree.ts +13 -0
  59. package/packages/intable/src/types/auto-imports.d.ts +13 -0
  60. package/packages/intable/src/utils.ts +122 -0
  61. package/packages/intable/src/wc.tsx +35 -0
  62. package/packages/intable/src/web-component.ts +1 -0
  63. package/packages/react/package.json +31 -0
  64. package/packages/react/src/index.ts +44 -0
  65. package/packages/react/src/plugins/antd.ts +94 -0
  66. package/packages/react/src/style.scss +12 -0
  67. package/packages/react/src/types/auto-imports.d.ts +10 -0
  68. package/packages/vue/package.json +34 -0
  69. package/packages/vue/src/index.ts +63 -0
  70. package/packages/vue/src/plugins/element-plus.ts +69 -0
  71. package/packages/vue/src/style.scss +12 -0
  72. package/packages/vue/src/types/auto-imports.d.ts +10 -0
  73. package/pnpm-workspace.yaml +2 -0
  74. package/public/vite.svg +1 -0
  75. package/scripts/build.js +184 -0
  76. package/scripts/publish.js +95 -0
  77. package/src/assets/ClearFormat.svg +3 -0
  78. package/src/assets/Forms.svg +4 -0
  79. package/src/assets/MergeCell.svg +4 -0
  80. package/src/assets/SplitCell.svg +4 -0
  81. package/src/assets/gap.svg +3 -0
  82. package/src/assets/loading.svg +12 -0
  83. package/src/assets/paint.svg +9 -0
  84. package/src/assets/solid.svg +1 -0
  85. package/src/demo-vue.ts +54 -0
  86. package/src/demo.tsx +107 -0
  87. package/src/index.scss +105 -0
  88. package/src/styles/index.scss +172 -0
  89. package/src/types/auto-imports.d.ts +13 -0
  90. package/stats.html +4949 -0
  91. package/tsconfig.app.json +34 -0
  92. package/tsconfig.json +7 -0
  93. package/tsconfig.node.json +26 -0
  94. package/vite.config.ts +63 -0
  95. package/dist/__uno.css +0 -1
  96. package/dist/chevron-right.js +0 -6
  97. package/dist/components/Columns.d.ts +0 -3
  98. package/dist/components/Columns.js +0 -71
  99. package/dist/components/DocTree.d.ts +0 -4
  100. package/dist/components/DocTree.js +0 -32
  101. package/dist/components/Menu.d.ts +0 -1
  102. package/dist/components/Menu.js +0 -107
  103. package/dist/components/Popover.d.ts +0 -14
  104. package/dist/components/Popover.js +0 -41
  105. package/dist/components/Render.d.ts +0 -4
  106. package/dist/components/Render.js +0 -20
  107. package/dist/components/Split.d.ts +0 -15
  108. package/dist/components/Split.js +0 -76
  109. package/dist/components/Tree.d.ts +0 -37
  110. package/dist/components/Tree.js +0 -82
  111. package/dist/components/utils.d.ts +0 -3
  112. package/dist/components/utils.js +0 -8
  113. package/dist/hooks/index.d.ts +0 -40
  114. package/dist/hooks/index.js +0 -157
  115. package/dist/hooks/useDir.d.ts +0 -11
  116. package/dist/hooks/useDir.js +0 -42
  117. package/dist/hooks/useSelector.d.ts +0 -16
  118. package/dist/hooks/useSelector.js +0 -35
  119. package/dist/hooks/useSort.d.ts +0 -18
  120. package/dist/hooks/useSort.js +0 -83
  121. package/dist/hooks/useVirtualizer.d.ts +0 -25
  122. package/dist/hooks/useVirtualizer.js +0 -67
  123. package/dist/index.d.ts +0 -130
  124. package/dist/index.js +0 -347
  125. package/dist/loading.js +0 -6
  126. package/dist/plugins/CellChangeHighlightPlugin.d.ts +0 -2
  127. package/dist/plugins/CellChangeHighlightPlugin.js +0 -4
  128. package/dist/plugins/CellMergePlugin.d.ts +0 -12
  129. package/dist/plugins/CellMergePlugin.js +0 -2
  130. package/dist/plugins/CellSelectionPlugin.d.ts +0 -15
  131. package/dist/plugins/CellSelectionPlugin.js +0 -115
  132. package/dist/plugins/CommandPlugin.d.ts +0 -14
  133. package/dist/plugins/CommandPlugin.js +0 -12
  134. package/dist/plugins/CopyPastePlugin.d.ts +0 -14
  135. package/dist/plugins/CopyPastePlugin.js +0 -42
  136. package/dist/plugins/DiffPlugin.d.ts +0 -23
  137. package/dist/plugins/DiffPlugin.js +0 -56
  138. package/dist/plugins/DragPlugin.d.ts +0 -14
  139. package/dist/plugins/DragPlugin.js +0 -47
  140. package/dist/plugins/EditablePlugin.d.ts +0 -48
  141. package/dist/plugins/EditablePlugin.js +0 -141
  142. package/dist/plugins/ExpandPlugin.d.ts +0 -18
  143. package/dist/plugins/ExpandPlugin.js +0 -50
  144. package/dist/plugins/HistoryPlugin.d.ts +0 -10
  145. package/dist/plugins/HistoryPlugin.js +0 -30
  146. package/dist/plugins/MenuPlugin.d.ts +0 -18
  147. package/dist/plugins/MenuPlugin.js +0 -107
  148. package/dist/plugins/RenderPlugin/components.d.ts +0 -5
  149. package/dist/plugins/RenderPlugin/components.js +0 -87
  150. package/dist/plugins/RenderPlugin/index.d.ts +0 -30
  151. package/dist/plugins/RenderPlugin/index.js +0 -49
  152. package/dist/plugins/ResizePlugin.d.ts +0 -27
  153. package/dist/plugins/ResizePlugin.js +0 -81
  154. package/dist/plugins/RowGroupPlugin.d.ts +0 -17
  155. package/dist/plugins/RowGroupPlugin.js +0 -83
  156. package/dist/plugins/RowSelectionPlugin.d.ts +0 -20
  157. package/dist/plugins/RowSelectionPlugin.js +0 -42
  158. package/dist/plugins/VirtualScrollPlugin.d.ts +0 -15
  159. package/dist/plugins/VirtualScrollPlugin.js +0 -96
  160. package/dist/plus.js +0 -6
  161. package/dist/style.css +0 -3
  162. package/dist/types/auto-imports.d.js +0 -0
  163. package/dist/utils.d.ts +0 -30
  164. package/dist/utils.js +0 -70
  165. package/dist/wc.d.ts +0 -1
  166. package/dist/wc.js +0 -21
  167. package/dist/web-component.d.ts +0 -1
  168. package/dist/web-component.js +0 -2
  169. package/dist/x.js +0 -6
@@ -0,0 +1,212 @@
1
+ import { useContext } from 'solid-js'
2
+ import { combineProps } from '@solid-primitives/props'
3
+ import { type Plugin, type Plugin$0, Ctx } from '../index'
4
+ import { useSelector } from '../hooks/useSelector'
5
+ import { createLazyMemo } from '@solid-primitives/memo'
6
+
7
+ // ------------------------------------------------------------
8
+ // Module augmentation
9
+ // ------------------------------------------------------------
10
+
11
+ declare module '../index' {
12
+ interface TableProps {
13
+ tree?: {
14
+ /** Field name that holds children rows. Default: `'children'`. */
15
+ children?: string
16
+ }
17
+ }
18
+ interface TableStore {
19
+ tree: ReturnType<typeof useSelector<any[]>>
20
+ /**
21
+ * Lookup map populated as a side-effect of the `data` rewriteProp.
22
+ * Maps each row's rowKey → its tree metadata.
23
+ * Written via createMutable so reads are reactive.
24
+ */
25
+ _treeMeta: Map<any, { depth: number; hasChildren: boolean; parentKey: any }>
26
+ }
27
+ }
28
+
29
+ // ------------------------------------------------------------
30
+ // Helpers
31
+ // ------------------------------------------------------------
32
+
33
+ interface FlattenResult {
34
+ flat: any[]
35
+ meta: Map<any, { depth: number; hasChildren: boolean; parentKey: any }>
36
+ }
37
+
38
+ function flattenTree(
39
+ rows: any[],
40
+ childrenField: string,
41
+ rowKeyField: string,
42
+ isExpand: (key: any) => boolean,
43
+ depth = 0,
44
+ parentKey: any = null,
45
+ flat: any[] = [],
46
+ meta: Map<any, { depth: number; hasChildren: boolean; parentKey: any }> = new Map(),
47
+ ): FlattenResult {
48
+ for (const row of rows || []) {
49
+ const key = row[rowKeyField]
50
+ const children: any[] = row[childrenField]
51
+ const hasChildren = Array.isArray(children) && children.length > 0
52
+ meta.set(key, { depth, hasChildren, parentKey })
53
+ flat.push(row)
54
+ if (hasChildren && isExpand(key)) {
55
+ flattenTree(children, childrenField, rowKeyField, isExpand, depth + 1, key, flat, meta)
56
+ }
57
+ }
58
+ return { flat, meta }
59
+ }
60
+
61
+ // ------------------------------------------------------------
62
+ // Plugin
63
+ // ------------------------------------------------------------
64
+
65
+ export const TreePlugin: Plugin$0 = store => {
66
+ const firstCol = createLazyMemo(() => store.props.columns?.findIndex(e => !e[store.internal]) ?? -1)
67
+
68
+ return {
69
+ name: 'tree',
70
+
71
+ store: (store) => ({
72
+ tree: useSelector({ multiple: true }),
73
+ _treeMeta: new Map(),
74
+ }),
75
+
76
+ rewriteProps: {
77
+ tree: ({ tree }) => ({
78
+ children: 'children',
79
+ ...tree,
80
+ }),
81
+
82
+ /**
83
+ * Flatten nested `children` arrays into a single display list.
84
+ * Simultaneously populates `store._treeMeta` (depth, hasChildren, parentKey)
85
+ * so that `Td` can read tree metadata without a second traversal.
86
+ */
87
+ data: ({ data }, { store }) => {
88
+ if (!store.props?.tree) return data
89
+
90
+ const childrenField = store.props.tree.children || 'children'
91
+ const rowKeyField = store.props.rowKey
92
+
93
+ const { flat, meta } = flattenTree(
94
+ data,
95
+ childrenField,
96
+ rowKeyField,
97
+ (key) => store.tree.has(key),
98
+ )
99
+
100
+ // Reactive write: any context reading store._treeMeta will re-run
101
+ store._treeMeta = meta
102
+
103
+ return flat
104
+ },
105
+
106
+ /**
107
+ * Render the first non-internal column with indentation and an
108
+ * expand / collapse chevron for rows that have children.
109
+ */
110
+ Td: ({ Td }, { store }) => o => {
111
+ const rowKey = () => store.props.rowKey
112
+ const meta = () => store._treeMeta?.get(o.data?.[rowKey()])
113
+
114
+ o = combineProps(o, {
115
+ onDblClick() {
116
+ meta()?.hasChildren && store.tree.toggle(o.data?.[rowKey()])
117
+ },
118
+ })
119
+
120
+ return (
121
+ <Td {...o}>
122
+ {/* todo */}
123
+ {o.x === firstCol() ? (
124
+ <div class='flex items-center' style={`padding-left: ${meta()?.depth! * 16}px`}>
125
+ {meta()?.hasChildren ? (
126
+ <ILucideChevronRight
127
+ class='icon-clickable mr-1'
128
+ style={`transform: rotate(${store.tree.has(o.data?.[rowKey()]) ? 90 : 0}deg); opacity: .6; flex-shrink: 0; transition: transform .15s`}
129
+ onClick={(e: MouseEvent) => { e.stopPropagation(); store.tree.toggle(o.data?.[rowKey()]) }}
130
+ />
131
+ ) : (
132
+ // Spacer keeps text aligned with sibling rows that do have an icon
133
+ <span style='display: inline-block; width: 16px; flex-shrink: 0; margin-right: 4px' />
134
+ )}
135
+ {o.children}
136
+ </div>
137
+ ) : (
138
+ o.children
139
+ )}
140
+ </Td>
141
+ )
142
+ },
143
+ },
144
+
145
+ commands: (store, { addRows }) => ({
146
+ // add to parent children array if adding inside an expanded group, otherwise add to root
147
+ addRows(i, rows, before = true) {
148
+ if (!store.props?.tree) return addRows?.(i, rows, before)
149
+
150
+ const { data: flatData, rowKey } = store.props!
151
+ const childrenField = store.props.tree.children || 'children'
152
+
153
+ // Skip internal rows – find the nearest real anchor (same logic as base addRows)
154
+ let anchor = flatData[i]
155
+ if (anchor?.[store.internal]) {
156
+ let p: any = null, n: any = null
157
+ for (let j = i - 1; j >= 0; j--) { if (!flatData[j]?.[store.internal]) { p = flatData[j]; break } }
158
+ for (let j = i + 1; j < flatData.length; j++) { if (!flatData[j]?.[store.internal]) { n = flatData[j]; break } }
159
+ anchor = before ? (p || n) : (n || p)
160
+ }
161
+
162
+ // No anchor or anchor is at root depth → delegate to base addRows untouched
163
+ if (!anchor) return addRows?.(i, rows, before)
164
+ const anchorKey = anchor[rowKey]
165
+ const meta = store._treeMeta?.get(anchorKey)
166
+ if (!meta || meta.depth === 0) return addRows?.(i, rows, before)
167
+
168
+ const parentKey = meta.parentKey
169
+
170
+ // Update cell selection to point at the inserted rows
171
+ const anchorFlatIdx = flatData.findIndex((r: any) => r[rowKey] === anchorKey)
172
+ if (anchorFlatIdx >= 0 && store.selected) {
173
+ const insertAt = anchorFlatIdx + (before ? 0 : 1)
174
+ store.selected.start = [0, insertAt]
175
+ store.selected.end = [Infinity, insertAt + rows.length - 1]
176
+ }
177
+
178
+ // Splice into the parent's children array (immutably clone up to the splice point)
179
+ const rawData = [...(store.rawProps.data || [])]
180
+
181
+ function insertInto(nodes: any[]): boolean {
182
+ for (let j = 0; j < nodes.length; j++) {
183
+ const node = nodes[j]
184
+ if (node[rowKey] === parentKey) {
185
+ const children = [...(node[childrenField] || [])]
186
+ const childIdx = children.findIndex((c: any) => c[rowKey] === anchorKey)
187
+ const at = childIdx >= 0 ? childIdx + (before ? 0 : 1) : children.length
188
+ children.splice(at, 0, ...rows)
189
+ nodes[j] = { ...node, [childrenField]: children }
190
+ return true
191
+ }
192
+ if (Array.isArray(node[childrenField]) && node[childrenField].length) {
193
+ const childrenCopy = [...node[childrenField]]
194
+ if (insertInto(childrenCopy)) {
195
+ nodes[j] = { ...node, [childrenField]: childrenCopy }
196
+ return true
197
+ }
198
+ }
199
+ }
200
+ return false
201
+ }
202
+
203
+ insertInto(rawData)
204
+
205
+ // Ensure the parent branch is expanded so the new row is visible
206
+ if (!store.tree.has(parentKey)) store.tree.toggle(parentKey)
207
+
208
+ store.props?.onDataChange?.(rawData)
209
+ },
210
+ }),
211
+ }
212
+ }
@@ -0,0 +1,190 @@
1
+ import { createEffect, createMemo, useContext, mergeProps } from 'solid-js'
2
+ import { combineProps } from '@solid-primitives/props'
3
+ import { defaultRangeExtractor } from '@tanstack/solid-virtual'
4
+ import { defaultsDeep } from 'es-toolkit/compat'
5
+ import { useVirtualizer } from '../hooks/useVirtualizer'
6
+ import { RecycleList } from '../components/RecycleList'
7
+ import { Ctx, type Plugin } from '..'
8
+
9
+ const $ML = Symbol()
10
+
11
+ declare module '../index' {
12
+ interface TableProps {
13
+ virtual?: {
14
+ x?: Partial<Parameters<typeof useVirtualizer>[0]>
15
+ y?: Partial<Parameters<typeof useVirtualizer>[0]>
16
+ }
17
+ }
18
+ interface TableStore {
19
+ // virtualizerY: Virtualizer<HTMLElement, Element>
20
+ // virtualizerX: Virtualizer<HTMLElement, Element>
21
+ virtualizerY: ReturnType<typeof useVirtualizer>
22
+ virtualizerX: ReturnType<typeof useVirtualizer>
23
+ }
24
+ }
25
+
26
+ export const VirtualScrollPlugin: Plugin = {
27
+ name: 'virtual-scroll',
28
+ rewriteProps: {
29
+ virtual: ({ virtual }) => defaultsDeep(virtual, {
30
+ x: { overscan: 5 },
31
+ y: { overscan: 10 },
32
+ // x: { batch: 3, overscan: 2 },
33
+ // y: { batch: 5, overscan: 5 },
34
+ }),
35
+ Table: ({ Table }, { store }) => (o) => {
36
+ let el: HTMLElement
37
+
38
+ const { props } = useContext(Ctx)
39
+
40
+ const virtualizerY = useVirtualizer(mergeProps(() => props.virtual?.y, {
41
+ getScrollElement: () => el,
42
+ get count() { return props.data?.length || 0 },
43
+ estimateSize: () => 32,
44
+ indexAttribute: 'y',
45
+ extras: (yStart, yEnd) => {
46
+ const mergeMap = store._mergeMap?.()
47
+ if (!mergeMap?.spans.size) return []
48
+ const vx = store.virtualizerX
49
+ const xStart = vx ? vx.startIdx() : 0
50
+ const xEnd = vx ? vx.endIdx() : Infinity
51
+ const extras: number[] = []
52
+ for (const [key, span] of mergeMap.spans) {
53
+ if (span.rowspan <= 1) continue
54
+ const [ay, ax] = key.split(',').map(Number)
55
+ if (ay > yEnd || ay + span.rowspan - 1 < yStart) continue
56
+ if (ax > xEnd || ax + span.colspan - 1 < xStart) continue
57
+ for (let dy = 0; dy < span.rowspan; dy++) extras.push(ay + dy)
58
+ }
59
+ return extras
60
+ },
61
+ }))
62
+
63
+ const virtualizerX = useVirtualizer(mergeProps(() => props.virtual?.x, {
64
+ horizontal: true,
65
+ getScrollElement: () => el,
66
+ get count() { return props.columns?.length || 0 },
67
+ estimateSize: i => props.columns?.[i].width ?? 40,
68
+ indexAttribute: 'x',
69
+ rangeExtractor(range) {
70
+ return [
71
+ ...new Set([
72
+ ...props.columns?.map((e, i) => e.fixed ? i : void 0).filter(e => e != null) || [],
73
+ ...defaultRangeExtractor(range)
74
+ ])
75
+ ]
76
+ },
77
+ extras: (xStart, xEnd) => {
78
+ const fixed = props.columns?.map((e, i) => e.fixed ? i : void 0).filter(e => e != null) || []
79
+ const headerAnchors = store._headerGroupAnchors?.(xStart, xEnd) || []
80
+ const base = new Set<number>([...fixed, ...headerAnchors])
81
+ const mergeMap = store._mergeMap?.()
82
+ if (!mergeMap?.spans.size) return [...base]
83
+ const yStart = virtualizerY.startIdx(), yEnd = virtualizerY.endIdx()
84
+ for (const [key, span] of mergeMap.spans) {
85
+ if (span.colspan <= 1) continue
86
+ const [ay, ax] = key.split(',').map(Number)
87
+ if (ay > yEnd || ay + span.rowspan - 1 < yStart) continue
88
+ if (ax > xEnd || ax + span.colspan - 1 < xStart) continue
89
+ for (let dx = 0; dx < span.colspan; dx++) base.add(ax + dx)
90
+ }
91
+ return [...base]
92
+ },
93
+ }))
94
+
95
+ store.virtualizerY = virtualizerY
96
+ store.virtualizerX = virtualizerX
97
+
98
+ store[$ML] = createMemo(() => {
99
+ const items = store.virtualizerX.getVirtualItems()
100
+ const ret = {}
101
+ for (let i = 1; i < items.length; i++) {
102
+ const item = items[i], prev = items[i - 1]
103
+ if (item.index - prev.index > 1) ret[item.index] = { item, offset: item.start - prev.end }
104
+ }
105
+ return ret
106
+ })
107
+
108
+ o = combineProps({ ref: e => el = e, class: 'virtual' }, o)
109
+
110
+ createEffect(() => {
111
+ const { table } = store
112
+ table.style.width = store.virtualizerX.getTotalSize() + 'px'
113
+ table.style.height = store.virtualizerY.getTotalSize() + (store.thead?.offsetHeight || 0) + 'px'
114
+ })
115
+
116
+ return <Table {...o} />
117
+ },
118
+ Thead: ({ Thead }, { store }) => o => {
119
+ o = combineProps(({
120
+ get style() { return `transform: translate(${store.virtualizerX.getVirtualItems()[0]?.start}px, ${0}px);` }
121
+ }), o)
122
+ return <Thead {...o} />
123
+ },
124
+ Tbody: ({ Tbody }, { store }) => o => {
125
+ o = combineProps({
126
+ get style() { return `transform: translate(${store.virtualizerX.getVirtualItems()[0]?.start}px, ${store.virtualizerY.getVirtualItems()[0]?.start}px)` }
127
+ }, o)
128
+ return <Tbody {...o} />
129
+ },
130
+ Tr: ({ Tr }, { store }) => (o) => {
131
+ createEffect(() => o.y != null && store.trSizes[o.y] && store.virtualizerY.resizeItem(o.y!, store.trSizes[o.y!]!.height))
132
+ return <Tr {...o} />
133
+ },
134
+ Td: ({ Td }, { store }) => (o) => {
135
+ const ml = createMemo(() => store[$ML]()[o.x])
136
+ const mo = combineProps({ get style() {
137
+ const cs = o.colspan ?? 1
138
+ const w = cs > 1
139
+ ? Array.from({ length: cs }, (_, dx) => store.props.columns?.[o.x + dx]?.width ?? 80).reduce((a, b) => a + b, 0)
140
+ : (o.col.width || 80)
141
+ return `width: ${w}px; margin-left: ${o.col.fixed ? 0 : ml()?.offset ?? 0}px`
142
+ } }, o)
143
+ return <Td {...mo} />
144
+ },
145
+ Th: ({ Th }, { store }) => (o) => {
146
+ // Only resize the virtualizer for single-column cells; colspan cells would report
147
+ // their combined width which shouldn't override individual column sizes.
148
+ createEffect(() => (o.colspan ?? 1) === 1 && store.thSizes[o.x] && store.virtualizerX.resizeItem(o.x, store.thSizes[o.x]!.width))
149
+ const ml = createMemo(() => store[$ML]?.()[o.x])
150
+ const mo = combineProps({ get style() {
151
+ const cs = o.colspan ?? 1
152
+ const w = cs > 1
153
+ ? Array.from({ length: cs }, (_, dx) => store.props.columns?.[o.x + dx]?.width ?? 80).reduce((a, b) => a + b, 0)
154
+ : (o.col.width || 80)
155
+ return `width: ${w}px; margin-left: ${o.col.fixed ? 0 : ml()?.offset ?? 0}px`
156
+ } }, o)
157
+ return <Th {...mo} />
158
+ },
159
+
160
+ EachRows: ({ EachRows }, { store }) => (o) => {
161
+ // use recycle-list
162
+ EachRows = RecycleList
163
+ const list = createMemo(() => store.virtualizerY.getVirtualItems().map(e => o.each[e.index]))
164
+ return (
165
+ <EachRows {...o} each={list()}>
166
+ {(e, i) => {
167
+ return o.children(e, createMemo(() => store.virtualizerY.getVirtualItems()[i()]?.index))
168
+ }}
169
+ </EachRows>
170
+ )
171
+ },
172
+ EachCells: ({ EachCells }, { store }) => (o) => {
173
+ // use recycle-list
174
+ EachCells = RecycleList
175
+ // Skip X virtualization when the array doesn't match the column virtualizer count
176
+ // (e.g. header group rows have fewer entries than leaf columns)
177
+ if (o.each?.length !== store.virtualizerX?.options?.count) {
178
+ return <EachCells {...o} />
179
+ }
180
+ const list = createMemo(() => store.virtualizerX.getVirtualItems().map(e => o.each[e.index]))
181
+ return (
182
+ <EachCells {...o} each={list()}>
183
+ {(e, i) => {
184
+ return o.children(e, createMemo(() => store.virtualizerX.getVirtualItems()[i()]?.index))
185
+ }}
186
+ </EachCells>
187
+ )
188
+ },
189
+ }
190
+ }
@@ -0,0 +1,61 @@
1
+ import type { ZodType } from 'zod'
2
+ import type { Plugin, TableColumn } from '..'
3
+
4
+ declare module '../index' {
5
+ interface TableColumn {
6
+ /**
7
+ * Zod schema to validate the edited value for this column.
8
+ *
9
+ * ```ts
10
+ * { id: 'age', zodSchema: z.number().int().min(0).max(150) }
11
+ * ```
12
+ */
13
+ zodSchema?: ZodType<any, any, any>
14
+ }
15
+ }
16
+
17
+ /**
18
+ * ZodValidatorPlugin — three-level validation pipeline for editable cells.
19
+ *
20
+ * Validation runs in order; the first failure short-circuits the rest:
21
+ * 1. `col.zodSchema` — Zod schema declared on the column
22
+ * 2. `col.validator` — per-column custom validator function
23
+ * 3. `props.validator` — table-level validator (passed as a prop to `<Intable>`)
24
+ *
25
+ * ```tsx
26
+ * const cols = [
27
+ * {
28
+ * id: 'email',
29
+ * zodSchema: z.string().email('Invalid email'),
30
+ * validator: async (value) => {
31
+ * const taken = await checkEmailTaken(value)
32
+ * return taken ? 'Email already in use' : true
33
+ * },
34
+ * },
35
+ * { id: 'age', zodSchema: z.coerce.number().int().min(0).max(150) },
36
+ * ]
37
+ * <Intable columns={cols} plugins={[ZodValidatorPlugin]} />
38
+ * ```
39
+ */
40
+ export const ZodValidatorPlugin: Plugin = {
41
+ name: 'zod-validator',
42
+ rewriteProps: {
43
+ validator: ({ validator }) => async (value, data, col: TableColumn) => {
44
+ // 1. Zod schema on the column
45
+ const schema = col.zodSchema
46
+ if (schema) {
47
+ const result = schema.safeParse(value)
48
+ if (!result.success) {
49
+ return result.error.issues[0]?.message ?? false
50
+ }
51
+ }
52
+
53
+ // 2. Table-level validator
54
+ if (validator) {
55
+ return validator(value, data, col)
56
+ }
57
+
58
+ return true
59
+ },
60
+ },
61
+ }
@@ -0,0 +1,244 @@
1
+ .data-table {
2
+ --bg: #fff;
3
+ --c-primary: #51a2ff;
4
+
5
+ --menu-bg: #fff;
6
+ --li-hover-bg: rgb(153, 161, 175, .2);
7
+
8
+ --table-b: 1px solid var(--table-b-c);
9
+ --table-b-c: #ebeef5;
10
+ --table-c: #606266;
11
+ --table-bg: #fff;
12
+
13
+ --table-header-c: #909399;
14
+ --table-header-bg: var(--table-bg);
15
+
16
+ --table-row-hover-bg: #f5f7fa;
17
+
18
+ --select-area-bg: #5292f71a;
19
+
20
+
21
+ @apply relative text-[14px] c-[--table-c] b-[--table-b-c];
22
+
23
+ &--table {
24
+ @apply table-fixed w-max border-collapse b-0 outline-0 c-[--table-c];
25
+ border-collapse: separate;
26
+ border-spacing: 0;
27
+ }
28
+
29
+ thead {
30
+ @apply c-[--table-header-c];
31
+ }
32
+
33
+ tr:hover {
34
+ & > td {
35
+ @apply bg-[--table-row-hover-bg];
36
+ }
37
+ }
38
+
39
+ th {
40
+ @apply bg-[--table-header-bg];
41
+ }
42
+
43
+ td {
44
+ @apply bg-[--table-bg];
45
+ }
46
+
47
+ td, th {
48
+ @apply px-2 py-.5 outline-0 b-(0 solid [--table-b-c]) b-b-0 box-border v-mid;
49
+ text-align: inherit;
50
+ background-image: linear-gradient(var(--table-b-c), var(--table-b-c));
51
+ background-repeat: no-repeat;
52
+ background-size: 100% 1px, 1px 100%;
53
+ background-position: 100% 100%, 100% 0;
54
+ }
55
+
56
+ td:empty::after {
57
+ content: 'ㅤ' !important;
58
+ }
59
+
60
+ &--border {
61
+ @apply b-1;
62
+
63
+ th, td {
64
+ background-image: linear-gradient(var(--table-b-c), var(--table-b-c)), linear-gradient(var(--table-b-c), var(--table-b-c));
65
+ }
66
+ }
67
+
68
+ &--scroll-view {
69
+ @apply of-auto;
70
+ }
71
+
72
+ &__layers {
73
+ @apply pointer-events-none absolute left-0 top-0 [&>*]:absolute z-1;
74
+ }
75
+ }
76
+
77
+ .range-selected {
78
+ @apply relative;
79
+ & > .area {
80
+ @apply absolute inset-0 bg-[--select-area-bg] b-(0px solid [--c-primary]) pointer-events-none;
81
+ }
82
+
83
+ &-l > .area { @apply b-l-1.5px }
84
+ &-r > .area { @apply b-r-1.5px }
85
+ &-t > .area { @apply b-t-1.5px }
86
+ &-b > .area { @apply b-b-1.5px }
87
+ }
88
+
89
+ .row-range-highlight, .col-range-highlight {
90
+ @apply relative;
91
+ & > .area {
92
+ @apply absolute inset-0 bg-[--c-primary]/10 b-(0 solid [--c-primary]) pointer-events-none;
93
+ }
94
+ }
95
+
96
+ .row-range-highlight.index > .area {
97
+ @apply b-r-1;
98
+ }
99
+
100
+ .col-range-highlight > .area {
101
+ @apply b-b-1;
102
+ }
103
+
104
+ .sticky-header {
105
+ @apply sticky top-0 bg-#fff z-9;
106
+ &::after {
107
+ @apply content-[''] absolute top-full w-full h-[10px] pointer-events-none;
108
+ box-shadow: inset 0 10px 10px -10px rgba(0, 0, 0, .15);
109
+ }
110
+ }
111
+
112
+ .fixed-left, .fixed-right {
113
+ @apply sticky! z-1 bg-#fff;
114
+ &.is-first::after, &.is-last::after {
115
+ @apply content-[''] absolute top-0 w-[10px] h-full pointer-events-none;
116
+ }
117
+ }
118
+
119
+ .is-scroll-right, .is-scroll-mid {
120
+ .fixed-left.is-last::after {
121
+ @apply left-full;
122
+ box-shadow: inset 10px 0 10px -10px rgba(0, 0, 0, .15);
123
+ }
124
+ }
125
+
126
+ .is-scroll-left, .is-scroll-mid {
127
+ .fixed-right.is-first::after {
128
+ @apply right-full;
129
+ box-shadow: inset -10px 0 10px -10px rgba(0, 0, 0, .15);
130
+ }
131
+ }
132
+
133
+ .copied .range-selected {
134
+ & > .area { border-style: dashed; }
135
+ }
136
+
137
+ //
138
+ .data-table.virtual {
139
+ @apply block of-auto;
140
+
141
+ thead { @apply block w-fit [&>tr]-(flex) [&>tr>th]-(block flex-[0_0_auto]); }
142
+ tbody { @apply block w-fit [&>tr]-(flex) [&>tr>td]-(block flex-[0_0_auto]); }
143
+ }
144
+
145
+ .row-selection {
146
+ @apply relative w-10;
147
+ & > label {
148
+ @apply absolute inset-0 flex justify-center items-center w-full h-full;
149
+ }
150
+ }
151
+
152
+ .icon-clickable {
153
+ @apply flex justify-center items-center p-.5 w-5 h-5 box-border rd-1 hover:bg-gray/30 [&>svg]:(w-full h-full);
154
+ }
155
+
156
+ input[type="checkbox"].you-checkbox {
157
+ @apply relative size-4 m-1 rd-1 c-#2196f3 b-(2 solid current) outline-offset--0 focus:outline-(4 solid #2196f3/40) cusor-pointer appearance-none checked:bg-current;
158
+ &.checked::after {
159
+ @apply content-["✓"] absolute top-50% left-50% translate--50% text-3 c-#fff z-1;
160
+ }
161
+ }
162
+
163
+ td.is-editing {
164
+ @apply relative;
165
+ }
166
+
167
+ td.is-invalid {
168
+ @apply relative;
169
+ outline: 1.5px solid #ff4d4f;
170
+ outline-offset: -1.5px;
171
+ }
172
+
173
+ .in-cell-edit-wrapper {
174
+ @apply absolute inset-0 z-1;
175
+ box-shadow: 0 0 10px -2px #00000050;
176
+ }
177
+
178
+ .cell-validating {
179
+ @apply absolute right-1 top-50% translate-y--50% w-3 h-3 rd-full border-2 border-[--c-primary] border-t-transparent animate-spin block pointer-events-none;
180
+ }
181
+
182
+ .cell-validation-error {
183
+ position: absolute;
184
+ left: 0;
185
+ top: 100%;
186
+ z-index: 10;
187
+ background: #fff1f0;
188
+ color: #ff4d4f;
189
+ border: 1px solid #ffccc7;
190
+ border-radius: 4px;
191
+ padding: 2px 8px;
192
+ font-size: 12px;
193
+ white-space: nowrap;
194
+ box-shadow: 0 2px 8px #00000020;
195
+ pointer-events: none;
196
+ &:empty {
197
+ display: none;
198
+ }
199
+ }
200
+
201
+ .in-cell__resize-handle {
202
+ @apply absolute size-full hover:after:bg-[--c-primary]/40 active:after:bg-[--c-primary];
203
+ &::after {
204
+ @apply content-[''];
205
+ }
206
+ }
207
+
208
+ //
209
+ .li {
210
+ @apply relative cursor-pointer;
211
+ &:hover, &.hover {
212
+ @apply bg-[--li-hover-bg];
213
+ }
214
+ &:active, &.selected, &.active {
215
+ &::before {
216
+ content: '';
217
+ @apply absolute inset-0 rd-inherit bg-gray/15;
218
+ }
219
+ }
220
+ &.disabled, &[disabled] {
221
+ opacity: .4;
222
+ }
223
+ }
224
+
225
+ .tt-menu {
226
+ @apply py-1 shadow-lg rd-2 space-y-.5 text-sm b-(1 solid gray/30) bg-[--menu-bg] cursor-default;
227
+ }
228
+
229
+ .tt-menu-x {
230
+ @apply px-1 shadow-lg rd-2 space-x-.5 text-sm b-(1 solid gray/30) bg-[--menu-bg] cursor-default;
231
+
232
+ & > .hr {
233
+ @apply w-[1px] my-1.5 bg-gray/30;
234
+ }
235
+ }
236
+
237
+ // drag
238
+ .col__guide-line, .row__guide-line {
239
+ @apply fixed top-0 left-0 bg-[--c-primary] z-9 pointer-events-none;
240
+ }
241
+
242
+ th[draggable="true"], td[draggable="true"] {
243
+ @apply cursor-move;
244
+ }