@aihu/css-engine 0.1.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 +21 -0
- package/README.md +122 -0
- package/crates/aihu-css-core/Cargo.toml +22 -0
- package/crates/aihu-css-core/src/ast.rs +173 -0
- package/crates/aihu-css-core/src/bin/main.rs +73 -0
- package/crates/aihu-css-core/src/cache.rs +182 -0
- package/crates/aihu-css-core/src/emit.rs +236 -0
- package/crates/aihu-css-core/src/features/anchor.rs +41 -0
- package/crates/aihu-css-core/src/features/mod.rs +33 -0
- package/crates/aihu-css-core/src/features/popover.rs +40 -0
- package/crates/aihu-css-core/src/features/text_balance.rs +36 -0
- package/crates/aihu-css-core/src/features/view_transition.rs +38 -0
- package/crates/aihu-css-core/src/lib.rs +67 -0
- package/crates/aihu-css-core/src/progressive.rs +200 -0
- package/crates/aihu-css-core/src/scanner.rs +235 -0
- package/crates/aihu-css-core/src/theme.rs +179 -0
- package/crates/aihu-css-core/src/tokens.rs +470 -0
- package/crates/aihu-css-core/src/variants.rs +124 -0
- package/crates/aihu-css-core/tests/cache.rs +71 -0
- package/crates/aihu-css-core/tests/emit.rs +148 -0
- package/crates/aihu-css-core/tests/fixtures/button.ast.json +19 -0
- package/crates/aihu-css-core/tests/progressive_snapshot.rs +102 -0
- package/crates/aihu-css-core/tests/scanner.rs +99 -0
- package/crates/aihu-css-core/tests/scoped_snapshot.rs +73 -0
- package/crates/aihu-css-core/tests/snapshot.rs +24 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__anchor_snapshot.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__popover_snapshot.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__text_balance_snapshot.snap +23 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__view_transition_snapshot.snap +25 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__flat_output_for_class_list.snap +6 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_output_for_sfc.snap +25 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_authored_style_block.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_global_style_block.snap +24 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__standard_variants.snap +33 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__theme_default_vs_override.snap +45 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__wc_native_variants.snap +28 -0
- package/crates/aihu-css-core/tests/snapshots/snapshot__compiles_basic_class.snap +5 -0
- package/crates/aihu-css-core/tests/snapshots/snapshot__compiles_multiple_classes.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__arbitrary_values.snap +9 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_borders.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_colors.snap +10 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_effects.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_layout.snap +12 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_spacing.snap +11 -0
- package/crates/aihu-css-core/tests/snapshots/tokens__category_typography.snap +11 -0
- package/crates/aihu-css-core/tests/tokens.rs +79 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/cn.d.ts +14 -0
- package/dist/runtime/cn.d.ts.map +1 -0
- package/dist/runtime/cn.js +107 -0
- package/dist/runtime/cn.js.map +1 -0
- package/dist/runtime/progressive.d.ts +54 -0
- package/dist/runtime/progressive.d.ts.map +1 -0
- package/dist/runtime/progressive.js +132 -0
- package/dist/runtime/progressive.js.map +1 -0
- package/package.json +54 -0
- package/styles/aihu-default.css +73 -0
- package/styles/aihu-graphite.css +71 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cn.js","names":[],"sources":["../../src/runtime/cn-conflict-map.generated.ts","../../src/runtime/cn.ts"],"sourcesContent":["// AUTO-GENERATED by scripts/gen-cn-conflict-map.ts — DO NOT EDIT.\n// Source of truth: aihu-css-core `tokens::conflict_groups()` (the utility\n// registry). Regenerate with `bun run gen:cn-map`. Hand-edits will be lost.\n//\n// Maps a utility class PREFIX to the CSS property group it controls. Two\n// classes conflict (last wins) when they resolve to the same group.\n\n/** Prefix → conflict-group key, sorted longest-prefix-first. */\nexport const CONFLICT_GROUPS: Record<string, string> = {\n outline: 'outline-color',\n opacity: 'opacity',\n rounded: 'border-radius',\n border: 'border-color',\n stroke: 'stroke',\n shadow: 'box-shadow',\n 'gap-x': 'column-gap',\n 'gap-y': 'row-gap',\n 'min-w': 'min-width',\n 'max-w': 'max-width',\n 'min-h': 'min-height',\n 'max-h': 'max-height',\n text: 'color',\n fill: 'fill',\n ring: '--tw-ring-color',\n font: 'font-weight',\n gap: 'gap',\n px: 'padding-inline',\n py: 'padding-block',\n pt: 'padding-top',\n pr: 'padding-right',\n pb: 'padding-bottom',\n pl: 'padding-left',\n mx: 'margin-inline',\n my: 'margin-block',\n mt: 'margin-top',\n mr: 'margin-right',\n mb: 'margin-bottom',\n ml: 'margin-left',\n bg: 'background-color',\n p: 'padding',\n m: 'margin',\n w: 'width',\n h: 'height',\n z: 'z-index',\n}\n","/**\n * `@aihu/css-engine/runtime/cn` — the class-merge runtime helper (Plan 3 Task 9).\n *\n * `cn(...inputs)` merges class strings / arrays / conditionals into a single\n * deduplicated class string, resolving Tailwind-style conflicts last-wins per\n * property group (`cn('p-2', 'p-4')` → `'p-4'`).\n *\n * The conflict map (`CONFLICT_GROUPS`) is GENERATED at engine build time from\n * the utility registry (`scripts/gen-cn-conflict-map.ts` → Rust\n * `tokens::conflict_groups()`), NOT hand-maintained — so it never drifts from\n * the utility table. Separate < 1 KB gz sub-export from `runtime/progressive`\n * (Risk #4 size-split).\n *\n * This is the runtime-merge helper for consumer-provided overrides (spec §9.3):\n * recipes use static utility strings at compile time; `cn()` is only for the\n * runtime override case.\n */\nimport { CONFLICT_GROUPS } from './cn-conflict-map.generated.ts'\n\n/** A class value: string, falsy (dropped), or a nested array of the same. */\nexport type ClassValue = string | number | null | undefined | false | ClassValue[]\n\n/** Flatten the (possibly nested / conditional) inputs into a token list. */\nfunction tokens(inputs: ClassValue[], out: string[]): void {\n for (const input of inputs) {\n if (!input) continue\n if (Array.isArray(input)) {\n tokens(input, out)\n } else {\n for (const t of String(input).split(' ')) {\n if (t) out.push(t)\n }\n }\n }\n}\n\n/**\n * The conflict-group key for a class, or the class itself when it belongs to no\n * known group (so unrelated classes always coexist). Strips variant prefixes\n * (`md:`, `hover:`, …) so `hover:p-2` and `hover:p-4` still conflict, while\n * `p-2` and `hover:p-4` do not (different variant scope).\n */\nfunction groupKey(cls: string): string {\n const colon = cls.lastIndexOf(':')\n const variant = colon === -1 ? '' : cls.slice(0, colon + 1)\n const base = colon === -1 ? cls : cls.slice(colon + 1)\n const dash = base.indexOf('-')\n // The prefix is the segment before the first dash (`bg-red-500` → `bg`); for\n // dashless bases (`flex`) there is no group, so the class keys to itself.\n const prefix = dash === -1 ? base : base.slice(0, dash)\n const group = CONFLICT_GROUPS[prefix]\n return group ? `${variant}${group}` : cls\n}\n\n/**\n * Merge class values, resolving last-wins conflicts per property group.\n *\n * @example cn('p-2', 'p-4') // 'p-4'\n * @example cn('a', false && 'b', ['c']) // 'a c'\n * @example cn('bg-red-500', 'bg-blue-500') // 'bg-blue-500'\n */\nexport function cn(...inputs: ClassValue[]): string {\n const flat: string[] = []\n tokens(inputs, flat)\n\n // Last occurrence per group key wins; preserve final order by re-scanning.\n const winner = new Map<string, string>()\n for (const t of flat) winner.set(groupKey(t), t)\n\n const seen = new Set<string>()\n const result: string[] = []\n for (const t of flat) {\n const key = groupKey(t)\n if (winner.get(key) === t && !seen.has(t)) {\n seen.add(t)\n result.push(t)\n }\n }\n return result.join(' ')\n}\n"],"mappings":";;AAQA,MAAa,kBAA0C;CACrD,SAAS;CACT,SAAS;CACT,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;CACT,SAAS;CACT,SAAS;CACT,SAAS;CACT,SAAS;CACT,MAAM;CACN,MAAM;CACN,MAAM;CACN,MAAM;CACN,KAAK;CACL,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACJ;;;;;;;;;;;;;;;;;;;;;ACrBD,SAAS,OAAO,QAAsB,KAAqB;CACzD,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,CAAC,OAAO;EACZ,IAAI,MAAM,QAAQ,MAAM,EACtB,OAAO,OAAO,IAAI;OAElB,KAAK,MAAM,KAAK,OAAO,MAAM,CAAC,MAAM,IAAI,EACtC,IAAI,GAAG,IAAI,KAAK,EAAE;;;;;;;;;AAY1B,SAAS,SAAS,KAAqB;CACrC,MAAM,QAAQ,IAAI,YAAY,IAAI;CAClC,MAAM,UAAU,UAAU,KAAK,KAAK,IAAI,MAAM,GAAG,QAAQ,EAAE;CAC3D,MAAM,OAAO,UAAU,KAAK,MAAM,IAAI,MAAM,QAAQ,EAAE;CACtD,MAAM,OAAO,KAAK,QAAQ,IAAI;CAI9B,MAAM,QAAQ,gBADC,SAAS,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;CAEvD,OAAO,QAAQ,GAAG,UAAU,UAAU;;;;;;;;;AAUxC,SAAgB,GAAG,GAAG,QAA8B;CAClD,MAAM,OAAiB,EAAE;CACzB,OAAO,QAAQ,KAAK;CAGpB,MAAM,yBAAS,IAAI,KAAqB;CACxC,KAAK,MAAM,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,EAAE,EAAE;CAEhD,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,SAAmB,EAAE;CAC3B,KAAK,MAAM,KAAK,MAAM;EACpB,MAAM,MAAM,SAAS,EAAE;EACvB,IAAI,OAAO,IAAI,IAAI,KAAK,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE;GACzC,KAAK,IAAI,EAAE;GACX,OAAO,KAAK,EAAE;;;CAGlB,OAAO,OAAO,KAAK,IAAI"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/runtime/progressive.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `@aihu/css-engine/runtime/progressive` — runtime fallbacks for the
|
|
4
|
+
* `@supports`-gated progressive features (`anchor:`, `popover:`).
|
|
5
|
+
*
|
|
6
|
+
* This is the **separate** browser sub-export from `cn` (Plan 3 Risk #4
|
|
7
|
+
* size-split): merging the two would blow `cn`'s 1 KB budget. This module
|
|
8
|
+
* carries the heavier positioning code under its own 3 KB row.
|
|
9
|
+
*
|
|
10
|
+
* It is a tiny hand-written floating-ui-style positioning shim — NOT the npm
|
|
11
|
+
* `@floating-ui/dom` package. aihu's thesis is dependency-free / tiny bundles,
|
|
12
|
+
* so the shim is ~2 KB of vanilla DOM math, SHARED by `anchorFallback` and
|
|
13
|
+
* `popoverFallback`. Only loaded when native CSS anchor positioning / the
|
|
14
|
+
* Popover API is unsupported (the engine emits a
|
|
15
|
+
* `/* aihu:progressive-fallback ... */` marker the consumer wires to these).
|
|
16
|
+
*/
|
|
17
|
+
/** Where to place the floating element relative to its anchor. */
|
|
18
|
+
type Placement = 'top' | 'bottom' | 'left' | 'right';
|
|
19
|
+
interface PositionOptions {
|
|
20
|
+
/** Preferred side. Default `'bottom'`. */
|
|
21
|
+
placement?: Placement;
|
|
22
|
+
/** Gap between anchor and floating element, in px. Default `4`. */
|
|
23
|
+
offset?: number;
|
|
24
|
+
/** Flip to the opposite side if it would overflow the viewport. Default `true`. */
|
|
25
|
+
flip?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Position `floating` against `anchor` and apply `position: fixed; left/top`.
|
|
29
|
+
* The shared core for both fallbacks. Returns the resolved placement.
|
|
30
|
+
*/
|
|
31
|
+
declare function position(anchor: Element, floating: HTMLElement, opts?: PositionOptions): Placement;
|
|
32
|
+
/**
|
|
33
|
+
* Fallback for the `anchor:` feature: position `floating` against `anchor`
|
|
34
|
+
* with JS when native CSS anchor positioning is unsupported. Re-positions on
|
|
35
|
+
* scroll/resize; returns a cleanup function that removes the listeners.
|
|
36
|
+
*/
|
|
37
|
+
declare function anchorFallback(anchor: Element, floating: HTMLElement, opts?: PositionOptions): () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Portal `el` to a top-layer-emulating container appended to `<body>` with a
|
|
40
|
+
* high z-index. Returns a restore function that moves `el` back to its original
|
|
41
|
+
* parent and removes the container. Used by `popoverFallback` when the native
|
|
42
|
+
* Popover API (and its real top layer) is unavailable.
|
|
43
|
+
*/
|
|
44
|
+
declare function portal(el: HTMLElement): () => void;
|
|
45
|
+
/**
|
|
46
|
+
* Fallback for the `popover:` feature: emulate the Popover API's top layer by
|
|
47
|
+
* portaling `panel` out of the normal flow and positioning it against `anchor`
|
|
48
|
+
* with the SHARED positioning shim (`position`, also used by `anchorFallback`).
|
|
49
|
+
* Returns a cleanup function that tears down the portal + listeners.
|
|
50
|
+
*/
|
|
51
|
+
declare function popoverFallback(anchor: Element, panel: HTMLElement, opts?: PositionOptions): () => void;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { Placement, PositionOptions, anchorFallback, popoverFallback, portal, position };
|
|
54
|
+
//# sourceMappingURL=progressive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progressive.d.ts","names":[],"sources":["../../src/runtime/progressive.ts"],"mappings":";;AAiBA;;;;;AAEA;;;;;;;;;;KAFY,SAAA;AAAA,UAEK,eAAA;;EAEf,SAAA,GAAY,SAAA;EA0DF;EAxDV,MAAA;EA0DC;EAxDD,IAAA;AAAA;;;;;iBAoDc,QAAA,CACd,MAAA,EAAQ,OAAA,EACR,QAAA,EAAU,WAAA,EACV,IAAA,GAAM,eAAA,GACL,SAAA;;;;;AAiCH;iBAAgB,cAAA,CACd,MAAA,EAAQ,OAAA,EACR,QAAA,EAAU,WAAA,EACV,IAAA,GAAM,eAAA;;;;;;;iBAoBQ,MAAA,CAAO,EAAA,EAAI,WAAA;;;;;;;iBA0BX,eAAA,CACd,MAAA,EAAQ,OAAA,EACR,KAAA,EAAO,WAAA,EACP,IAAA,GAAM,eAAA"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
//#region src/runtime/progressive.ts
|
|
2
|
+
const OPPOSITE = {
|
|
3
|
+
top: "bottom",
|
|
4
|
+
bottom: "top",
|
|
5
|
+
left: "right",
|
|
6
|
+
right: "left"
|
|
7
|
+
};
|
|
8
|
+
/** Compute `{ x, y }` for `floating` placed against `anchor` at `placement`. */
|
|
9
|
+
function computeXY(anchor, floating, placement, offset) {
|
|
10
|
+
switch (placement) {
|
|
11
|
+
case "top": return {
|
|
12
|
+
x: anchor.left + (anchor.width - floating.width) / 2,
|
|
13
|
+
y: anchor.top - floating.height - offset
|
|
14
|
+
};
|
|
15
|
+
case "bottom": return {
|
|
16
|
+
x: anchor.left + (anchor.width - floating.width) / 2,
|
|
17
|
+
y: anchor.bottom + offset
|
|
18
|
+
};
|
|
19
|
+
case "left": return {
|
|
20
|
+
x: anchor.left - floating.width - offset,
|
|
21
|
+
y: anchor.top + (anchor.height - floating.height) / 2
|
|
22
|
+
};
|
|
23
|
+
case "right": return {
|
|
24
|
+
x: anchor.right + offset,
|
|
25
|
+
y: anchor.top + (anchor.height - floating.height) / 2
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** True if a rect at (x, y) with size would overflow the viewport. */
|
|
30
|
+
function overflows(x, y, w, h) {
|
|
31
|
+
const vw = window.innerWidth;
|
|
32
|
+
const vh = window.innerHeight;
|
|
33
|
+
return x < 0 || y < 0 || x + w > vw || y + h > vh;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Position `floating` against `anchor` and apply `position: fixed; left/top`.
|
|
37
|
+
* The shared core for both fallbacks. Returns the resolved placement.
|
|
38
|
+
*/
|
|
39
|
+
function position(anchor, floating, opts = {}) {
|
|
40
|
+
const placement = opts.placement ?? "bottom";
|
|
41
|
+
const offset = opts.offset ?? 4;
|
|
42
|
+
const flip = opts.flip ?? true;
|
|
43
|
+
const aRect = anchor.getBoundingClientRect();
|
|
44
|
+
const fRect = {
|
|
45
|
+
width: floating.offsetWidth,
|
|
46
|
+
height: floating.offsetHeight
|
|
47
|
+
};
|
|
48
|
+
let resolved = placement;
|
|
49
|
+
let { x, y } = computeXY(aRect, fRect, resolved, offset);
|
|
50
|
+
if (flip && overflows(x, y, fRect.width, fRect.height)) {
|
|
51
|
+
const alt = OPPOSITE[resolved];
|
|
52
|
+
const altXY = computeXY(aRect, fRect, alt, offset);
|
|
53
|
+
if (!overflows(altXY.x, altXY.y, fRect.width, fRect.height)) {
|
|
54
|
+
resolved = alt;
|
|
55
|
+
x = altXY.x;
|
|
56
|
+
y = altXY.y;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
floating.style.position = "fixed";
|
|
60
|
+
floating.style.left = `${Math.round(x)}px`;
|
|
61
|
+
floating.style.top = `${Math.round(y)}px`;
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fallback for the `anchor:` feature: position `floating` against `anchor`
|
|
66
|
+
* with JS when native CSS anchor positioning is unsupported. Re-positions on
|
|
67
|
+
* scroll/resize; returns a cleanup function that removes the listeners.
|
|
68
|
+
*/
|
|
69
|
+
function anchorFallback(anchor, floating, opts = {}) {
|
|
70
|
+
const update = () => {
|
|
71
|
+
position(anchor, floating, opts);
|
|
72
|
+
};
|
|
73
|
+
update();
|
|
74
|
+
window.addEventListener("scroll", update, {
|
|
75
|
+
passive: true,
|
|
76
|
+
capture: true
|
|
77
|
+
});
|
|
78
|
+
window.addEventListener("resize", update, { passive: true });
|
|
79
|
+
return () => {
|
|
80
|
+
window.removeEventListener("scroll", update, { capture: true });
|
|
81
|
+
window.removeEventListener("resize", update);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Portal `el` to a top-layer-emulating container appended to `<body>` with a
|
|
86
|
+
* high z-index. Returns a restore function that moves `el` back to its original
|
|
87
|
+
* parent and removes the container. Used by `popoverFallback` when the native
|
|
88
|
+
* Popover API (and its real top layer) is unavailable.
|
|
89
|
+
*/
|
|
90
|
+
function portal(el) {
|
|
91
|
+
const originalParent = el.parentNode;
|
|
92
|
+
const originalNext = el.nextSibling;
|
|
93
|
+
const layer = document.createElement("div");
|
|
94
|
+
layer.style.position = "fixed";
|
|
95
|
+
layer.style.left = "0";
|
|
96
|
+
layer.style.top = "0";
|
|
97
|
+
layer.style.zIndex = "2147483647";
|
|
98
|
+
document.body.appendChild(layer);
|
|
99
|
+
layer.appendChild(el);
|
|
100
|
+
return () => {
|
|
101
|
+
if (originalParent) originalParent.insertBefore(el, originalNext);
|
|
102
|
+
else el.remove();
|
|
103
|
+
layer.remove();
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Fallback for the `popover:` feature: emulate the Popover API's top layer by
|
|
108
|
+
* portaling `panel` out of the normal flow and positioning it against `anchor`
|
|
109
|
+
* with the SHARED positioning shim (`position`, also used by `anchorFallback`).
|
|
110
|
+
* Returns a cleanup function that tears down the portal + listeners.
|
|
111
|
+
*/
|
|
112
|
+
function popoverFallback(anchor, panel, opts = {}) {
|
|
113
|
+
const restore = portal(panel);
|
|
114
|
+
const update = () => {
|
|
115
|
+
position(anchor, panel, opts);
|
|
116
|
+
};
|
|
117
|
+
update();
|
|
118
|
+
window.addEventListener("scroll", update, {
|
|
119
|
+
passive: true,
|
|
120
|
+
capture: true
|
|
121
|
+
});
|
|
122
|
+
window.addEventListener("resize", update, { passive: true });
|
|
123
|
+
return () => {
|
|
124
|
+
window.removeEventListener("scroll", update, { capture: true });
|
|
125
|
+
window.removeEventListener("resize", update);
|
|
126
|
+
restore();
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { anchorFallback, popoverFallback, portal, position };
|
|
131
|
+
|
|
132
|
+
//# sourceMappingURL=progressive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progressive.js","names":[],"sources":["../../src/runtime/progressive.ts"],"sourcesContent":["/**\n * `@aihu/css-engine/runtime/progressive` — runtime fallbacks for the\n * `@supports`-gated progressive features (`anchor:`, `popover:`).\n *\n * This is the **separate** browser sub-export from `cn` (Plan 3 Risk #4\n * size-split): merging the two would blow `cn`'s 1 KB budget. This module\n * carries the heavier positioning code under its own 3 KB row.\n *\n * It is a tiny hand-written floating-ui-style positioning shim — NOT the npm\n * `@floating-ui/dom` package. aihu's thesis is dependency-free / tiny bundles,\n * so the shim is ~2 KB of vanilla DOM math, SHARED by `anchorFallback` and\n * `popoverFallback`. Only loaded when native CSS anchor positioning / the\n * Popover API is unsupported (the engine emits a\n * `/* aihu:progressive-fallback ... */` marker the consumer wires to these).\n */\n\n/** Where to place the floating element relative to its anchor. */\nexport type Placement = 'top' | 'bottom' | 'left' | 'right'\n\nexport interface PositionOptions {\n /** Preferred side. Default `'bottom'`. */\n placement?: Placement\n /** Gap between anchor and floating element, in px. Default `4`. */\n offset?: number\n /** Flip to the opposite side if it would overflow the viewport. Default `true`. */\n flip?: boolean\n}\n\nconst OPPOSITE: Record<Placement, Placement> = {\n top: 'bottom',\n bottom: 'top',\n left: 'right',\n right: 'left',\n}\n\n/** Compute `{ x, y }` for `floating` placed against `anchor` at `placement`. */\nfunction computeXY(\n anchor: DOMRect,\n floating: { width: number; height: number },\n placement: Placement,\n offset: number,\n): { x: number; y: number } {\n switch (placement) {\n case 'top':\n return {\n x: anchor.left + (anchor.width - floating.width) / 2,\n y: anchor.top - floating.height - offset,\n }\n case 'bottom':\n return {\n x: anchor.left + (anchor.width - floating.width) / 2,\n y: anchor.bottom + offset,\n }\n case 'left':\n return {\n x: anchor.left - floating.width - offset,\n y: anchor.top + (anchor.height - floating.height) / 2,\n }\n case 'right':\n return {\n x: anchor.right + offset,\n y: anchor.top + (anchor.height - floating.height) / 2,\n }\n }\n}\n\n/** True if a rect at (x, y) with size would overflow the viewport. */\nfunction overflows(x: number, y: number, w: number, h: number): boolean {\n const vw = window.innerWidth\n const vh = window.innerHeight\n return x < 0 || y < 0 || x + w > vw || y + h > vh\n}\n\n/**\n * Position `floating` against `anchor` and apply `position: fixed; left/top`.\n * The shared core for both fallbacks. Returns the resolved placement.\n */\nexport function position(\n anchor: Element,\n floating: HTMLElement,\n opts: PositionOptions = {},\n): Placement {\n const placement = opts.placement ?? 'bottom'\n const offset = opts.offset ?? 4\n const flip = opts.flip ?? true\n\n const aRect = anchor.getBoundingClientRect()\n const fRect = { width: floating.offsetWidth, height: floating.offsetHeight }\n\n let resolved = placement\n let { x, y } = computeXY(aRect, fRect, resolved, offset)\n\n // Viewport-collision flip to the opposite side if the preferred one overflows.\n if (flip && overflows(x, y, fRect.width, fRect.height)) {\n const alt = OPPOSITE[resolved]\n const altXY = computeXY(aRect, fRect, alt, offset)\n if (!overflows(altXY.x, altXY.y, fRect.width, fRect.height)) {\n resolved = alt\n x = altXY.x\n y = altXY.y\n }\n }\n\n floating.style.position = 'fixed'\n floating.style.left = `${Math.round(x)}px`\n floating.style.top = `${Math.round(y)}px`\n return resolved\n}\n\n/**\n * Fallback for the `anchor:` feature: position `floating` against `anchor`\n * with JS when native CSS anchor positioning is unsupported. Re-positions on\n * scroll/resize; returns a cleanup function that removes the listeners.\n */\nexport function anchorFallback(\n anchor: Element,\n floating: HTMLElement,\n opts: PositionOptions = {},\n): () => void {\n const update = () => {\n position(anchor, floating, opts)\n }\n update()\n window.addEventListener('scroll', update, { passive: true, capture: true })\n window.addEventListener('resize', update, { passive: true })\n return () => {\n window.removeEventListener('scroll', update, { capture: true } as EventListenerOptions)\n window.removeEventListener('resize', update)\n }\n}\n\n/**\n * Portal `el` to a top-layer-emulating container appended to `<body>` with a\n * high z-index. Returns a restore function that moves `el` back to its original\n * parent and removes the container. Used by `popoverFallback` when the native\n * Popover API (and its real top layer) is unavailable.\n */\nexport function portal(el: HTMLElement): () => void {\n const originalParent = el.parentNode\n const originalNext = el.nextSibling\n const layer = document.createElement('div')\n layer.style.position = 'fixed'\n layer.style.left = '0'\n layer.style.top = '0'\n layer.style.zIndex = '2147483647'\n document.body.appendChild(layer)\n layer.appendChild(el)\n return () => {\n if (originalParent) {\n originalParent.insertBefore(el, originalNext)\n } else {\n el.remove()\n }\n layer.remove()\n }\n}\n\n/**\n * Fallback for the `popover:` feature: emulate the Popover API's top layer by\n * portaling `panel` out of the normal flow and positioning it against `anchor`\n * with the SHARED positioning shim (`position`, also used by `anchorFallback`).\n * Returns a cleanup function that tears down the portal + listeners.\n */\nexport function popoverFallback(\n anchor: Element,\n panel: HTMLElement,\n opts: PositionOptions = {},\n): () => void {\n const restore = portal(panel)\n const update = () => {\n position(anchor, panel, opts)\n }\n update()\n window.addEventListener('scroll', update, { passive: true, capture: true })\n window.addEventListener('resize', update, { passive: true })\n return () => {\n window.removeEventListener('scroll', update, { capture: true } as EventListenerOptions)\n window.removeEventListener('resize', update)\n restore()\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aihu/css-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./runtime/cn": {
|
|
15
|
+
"types": "./dist/runtime/cn.d.ts",
|
|
16
|
+
"import": "./dist/runtime/cn.js"
|
|
17
|
+
},
|
|
18
|
+
"./runtime/progressive": {
|
|
19
|
+
"types": "./dist/runtime/progressive.d.ts",
|
|
20
|
+
"import": "./dist/runtime/progressive.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"crates",
|
|
26
|
+
"styles",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@aihu/compiler": "0.4.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "rolldown -c",
|
|
36
|
+
"build:rust": "cargo build --release -p aihu-css-core",
|
|
37
|
+
"gen:cn-map": "bun scripts/gen-cn-conflict-map.ts",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:rust": "cargo test -p aihu-css-core",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"prepublishOnly": "bun run build:rust && bun run gen:cn-map && bun run build"
|
|
42
|
+
},
|
|
43
|
+
"description": "aihu CSS engine — Tailwind v4 hard fork with WC-native scoped output.",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/fellwork/aihu.git",
|
|
47
|
+
"directory": "packages/css-engine"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/fellwork/aihu/tree/main/packages/css-engine#readme",
|
|
50
|
+
"bugs": "https://github.com/fellwork/aihu/issues",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
2
|
+
aihu-default — style pack (brand token CSS bundle)
|
|
3
|
+
───────────────────────────────────────────────────────────────────
|
|
4
|
+
The consumer-shippable CSS form of the aihu brand defaults that the
|
|
5
|
+
engine's theme registry (crates/aihu-css-core/src/theme.rs) bakes in.
|
|
6
|
+
Token NAMES here are the contract: any recipe styled against these
|
|
7
|
+
names works under any interchangeable style pack (e.g. aihu-graphite)
|
|
8
|
+
with no markup change.
|
|
9
|
+
|
|
10
|
+
Extracted from apps/docs/style.css (the canonical brand palette,
|
|
11
|
+
derived from aihu-logo.html / aihu-wordmark.svg). Light values live in
|
|
12
|
+
:root; dark overrides in .dark.
|
|
13
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
/* ── Color tokens (the utility-table contract: bg-primary → var(--color-primary)) ── */
|
|
17
|
+
--color-primary: #1a1d24;
|
|
18
|
+
--color-primary-foreground: #faf8f4;
|
|
19
|
+
--color-secondary: #5a5a55;
|
|
20
|
+
--color-secondary-foreground: #faf8f4;
|
|
21
|
+
--color-accent: #c8543a;
|
|
22
|
+
--color-accent-foreground: #faf8f4;
|
|
23
|
+
--color-surface: #faf8f4;
|
|
24
|
+
--color-surface-foreground: #1a1d24;
|
|
25
|
+
--color-background: #faf8f4;
|
|
26
|
+
--color-foreground: #1a1d24;
|
|
27
|
+
--color-muted: #5a5a55;
|
|
28
|
+
--color-muted-foreground: #8a8880;
|
|
29
|
+
--color-border: #ddd9d2;
|
|
30
|
+
--color-ring: #c8543a;
|
|
31
|
+
--color-destructive: #a8432b;
|
|
32
|
+
--color-destructive-foreground: #faf8f4;
|
|
33
|
+
|
|
34
|
+
/* ── Radius scale ── */
|
|
35
|
+
--radius-sm: 4px;
|
|
36
|
+
--radius-md: 8px;
|
|
37
|
+
--radius-lg: 12px;
|
|
38
|
+
--radius-pill: 999px;
|
|
39
|
+
|
|
40
|
+
/* ── Spacing scale ── */
|
|
41
|
+
--space-1: 0.25rem;
|
|
42
|
+
--space-2: 0.5rem;
|
|
43
|
+
--space-3: 0.75rem;
|
|
44
|
+
--space-4: 1rem;
|
|
45
|
+
--space-6: 1.5rem;
|
|
46
|
+
--space-8: 2rem;
|
|
47
|
+
--space-12: 3rem;
|
|
48
|
+
--space-16: 4rem;
|
|
49
|
+
|
|
50
|
+
/* ── Typography ── */
|
|
51
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
52
|
+
--font-mono: "JetBrains Mono", ui-monospace, "Fira Code", "Cascadia Code", monospace;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ── Dark theme — same token names, dark values (consumer toggles .dark) ── */
|
|
56
|
+
.dark {
|
|
57
|
+
--color-primary: #ede8e0;
|
|
58
|
+
--color-primary-foreground: #1a1d24;
|
|
59
|
+
--color-secondary: #9e9890;
|
|
60
|
+
--color-secondary-foreground: #1a1d24;
|
|
61
|
+
--color-accent: #e8705a;
|
|
62
|
+
--color-accent-foreground: #1a1d24;
|
|
63
|
+
--color-surface: #1a1d24;
|
|
64
|
+
--color-surface-foreground: #ede8e0;
|
|
65
|
+
--color-background: #13151b;
|
|
66
|
+
--color-foreground: #ede8e0;
|
|
67
|
+
--color-muted: #9e9890;
|
|
68
|
+
--color-muted-foreground: #6e6860;
|
|
69
|
+
--color-border: #2e3240;
|
|
70
|
+
--color-ring: #e8705a;
|
|
71
|
+
--color-destructive: #f08070;
|
|
72
|
+
--color-destructive-foreground: #1a1d24;
|
|
73
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
2
|
+
aihu-graphite — style pack (monochrome variant)
|
|
3
|
+
───────────────────────────────────────────────────────────────────
|
|
4
|
+
A neutral graphite ramp expressed in oklch(). Mirrors aihu-default.css's
|
|
5
|
+
token NAMES exactly so the two packs are interchangeable: any recipe
|
|
6
|
+
styled against `--color-*` / `--radius-*` / `--space-*` works under
|
|
7
|
+
either pack with no markup change. Only the VALUES differ — graphite is
|
|
8
|
+
a desaturated (chroma ≈ 0) monochrome scale.
|
|
9
|
+
|
|
10
|
+
Same :root (light) + .dark structure as aihu-default.
|
|
11
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
12
|
+
|
|
13
|
+
:root {
|
|
14
|
+
/* ── Color tokens — monochrome oklch ramp (light) ── */
|
|
15
|
+
--color-primary: oklch(0.22 0 0);
|
|
16
|
+
--color-primary-foreground: oklch(0.98 0 0);
|
|
17
|
+
--color-secondary: oklch(0.45 0 0);
|
|
18
|
+
--color-secondary-foreground: oklch(0.98 0 0);
|
|
19
|
+
--color-accent: oklch(0.35 0 0);
|
|
20
|
+
--color-accent-foreground: oklch(0.98 0 0);
|
|
21
|
+
--color-surface: oklch(0.99 0 0);
|
|
22
|
+
--color-surface-foreground: oklch(0.22 0 0);
|
|
23
|
+
--color-background: oklch(0.99 0 0);
|
|
24
|
+
--color-foreground: oklch(0.22 0 0);
|
|
25
|
+
--color-muted: oklch(0.55 0 0);
|
|
26
|
+
--color-muted-foreground: oklch(0.65 0 0);
|
|
27
|
+
--color-border: oklch(0.88 0 0);
|
|
28
|
+
--color-ring: oklch(0.35 0 0);
|
|
29
|
+
--color-destructive: oklch(0.4 0 0);
|
|
30
|
+
--color-destructive-foreground: oklch(0.98 0 0);
|
|
31
|
+
|
|
32
|
+
/* ── Radius scale (shared contract) ── */
|
|
33
|
+
--radius-sm: 4px;
|
|
34
|
+
--radius-md: 8px;
|
|
35
|
+
--radius-lg: 12px;
|
|
36
|
+
--radius-pill: 999px;
|
|
37
|
+
|
|
38
|
+
/* ── Spacing scale (shared contract) ── */
|
|
39
|
+
--space-1: 0.25rem;
|
|
40
|
+
--space-2: 0.5rem;
|
|
41
|
+
--space-3: 0.75rem;
|
|
42
|
+
--space-4: 1rem;
|
|
43
|
+
--space-6: 1.5rem;
|
|
44
|
+
--space-8: 2rem;
|
|
45
|
+
--space-12: 3rem;
|
|
46
|
+
--space-16: 4rem;
|
|
47
|
+
|
|
48
|
+
/* ── Typography (shared contract) ── */
|
|
49
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
50
|
+
--font-mono: "JetBrains Mono", ui-monospace, "Fira Code", "Cascadia Code", monospace;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ── Dark theme — same token names, inverted monochrome ramp ── */
|
|
54
|
+
.dark {
|
|
55
|
+
--color-primary: oklch(0.92 0 0);
|
|
56
|
+
--color-primary-foreground: oklch(0.18 0 0);
|
|
57
|
+
--color-secondary: oklch(0.68 0 0);
|
|
58
|
+
--color-secondary-foreground: oklch(0.18 0 0);
|
|
59
|
+
--color-accent: oklch(0.78 0 0);
|
|
60
|
+
--color-accent-foreground: oklch(0.18 0 0);
|
|
61
|
+
--color-surface: oklch(0.22 0 0);
|
|
62
|
+
--color-surface-foreground: oklch(0.92 0 0);
|
|
63
|
+
--color-background: oklch(0.16 0 0);
|
|
64
|
+
--color-foreground: oklch(0.92 0 0);
|
|
65
|
+
--color-muted: oklch(0.62 0 0);
|
|
66
|
+
--color-muted-foreground: oklch(0.5 0 0);
|
|
67
|
+
--color-border: oklch(0.32 0 0);
|
|
68
|
+
--color-ring: oklch(0.78 0 0);
|
|
69
|
+
--color-destructive: oklch(0.7 0 0);
|
|
70
|
+
--color-destructive-foreground: oklch(0.18 0 0);
|
|
71
|
+
}
|