@hypen-space/web 0.4.956 → 0.5.2
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/README.md +20 -13
- package/dist/canvas/events.js +46 -7
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/index.d.ts +0 -1
- package/dist/canvas/index.js +0 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.js +57 -44
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.js +159 -60
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +8 -3
- package/dist/canvas/renderer.js +32 -43
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/types.d.ts +3 -0
- package/dist/canvas/utils.d.ts +0 -8
- package/dist/canvas/utils.js +3 -34
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/variants.d.ts +49 -0
- package/dist/canvas/variants.js +137 -0
- package/dist/canvas/variants.js.map +1 -0
- package/dist/dom/applicators/events.js +1 -0
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/font.d.ts +11 -6
- package/dist/dom/applicators/font.js +16 -27
- package/dist/dom/applicators/font.js.map +1 -1
- package/dist/dom/applicators/index.js +47 -45
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/canvas/index.js +2 -2
- package/dist/dom/canvas/index.js.map +1 -1
- package/dist/dom/components/app.js +2 -2
- package/dist/dom/components/app.js.map +1 -1
- package/dist/dom/components/audio.js +2 -2
- package/dist/dom/components/audio.js.map +1 -1
- package/dist/dom/components/avatar.js +3 -4
- package/dist/dom/components/avatar.js.map +1 -1
- package/dist/dom/components/badge.js +2 -2
- package/dist/dom/components/badge.js.map +1 -1
- package/dist/dom/components/button.js +2 -2
- package/dist/dom/components/button.js.map +1 -1
- package/dist/dom/components/card.js +2 -2
- package/dist/dom/components/card.js.map +1 -1
- package/dist/dom/components/center.js +2 -2
- package/dist/dom/components/center.js.map +1 -1
- package/dist/dom/components/checkbox.js +4 -5
- package/dist/dom/components/checkbox.js.map +1 -1
- package/dist/dom/components/column.js +2 -2
- package/dist/dom/components/column.js.map +1 -1
- package/dist/dom/components/container.js +2 -2
- package/dist/dom/components/container.js.map +1 -1
- package/dist/dom/components/divider.js +2 -2
- package/dist/dom/components/divider.js.map +1 -1
- package/dist/dom/components/grid.js +10 -10
- package/dist/dom/components/grid.js.map +1 -1
- package/dist/dom/components/heading.js +3 -4
- package/dist/dom/components/heading.js.map +1 -1
- package/dist/dom/components/hypenapp.js +2 -2
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/icon.js +4 -5
- package/dist/dom/components/icon.js.map +1 -1
- package/dist/dom/components/image.js +2 -2
- package/dist/dom/components/image.js.map +1 -1
- package/dist/dom/components/index.d.ts +2 -2
- package/dist/dom/components/index.js +2 -2
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/input.js +2 -2
- package/dist/dom/components/input.js.map +1 -1
- package/dist/dom/components/link.js +2 -2
- package/dist/dom/components/link.js.map +1 -1
- package/dist/dom/components/list.js +2 -2
- package/dist/dom/components/list.js.map +1 -1
- package/dist/dom/components/paragraph.js +2 -2
- package/dist/dom/components/paragraph.js.map +1 -1
- package/dist/dom/components/progressbar.js +3 -3
- package/dist/dom/components/progressbar.js.map +1 -1
- package/dist/dom/components/route.js +2 -2
- package/dist/dom/components/route.js.map +1 -1
- package/dist/dom/components/router.js +2 -2
- package/dist/dom/components/router.js.map +1 -1
- package/dist/dom/components/row.js +10 -10
- package/dist/dom/components/row.js.map +1 -1
- package/dist/dom/components/select.js +3 -4
- package/dist/dom/components/select.js.map +1 -1
- package/dist/dom/components/slider.js +2 -2
- package/dist/dom/components/slider.js.map +1 -1
- package/dist/dom/components/spacer.js +2 -2
- package/dist/dom/components/spacer.js.map +1 -1
- package/dist/dom/components/spinner.js +4 -4
- package/dist/dom/components/spinner.js.map +1 -1
- package/dist/dom/components/stack.js +10 -10
- package/dist/dom/components/stack.js.map +1 -1
- package/dist/dom/components/switch.js +5 -6
- package/dist/dom/components/switch.js.map +1 -1
- package/dist/dom/components/text.js +2 -2
- package/dist/dom/components/text.js.map +1 -1
- package/dist/dom/components/textarea.js +2 -2
- package/dist/dom/components/textarea.js.map +1 -1
- package/dist/dom/components/video.js +2 -2
- package/dist/dom/components/video.js.map +1 -1
- package/dist/dom/debug.js +6 -8
- package/dist/dom/debug.js.map +1 -1
- package/dist/dom/index.d.ts +0 -1
- package/dist/dom/index.js +0 -1
- package/dist/dom/index.js.map +1 -1
- package/dist/dom/renderer.js +2 -3
- package/dist/dom/renderer.js.map +1 -1
- package/dist/variants.d.ts +90 -0
- package/dist/variants.js +206 -0
- package/dist/variants.js.map +1 -0
- package/package.json +2 -2
- package/src/canvas/QUICKSTART.md +12 -7
- package/src/canvas/README.md +8 -5
- package/src/canvas/events.ts +49 -7
- package/src/canvas/index.ts +0 -8
- package/src/canvas/layout.ts +57 -49
- package/src/canvas/paint.ts +163 -58
- package/src/canvas/renderer.ts +38 -46
- package/src/canvas/types.ts +14 -0
- package/src/canvas/utils.ts +3 -37
- package/src/canvas/variants.ts +153 -0
- package/src/dom/README.md +8 -7
- package/src/dom/applicators/events.ts +1 -0
- package/src/dom/applicators/font.ts +16 -29
- package/src/dom/applicators/index.ts +56 -46
- package/src/dom/canvas/index.ts +2 -2
- package/src/dom/components/app.ts +2 -2
- package/src/dom/components/audio.ts +2 -2
- package/src/dom/components/avatar.ts +3 -4
- package/src/dom/components/badge.ts +2 -2
- package/src/dom/components/button.ts +2 -2
- package/src/dom/components/card.ts +2 -2
- package/src/dom/components/center.ts +2 -2
- package/src/dom/components/checkbox.ts +4 -5
- package/src/dom/components/column.ts +2 -2
- package/src/dom/components/container.ts +2 -2
- package/src/dom/components/divider.ts +2 -2
- package/src/dom/components/grid.ts +10 -10
- package/src/dom/components/heading.ts +3 -4
- package/src/dom/components/hypenapp.ts +2 -2
- package/src/dom/components/icon.ts +4 -5
- package/src/dom/components/image.ts +2 -2
- package/src/dom/components/index.ts +3 -3
- package/src/dom/components/input.ts +2 -2
- package/src/dom/components/link.ts +2 -2
- package/src/dom/components/list.ts +2 -2
- package/src/dom/components/paragraph.ts +2 -2
- package/src/dom/components/progressbar.ts +3 -3
- package/src/dom/components/route.ts +2 -2
- package/src/dom/components/router.ts +2 -2
- package/src/dom/components/row.ts +10 -10
- package/src/dom/components/select.ts +3 -4
- package/src/dom/components/slider.ts +2 -2
- package/src/dom/components/spacer.ts +2 -2
- package/src/dom/components/spinner.ts +4 -4
- package/src/dom/components/stack.ts +10 -10
- package/src/dom/components/switch.ts +5 -6
- package/src/dom/components/text.ts +2 -2
- package/src/dom/components/textarea.ts +2 -2
- package/src/dom/components/video.ts +2 -2
- package/src/dom/debug.ts +6 -8
- package/src/dom/index.ts +0 -8
- package/src/dom/renderer.ts +2 -3
- package/src/variants.ts +249 -0
- package/dist/client.d.ts +0 -36
- package/dist/client.js +0 -47
- package/dist/client.js.map +0 -1
- package/src/client.ts +0 -88
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas variant application.
|
|
3
|
+
*
|
|
4
|
+
* The Canvas renderer reads winning values directly off `node.props.<base>`
|
|
5
|
+
* (e.g. `props.padding`, `props.backgroundColor`) in both layout.ts and
|
|
6
|
+
* paint.ts. To support responsive breakpoints and interaction-state variants
|
|
7
|
+
* WITHOUT rewriting every call site, we resolve variant keys once per frame
|
|
8
|
+
* (just before layout) and stamp the winning value onto the base key.
|
|
9
|
+
*
|
|
10
|
+
* Resolution itself is delegated to the shared, renderer-agnostic helper in
|
|
11
|
+
* `../variants.ts`, so breakpoint pixels, state names, and precedence stay
|
|
12
|
+
* byte-compatible with the DOM renderer.
|
|
13
|
+
*
|
|
14
|
+
* Per-node bookkeeping (`variantBases`, `variantOriginals`) lives on the
|
|
15
|
+
* VirtualNode so each frame:
|
|
16
|
+
* 1. restores the original (variant-free) base value, then
|
|
17
|
+
* 2. re-applies the current winner for the present width + active states.
|
|
18
|
+
* This makes the pass idempotent and reversible — when a hover ends or the
|
|
19
|
+
* canvas shrinks below a breakpoint, the base value comes back automatically.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { VirtualNode } from "./types.js";
|
|
23
|
+
import {
|
|
24
|
+
resolveVariantProps,
|
|
25
|
+
parseVariantKey,
|
|
26
|
+
type ActiveStates,
|
|
27
|
+
} from "../variants.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Lazily compute (and cache) the set of applicator base names on this node that
|
|
31
|
+
* carry at least one variant key. Returns the cached set on subsequent calls.
|
|
32
|
+
*
|
|
33
|
+
* The cache is invalidated by `invalidateVariantCache`, called from the
|
|
34
|
+
* renderer whenever a SetProp/RemoveProp changes the node's prop keys.
|
|
35
|
+
*/
|
|
36
|
+
function getVariantBases(node: VirtualNode): Set<string> {
|
|
37
|
+
if (node.variantBases != null) return node.variantBases;
|
|
38
|
+
const bases = new Set<string>();
|
|
39
|
+
for (const key of Object.keys(node.props)) {
|
|
40
|
+
const parsed = parseVariantKey(key);
|
|
41
|
+
if (parsed.breakpoint || parsed.state) bases.add(parsed.base);
|
|
42
|
+
}
|
|
43
|
+
node.variantBases = bases;
|
|
44
|
+
return bases;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Drop the cached variant state so it is recomputed on the next frame.
|
|
49
|
+
* Call after any prop mutation that may add/remove variant keys.
|
|
50
|
+
*
|
|
51
|
+
* Also drops the `variantOriginals` snapshot: a SetProp can change the *base*
|
|
52
|
+
* value of a base that carries a variant (e.g. `padding.0` updated while
|
|
53
|
+
* `padding@md.0` exists). The snapshot was taken from the previous frame's
|
|
54
|
+
* props, so restoring it would clobber the engine's new base value with the
|
|
55
|
+
* stale one whenever the variant isn't currently active. Clearing it forces the
|
|
56
|
+
* next frame to re-snapshot from the current (post-SetProp) props.
|
|
57
|
+
*/
|
|
58
|
+
export function invalidateVariantCache(node: VirtualNode): void {
|
|
59
|
+
node.variantBases = null;
|
|
60
|
+
node.variantOriginals = undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Derive the active interaction states for a node from its tracked flags and
|
|
65
|
+
* props, mirroring the DOM/CSS pseudo-class semantics:
|
|
66
|
+
* - hover : pointer is over the node (or a child resolved to it)
|
|
67
|
+
* - focus : node holds canvas focus
|
|
68
|
+
* - active : pointer is currently pressed on the node
|
|
69
|
+
* - disabled : node opts out via `disabled`/`enabled:false`
|
|
70
|
+
*/
|
|
71
|
+
function activeStatesFor(node: VirtualNode): ActiveStates {
|
|
72
|
+
const disabled =
|
|
73
|
+
node.props.disabled === true ||
|
|
74
|
+
node.props.disabled === "true" ||
|
|
75
|
+
node.props.enabled === false ||
|
|
76
|
+
node.props.enabled === "false";
|
|
77
|
+
return {
|
|
78
|
+
hover: !!node.hovered,
|
|
79
|
+
focus: !!node.focused,
|
|
80
|
+
active: !!node.pressed,
|
|
81
|
+
disabled,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve and apply variant overrides for a single node, in place.
|
|
87
|
+
*
|
|
88
|
+
* No-op for nodes without any variant keys (the common case), so the per-frame
|
|
89
|
+
* cost on a plain tree is one cached set lookup per node.
|
|
90
|
+
*/
|
|
91
|
+
function applyVariantsToNode(node: VirtualNode, width: number): void {
|
|
92
|
+
const bases = getVariantBases(node);
|
|
93
|
+
if (bases.size === 0) return;
|
|
94
|
+
|
|
95
|
+
// Restore originals from the previous frame so we resolve from a clean base.
|
|
96
|
+
if (node.variantOriginals) {
|
|
97
|
+
for (const base of bases) {
|
|
98
|
+
const orig = node.variantOriginals[base];
|
|
99
|
+
if (orig === undefined) {
|
|
100
|
+
// The base had no plain value originally — remove any override we wrote.
|
|
101
|
+
delete node.props[base];
|
|
102
|
+
} else {
|
|
103
|
+
node.props[base] = orig;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const resolved = resolveVariantProps(
|
|
109
|
+
node.props as Record<string, unknown>,
|
|
110
|
+
width,
|
|
111
|
+
activeStatesFor(node),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Capture originals (once we know which bases are involved) and apply winners.
|
|
115
|
+
const originals: Record<string, unknown> = {};
|
|
116
|
+
for (const base of bases) {
|
|
117
|
+
originals[base] = node.props[base];
|
|
118
|
+
}
|
|
119
|
+
node.variantOriginals = originals;
|
|
120
|
+
|
|
121
|
+
for (const base of bases) {
|
|
122
|
+
if (base in resolved) {
|
|
123
|
+
node.props[base] = resolved[base];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Walk the tree from `root` and apply variant overrides to every node using
|
|
130
|
+
* `width` (the canvas/content width) as the viewport for breakpoint matching.
|
|
131
|
+
* Called once per frame before layout.
|
|
132
|
+
*/
|
|
133
|
+
export function applyVariants(root: VirtualNode, width: number): void {
|
|
134
|
+
applyVariantsToNode(root, width);
|
|
135
|
+
for (const child of root.children) {
|
|
136
|
+
applyVariants(child, width);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Re-derive a node's cached computed fields (`visible`, `opacity`) from its
|
|
142
|
+
* current `props`. These are read on the hot paint/hit-test path, so they are
|
|
143
|
+
* cached on the node — but {@link applyVariants} can rewrite the underlying
|
|
144
|
+
* props (`visible@md`, `opacity:disabled`) after the create/setProp caches were
|
|
145
|
+
* set, so this must run after every variant pass or the cache goes stale.
|
|
146
|
+
* Single source of truth so the create / setProp / post-variant sites can't
|
|
147
|
+
* drift apart.
|
|
148
|
+
*/
|
|
149
|
+
export function deriveNodeComputed(node: VirtualNode): void {
|
|
150
|
+
const o = parseFloat(node.props.opacity as string);
|
|
151
|
+
node.opacity = Number.isFinite(o) ? o : 1;
|
|
152
|
+
node.visible = node.props.visible === undefined ? true : !!node.props.visible;
|
|
153
|
+
}
|
package/src/dom/README.md
CHANGED
|
@@ -37,17 +37,18 @@ src/dom/
|
|
|
37
37
|
|
|
38
38
|
### Browser Import
|
|
39
39
|
|
|
40
|
-
The recommended one-call wiring is `createHypenClient`. The bare `DOMRenderer` constructor is still exported for advanced cases (custom subscription, multi-listener fan-out, etc.).
|
|
41
|
-
|
|
42
40
|
```typescript
|
|
43
41
|
import { app, HypenModuleInstance } from "@hypen-space/core";
|
|
44
42
|
import { Engine } from "@hypen-space/web-engine";
|
|
45
|
-
import {
|
|
43
|
+
import { DOMRenderer } from "@hypen-space/web";
|
|
46
44
|
|
|
47
45
|
const engine = new Engine();
|
|
48
46
|
await engine.init();
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
const renderer = new DOMRenderer(document.body, engine);
|
|
49
|
+
engine.setRenderCallback((patches) => {
|
|
50
|
+
renderer.applyPatches(patches);
|
|
51
|
+
});
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
### Server Import
|
|
@@ -78,7 +79,7 @@ All components are registered by default:
|
|
|
78
79
|
### Custom Components
|
|
79
80
|
|
|
80
81
|
```typescript
|
|
81
|
-
const
|
|
82
|
+
const renderer = new DOMRenderer(document.body, engine);
|
|
82
83
|
const registry = renderer.getComponentRegistry();
|
|
83
84
|
|
|
84
85
|
registry.register("mycomponent", {
|
|
@@ -140,7 +141,7 @@ Style applicators are registered by default:
|
|
|
140
141
|
### Custom Applicators
|
|
141
142
|
|
|
142
143
|
```typescript
|
|
143
|
-
const
|
|
144
|
+
const renderer = new DOMRenderer(document.body, engine);
|
|
144
145
|
const applicators = renderer.getApplicatorRegistry();
|
|
145
146
|
|
|
146
147
|
applicators.register("shadow", (el, value) => {
|
|
@@ -182,7 +183,7 @@ Canvas components have special applicators for drawing:
|
|
|
182
183
|
|
|
183
184
|
```typescript
|
|
184
185
|
// Register canvas applicators
|
|
185
|
-
const
|
|
186
|
+
const renderer = new DOMRenderer(document.body, engine);
|
|
186
187
|
const applicators = renderer.getApplicatorRegistry();
|
|
187
188
|
|
|
188
189
|
// Canvas-specific applicators
|
|
@@ -496,6 +496,7 @@ export const eventHandlers: Record<string, ApplicatorHandler> = {
|
|
|
496
496
|
// Mouse hover events
|
|
497
497
|
onMouseEnter: createEventHandler("mouseenter", { extractPayload: mousePayload }),
|
|
498
498
|
onMouseLeave: createEventHandler("mouseleave", { extractPayload: mousePayload }),
|
|
499
|
+
onHover: createEventHandler("mouseenter", { extractPayload: mousePayload }), // Alias for onMouseEnter
|
|
499
500
|
|
|
500
501
|
// Two-way binding for .bind(@state.x)
|
|
501
502
|
bind: ((element: HTMLElement, value: unknown) => {
|
|
@@ -5,17 +5,8 @@
|
|
|
5
5
|
import type { ApplicatorHandler } from "./types.js";
|
|
6
6
|
import { toCssLength } from "./size.js";
|
|
7
7
|
|
|
8
|
-
// Track loaded Google Fonts
|
|
9
|
-
const loadedGoogleFonts = new
|
|
10
|
-
|
|
11
|
-
function getLoadedSet(doc: Document): Set<string> {
|
|
12
|
-
let set = loadedGoogleFonts.get(doc);
|
|
13
|
-
if (!set) {
|
|
14
|
-
set = new Set<string>();
|
|
15
|
-
loadedGoogleFonts.set(doc, set);
|
|
16
|
-
}
|
|
17
|
-
return set;
|
|
18
|
-
}
|
|
8
|
+
// Track loaded Google Fonts to avoid duplicate link tags
|
|
9
|
+
const loadedGoogleFonts = new Set<string>();
|
|
19
10
|
|
|
20
11
|
// System font keywords that shouldn't be loaded from Google Fonts
|
|
21
12
|
const systemFontKeywords = new Set([
|
|
@@ -36,42 +27,40 @@ function isSystemFont(fontName: string): boolean {
|
|
|
36
27
|
}
|
|
37
28
|
|
|
38
29
|
/**
|
|
39
|
-
* Load a Google Font by injecting a link tag
|
|
30
|
+
* Load a Google Font by injecting a link tag.
|
|
40
31
|
* @param fontName - The Google Font name (e.g., "Roboto", "Open Sans")
|
|
41
|
-
* @param doc - The Document to inject into.
|
|
42
32
|
*/
|
|
43
|
-
function loadGoogleFont(fontName: string
|
|
33
|
+
function loadGoogleFont(fontName: string): void {
|
|
44
34
|
const normalized = fontName.trim();
|
|
45
|
-
const loaded = getLoadedSet(doc);
|
|
46
35
|
|
|
47
36
|
// Skip if already loaded or is a system font
|
|
48
|
-
if (
|
|
37
|
+
if (loadedGoogleFonts.has(normalized) || isSystemFont(normalized)) {
|
|
49
38
|
return;
|
|
50
39
|
}
|
|
51
40
|
|
|
52
41
|
// Mark as loading to avoid duplicates
|
|
53
|
-
|
|
42
|
+
loadedGoogleFonts.add(normalized);
|
|
54
43
|
|
|
55
44
|
// Create link element for Google Fonts
|
|
56
|
-
const link =
|
|
45
|
+
const link = document.createElement("link");
|
|
57
46
|
link.rel = "stylesheet";
|
|
58
47
|
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(normalized)}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
|
|
59
48
|
|
|
60
49
|
// Add to document head
|
|
61
|
-
|
|
50
|
+
document.head.appendChild(link);
|
|
62
51
|
}
|
|
63
52
|
|
|
64
53
|
/**
|
|
65
54
|
* Parse a fontFamily value and load any Google Fonts.
|
|
66
55
|
* Returns a CSS-safe font-family string.
|
|
67
56
|
*/
|
|
68
|
-
function processFontFamily(value: string
|
|
57
|
+
function processFontFamily(value: string): string {
|
|
69
58
|
// Split by comma to handle font stacks
|
|
70
59
|
const fonts = value.split(",").map(f => f.trim().replace(/["']/g, ""));
|
|
71
60
|
|
|
72
61
|
for (const font of fonts) {
|
|
73
62
|
if (!isSystemFont(font)) {
|
|
74
|
-
loadGoogleFont(font
|
|
63
|
+
loadGoogleFont(font);
|
|
75
64
|
}
|
|
76
65
|
}
|
|
77
66
|
|
|
@@ -97,7 +86,7 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
|
|
|
97
86
|
fontFamily: (el, value) => {
|
|
98
87
|
const fontValue = String(value);
|
|
99
88
|
// Process font family and load Google Fonts as needed
|
|
100
|
-
el.style.fontFamily = processFontFamily(fontValue
|
|
89
|
+
el.style.fontFamily = processFontFamily(fontValue);
|
|
101
90
|
},
|
|
102
91
|
|
|
103
92
|
textAlign: (el, value) => {
|
|
@@ -138,20 +127,18 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
|
|
|
138
127
|
export const GoogleFonts = {
|
|
139
128
|
/**
|
|
140
129
|
* Preload a Google Font before it's used.
|
|
141
|
-
* Defaults to the global `document` if no doc is provided.
|
|
142
130
|
*/
|
|
143
|
-
preload:
|
|
131
|
+
preload: loadGoogleFont,
|
|
144
132
|
|
|
145
133
|
/**
|
|
146
|
-
* Check if a font has been loaded
|
|
134
|
+
* Check if a font has been loaded.
|
|
147
135
|
*/
|
|
148
|
-
isLoaded: (fontName: string
|
|
149
|
-
getLoadedSet(doc).has(fontName.trim()),
|
|
136
|
+
isLoaded: (fontName: string) => loadedGoogleFonts.has(fontName.trim()),
|
|
150
137
|
|
|
151
138
|
/**
|
|
152
|
-
* Get list of loaded fonts
|
|
139
|
+
* Get list of loaded fonts.
|
|
153
140
|
*/
|
|
154
|
-
getLoadedFonts: (
|
|
141
|
+
getLoadedFonts: () => Array.from(loadedGoogleFonts),
|
|
155
142
|
|
|
156
143
|
/**
|
|
157
144
|
* Popular Google Fonts for reference.
|
|
@@ -40,53 +40,38 @@ import { advancedLayoutHandlers } from "./advanced-layout.js";
|
|
|
40
40
|
import { backgroundHandlers } from "./background.js";
|
|
41
41
|
import { displayHandlers } from "./display.js";
|
|
42
42
|
import { transitionHandlers } from "./transition.js";
|
|
43
|
+
import { BREAKPOINTS as VARIANT_BREAKPOINTS, VALID_STATES } from "../../variants.js";
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
|
-
* Tailwind breakpoint values for responsive variants
|
|
46
|
+
* Tailwind breakpoint values for responsive variants.
|
|
47
|
+
* Derived from the shared `variants.ts` table (single source of truth across
|
|
48
|
+
* DOM + Canvas renderers) and expressed as CSS `px` strings for media queries.
|
|
46
49
|
*/
|
|
47
|
-
const BREAKPOINTS: Record<string, string> =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
lg: '1024px',
|
|
51
|
-
xl: '1280px',
|
|
52
|
-
'2xl': '1536px',
|
|
53
|
-
};
|
|
50
|
+
const BREAKPOINTS: Record<string, string> = Object.fromEntries(
|
|
51
|
+
Object.entries(VARIANT_BREAKPOINTS).map(([name, px]) => [name, `${px}px`]),
|
|
52
|
+
);
|
|
54
53
|
|
|
55
54
|
/**
|
|
56
|
-
*
|
|
55
|
+
* Singleton stylesheet for variant CSS rules
|
|
57
56
|
*/
|
|
58
|
-
|
|
57
|
+
let variantStyleSheet: CSSStyleSheet | null = null;
|
|
59
58
|
|
|
60
59
|
/**
|
|
61
|
-
* Track inserted rules to avoid duplicates
|
|
60
|
+
* Track inserted rules to avoid duplicates
|
|
62
61
|
*/
|
|
63
|
-
const insertedRules = new
|
|
62
|
+
const insertedRules = new Set<string>();
|
|
64
63
|
|
|
65
64
|
/**
|
|
66
|
-
* Get or create the variant stylesheet
|
|
65
|
+
* Get or create the variant stylesheet
|
|
67
66
|
*/
|
|
68
|
-
function getVariantStyleSheet(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const style = doc.createElement('style');
|
|
67
|
+
function getVariantStyleSheet(): CSSStyleSheet {
|
|
68
|
+
if (!variantStyleSheet) {
|
|
69
|
+
const style = document.createElement('style');
|
|
72
70
|
style.id = 'hypen-variants';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
variantStyleSheets.set(doc, sheet);
|
|
71
|
+
document.head.appendChild(style);
|
|
72
|
+
variantStyleSheet = style.sheet as CSSStyleSheet;
|
|
76
73
|
}
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Get or create the inserted-rules dedup set for a document
|
|
82
|
-
*/
|
|
83
|
-
function getInsertedRules(doc: Document): Set<string> {
|
|
84
|
-
let set = insertedRules.get(doc);
|
|
85
|
-
if (!set) {
|
|
86
|
-
set = new Set<string>();
|
|
87
|
-
insertedRules.set(doc, set);
|
|
88
|
-
}
|
|
89
|
-
return set;
|
|
74
|
+
return variantStyleSheet;
|
|
90
75
|
}
|
|
91
76
|
|
|
92
77
|
/**
|
|
@@ -341,6 +326,36 @@ export class ApplicatorRegistry {
|
|
|
341
326
|
const atIndex = name.indexOf('@');
|
|
342
327
|
const colonIndex = name.indexOf(':');
|
|
343
328
|
|
|
329
|
+
// Combined responsive + state variant: backgroundColor@md:hover.
|
|
330
|
+
// Canonical key order is `<prop>@<bp>:<state>`, so the breakpoint sits
|
|
331
|
+
// between '@' and ':'. Emitted as a pseudo-class rule nested in a media
|
|
332
|
+
// query so it applies only when BOTH the width matches AND the state holds.
|
|
333
|
+
if (atIndex !== -1 && colonIndex !== -1 && colonIndex > atIndex) {
|
|
334
|
+
const prop = name.slice(0, atIndex);
|
|
335
|
+
const breakpoint = name.slice(atIndex + 1, colonIndex);
|
|
336
|
+
const state = name.slice(colonIndex + 1);
|
|
337
|
+
const minWidth = BREAKPOINTS[breakpoint];
|
|
338
|
+
|
|
339
|
+
if (minWidth && VALID_STATES.includes(state)) {
|
|
340
|
+
const cssName = this.toKebabCase(prop);
|
|
341
|
+
const cssValue = this.formatCssValue(cssName, value);
|
|
342
|
+
const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${breakpoint}-${state}-${hashValue(value)}`;
|
|
343
|
+
const ruleKey = `${className}:${cssValue}`;
|
|
344
|
+
|
|
345
|
+
if (!insertedRules.has(ruleKey)) {
|
|
346
|
+
const sheet = getVariantStyleSheet();
|
|
347
|
+
sheet.insertRule(
|
|
348
|
+
`@media (min-width: ${minWidth}) { .${className}:${state} { ${cssName}: ${cssValue}; } }`,
|
|
349
|
+
sheet.cssRules.length
|
|
350
|
+
);
|
|
351
|
+
insertedRules.add(ruleKey);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
element.classList.add(className);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
344
359
|
// Responsive variant: padding@md, width@lg, etc.
|
|
345
360
|
if (atIndex !== -1) {
|
|
346
361
|
const prop = name.slice(0, atIndex);
|
|
@@ -348,21 +363,19 @@ export class ApplicatorRegistry {
|
|
|
348
363
|
const minWidth = BREAKPOINTS[breakpoint];
|
|
349
364
|
|
|
350
365
|
if (minWidth) {
|
|
351
|
-
const doc = element.ownerDocument as Document;
|
|
352
|
-
const dedup = getInsertedRules(doc);
|
|
353
366
|
const cssName = this.toKebabCase(prop);
|
|
354
367
|
const cssValue = this.formatCssValue(cssName, value);
|
|
355
368
|
const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${breakpoint}-${hashValue(value)}`;
|
|
356
369
|
const ruleKey = `${className}:${cssValue}`;
|
|
357
370
|
|
|
358
371
|
// Avoid duplicate rule insertion
|
|
359
|
-
if (!
|
|
360
|
-
const sheet = getVariantStyleSheet(
|
|
372
|
+
if (!insertedRules.has(ruleKey)) {
|
|
373
|
+
const sheet = getVariantStyleSheet();
|
|
361
374
|
sheet.insertRule(
|
|
362
375
|
`@media (min-width: ${minWidth}) { .${className} { ${cssName}: ${cssValue}; } }`,
|
|
363
376
|
sheet.cssRules.length
|
|
364
377
|
);
|
|
365
|
-
|
|
378
|
+
insertedRules.add(ruleKey);
|
|
366
379
|
}
|
|
367
380
|
|
|
368
381
|
element.classList.add(className);
|
|
@@ -375,24 +388,21 @@ export class ApplicatorRegistry {
|
|
|
375
388
|
const prop = name.slice(0, colonIndex);
|
|
376
389
|
const state = name.slice(colonIndex + 1);
|
|
377
390
|
|
|
378
|
-
// Only handle known CSS pseudo-states
|
|
379
|
-
|
|
380
|
-
if (validStates.includes(state)) {
|
|
381
|
-
const doc = element.ownerDocument as Document;
|
|
382
|
-
const dedup = getInsertedRules(doc);
|
|
391
|
+
// Only handle known CSS pseudo-states (shared with the Canvas resolver)
|
|
392
|
+
if (VALID_STATES.includes(state)) {
|
|
383
393
|
const cssName = this.toKebabCase(prop);
|
|
384
394
|
const cssValue = this.formatCssValue(cssName, value);
|
|
385
395
|
const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${state}-${hashValue(value)}`;
|
|
386
396
|
const ruleKey = `${className}:${cssValue}`;
|
|
387
397
|
|
|
388
398
|
// Avoid duplicate rule insertion
|
|
389
|
-
if (!
|
|
390
|
-
const sheet = getVariantStyleSheet(
|
|
399
|
+
if (!insertedRules.has(ruleKey)) {
|
|
400
|
+
const sheet = getVariantStyleSheet();
|
|
391
401
|
sheet.insertRule(
|
|
392
402
|
`.${className}:${state} { ${cssName}: ${cssValue}; }`,
|
|
393
403
|
sheet.cssRules.length
|
|
394
404
|
);
|
|
395
|
-
|
|
405
|
+
insertedRules.add(ruleKey);
|
|
396
406
|
}
|
|
397
407
|
|
|
398
408
|
element.classList.add(className);
|
package/src/dom/canvas/index.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type { ComponentHandler } from "../components/index.js";
|
|
|
8
8
|
import type { ApplicatorHandler } from "../applicators/index.js";
|
|
9
9
|
|
|
10
10
|
export const canvasHandler: ComponentHandler = {
|
|
11
|
-
create(
|
|
12
|
-
const el =
|
|
11
|
+
create(): HTMLElement {
|
|
12
|
+
const el = document.createElement("canvas");
|
|
13
13
|
el.dataset.hypenType = "canvas";
|
|
14
14
|
return el as any as HTMLElement;
|
|
15
15
|
},
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import type { ComponentHandler } from "./index.js";
|
|
9
9
|
|
|
10
10
|
export const appHandler: ComponentHandler = {
|
|
11
|
-
create(
|
|
12
|
-
const el =
|
|
11
|
+
create(): HTMLElement {
|
|
12
|
+
const el = document.createElement("div");
|
|
13
13
|
el.style.display = "flex";
|
|
14
14
|
el.style.flexDirection = "column";
|
|
15
15
|
el.style.minHeight = "100vh";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { ComponentHandler } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const audioHandler: ComponentHandler = {
|
|
8
|
-
create(
|
|
9
|
-
const el =
|
|
8
|
+
create(): HTMLElement {
|
|
9
|
+
const el = document.createElement("audio");
|
|
10
10
|
el.dataset.hypenType = "audio";
|
|
11
11
|
return el as any as HTMLElement;
|
|
12
12
|
},
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { ComponentHandler } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const avatarHandler: ComponentHandler = {
|
|
8
|
-
create(
|
|
9
|
-
const el =
|
|
8
|
+
create(): HTMLElement {
|
|
9
|
+
const el = document.createElement("div");
|
|
10
10
|
el.dataset.hypenType = "avatar";
|
|
11
11
|
el.style.display = "inline-flex";
|
|
12
12
|
el.style.alignItems = "center";
|
|
@@ -23,11 +23,10 @@ export const avatarHandler: ComponentHandler = {
|
|
|
23
23
|
},
|
|
24
24
|
|
|
25
25
|
applyProps(el: HTMLElement, props: Record<string, any>): void {
|
|
26
|
-
const doc = el.ownerDocument as Document;
|
|
27
26
|
// Image source - support named arg with .0 suffix, positional arg, or plain name
|
|
28
27
|
const src = props["src.0"] || props["0"] || props.src || props.source;
|
|
29
28
|
if (src !== undefined) {
|
|
30
|
-
const img =
|
|
29
|
+
const img = document.createElement("img");
|
|
31
30
|
img.src = String(src);
|
|
32
31
|
img.style.width = "100%";
|
|
33
32
|
img.style.height = "100%";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { ComponentHandler } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const badgeHandler: ComponentHandler = {
|
|
8
|
-
create(
|
|
9
|
-
const el =
|
|
8
|
+
create(): HTMLElement {
|
|
9
|
+
const el = document.createElement("span");
|
|
10
10
|
el.dataset.hypenType = "badge";
|
|
11
11
|
el.style.display = "inline-block";
|
|
12
12
|
el.style.padding = "4px 8px";
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import type { ComponentHandler } from "./index.js";
|
|
10
10
|
|
|
11
11
|
export const buttonHandler: ComponentHandler = {
|
|
12
|
-
create(
|
|
13
|
-
const el =
|
|
12
|
+
create(): HTMLElement {
|
|
13
|
+
const el = document.createElement("button");
|
|
14
14
|
// Reset default button styles for cross-platform consistency
|
|
15
15
|
el.style.border = "none";
|
|
16
16
|
el.style.background = "none";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { ComponentHandler } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const cardHandler: ComponentHandler = {
|
|
8
|
-
create(
|
|
9
|
-
const el =
|
|
8
|
+
create(): HTMLElement {
|
|
9
|
+
const el = document.createElement("div");
|
|
10
10
|
el.dataset.hypenType = "card";
|
|
11
11
|
el.style.backgroundColor = "#ffffff";
|
|
12
12
|
el.style.borderRadius = "8px";
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import type { ComponentHandler } from "./index.js";
|
|
10
10
|
|
|
11
11
|
export const centerHandler: ComponentHandler = {
|
|
12
|
-
create(
|
|
13
|
-
const el =
|
|
12
|
+
create(): HTMLElement {
|
|
13
|
+
const el = document.createElement("div");
|
|
14
14
|
el.style.display = "flex";
|
|
15
15
|
el.style.alignItems = "center";
|
|
16
16
|
el.style.justifyContent = "center";
|
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
import type { ComponentHandler } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const checkboxHandler: ComponentHandler = {
|
|
8
|
-
create(
|
|
9
|
-
const wrapper =
|
|
8
|
+
create(): HTMLElement {
|
|
9
|
+
const wrapper = document.createElement("label");
|
|
10
10
|
wrapper.dataset.hypenType = "checkbox";
|
|
11
11
|
wrapper.style.display = "inline-flex";
|
|
12
12
|
wrapper.style.alignItems = "center";
|
|
13
13
|
wrapper.style.gap = "8px";
|
|
14
14
|
wrapper.style.cursor = "pointer";
|
|
15
15
|
|
|
16
|
-
const input =
|
|
16
|
+
const input = document.createElement("input");
|
|
17
17
|
input.type = "checkbox";
|
|
18
18
|
input.dataset.hypenCheckbox = "true";
|
|
19
19
|
|
|
@@ -23,7 +23,6 @@ export const checkboxHandler: ComponentHandler = {
|
|
|
23
23
|
},
|
|
24
24
|
|
|
25
25
|
applyProps(el: HTMLElement, props: Record<string, any>): void {
|
|
26
|
-
const doc = el.ownerDocument as Document;
|
|
27
26
|
const input = el.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
28
27
|
if (!input) return;
|
|
29
28
|
|
|
@@ -47,7 +46,7 @@ export const checkboxHandler: ComponentHandler = {
|
|
|
47
46
|
textNodes.forEach(node => node.remove());
|
|
48
47
|
|
|
49
48
|
// Add new label text
|
|
50
|
-
el.appendChild(
|
|
49
|
+
el.appendChild(document.createTextNode(String(label)));
|
|
51
50
|
}
|
|
52
51
|
},
|
|
53
52
|
};
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
import type { ComponentHandler } from "./index.js";
|
|
18
18
|
|
|
19
19
|
export const columnHandler: ComponentHandler = {
|
|
20
|
-
create(
|
|
21
|
-
const el =
|
|
20
|
+
create(): HTMLElement {
|
|
21
|
+
const el = document.createElement("div");
|
|
22
22
|
el.style.display = "flex";
|
|
23
23
|
el.style.flexDirection = "column";
|
|
24
24
|
el.style.alignItems = "stretch";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { ComponentHandler } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const containerHandler: ComponentHandler = {
|
|
8
|
-
create(
|
|
9
|
-
const el =
|
|
8
|
+
create(): HTMLElement {
|
|
9
|
+
const el = document.createElement("div");
|
|
10
10
|
// Simple block container - wraps to content by default
|
|
11
11
|
// Use .fillMaxWidth(true) to stretch
|
|
12
12
|
el.dataset.hypenType = "container";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { ComponentHandler } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const dividerHandler: ComponentHandler = {
|
|
8
|
-
create(
|
|
9
|
-
const el =
|
|
8
|
+
create(): HTMLElement {
|
|
9
|
+
const el = document.createElement("hr");
|
|
10
10
|
el.dataset.hypenType = "divider";
|
|
11
11
|
el.style.border = "none";
|
|
12
12
|
el.style.borderTop = "1px solid #e0e0e0";
|