@epoch32/visage 0.0.0-stage → 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/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- package/src/vite/types.ts +65 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { For } from "../dom/control-flow";
|
|
2
|
+
import { computed, signal } from "../signals";
|
|
3
|
+
import { createVirtualizer } from "./virtualizer";
|
|
4
|
+
|
|
5
|
+
export interface VirtualListProps<T> {
|
|
6
|
+
each: T[] | (() => T[]);
|
|
7
|
+
itemHeight: number | ((index: number) => number);
|
|
8
|
+
height?: number | string;
|
|
9
|
+
width?: number | string;
|
|
10
|
+
overscan?: number;
|
|
11
|
+
class?: string;
|
|
12
|
+
style?: Record<string, any> | (() => Record<string, any>);
|
|
13
|
+
children: (item: T, index: () => number) => any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function VirtualList<T>(props: VirtualListProps<T>) {
|
|
17
|
+
const scrollElement = signal<HTMLElement | null>(null);
|
|
18
|
+
|
|
19
|
+
const getItems = (): T[] =>
|
|
20
|
+
typeof props.each === "function" ? (props.each as () => T[])() : props.each;
|
|
21
|
+
const count = computed(() => getItems().length);
|
|
22
|
+
|
|
23
|
+
const parsedHeight =
|
|
24
|
+
typeof props.height === "number"
|
|
25
|
+
? props.height
|
|
26
|
+
: Number.parseInt(String(props.height || 400), 10) || 400;
|
|
27
|
+
|
|
28
|
+
const virtualizer = createVirtualizer({
|
|
29
|
+
count,
|
|
30
|
+
itemHeight: props.itemHeight,
|
|
31
|
+
overscan: props.overscan ?? 3,
|
|
32
|
+
initialSize: parsedHeight,
|
|
33
|
+
getScrollElement: () => scrollElement()
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const containerHeight =
|
|
37
|
+
typeof props.height === "number" ? `${props.height}px` : (props.height ?? "400px");
|
|
38
|
+
|
|
39
|
+
const containerWidth =
|
|
40
|
+
typeof props.width === "number" ? `${props.width}px` : (props.width ?? "100%");
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
ref={(el: HTMLElement) => {
|
|
45
|
+
scrollElement.set(el);
|
|
46
|
+
}}
|
|
47
|
+
class={props.class}
|
|
48
|
+
style={() => {
|
|
49
|
+
const customStyle = typeof props.style === "function" ? props.style() : props.style || {};
|
|
50
|
+
return {
|
|
51
|
+
overflowY: "auto",
|
|
52
|
+
overflowX: "hidden",
|
|
53
|
+
position: "relative",
|
|
54
|
+
height: containerHeight,
|
|
55
|
+
width: containerWidth,
|
|
56
|
+
WebkitOverflowScrolling: "touch",
|
|
57
|
+
...customStyle
|
|
58
|
+
};
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
style={() => ({
|
|
63
|
+
height: `${virtualizer.totalSize()}px`,
|
|
64
|
+
width: "100%",
|
|
65
|
+
position: "relative",
|
|
66
|
+
pointerEvents: "none"
|
|
67
|
+
})}
|
|
68
|
+
>
|
|
69
|
+
<For each={() => virtualizer.virtualItems()}>
|
|
70
|
+
{(vItem) => {
|
|
71
|
+
const itemData = computed(() => getItems()[vItem.index]);
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
style={() => ({
|
|
75
|
+
position: "absolute",
|
|
76
|
+
top: 0,
|
|
77
|
+
left: 0,
|
|
78
|
+
width: "100%",
|
|
79
|
+
height: `${vItem.size}px`,
|
|
80
|
+
transform: `translateY(${vItem.start}px)`,
|
|
81
|
+
pointerEvents: "auto",
|
|
82
|
+
willChange: "transform"
|
|
83
|
+
})}
|
|
84
|
+
>
|
|
85
|
+
{props.children(itemData() as T, () => vItem.index)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}}
|
|
89
|
+
</For>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface VirtualGridProps<T> {
|
|
96
|
+
data: T[][] | (() => T[][]);
|
|
97
|
+
rowHeight: number;
|
|
98
|
+
columnWidth: number;
|
|
99
|
+
height?: number | string;
|
|
100
|
+
width?: number | string;
|
|
101
|
+
overscan?: number;
|
|
102
|
+
children: (item: T, rowIndex: number, colIndex: number) => any;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function VirtualGrid<T>(props: VirtualGridProps<T>) {
|
|
106
|
+
const scrollElement = signal<HTMLElement | null>(null);
|
|
107
|
+
const getData = (): T[][] =>
|
|
108
|
+
typeof props.data === "function" ? (props.data as () => T[][])() : props.data;
|
|
109
|
+
|
|
110
|
+
const rowCount = computed(() => getData().length);
|
|
111
|
+
const colCount = computed(() => {
|
|
112
|
+
const d = getData();
|
|
113
|
+
return d[0] ? d[0].length : 0;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const rowVirtualizer = createVirtualizer({
|
|
117
|
+
count: rowCount,
|
|
118
|
+
itemHeight: props.rowHeight,
|
|
119
|
+
overscan: props.overscan ?? 3,
|
|
120
|
+
getScrollElement: () => scrollElement()
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const colVirtualizer = createVirtualizer({
|
|
124
|
+
count: colCount,
|
|
125
|
+
itemHeight: props.columnWidth,
|
|
126
|
+
overscan: props.overscan ?? 3,
|
|
127
|
+
horizontal: true,
|
|
128
|
+
getScrollElement: () => scrollElement()
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const containerHeight =
|
|
132
|
+
typeof props.height === "number" ? `${props.height}px` : (props.height ?? "400px");
|
|
133
|
+
|
|
134
|
+
const containerWidth =
|
|
135
|
+
typeof props.width === "number" ? `${props.width}px` : (props.width ?? "100%");
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
ref={(el: HTMLElement) => {
|
|
140
|
+
scrollElement.set(el);
|
|
141
|
+
}}
|
|
142
|
+
style={{
|
|
143
|
+
overflow: "auto",
|
|
144
|
+
position: "relative",
|
|
145
|
+
height: containerHeight,
|
|
146
|
+
width: containerWidth,
|
|
147
|
+
WebkitOverflowScrolling: "touch"
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<div
|
|
151
|
+
style={() => ({
|
|
152
|
+
height: `${rowVirtualizer.totalSize()}px`,
|
|
153
|
+
width: `${colVirtualizer.totalSize()}px`,
|
|
154
|
+
position: "relative"
|
|
155
|
+
})}
|
|
156
|
+
>
|
|
157
|
+
<For each={() => rowVirtualizer.virtualItems()}>
|
|
158
|
+
{(r) => (
|
|
159
|
+
<For each={() => colVirtualizer.virtualItems()}>
|
|
160
|
+
{(c) => {
|
|
161
|
+
const rowData = getData()[r.index];
|
|
162
|
+
const item = rowData ? rowData[c.index] : undefined;
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
style={() => ({
|
|
166
|
+
position: "absolute",
|
|
167
|
+
top: 0,
|
|
168
|
+
left: 0,
|
|
169
|
+
width: `${c.size}px`,
|
|
170
|
+
height: `${r.size}px`,
|
|
171
|
+
transform: `translate(${c.start}px, ${r.start}px)`,
|
|
172
|
+
willChange: "transform"
|
|
173
|
+
})}
|
|
174
|
+
>
|
|
175
|
+
{props.children(item as T, r.index, c.index)}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}}
|
|
179
|
+
</For>
|
|
180
|
+
)}
|
|
181
|
+
</For>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { computed, effect, onCleanup, type ReadonlySignal, signal } from "../signals";
|
|
2
|
+
|
|
3
|
+
export interface VirtualItem {
|
|
4
|
+
index: number;
|
|
5
|
+
start: number;
|
|
6
|
+
size: number;
|
|
7
|
+
end: number;
|
|
8
|
+
key: number | string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface VirtualizerOptions {
|
|
12
|
+
count: number | (() => number);
|
|
13
|
+
itemHeight: number | ((index: number) => number);
|
|
14
|
+
getScrollElement: () => HTMLElement | null;
|
|
15
|
+
overscan?: number;
|
|
16
|
+
horizontal?: boolean;
|
|
17
|
+
initialSize?: number;
|
|
18
|
+
estimateSize?: (index: number) => number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Virtualizer {
|
|
22
|
+
virtualItems: ReadonlySignal<VirtualItem[]>;
|
|
23
|
+
totalSize: ReadonlySignal<number>;
|
|
24
|
+
scrollOffset: ReadonlySignal<number>;
|
|
25
|
+
containerSize: ReadonlySignal<number>;
|
|
26
|
+
startIndex: ReadonlySignal<number>;
|
|
27
|
+
endIndex: ReadonlySignal<number>;
|
|
28
|
+
scrollToIndex: (index: number, options?: { align?: "start" | "center" | "end" | "auto" }) => void;
|
|
29
|
+
scrollToOffset: (offset: number) => void;
|
|
30
|
+
measureElement: (element: HTMLElement | null, index: number) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
|
|
34
|
+
const overscan = options.overscan ?? 3;
|
|
35
|
+
const isHorizontal = Boolean(options.horizontal);
|
|
36
|
+
|
|
37
|
+
const getCount = (): number =>
|
|
38
|
+
typeof options.count === "function"
|
|
39
|
+
? (options.count as () => number)()
|
|
40
|
+
: (options.count as number);
|
|
41
|
+
|
|
42
|
+
const getItemHeight =
|
|
43
|
+
typeof options.itemHeight === "function"
|
|
44
|
+
? options.itemHeight
|
|
45
|
+
: () => options.itemHeight as number;
|
|
46
|
+
|
|
47
|
+
const scrollOffset = signal(0);
|
|
48
|
+
const containerSize = signal(options.initialSize ?? 400);
|
|
49
|
+
|
|
50
|
+
// Dynamic measurement cache for variable height rows
|
|
51
|
+
const dynamicSizeCache = new Map<number, number>();
|
|
52
|
+
|
|
53
|
+
const getItemSize = (index: number): number => {
|
|
54
|
+
const cached = dynamicSizeCache.get(index);
|
|
55
|
+
if (cached !== undefined) return cached;
|
|
56
|
+
return getItemHeight(index);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Compute prefix offsets for fast range calculation
|
|
60
|
+
const totalSize = computed(() => {
|
|
61
|
+
const count = getCount();
|
|
62
|
+
if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
|
|
63
|
+
return count * options.itemHeight;
|
|
64
|
+
}
|
|
65
|
+
let total = 0;
|
|
66
|
+
for (let i = 0; i < count; i++) {
|
|
67
|
+
total += getItemSize(i);
|
|
68
|
+
}
|
|
69
|
+
return total;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const getOffsetForIndex = (targetIndex: number): number => {
|
|
73
|
+
if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
|
|
74
|
+
return targetIndex * options.itemHeight;
|
|
75
|
+
}
|
|
76
|
+
let offset = 0;
|
|
77
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
78
|
+
offset += getItemSize(i);
|
|
79
|
+
}
|
|
80
|
+
return offset;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const findIndexAtOffset = (offset: number): number => {
|
|
84
|
+
const count = getCount();
|
|
85
|
+
if (count === 0) return 0;
|
|
86
|
+
|
|
87
|
+
if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
|
|
88
|
+
const idx = Math.floor(offset / options.itemHeight);
|
|
89
|
+
return Math.max(0, Math.min(count - 1, idx));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let low = 0;
|
|
93
|
+
let high = count - 1;
|
|
94
|
+
|
|
95
|
+
while (low <= high) {
|
|
96
|
+
const mid = (low + high) >> 1;
|
|
97
|
+
const midStart = getOffsetForIndex(mid);
|
|
98
|
+
const midEnd = midStart + getItemSize(mid);
|
|
99
|
+
|
|
100
|
+
if (offset < midStart) {
|
|
101
|
+
high = mid - 1;
|
|
102
|
+
} else if (offset >= midEnd) {
|
|
103
|
+
low = mid + 1;
|
|
104
|
+
} else {
|
|
105
|
+
return mid;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Math.max(0, Math.min(count - 1, low));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const startIndex = computed(() => {
|
|
113
|
+
const offset = scrollOffset();
|
|
114
|
+
const rawStart = findIndexAtOffset(offset);
|
|
115
|
+
return Math.max(0, rawStart - overscan);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const endIndex = computed(() => {
|
|
119
|
+
const count = getCount();
|
|
120
|
+
if (count === 0) return 0;
|
|
121
|
+
const endOffset = scrollOffset() + (containerSize() || 500);
|
|
122
|
+
const rawEnd = findIndexAtOffset(endOffset);
|
|
123
|
+
return Math.min(count - 1, rawEnd + overscan);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const itemCache = new Map<number, VirtualItem>();
|
|
127
|
+
|
|
128
|
+
const virtualItems = computed(() => {
|
|
129
|
+
const count = getCount();
|
|
130
|
+
if (count === 0) return [];
|
|
131
|
+
|
|
132
|
+
const start = startIndex();
|
|
133
|
+
const end = endIndex();
|
|
134
|
+
const items: VirtualItem[] = [];
|
|
135
|
+
|
|
136
|
+
let currentStart = getOffsetForIndex(start);
|
|
137
|
+
|
|
138
|
+
for (let i = start; i <= end; i++) {
|
|
139
|
+
const size = getItemSize(i);
|
|
140
|
+
let item = itemCache.get(i);
|
|
141
|
+
if (!item || item.start !== currentStart || item.size !== size) {
|
|
142
|
+
item = {
|
|
143
|
+
index: i,
|
|
144
|
+
start: currentStart,
|
|
145
|
+
size,
|
|
146
|
+
end: currentStart + size,
|
|
147
|
+
key: i
|
|
148
|
+
};
|
|
149
|
+
itemCache.set(i, item);
|
|
150
|
+
}
|
|
151
|
+
items.push(item);
|
|
152
|
+
currentStart += size;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return items;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Setup DOM event listeners and ResizeObserver
|
|
159
|
+
effect(() => {
|
|
160
|
+
const el = options.getScrollElement();
|
|
161
|
+
if (!el || typeof el.addEventListener !== "function") return;
|
|
162
|
+
|
|
163
|
+
const handleScroll = () => {
|
|
164
|
+
const offset = isHorizontal ? el.scrollLeft : el.scrollTop;
|
|
165
|
+
scrollOffset.set(offset);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleResize = () => {
|
|
169
|
+
const size = isHorizontal ? el.clientWidth : el.clientHeight;
|
|
170
|
+
if (size > 0) {
|
|
171
|
+
containerSize.set(size);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Initial sync
|
|
176
|
+
handleResize();
|
|
177
|
+
handleScroll();
|
|
178
|
+
|
|
179
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
180
|
+
|
|
181
|
+
let resizeObserver: any;
|
|
182
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
183
|
+
resizeObserver = new ResizeObserver(() => {
|
|
184
|
+
handleResize();
|
|
185
|
+
});
|
|
186
|
+
resizeObserver.observe(el);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
onCleanup(() => {
|
|
190
|
+
el.removeEventListener("scroll", handleScroll);
|
|
191
|
+
if (resizeObserver) {
|
|
192
|
+
resizeObserver.disconnect();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const scrollToIndex = (
|
|
198
|
+
index: number,
|
|
199
|
+
scrollOpts?: { align?: "start" | "center" | "end" | "auto" }
|
|
200
|
+
) => {
|
|
201
|
+
const el = options.getScrollElement();
|
|
202
|
+
if (!el) return;
|
|
203
|
+
|
|
204
|
+
const align = scrollOpts?.align ?? "auto";
|
|
205
|
+
const itemStart = getOffsetForIndex(index);
|
|
206
|
+
const itemSize = getItemSize(index);
|
|
207
|
+
const itemEnd = itemStart + itemSize;
|
|
208
|
+
const currentScroll = scrollOffset();
|
|
209
|
+
const cSize = containerSize();
|
|
210
|
+
|
|
211
|
+
let targetOffset = itemStart;
|
|
212
|
+
|
|
213
|
+
if (align === "start") {
|
|
214
|
+
targetOffset = itemStart;
|
|
215
|
+
} else if (align === "end") {
|
|
216
|
+
targetOffset = itemEnd - cSize;
|
|
217
|
+
} else if (align === "center") {
|
|
218
|
+
targetOffset = itemStart - (cSize - itemSize) / 2;
|
|
219
|
+
} else {
|
|
220
|
+
// auto
|
|
221
|
+
if (itemStart < currentScroll) {
|
|
222
|
+
targetOffset = itemStart;
|
|
223
|
+
} else if (itemEnd > currentScroll + cSize) {
|
|
224
|
+
targetOffset = itemEnd - cSize;
|
|
225
|
+
} else {
|
|
226
|
+
return; // already fully in view
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
targetOffset = Math.max(0, targetOffset);
|
|
231
|
+
if (isHorizontal) {
|
|
232
|
+
el.scrollLeft = targetOffset;
|
|
233
|
+
} else {
|
|
234
|
+
el.scrollTop = targetOffset;
|
|
235
|
+
}
|
|
236
|
+
scrollOffset.set(targetOffset);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const scrollToOffset = (offset: number) => {
|
|
240
|
+
const el = options.getScrollElement();
|
|
241
|
+
if (el) {
|
|
242
|
+
if (isHorizontal) {
|
|
243
|
+
el.scrollLeft = offset;
|
|
244
|
+
} else {
|
|
245
|
+
el.scrollTop = offset;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
scrollOffset.set(offset);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const measureElement = (element: HTMLElement | null, index: number) => {
|
|
252
|
+
if (!element) return;
|
|
253
|
+
const measuredSize = isHorizontal
|
|
254
|
+
? element.getBoundingClientRect().width
|
|
255
|
+
: element.getBoundingClientRect().height;
|
|
256
|
+
if (measuredSize > 0 && dynamicSizeCache.get(index) !== measuredSize) {
|
|
257
|
+
dynamicSizeCache.set(index, measuredSize);
|
|
258
|
+
// Force recalculation by touching scrollOffset
|
|
259
|
+
scrollOffset.update((o) => o);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
virtualItems,
|
|
265
|
+
totalSize,
|
|
266
|
+
scrollOffset,
|
|
267
|
+
containerSize,
|
|
268
|
+
startIndex,
|
|
269
|
+
endIndex,
|
|
270
|
+
scrollToIndex,
|
|
271
|
+
scrollToOffset,
|
|
272
|
+
measureElement
|
|
273
|
+
};
|
|
274
|
+
}
|
package/src/vite/hmr.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform JSX/TSX modules to attach Hot Module Replacement (HMR) acceptance
|
|
3
|
+
*/
|
|
4
|
+
export function transformHmr(code: string, id: string): { code: string; map?: any } | null {
|
|
5
|
+
// Only target user code JSX/TSX files
|
|
6
|
+
if (!/\.[jt]sx$/.test(id) || id.includes("node_modules")) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Check if file exports any functions or components
|
|
11
|
+
const hasExports =
|
|
12
|
+
/\bexport\s+(?:default\s+)?(?:function|const|class)\s+([A-Z][A-Za-z0-9_]*)/.test(code) ||
|
|
13
|
+
/\bexport\s+\{\s*([A-Za-z0-9_,\s]+)\s*\}/.test(code);
|
|
14
|
+
|
|
15
|
+
if (!hasExports) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Append lightweight HMR boundary
|
|
20
|
+
const hmrFooter = `
|
|
21
|
+
/* @visage/vite-hmr */
|
|
22
|
+
if (import.meta.hot) {
|
|
23
|
+
import.meta.hot.accept((newMod) => {
|
|
24
|
+
if (newMod) {
|
|
25
|
+
// Re-trigger reactive components without wiping signal stores
|
|
26
|
+
if (typeof window !== "undefined" && window.__VISAGE_DEVTOOLS__) {
|
|
27
|
+
window.__VISAGE_DEVTOOLS__.refresh?.();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
code: `${code}\n;${hmrFooter}`,
|
|
36
|
+
map: null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { VisageVitePlugin } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface VisageLucideOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Module aliases to intercept and translate.
|
|
6
|
+
* Defaults to `["lucide-react"]`.
|
|
7
|
+
*/
|
|
8
|
+
aliases?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ParsedImport {
|
|
12
|
+
raw: string;
|
|
13
|
+
importedName: string;
|
|
14
|
+
localName: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseNamedImports(importClause: string): ParsedImport[] {
|
|
18
|
+
return importClause
|
|
19
|
+
.split(",")
|
|
20
|
+
.map((s) => s.trim())
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((item) => {
|
|
23
|
+
const parts = item.split(/\s+as\s+/).map((p) => p.trim());
|
|
24
|
+
const importedName = parts[0]!;
|
|
25
|
+
const localName = parts[1] ?? importedName;
|
|
26
|
+
return {
|
|
27
|
+
raw: item,
|
|
28
|
+
importedName,
|
|
29
|
+
localName
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Official Visage Lucide Vite Plugin
|
|
36
|
+
*
|
|
37
|
+
* Statically transforms `import { Icon } from "lucide-react"` at build time into
|
|
38
|
+
* native, tree-shaken Visage SVG component instances using `createIcon` and `lucide`.
|
|
39
|
+
*
|
|
40
|
+
* Example:
|
|
41
|
+
* ```ts
|
|
42
|
+
* // vite.config.ts
|
|
43
|
+
* import { defineConfig } from "vite";
|
|
44
|
+
* import visage, { visageLucide } from "visage/vite";
|
|
45
|
+
*
|
|
46
|
+
* export default defineConfig({
|
|
47
|
+
* plugins: [visage(), visageLucide()]
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function visageLucide(options: VisageLucideOptions = {}): VisageVitePlugin {
|
|
52
|
+
const aliases = options.aliases ?? ["lucide-react"];
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: "visage:lucide",
|
|
56
|
+
enforce: "pre",
|
|
57
|
+
|
|
58
|
+
config() {
|
|
59
|
+
return {
|
|
60
|
+
optimizeDeps: {
|
|
61
|
+
exclude: aliases
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
resolveId(id: string) {
|
|
67
|
+
if (aliases.includes(id)) {
|
|
68
|
+
return `\0visage-lucide:${id}`;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
load(id: string) {
|
|
74
|
+
if (id.startsWith("\0visage-lucide:")) {
|
|
75
|
+
return "export default {};";
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
transform(code: string, id: string) {
|
|
81
|
+
if (!id.match(/\.[jt]sx?$/) || id.includes("node_modules")) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let transformed = code;
|
|
86
|
+
let hasMatches = false;
|
|
87
|
+
|
|
88
|
+
for (const alias of aliases) {
|
|
89
|
+
// Regex to match: import { ... } from "lucide-react" or 'lucide-react'
|
|
90
|
+
const regex = new RegExp(`import\\s*\\{([^}]+)\\}\\s*from\\s*['"]${alias}['"];?`, "g");
|
|
91
|
+
|
|
92
|
+
transformed = transformed.replace(regex, (_match, importClause) => {
|
|
93
|
+
const imports = parseNamedImports(importClause);
|
|
94
|
+
if (imports.length === 0) return "";
|
|
95
|
+
|
|
96
|
+
hasMatches = true;
|
|
97
|
+
|
|
98
|
+
const lucideNamedImports = imports
|
|
99
|
+
.map((imp) => `${imp.importedName} as __icon_node_${imp.importedName}`)
|
|
100
|
+
.join(", ");
|
|
101
|
+
|
|
102
|
+
const componentDeclarations = imports
|
|
103
|
+
.map((imp) => {
|
|
104
|
+
return `const ${imp.localName} = /* @__PURE__ */ __visageCreateIcon("${imp.importedName}", __icon_node_${imp.importedName});`;
|
|
105
|
+
})
|
|
106
|
+
.join("\n");
|
|
107
|
+
|
|
108
|
+
return `import { createIcon as __visageCreateIcon } from "visage/ui";\nimport { ${lucideNamedImports} } from "lucide";\n${componentDeclarations}`;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!hasMatches) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
code: transformed,
|
|
118
|
+
map: null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default visageLucide;
|