@epoch32/visage 0.0.0-stage → 0.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.
Files changed (138) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +110 -2
  3. package/dist/bundle.dev.js +4875 -0
  4. package/dist/index.js +9633 -0
  5. package/dist/src/api/index.js +4002 -0
  6. package/dist/src/charts/index.js +2717 -0
  7. package/dist/src/devtools/index.js +4677 -0
  8. package/dist/src/dom/index.js +1550 -0
  9. package/dist/src/form/index.js +1415 -0
  10. package/dist/src/index.js +11826 -0
  11. package/dist/src/jsx-runtime/index.js +1630 -0
  12. package/dist/src/query/index.js +2422 -0
  13. package/dist/src/router/index.js +1856 -0
  14. package/dist/src/rpc/index.js +3484 -0
  15. package/dist/src/server/index.js +1173 -0
  16. package/dist/src/signals/index.js +1077 -0
  17. package/dist/src/style/index.js +2340 -0
  18. package/dist/src/template/index.js +1149 -0
  19. package/dist/src/ui/index.js +4155 -0
  20. package/dist/src/virtual/index.js +1955 -0
  21. package/dist/src/vite/index.js +984 -0
  22. package/dist/vite/index.js +707 -0
  23. package/package.json +140 -4
  24. package/src/api/client.ts +371 -0
  25. package/src/api/file-router.ts +183 -0
  26. package/src/api/handler.ts +326 -0
  27. package/src/api/index.ts +71 -0
  28. package/src/api/middleware.ts +54 -0
  29. package/src/api/pragma.ts +155 -0
  30. package/src/api/procedure.ts +172 -0
  31. package/src/api/types.ts +249 -0
  32. package/src/charts/Area.tsx +111 -0
  33. package/src/charts/Axis.tsx +126 -0
  34. package/src/charts/Bar.tsx +86 -0
  35. package/src/charts/CartesianGrid.tsx +72 -0
  36. package/src/charts/Chart.tsx +269 -0
  37. package/src/charts/Cursor.tsx +42 -0
  38. package/src/charts/Legend.tsx +56 -0
  39. package/src/charts/Line.tsx +106 -0
  40. package/src/charts/Pie.tsx +88 -0
  41. package/src/charts/Tooltip.tsx +127 -0
  42. package/src/charts/context.ts +30 -0
  43. package/src/charts/index.ts +14 -0
  44. package/src/charts/path.ts +155 -0
  45. package/src/charts/scale.ts +133 -0
  46. package/src/charts/types.ts +50 -0
  47. package/src/devtools/DomTab.tsx +134 -0
  48. package/src/devtools/GraphInspector.tsx +196 -0
  49. package/src/devtools/GraphTab.tsx +315 -0
  50. package/src/devtools/QueryTab.tsx +200 -0
  51. package/src/devtools/RpcTab.tsx +190 -0
  52. package/src/devtools/SignalsTab.tsx +129 -0
  53. package/src/devtools/component.tsx +243 -0
  54. package/src/devtools/highlighter.ts +134 -0
  55. package/src/devtools/hub.ts +175 -0
  56. package/src/devtools/index.ts +20 -0
  57. package/src/devtools/styles.ts +313 -0
  58. package/src/devtools/types.ts +45 -0
  59. package/src/dom/async.ts +232 -0
  60. package/src/dom/control-flow.ts +353 -0
  61. package/src/dom/index.ts +32 -0
  62. package/src/dom/lazy.ts +82 -0
  63. package/src/dom/render.ts +286 -0
  64. package/src/dom/resource.ts +120 -0
  65. package/src/form/form.ts +218 -0
  66. package/src/form/index.ts +17 -0
  67. package/src/form/schema.ts +299 -0
  68. package/src/index.ts +413 -0
  69. package/src/jsx-dev-runtime/index.ts +1 -0
  70. package/src/jsx-runtime/index.ts +112 -0
  71. package/src/jsx-runtime/jsx.d.ts +278 -0
  72. package/src/query/create-mutation.ts +64 -0
  73. package/src/query/create-query.ts +426 -0
  74. package/src/query/focus-manager.ts +68 -0
  75. package/src/query/index.ts +63 -0
  76. package/src/query/mutation-cache.ts +138 -0
  77. package/src/query/online-manager.ts +67 -0
  78. package/src/query/query-cache.ts +369 -0
  79. package/src/query/query-client.ts +248 -0
  80. package/src/query/types.ts +221 -0
  81. package/src/query/utils.ts +132 -0
  82. package/src/router/index.ts +19 -0
  83. package/src/router/router.tsx +352 -0
  84. package/src/rpc/client.ts +317 -0
  85. package/src/rpc/error.ts +70 -0
  86. package/src/rpc/index.ts +52 -0
  87. package/src/rpc/procedure.ts +88 -0
  88. package/src/rpc/router.ts +105 -0
  89. package/src/rpc/server.ts +511 -0
  90. package/src/rpc/types.ts +328 -0
  91. package/src/server/hydrate.ts +21 -0
  92. package/src/server/index.ts +10 -0
  93. package/src/server/render-to-stream.ts +31 -0
  94. package/src/server/render-to-string.ts +125 -0
  95. package/src/signals/computed.ts +217 -0
  96. package/src/signals/context.ts +89 -0
  97. package/src/signals/effect.ts +161 -0
  98. package/src/signals/index.ts +57 -0
  99. package/src/signals/scope.ts +132 -0
  100. package/src/signals/signal.ts +203 -0
  101. package/src/signals/store.ts +163 -0
  102. package/src/signals/system.ts +124 -0
  103. package/src/signals/types.ts +64 -0
  104. package/src/style/component.tsx +122 -0
  105. package/src/style/create-style.ts +229 -0
  106. package/src/style/cx.ts +75 -0
  107. package/src/style/hash.ts +19 -0
  108. package/src/style/index.ts +8 -0
  109. package/src/style/layout.tsx +406 -0
  110. package/src/style/manager.ts +87 -0
  111. package/src/style/parser.ts +257 -0
  112. package/src/style/scale.ts +237 -0
  113. package/src/template/index.ts +4 -0
  114. package/src/template/template.ts +126 -0
  115. package/src/ui/accordion.tsx +286 -0
  116. package/src/ui/command-dialog.tsx +49 -0
  117. package/src/ui/command.tsx +400 -0
  118. package/src/ui/data-table.tsx +322 -0
  119. package/src/ui/dialog.tsx +362 -0
  120. package/src/ui/dropdown-menu.tsx +381 -0
  121. package/src/ui/icon.tsx +69 -0
  122. package/src/ui/index.ts +138 -0
  123. package/src/ui/layout.tsx +13 -0
  124. package/src/ui/popover.tsx +249 -0
  125. package/src/ui/switch.tsx +114 -0
  126. package/src/ui/tabs.tsx +297 -0
  127. package/src/ui/toast.tsx +280 -0
  128. package/src/ui/tooltip.tsx +262 -0
  129. package/src/ui/utils.ts +110 -0
  130. package/src/virtual/index.ts +12 -0
  131. package/src/virtual/virtual-list.tsx +185 -0
  132. package/src/virtual/virtualizer.ts +274 -0
  133. package/src/vite/hmr.ts +38 -0
  134. package/src/vite/index.ts +3 -0
  135. package/src/vite/lucide.ts +124 -0
  136. package/src/vite/plugin.ts +102 -0
  137. package/src/vite/rpc-middleware.ts +82 -0
  138. package/src/vite/types.ts +65 -0
