@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,257 @@
|
|
|
1
|
+
export interface ScopeOptions {
|
|
2
|
+
scopeId: string;
|
|
3
|
+
passToChildren?: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scope a single CSS selector string with the given scope identifier
|
|
8
|
+
*/
|
|
9
|
+
export function scopeSelector(selector: string, options: ScopeOptions): string {
|
|
10
|
+
const { scopeId, passToChildren = false } = options;
|
|
11
|
+
const scopeAttr = `[data-${scopeId}]`;
|
|
12
|
+
|
|
13
|
+
// Split by comma for multiple selectors (e.g. "h1, h2, .title")
|
|
14
|
+
const parts = splitSelectorList(selector);
|
|
15
|
+
|
|
16
|
+
return parts
|
|
17
|
+
.map((part) => {
|
|
18
|
+
const trimmed = part.trim();
|
|
19
|
+
if (!trimmed) return "";
|
|
20
|
+
|
|
21
|
+
// 1. Check for :global() wrapper
|
|
22
|
+
if (trimmed.includes(":global(") || trimmed.includes("::v-global(")) {
|
|
23
|
+
return trimmed
|
|
24
|
+
.replace(
|
|
25
|
+
/(.*?)(?:::v-global|:global)\(([^)]+)\)(.*)/g,
|
|
26
|
+
(_match, before, globalTarget, after) => {
|
|
27
|
+
const prefix = before.trim();
|
|
28
|
+
const scopedPrefix = prefix ? scopeCompoundSelector(prefix, scopeAttr) : "";
|
|
29
|
+
if (scopedPrefix) {
|
|
30
|
+
return `${scopedPrefix} ${globalTarget.trim()}${after}`;
|
|
31
|
+
}
|
|
32
|
+
return `${globalTarget.trim()}${after}`;
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Check for :deep() / ::v-deep() wrapper
|
|
39
|
+
if (trimmed.includes(":deep(") || trimmed.includes("::v-deep(")) {
|
|
40
|
+
// e.g. ".parent :deep(.child)" -> ".parent[data-v-xyz] .child" or "[data-v-xyz] .child"
|
|
41
|
+
return trimmed
|
|
42
|
+
.replace(
|
|
43
|
+
/(.*?)(?:::v-deep|:deep)\(([^)]+)\)(.*)/g,
|
|
44
|
+
(_match, before, deepTarget, after) => {
|
|
45
|
+
const prefix = before.trim();
|
|
46
|
+
const scopedPrefix = prefix ? scopeCompoundSelector(prefix, scopeAttr) : scopeAttr;
|
|
47
|
+
return `${scopedPrefix} ${deepTarget.trim()}${after}`;
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Standard scoped selector
|
|
54
|
+
if (passToChildren) {
|
|
55
|
+
const compound = scopeCompoundSelector(trimmed, scopeAttr);
|
|
56
|
+
return `${scopeAttr} ${trimmed}, ${compound}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return scopeCompoundSelector(trimmed, scopeAttr);
|
|
60
|
+
})
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(", ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Scope a compound selector (e.g. ".card > .title.active:hover")
|
|
67
|
+
*/
|
|
68
|
+
function scopeCompoundSelector(selector: string, scopeAttr: string): string {
|
|
69
|
+
// Combinators: space, >, +, ~
|
|
70
|
+
const combinatorRegex = /([ >+~]+)/;
|
|
71
|
+
const tokens = selector.split(combinatorRegex);
|
|
72
|
+
|
|
73
|
+
const scopedTokens = tokens.map((token, index) => {
|
|
74
|
+
// If it's a combinator (odd index), leave as is
|
|
75
|
+
if (index % 2 === 1) return token;
|
|
76
|
+
|
|
77
|
+
const piece = token.trim();
|
|
78
|
+
if (!piece || piece === "*") return piece ? `${piece}${scopeAttr}` : "";
|
|
79
|
+
|
|
80
|
+
// If pseudo-element exists (e.g. ::before, ::after), insert before it
|
|
81
|
+
const pseudoElemIdx = piece.indexOf("::");
|
|
82
|
+
if (pseudoElemIdx !== -1) {
|
|
83
|
+
const base = piece.slice(0, pseudoElemIdx);
|
|
84
|
+
const pseudo = piece.slice(pseudoElemIdx);
|
|
85
|
+
return `${base}${scopeAttr}${pseudo}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If pseudo-class exists (e.g. :hover, :focus, :nth-child(2)), insert before first pseudo-class
|
|
89
|
+
const pseudoClassIdx = piece.indexOf(":");
|
|
90
|
+
if (pseudoClassIdx !== -1) {
|
|
91
|
+
const base = piece.slice(0, pseudoClassIdx);
|
|
92
|
+
const pseudo = piece.slice(pseudoClassIdx);
|
|
93
|
+
return `${base}${scopeAttr}${pseudo}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return `${piece}${scopeAttr}`;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return scopedTokens.join("");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Split selector by commas while respecting parentheses (e.g. :is(.a, .b))
|
|
104
|
+
*/
|
|
105
|
+
function splitSelectorList(selectorList: string): string[] {
|
|
106
|
+
const result: string[] = [];
|
|
107
|
+
let depth = 0;
|
|
108
|
+
let current = "";
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < selectorList.length; i++) {
|
|
111
|
+
const char = selectorList[i];
|
|
112
|
+
if (char === "(") {
|
|
113
|
+
depth++;
|
|
114
|
+
current += char;
|
|
115
|
+
} else if (char === ")") {
|
|
116
|
+
depth = Math.max(0, depth - 1);
|
|
117
|
+
current += char;
|
|
118
|
+
} else if (char === "," && depth === 0) {
|
|
119
|
+
if (current.trim()) result.push(current.trim());
|
|
120
|
+
current = "";
|
|
121
|
+
} else {
|
|
122
|
+
current += char;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (current.trim()) result.push(current.trim());
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Full CSS block scoping transformer
|
|
132
|
+
*/
|
|
133
|
+
export function scopeCSS(css: string, options: ScopeOptions): string {
|
|
134
|
+
const { scopeId } = options;
|
|
135
|
+
const keyframeNames = new Set<string>();
|
|
136
|
+
|
|
137
|
+
// 1. Identify and extract @keyframes names
|
|
138
|
+
const keyframeRegex = /@(?:-webkit-)?keyframes\s+([a-zA-Z0-9_-]+)/g;
|
|
139
|
+
let kMatch: RegExpExecArray | null = keyframeRegex.exec(css);
|
|
140
|
+
while (kMatch !== null) {
|
|
141
|
+
if (kMatch[1]) {
|
|
142
|
+
keyframeNames.add(kMatch[1]);
|
|
143
|
+
}
|
|
144
|
+
kMatch = keyframeRegex.exec(css);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Tokenize and parse rules
|
|
148
|
+
let processed = css;
|
|
149
|
+
|
|
150
|
+
// Rename @keyframes definitions
|
|
151
|
+
for (const name of keyframeNames) {
|
|
152
|
+
const scopedName = `${scopeId}-${name}`;
|
|
153
|
+
const defRegex = new RegExp(`(@(?:-webkit-)?keyframes\\s+)${name}\\b`, "g");
|
|
154
|
+
processed = processed.replace(defRegex, `$1${scopedName}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Replace animation property references to keyframes
|
|
158
|
+
if (keyframeNames.size > 0) {
|
|
159
|
+
for (const name of keyframeNames) {
|
|
160
|
+
const scopedName = `${scopeId}-${name}`;
|
|
161
|
+
const animRegex = new RegExp(`(animation(?:-name)?\\s*:[^;}]*?)\\b${name}\\b([^;}]*)`, "g");
|
|
162
|
+
processed = processed.replace(animRegex, `$1${scopedName}$2`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 3. Parse and rewrite CSS rules and nested blocks
|
|
167
|
+
return transformRules(processed, options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function transformRules(css: string, options: ScopeOptions): string {
|
|
171
|
+
let output = "";
|
|
172
|
+
let i = 0;
|
|
173
|
+
const len = css.length;
|
|
174
|
+
|
|
175
|
+
while (i < len) {
|
|
176
|
+
// Skip whitespace
|
|
177
|
+
while (i < len && /\s/.test(css[i] as string)) {
|
|
178
|
+
output += css[i];
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
if (i >= len) break;
|
|
182
|
+
|
|
183
|
+
// Handle comments
|
|
184
|
+
if (css[i] === "/" && css[i + 1] === "*") {
|
|
185
|
+
const endComment = css.indexOf("*/", i + 2);
|
|
186
|
+
if (endComment === -1) {
|
|
187
|
+
output += css.slice(i);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
output += css.slice(i, endComment + 2);
|
|
191
|
+
i = endComment + 2;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Find next block opener '{' or ';'
|
|
196
|
+
const openBrace = css.indexOf("{", i);
|
|
197
|
+
if (openBrace === -1) {
|
|
198
|
+
output += css.slice(i);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const header = css.slice(i, openBrace).trim();
|
|
203
|
+
|
|
204
|
+
// Check if at-rule (like @media, @supports, @container, @keyframes)
|
|
205
|
+
if (header.startsWith("@")) {
|
|
206
|
+
if (
|
|
207
|
+
header.startsWith("@media") ||
|
|
208
|
+
header.startsWith("@supports") ||
|
|
209
|
+
header.startsWith("@container")
|
|
210
|
+
) {
|
|
211
|
+
// Find matching closing brace for nested at-rule
|
|
212
|
+
const closeBrace = findMatchingBrace(css, openBrace);
|
|
213
|
+
const innerContent = css.slice(openBrace + 1, closeBrace);
|
|
214
|
+
const transformedInner = transformRules(innerContent, options);
|
|
215
|
+
|
|
216
|
+
output += `${header} {\n${transformedInner}\n}`;
|
|
217
|
+
i = closeBrace + 1;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (header.includes("keyframes") || header.startsWith("@font-face")) {
|
|
222
|
+
// Keep keyframe and font-face blocks intact without selector scoping
|
|
223
|
+
const closeBrace = findMatchingBrace(css, openBrace);
|
|
224
|
+
output += css.slice(i, closeBrace + 1);
|
|
225
|
+
i = closeBrace + 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Standard rule selector
|
|
231
|
+
const closeBrace = css.indexOf("}", openBrace);
|
|
232
|
+
if (closeBrace === -1) {
|
|
233
|
+
output += css.slice(i);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const body = css.slice(openBrace + 1, closeBrace);
|
|
238
|
+
const scopedSelector = scopeSelector(header, options);
|
|
239
|
+
|
|
240
|
+
output += `${scopedSelector} {${body}}`;
|
|
241
|
+
i = closeBrace + 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return output;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function findMatchingBrace(str: string, openIndex: number): number {
|
|
248
|
+
let depth = 0;
|
|
249
|
+
for (let i = openIndex; i < str.length; i++) {
|
|
250
|
+
if (str[i] === "{") depth++;
|
|
251
|
+
else if (str[i] === "}") {
|
|
252
|
+
depth--;
|
|
253
|
+
if (depth === 0) return i;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return str.length - 1;
|
|
257
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visage Spatial Engine & Style Primitives
|
|
3
|
+
*
|
|
4
|
+
* Eliminates measurement fatigue, layout bugs, and manual viewport calculations.
|
|
5
|
+
* Standardizes on a crisp 4px spatial scale, semantic viewport intents,
|
|
6
|
+
* fluid clamp calculations, and typed visual design tokens.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const UNITLESS_PROPERTIES = new Set([
|
|
10
|
+
"animationIterationCount",
|
|
11
|
+
"aspectRatio",
|
|
12
|
+
"borderImageSlice",
|
|
13
|
+
"columnCount",
|
|
14
|
+
"columns",
|
|
15
|
+
"flex",
|
|
16
|
+
"flexGrow",
|
|
17
|
+
"flexShrink",
|
|
18
|
+
"flexOrder",
|
|
19
|
+
"fontWeight",
|
|
20
|
+
"lineHeight",
|
|
21
|
+
"opacity",
|
|
22
|
+
"order",
|
|
23
|
+
"orphans",
|
|
24
|
+
"scale",
|
|
25
|
+
"tabSize",
|
|
26
|
+
"widows",
|
|
27
|
+
"zIndex",
|
|
28
|
+
"zoom",
|
|
29
|
+
"fillOpacity",
|
|
30
|
+
"strokeOpacity"
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export const DIMENSION_KEYWORDS: Record<string, string> = {
|
|
34
|
+
screen: "100dvh",
|
|
35
|
+
"screen-h": "100dvh",
|
|
36
|
+
"screen-w": "100dvw",
|
|
37
|
+
full: "100%",
|
|
38
|
+
half: "50%",
|
|
39
|
+
third: "33.333333%",
|
|
40
|
+
"two-thirds": "66.666667%",
|
|
41
|
+
quarter: "25%",
|
|
42
|
+
"three-quarters": "75%",
|
|
43
|
+
auto: "auto",
|
|
44
|
+
min: "min-content",
|
|
45
|
+
max: "max-content",
|
|
46
|
+
fit: "fit-content"
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolves a spatial scale step or dimensional intent to a valid CSS measurement.
|
|
51
|
+
* Numbers map to the 4px engineering grid (1 = 4px, 2 = 8px, 4 = 16px, 8 = 32px).
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* space(1) // "4px"
|
|
55
|
+
* space(2) // "8px"
|
|
56
|
+
* space(4) // "16px"
|
|
57
|
+
* space(6) // "24px"
|
|
58
|
+
* space("full") // "100%"
|
|
59
|
+
* space("screen") // "100dvh"
|
|
60
|
+
*/
|
|
61
|
+
export function space(val: number | string | undefined | null): string {
|
|
62
|
+
if (val === undefined || val === null || val === "") return "";
|
|
63
|
+
if (typeof val === "number") {
|
|
64
|
+
if (val === 0) return "0px";
|
|
65
|
+
return `${val * 4}px`;
|
|
66
|
+
}
|
|
67
|
+
const str = String(val).trim();
|
|
68
|
+
if (str in DIMENSION_KEYWORDS) {
|
|
69
|
+
return DIMENSION_KEYWORDS[str]!;
|
|
70
|
+
}
|
|
71
|
+
return str;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolves width, height, or grid sizing into valid CSS.
|
|
76
|
+
* Small numbers (<= 48) map to the 4px scale, while larger numbers (> 48)
|
|
77
|
+
* are treated as explicit pixel dimensions.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveDimension(val: number | string | undefined | null): string {
|
|
80
|
+
if (val === undefined || val === null || val === "") return "";
|
|
81
|
+
if (typeof val === "number") {
|
|
82
|
+
if (val === 0) return "0px";
|
|
83
|
+
if (val > 48) return `${val}px`;
|
|
84
|
+
return `${val * 4}px`;
|
|
85
|
+
}
|
|
86
|
+
const str = String(val).trim();
|
|
87
|
+
if (str in DIMENSION_KEYWORDS) {
|
|
88
|
+
return DIMENSION_KEYWORDS[str]!;
|
|
89
|
+
}
|
|
90
|
+
return str;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Explicit raw pixel measurement helper.
|
|
95
|
+
*/
|
|
96
|
+
export function px(val: number): string {
|
|
97
|
+
return `${val}px`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Explicit rem measurement helper.
|
|
102
|
+
*/
|
|
103
|
+
export function rem(val: number): string {
|
|
104
|
+
return `${val}rem`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Explicit em measurement helper.
|
|
109
|
+
*/
|
|
110
|
+
export function em(val: number): string {
|
|
111
|
+
return `${val}em`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Computes an optimal fluid clamp() expression.
|
|
116
|
+
* Eliminates the math nightmare of manual linear viewport interpolation slopes.
|
|
117
|
+
*
|
|
118
|
+
* @param minPx Minimum size in pixels at mobile viewport
|
|
119
|
+
* @param maxPx Maximum size in pixels at desktop viewport
|
|
120
|
+
* @param options Min/max viewport bounds (defaults to 375px mobile and 1280px desktop)
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* fluid(16, 32)
|
|
124
|
+
* // clamp(16px, 9.38px + 1.77vw, 32px)
|
|
125
|
+
*/
|
|
126
|
+
export function fluid(
|
|
127
|
+
minPx: number,
|
|
128
|
+
maxPx: number,
|
|
129
|
+
options?: { minVw?: number; maxVw?: number }
|
|
130
|
+
): string {
|
|
131
|
+
const minVw = options?.minVw ?? 375;
|
|
132
|
+
const maxVw = options?.maxVw ?? 1280;
|
|
133
|
+
const slope = (maxPx - minPx) / (maxVw - minVw);
|
|
134
|
+
const yIntercept = -minVw * slope + minPx;
|
|
135
|
+
const slopeVw = slope * 100;
|
|
136
|
+
const preferred = `${yIntercept.toFixed(2)}px + ${slopeVw.toFixed(2)}vw`;
|
|
137
|
+
const min = Math.min(minPx, maxPx);
|
|
138
|
+
const max = Math.max(minPx, maxPx);
|
|
139
|
+
return `clamp(${min}px, ${preferred}, ${max}px)`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type ShadowVariant = "none" | "sm" | "md" | "lg" | "xl" | "soft" | "glow" | "inner";
|
|
143
|
+
|
|
144
|
+
export const SHADOW_PRESETS: Record<ShadowVariant, string> = {
|
|
145
|
+
none: "none",
|
|
146
|
+
sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
|
147
|
+
md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
|
|
148
|
+
lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
|
|
149
|
+
xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)",
|
|
150
|
+
soft: "0 8px 30px rgba(0, 0, 0, 0.06)",
|
|
151
|
+
glow: "0 0 24px rgba(168, 85, 247, 0.35)",
|
|
152
|
+
inner: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)"
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns typed box-shadow tokens designed for high-contrast optical depth.
|
|
157
|
+
*/
|
|
158
|
+
export function shadow(variant: ShadowVariant | string): string {
|
|
159
|
+
if (variant in SHADOW_PRESETS) {
|
|
160
|
+
return SHADOW_PRESETS[variant as ShadowVariant]!;
|
|
161
|
+
}
|
|
162
|
+
return variant;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type RadiusVariant = "none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full" | number;
|
|
166
|
+
|
|
167
|
+
export const RADIUS_PRESETS: Record<string, string> = {
|
|
168
|
+
none: "0px",
|
|
169
|
+
sm: "4px",
|
|
170
|
+
md: "8px",
|
|
171
|
+
lg: "12px",
|
|
172
|
+
xl: "16px",
|
|
173
|
+
"2xl": "20px",
|
|
174
|
+
"3xl": "24px",
|
|
175
|
+
full: "9999px"
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns typed border-radius tokens or scale numbers.
|
|
180
|
+
*/
|
|
181
|
+
export function radius(variant: RadiusVariant): string {
|
|
182
|
+
if (typeof variant === "number") {
|
|
183
|
+
return `${variant}px`;
|
|
184
|
+
}
|
|
185
|
+
if (variant in RADIUS_PRESETS) {
|
|
186
|
+
return RADIUS_PRESETS[variant]!;
|
|
187
|
+
}
|
|
188
|
+
return String(variant);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface GlassOptions {
|
|
192
|
+
blur?: number | string;
|
|
193
|
+
tint?: string;
|
|
194
|
+
border?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Generates tactile glassmorphism CSS styles with backdrop filters and frosted borders.
|
|
199
|
+
*/
|
|
200
|
+
export function glass(options: GlassOptions = {}): Record<string, string> {
|
|
201
|
+
const blurVal = typeof options.blur === "number" ? `${options.blur}px` : (options.blur ?? "20px");
|
|
202
|
+
const tintVal = options.tint ?? "rgba(255, 255, 255, 0.65)";
|
|
203
|
+
const borderVal = options.border ?? "1px solid rgba(255, 255, 255, 0.75)";
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
backdropFilter: `blur(${blurVal})`,
|
|
207
|
+
WebkitBackdropFilter: `blur(${blurVal})`,
|
|
208
|
+
backgroundColor: tintVal,
|
|
209
|
+
border: borderVal
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Formats a style property value for direct DOM binding or SSR generation.
|
|
215
|
+
* Handles automatic unit resolution, dimension keywords, and custom CSS variables.
|
|
216
|
+
*/
|
|
217
|
+
export function formatStyleProp(prop: string, val: any): string {
|
|
218
|
+
if (val === null || val === undefined || val === "") return "";
|
|
219
|
+
if (typeof val === "number") {
|
|
220
|
+
if (val === 0 || UNITLESS_PROPERTIES.has(prop)) {
|
|
221
|
+
return String(val);
|
|
222
|
+
}
|
|
223
|
+
return `${val}px`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const str = String(val).trim();
|
|
227
|
+
if (str in DIMENSION_KEYWORDS) {
|
|
228
|
+
const lowerProp = prop.toLowerCase();
|
|
229
|
+
if (str === "screen") {
|
|
230
|
+
if (lowerProp.includes("width")) return "100dvw";
|
|
231
|
+
return "100dvh";
|
|
232
|
+
}
|
|
233
|
+
return DIMENSION_KEYWORDS[str]!;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return str;
|
|
237
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { insertChild, setAttribute } from "../dom/render";
|
|
2
|
+
|
|
3
|
+
const templateCache = new Map<string, HTMLTemplateElement>();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cache and clone pre-parsed HTML template strings with native browser cloneNode(true)
|
|
7
|
+
*/
|
|
8
|
+
export function template<T extends HTMLElement = HTMLElement>(htmlString: string): () => T {
|
|
9
|
+
let templateEl = templateCache.get(htmlString);
|
|
10
|
+
|
|
11
|
+
if (!templateEl && typeof document !== "undefined") {
|
|
12
|
+
templateEl = document.createElement("template");
|
|
13
|
+
templateEl.innerHTML = htmlString.trim();
|
|
14
|
+
templateCache.set(htmlString, templateEl);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return () => {
|
|
18
|
+
if (!templateEl) {
|
|
19
|
+
const wrapper = document.createElement("div");
|
|
20
|
+
wrapper.innerHTML = htmlString.trim();
|
|
21
|
+
return wrapper.firstElementChild as T;
|
|
22
|
+
}
|
|
23
|
+
return templateEl.content.firstElementChild!.cloneNode(true) as T;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tagged template literal for ultra-fast HTML string rendering with reactive signal bindings
|
|
29
|
+
*/
|
|
30
|
+
export function html(
|
|
31
|
+
strings: TemplateStringsArray,
|
|
32
|
+
...values: any[]
|
|
33
|
+
): HTMLElement | DocumentFragment {
|
|
34
|
+
const MARKER_PREFIX = "visage-bind-";
|
|
35
|
+
let htmlString = "";
|
|
36
|
+
const bindings: Array<{ id: string; value: any; isAttr: boolean; attrName?: string }> = [];
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < strings.length; i++) {
|
|
39
|
+
htmlString += strings[i];
|
|
40
|
+
if (i < values.length) {
|
|
41
|
+
const val = values[i];
|
|
42
|
+
const markerId = `${MARKER_PREFIX}${i}`;
|
|
43
|
+
|
|
44
|
+
const lastOpeningTag = htmlString.lastIndexOf("<");
|
|
45
|
+
const lastClosingTag = htmlString.lastIndexOf(">");
|
|
46
|
+
|
|
47
|
+
if (lastOpeningTag > lastClosingTag) {
|
|
48
|
+
const match = htmlString.match(/([a-zA-Z0-9_-]+)=["']?$/);
|
|
49
|
+
if (match?.[1]) {
|
|
50
|
+
const attrName = match[1];
|
|
51
|
+
bindings.push({ id: markerId, value: val, isAttr: true, attrName });
|
|
52
|
+
htmlString += `${markerId}`;
|
|
53
|
+
} else {
|
|
54
|
+
bindings.push({ id: markerId, value: val, isAttr: true });
|
|
55
|
+
htmlString += `data-visage-prop-${i}="${markerId}"`;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
bindings.push({ id: markerId, value: val, isAttr: false });
|
|
59
|
+
htmlString += `<!--${markerId}-->`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const clone = template(htmlString);
|
|
65
|
+
const root = clone();
|
|
66
|
+
|
|
67
|
+
for (const bind of bindings) {
|
|
68
|
+
if (bind.isAttr && bind.attrName) {
|
|
69
|
+
const targetEl = findElementWithAttributeValue(root, bind.attrName, bind.id);
|
|
70
|
+
if (targetEl) {
|
|
71
|
+
targetEl.removeAttribute(bind.attrName);
|
|
72
|
+
setAttribute(targetEl, bind.attrName, bind.value);
|
|
73
|
+
}
|
|
74
|
+
} else if (!bind.isAttr) {
|
|
75
|
+
const commentNode = findCommentNode(root, bind.id);
|
|
76
|
+
if (commentNode?.parentNode) {
|
|
77
|
+
insertChild(commentNode.parentNode, bind.value, commentNode);
|
|
78
|
+
commentNode.parentNode.removeChild(commentNode);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findElementWithAttributeValue(
|
|
87
|
+
root: HTMLElement,
|
|
88
|
+
attrName: string,
|
|
89
|
+
value: string
|
|
90
|
+
): HTMLElement | null {
|
|
91
|
+
if (root.getAttribute(attrName) === value) {
|
|
92
|
+
return root;
|
|
93
|
+
}
|
|
94
|
+
return root.querySelector(`[${attrName}="${value}"]`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findCommentNode(root: Node, text: string): Comment | null {
|
|
98
|
+
const iterator =
|
|
99
|
+
typeof document !== "undefined" && (document as any).createNodeIterator
|
|
100
|
+
? (document as any).createNodeIterator(root, 128 /* SHOW_COMMENT */)
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
if (iterator) {
|
|
104
|
+
let node: Comment | null = iterator.nextNode();
|
|
105
|
+
while (node) {
|
|
106
|
+
if (node.nodeValue === text) {
|
|
107
|
+
return node;
|
|
108
|
+
}
|
|
109
|
+
node = iterator.nextNode();
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const traverse = (node: Node): Comment | null => {
|
|
115
|
+
for (const child of node.childNodes || []) {
|
|
116
|
+
if (child.nodeType === 8 /* COMMENT */ && (child as any).data === text) {
|
|
117
|
+
return child as Comment;
|
|
118
|
+
}
|
|
119
|
+
const res = traverse(child);
|
|
120
|
+
if (res) return res;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return traverse(root);
|
|
126
|
+
}
|