@@ -0,0 +1,86 @@
1
+ import type { Child } from "../dom";
2
+ import { onCleanup } from "../signals";
3
+ import { useChartContext } from "./context";
4
+
5
+ export interface BarProps {
6
+ dataKey: string;
7
+ fill?: string;
8
+ radius?: number;
9
+ maxWidth?: number;
10
+ name?: string;
11
+ opacity?: number | string;
12
+ }
13
+
14
+ export function Bar(props: BarProps): Child {
15
+ return () => {
16
+ const ctx = useChartContext();
17
+
18
+ const fill = props.fill ?? "#6366f1";
19
+ const radius = props.radius ?? 4;
20
+ const id = `bar-${props.dataKey}-${Math.random().toString(36).slice(2, 7)}`;
21
+
22
+ ctx.registerSeries({
23
+ id,
24
+ dataKey: props.dataKey,
25
+ type: "bar",
26
+ color: fill,
27
+ name: props.name ?? props.dataKey
28
+ });
29
+
30
+ onCleanup(() => {
31
+ ctx.unregisterSeries(id);
32
+ });
33
+
34
+ return (
35
+ <g class={`visage-bar-${props.dataKey}`} opacity={props.opacity ?? 1}>
36
+ {() => {
37
+ const dataArr = ctx.data();
38
+ if (dataArr.length === 0) return null;
39
+
40
+ const xScale = ctx.xScale();
41
+ const yScale = ctx.yScale();
42
+ const baselineY = ctx.innerHeight();
43
+
44
+ // Check if there are other Bar series to calculate grouped offsets
45
+ const allBars = ctx.series().filter((s) => s.type === "bar");
46
+ const barIndex = allBars.findIndex((s) => s.id === id);
47
+ const totalBars = Math.max(1, allBars.length);
48
+
49
+ const bars: Child[] = [];
50
+
51
+ for (let i = 0; i < dataArr.length; i++) {
52
+ const item = dataArr[i];
53
+ const val = item[props.dataKey];
54
+ if (typeof val !== "number" || Number.isNaN(val)) continue;
55
+
56
+ let slotX = 0;
57
+ let slotW = 20;
58
+
59
+ if (xScale.type === "band") {
60
+ slotX = xScale(i);
61
+ slotW = xScale.bandwidth();
62
+ } else {
63
+ const stepW = ctx.innerWidth() / Math.max(1, dataArr.length);
64
+ slotX = xScale(i) - stepW / 2;
65
+ slotW = stepW * 0.8;
66
+ }
67
+
68
+ // Grouped bar width
69
+ const subBarW = slotW / totalBars;
70
+ const barW = props.maxWidth ? Math.min(props.maxWidth, subBarW) : subBarW;
71
+ const barX = slotX + (barIndex >= 0 ? barIndex * subBarW : 0) + (subBarW - barW) / 2;
72
+
73
+ const y = yScale(val);
74
+ const h = Math.max(0, baselineY - y);
75
+
76
+ bars.push(
77
+ <rect x={barX} y={y} width={barW} height={h} rx={radius} ry={radius} fill={fill} />
78
+ );
79
+ }
80
+
81
+ return bars;
82
+ }}
83
+ </g>
84
+ );
85
+ };
86
+ }
@@ -0,0 +1,72 @@
1
+ import type { Child } from "../dom";
2
+ import { useChartContext } from "./context";
3
+
4
+ export interface CartesianGridProps {
5
+ horizontal?: boolean;
6
+ vertical?: boolean;
7
+ stroke?: string;
8
+ strokeWidth?: number | string;
9
+ strokeDasharray?: string;
10
+ opacity?: number | string;
11
+ }
12
+
13
+ export function CartesianGrid(props: CartesianGridProps): Child {
14
+ return () => {
15
+ const ctx = useChartContext();
16
+
17
+ const horizontal = props.horizontal ?? true;
18
+ const vertical = props.vertical ?? false;
19
+ const stroke = props.stroke ?? "#e2e8f0";
20
+ const strokeWidth = props.strokeWidth ?? 1;
21
+ const strokeDasharray = props.strokeDasharray;
22
+ const opacity = props.opacity ?? 0.8;
23
+
24
+ const lines: Child[] = [];
25
+ const w = ctx.innerWidth();
26
+ const h = ctx.innerHeight();
27
+ const yScale = ctx.yScale();
28
+ const xScale = ctx.xScale();
29
+
30
+ if (horizontal) {
31
+ const ticks = yScale.ticks(5);
32
+ for (const t of ticks) {
33
+ const y = yScale(t);
34
+ lines.push(
35
+ <line
36
+ x1={0}
37
+ y1={y}
38
+ x2={w}
39
+ y2={y}
40
+ stroke={stroke}
41
+ stroke-width={strokeWidth}
42
+ stroke-dasharray={strokeDasharray}
43
+ />
44
+ );
45
+ }
46
+ }
47
+
48
+ if (vertical) {
49
+ const ticks = xScale.ticks(5);
50
+ for (const t of ticks) {
51
+ const x = xScale(t);
52
+ lines.push(
53
+ <line
54
+ x1={x}
55
+ y1={0}
56
+ x2={x}
57
+ y2={h}
58
+ stroke={stroke}
59
+ stroke-width={strokeWidth}
60
+ stroke-dasharray={strokeDasharray}
61
+ />
62
+ );
63
+ }
64
+ }
65
+
66
+ return (
67
+ <g class="visage-cartesian-grid" opacity={opacity}>
68
+ {lines}
69
+ </g>
70
+ );
71
+ };
72
+ }
@@ -0,0 +1,269 @@
1
+ import { type Child, resolveValue } from "../dom";
2
+ import type { ClassProp, ReactiveProp } from "../jsx-runtime/jsx";
3
+ import { computed, onCleanup, signal, untrack } from "../signals";
4
+ import { ChartContext, type ChartContextValue } from "./context";
5
+ import { createBandScale, createLinearScale } from "./scale";
6
+ import type { AnyScale, HoverState, Margin, SeriesMeta } from "./types";
7
+
8
+ export interface ChartProps {
9
+ data: ReactiveProp<any[]>;
10
+ width?: number | string;
11
+ height?: number | string;
12
+ margin?: Partial<Margin>;
13
+ yDomain?: [number, number];
14
+ class?: ClassProp;
15
+ className?: ClassProp;
16
+ style?: any;
17
+ children?: Child;
18
+ [key: string]: any;
19
+ }
20
+
21
+ const DEFAULT_MARGIN: Margin = {
22
+ top: 16,
23
+ right: 16,
24
+ bottom: 24,
25
+ left: 36
26
+ };
27
+
28
+ export function Chart(props: ChartProps): Child {
29
+ const margin: Margin = {
30
+ ...DEFAULT_MARGIN,
31
+ ...props.margin
32
+ };
33
+
34
+ const initialW = typeof props.width === "number" ? props.width : 500;
35
+ const initialH = typeof props.height === "number" ? props.height : 300;
36
+
37
+ const measuredWidth = signal<number>(initialW);
38
+ const measuredHeight = signal<number>(initialH);
39
+
40
+ let containerEl: HTMLElement | null = null;
41
+
42
+ const handleRef = (el: HTMLElement) => {
43
+ containerEl = el;
44
+ if (typeof ResizeObserver !== "undefined") {
45
+ const ro = new ResizeObserver((entries) => {
46
+ for (const entry of entries) {
47
+ const cr = entry.contentRect;
48
+ if (cr.width > 0) measuredWidth.set(cr.width);
49
+ if (cr.height > 0) measuredHeight.set(cr.height);
50
+ }
51
+ });
52
+ ro.observe(el);
53
+ onCleanup(() => ro.disconnect());
54
+ } else {
55
+ if (el.clientWidth > 0) measuredWidth.set(el.clientWidth);
56
+ if (el.clientHeight > 0) measuredHeight.set(el.clientHeight);
57
+ }
58
+ };
59
+
60
+ const innerWidth = computed(() => Math.max(10, measuredWidth() - margin.left - margin.right));
61
+ const innerHeight = computed(() => Math.max(10, measuredHeight() - margin.top - margin.bottom));
62
+
63
+ const rawData = () => resolveValue(props.data) || [];
64
+
65
+ const seriesList = signal<SeriesMeta[]>([]);
66
+ const xDataKey = signal<string | undefined>(undefined);
67
+
68
+ const registerSeries = (meta: SeriesMeta) => {
69
+ untrack(() => {
70
+ seriesList.update((prev) => {
71
+ if (prev.some((s) => s.id === meta.id)) return prev;
72
+ return [...prev, meta];
73
+ });
74
+ });
75
+ };
76
+
77
+ const unregisterSeries = (id: string) => {
78
+ untrack(() => {
79
+ seriesList.update((prev) => prev.filter((s) => s.id !== id));
80
+ });
81
+ };
82
+
83
+ const hoverState = signal<HoverState>({
84
+ active: false,
85
+ x: 0,
86
+ y: 0,
87
+ index: -1
88
+ });
89
+
90
+ const xScale = computed<AnyScale>(() => {
91
+ const dataArr = rawData();
92
+ const w = innerWidth();
93
+ const key = xDataKey();
94
+
95
+ const hasBarSeries = untrack(() => seriesList().some((s) => s.type === "bar"));
96
+
97
+ if (hasBarSeries && key && dataArr.length > 0) {
98
+ const categories = dataArr.map((d: any) => String(d[key]));
99
+ return createBandScale(categories, [0, w], {
100
+ paddingInner: 0.1,
101
+ paddingOuter: 0.05
102
+ });
103
+ }
104
+
105
+ const maxIdx = Math.max(1, dataArr.length - 1);
106
+ return createLinearScale([0, maxIdx], [0, w]);
107
+ });
108
+
109
+ const yScale = computed(() => {
110
+ const h = innerHeight();
111
+ const dataArr = rawData();
112
+ const currentSeries = seriesList();
113
+
114
+ let minY = 0;
115
+ let maxY = 10;
116
+
117
+ if (props.yDomain) {
118
+ minY = props.yDomain[0];
119
+ maxY = props.yDomain[1];
120
+ } else if (dataArr.length > 0 && currentSeries.length > 0) {
121
+ let foundMin = Infinity;
122
+ let foundMax = -Infinity;
123
+
124
+ for (const item of dataArr) {
125
+ for (const s of currentSeries) {
126
+ const val = item[s.dataKey];
127
+ if (typeof val === "number" && !Number.isNaN(val)) {
128
+ if (val < foundMin) foundMin = val;
129
+ if (val > foundMax) foundMax = val;
130
+ }
131
+ }
132
+ }
133
+
134
+ if (foundMax > -Infinity) {
135
+ minY = Math.min(0, foundMin);
136
+ maxY = foundMax === 0 ? 1 : foundMax * 1.1; // 10% headroom
137
+ }
138
+ }
139
+
140
+ // SVG coordinate space: 0 is top, innerHeight is bottom
141
+ return createLinearScale([minY, maxY], [h, 0], { nice: true });
142
+ });
143
+
144
+ const handlePointer = (clientX: number, clientY: number) => {
145
+ if (!containerEl) return;
146
+ const rect = containerEl.getBoundingClientRect();
147
+ const relX = clientX - rect.left - margin.left;
148
+ const relY = clientY - rect.top - margin.top;
149
+
150
+ const w = innerWidth();
151
+ const h = innerHeight();
152
+
153
+ if (relX < 0 || relX > w || relY < 0 || relY > h) {
154
+ hoverState.set({ active: false, x: 0, y: 0, index: -1 });
155
+ return;
156
+ }
157
+
158
+ const dataArr = rawData();
159
+ if (dataArr.length === 0) return;
160
+
161
+ const count = dataArr.length;
162
+ const stepW = w / Math.max(1, count - 1);
163
+ const rawIdx = Math.round(relX / stepW);
164
+ const clampedIdx = Math.max(0, Math.min(count - 1, rawIdx));
165
+
166
+ hoverState.set({
167
+ active: true,
168
+ x: relX + margin.left,
169
+ y: relY + margin.top,
170
+ index: clampedIdx,
171
+ datum: dataArr[clampedIdx]
172
+ });
173
+ };
174
+
175
+ const handleMouseMove = (e: MouseEvent) => {
176
+ handlePointer(e.clientX, e.clientY);
177
+ };
178
+
179
+ const handleMouseLeave = () => {
180
+ hoverState.set({ active: false, x: 0, y: 0, index: -1 });
181
+ };
182
+
183
+ const handleTouchMove = (e: TouchEvent) => {
184
+ const touch = e.touches[0];
185
+ if (touch) handlePointer(touch.clientX, touch.clientY);
186
+ };
187
+
188
+ const handleTouchEnd = () => {
189
+ hoverState.set({ active: false, x: 0, y: 0, index: -1 });
190
+ };
191
+
192
+ const ctxValue: ChartContextValue = {
193
+ data: rawData,
194
+ width: () => measuredWidth(),
195
+ height: () => measuredHeight(),
196
+ margin,
197
+ innerWidth: () => innerWidth(),
198
+ innerHeight: () => innerHeight(),
199
+ xScale: () => xScale(),
200
+ yScale: () => yScale(),
201
+ hoverState: () => hoverState(),
202
+ setHoverState: (s) => hoverState.set(s),
203
+ xDataKey: () => xDataKey(),
204
+ setXDataKey: (k) => xDataKey.set(k),
205
+ series: () => seriesList(),
206
+ registerSeries,
207
+ unregisterSeries
208
+ };
209
+
210
+ const containerStyle = {
211
+ position: "relative",
212
+ width: typeof props.width === "number" ? `${props.width}px` : (props.width ?? "100%"),
213
+ height: typeof props.height === "number" ? `${props.height}px` : (props.height ?? "300px"),
214
+ touchAction: "none",
215
+ userSelect: "none",
216
+ ...props.style
217
+ };
218
+
219
+ const rawChildren = Array.isArray(props.children) ? props.children : [props.children];
220
+ const childrenList: any[] = [];
221
+ const stack = [...rawChildren];
222
+ while (stack.length > 0) {
223
+ const item = stack.shift();
224
+ if (Array.isArray(item)) {
225
+ stack.unshift(...item);
226
+ } else if (item !== null && item !== undefined && item !== false) {
227
+ childrenList.push(item);
228
+ }
229
+ }
230
+
231
+ const svgChildren: Child[] = [];
232
+ const overlayChildren: Child[] = [];
233
+
234
+ for (const child of childrenList) {
235
+ if (typeof child === "function" && (child as any).__visage_overlay) {
236
+ overlayChildren.push(child);
237
+ } else {
238
+ svgChildren.push(child);
239
+ }
240
+ }
241
+
242
+ return ChartContext.Provider({
243
+ value: ctxValue,
244
+ children: () => (
245
+ <div
246
+ ref={handleRef}
247
+ class={props.class || props.className}
248
+ style={containerStyle}
249
+ onMouseMove={handleMouseMove}
250
+ onMouseLeave={handleMouseLeave}
251
+ onTouchMove={handleTouchMove}
252
+ onTouchEnd={handleTouchEnd}
253
+ >
254
+ <svg
255
+ role="img"
256
+ aria-label={props["aria-label"] ?? "Data visualization chart"}
257
+ viewBox={() => `0 0 ${measuredWidth()} ${measuredHeight()}`}
258
+ width="100%"
259
+ height="100%"
260
+ style={{ overflow: "visible", display: "block" }}
261
+ >
262
+ <title>{props.title ?? "Data Visualization Chart"}</title>
263
+ <g transform={`translate(${margin.left}, ${margin.top})`}>{svgChildren}</g>
264
+ </svg>
265
+ {overlayChildren}
266
+ </div>
267
+ )
268
+ });
269
+ }
@@ -0,0 +1,42 @@
1
+ import type { Child } from "../dom";
2
+ import { useChartContext } from "./context";
3
+
4
+ export interface ChartCursorProps {
5
+ stroke?: string;
6
+ strokeWidth?: number | string;
7
+ strokeDasharray?: string;
8
+ opacity?: number | string;
9
+ }
10
+
11
+ export function ChartCursor(props: ChartCursorProps): Child {
12
+ return () => {
13
+ const ctx = useChartContext();
14
+
15
+ const stroke = props.stroke ?? "#94a3b8";
16
+ const strokeWidth = props.strokeWidth ?? 1;
17
+ const strokeDasharray = props.strokeDasharray ?? "4 4";
18
+ const opacity = props.opacity ?? 0.8;
19
+
20
+ const hover = ctx.hoverState();
21
+ if (!hover.active) return null;
22
+
23
+ const h = ctx.innerHeight();
24
+ // hover.x is relative to container, subtract margin.left for group coordinate space
25
+ const x = hover.x - ctx.margin.left;
26
+
27
+ return (
28
+ <g class="visage-chart-cursor">
29
+ <line
30
+ x1={x}
31
+ y1={0}
32
+ x2={x}
33
+ y2={h}
34
+ stroke={stroke}
35
+ stroke-width={strokeWidth}
36
+ stroke-dasharray={strokeDasharray}
37
+ opacity={opacity}
38
+ />
39
+ </g>
40
+ );
41
+ };
42
+ }
@@ -0,0 +1,56 @@
1
+ import type { Child } from "../dom";
2
+ import type { ClassProp } from "../jsx-runtime/jsx";
3
+ import { useChartContext } from "./context";
4
+
5
+ export interface LegendProps {
6
+ align?: "left" | "center" | "right";
7
+ class?: ClassProp;
8
+ className?: ClassProp;
9
+ }
10
+
11
+ export function ChartLegend(props: LegendProps): Child {
12
+ const render = () => {
13
+ const ctx = useChartContext();
14
+ const align = props.align ?? "center";
15
+
16
+ const justifyMap = {
17
+ left: "flex-start",
18
+ center: "center",
19
+ right: "flex-end"
20
+ };
21
+
22
+ const series = ctx.series();
23
+ return (
24
+ <div
25
+ class={props.class || props.className}
26
+ style={{
27
+ display: "flex",
28
+ alignItems: "center",
29
+ justifyContent: justifyMap[align],
30
+ gap: "16px",
31
+ paddingTop: "8px",
32
+ fontSize: "12px",
33
+ color: "#475569"
34
+ }}
35
+ >
36
+ {series.map((s) => (
37
+ <div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
38
+ <span
39
+ style={{
40
+ width: "10px",
41
+ height: "10px",
42
+ borderRadius: "3px",
43
+ background: s.color ?? "#8b5cf6"
44
+ }}
45
+ />
46
+ <span>{s.name ?? s.dataKey}</span>
47
+ </div>
48
+ ))}
49
+ </div>
50
+ );
51
+ };
52
+ (render as any).__visage_overlay = true;
53
+ return render;
54
+ }
55
+
56
+ export const Legend = ChartLegend;
@@ -0,0 +1,106 @@
1
+ import type { Child } from "../dom";
2
+ import { onCleanup } from "../signals";
3
+ import { useChartContext } from "./context";
4
+ import { pathLine } from "./path";
5
+ import type { CurveType, Point } from "./types";
6
+
7
+ export interface LineProps {
8
+ dataKey: string;
9
+ stroke?: string;
10
+ strokeWidth?: number | string;
11
+ strokeDasharray?: string;
12
+ curve?: CurveType;
13
+ dot?: boolean | { r?: number; fill?: string; stroke?: string; strokeWidth?: number };
14
+ name?: string;
15
+ opacity?: number | string;
16
+ }
17
+
18
+ export function Line(props: LineProps): Child {
19
+ return () => {
20
+ const ctx = useChartContext();
21
+
22
+ const stroke = props.stroke ?? "#8b5cf6";
23
+ const strokeWidth = props.strokeWidth ?? 2;
24
+ const curve = props.curve ?? "linear";
25
+ const id = `line-${props.dataKey}-${Math.random().toString(36).slice(2, 7)}`;
26
+
27
+ ctx.registerSeries({
28
+ id,
29
+ dataKey: props.dataKey,
30
+ type: "line",
31
+ color: stroke,
32
+ name: props.name ?? props.dataKey
33
+ });
34
+
35
+ onCleanup(() => {
36
+ ctx.unregisterSeries(id);
37
+ });
38
+
39
+ return (
40
+ <g class={`visage-line-${props.dataKey}`} opacity={props.opacity ?? 1}>
41
+ {() => {
42
+ const dataArr = ctx.data();
43
+ if (dataArr.length === 0) return null;
44
+
45
+ const xScale = ctx.xScale();
46
+ const yScale = ctx.yScale();
47
+
48
+ const points: Point[] = [];
49
+ for (let i = 0; i < dataArr.length; i++) {
50
+ const item = dataArr[i];
51
+ const val = item[props.dataKey];
52
+ if (typeof val !== "number" || Number.isNaN(val)) continue;
53
+
54
+ let x = 0;
55
+ if (xScale.type === "band") {
56
+ x = xScale(i) + xScale.bandwidth() / 2;
57
+ } else {
58
+ x = xScale(i);
59
+ }
60
+
61
+ const y = yScale(val);
62
+ points.push({ x, y });
63
+ }
64
+
65
+ const d = pathLine(points, { curve });
66
+
67
+ const dots: Child[] = [];
68
+ if (props.dot && points.length > 0) {
69
+ const dotCfg = typeof props.dot === "object" ? props.dot : {};
70
+ const r = dotCfg.r ?? 3.5;
71
+ const fill = dotCfg.fill ?? "#ffffff";
72
+ const dotStroke = dotCfg.stroke ?? stroke;
73
+ const dotSw = dotCfg.strokeWidth ?? 2;
74
+
75
+ for (let i = 0; i < points.length; i++) {
76
+ const p = points[i]!;
77
+ dots.push(
78
+ <circle
79
+ cx={p.x}
80
+ cy={p.y}
81
+ r={r}
82
+ fill={fill}
83
+ stroke={dotStroke}
84
+ stroke-width={dotSw}
85
+ />
86
+ );
87
+ }
88
+ }
89
+
90
+ return [
91
+ <path
92
+ d={d}
93
+ fill="none"
94
+ stroke={stroke}
95
+ stroke-width={strokeWidth}
96
+ stroke-dasharray={props.strokeDasharray}
97
+ stroke-linejoin="round"
98
+ stroke-linecap="round"
99
+ />,
100
+ ...dots
101
+ ];
102
+ }}
103
+ </g>
104
+ );
105
+ };
106
+ }