@aihu/css-engine 0.2.4 → 0.3.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/README.md +129 -24
- package/crates/aihu-css-core/src/emit.rs +87 -8
- package/crates/aihu-css-core/src/lib.rs +5 -0
- package/crates/aihu-css-core/src/theme.rs +14 -0
- package/crates/aihu-css-core/src/tokens.rs +579 -8
- package/crates/aihu-css-core/src/variants.rs +101 -0
- package/crates/aihu-css-core/tests/emit.rs +208 -0
- package/crates/aihu-css-core/tests/scoped_snapshot.rs +35 -1
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_animate_spin_hoists_keyframes.snap +25 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_divide_y_nested_rule.snap +23 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_space_y_nested_rule.snap +23 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_transition_and_transform.snap +26 -0
- package/crates/aihu-css-core/tests/tokens.rs +474 -7
- package/dist/index.js +3 -19
- package/dist/index.js.map +1 -1
- package/dist/runtime/cn.js +13 -0
- package/dist/runtime/cn.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,38 +34,143 @@ as a per-platform `optionalDependencies` package
|
|
|
34
34
|
resolved automatically at build time — no Rust toolchain required. In a monorepo
|
|
35
35
|
dev clone the engine falls back to the workspace `target/release` binary.
|
|
36
36
|
|
|
37
|
-
###
|
|
37
|
+
### Enabling utility-class CSS in a user project
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
shadow root. Forward this through `viteAihuPlugin`'s `css` option:
|
|
39
|
+
Utility-class CSS is **auto-wired by presence** — install `@aihu/css-engine`
|
|
40
|
+
alongside `@aihu/app`, set one config knob, and every `.aihu` file in your
|
|
41
|
+
project is scanned and the matched rules land in your bundle's CSS asset.
|
|
42
|
+
There is no separate `viteCssEnginePlugin`, no `aihu.config.ts` file, no
|
|
43
|
+
preset selection step. The entire user surface is the `css` option on
|
|
44
|
+
`viteAihuPlugin()` in `vite.config.ts`:
|
|
46
45
|
|
|
47
46
|
```ts
|
|
48
|
-
// vite.config.ts
|
|
47
|
+
// vite.config.ts — the entire surface
|
|
49
48
|
import { viteAihuPlugin } from '@aihu/app'
|
|
50
49
|
import { defineConfig } from 'vite'
|
|
51
50
|
|
|
52
51
|
export default defineConfig({
|
|
53
52
|
plugins: [
|
|
54
53
|
viteAihuPlugin({
|
|
55
|
-
css: { shadowMode: 'none' },
|
|
54
|
+
css: { shadowMode: 'none' }, // ← styles this component + external light-DOM children
|
|
56
55
|
}),
|
|
57
56
|
],
|
|
58
57
|
})
|
|
59
58
|
```
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
```bash
|
|
61
|
+
bun add @aihu/css-engine
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```jsx
|
|
65
|
+
// any .aihu file — classes are scanned automatically
|
|
66
|
+
@template {
|
|
67
|
+
<section class="flex gap-6 px-4 py-6">
|
|
68
|
+
<span class="text-lg font-semibold">hi</span>
|
|
69
|
+
</section>
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
That's it. With the default `shadowMode: 'open'`, the scoped rules
|
|
74
|
+
(`.flex{display:flex}`, `.gap-6{gap:1.5rem}`, etc.) fold into each component's
|
|
75
|
+
shadow `<style>`. With `shadowMode: 'none'` (the example above) they instead
|
|
76
|
+
land in `dist/assets/index-*.css` after `bun run build`.
|
|
77
|
+
|
|
78
|
+
#### When do you need `shadowMode: 'none'`?
|
|
79
|
+
|
|
80
|
+
Scoped utility classes work fine behind a shadow root (the default `'open'`):
|
|
81
|
+
`@aihu/css-engine` compiles each SFC's classes to a per-component stylesheet and
|
|
82
|
+
folds it into that component's shadow `<style>`. It is scoped by design and does
|
|
83
|
+
**not** rely on the global cascade.
|
|
84
|
+
|
|
85
|
+
Use `'none'` only when you want the utility CSS in the light DOM — for example to
|
|
86
|
+
style external / slotted child elements that live outside your component's shadow
|
|
87
|
+
root, or to emit a single global sheet. (Truly global frameworks like Tailwind,
|
|
88
|
+
UnoCSS, or Pico do require `'none'`, but `@aihu/css-engine` does not.)
|
|
89
|
+
|
|
90
|
+
#### Style packs vs the utility scanner — they are separate
|
|
91
|
+
|
|
92
|
+
Two distinct things ship in `@aihu/css-engine`:
|
|
93
|
+
|
|
94
|
+
| Concern | What you do | Output |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| **Utility scanner** (this section) | Install the package (works in any shadow mode; default `'open'` folds into the shadow style) | Per-component scoped rules (or the Vite CSS bundle under `'none'`) |
|
|
97
|
+
| **Theme packs** (color tokens, fonts) | `import "@aihu/css-engine/styles/aihu-graphite.css"` in your entry | `--color-*` / `--font-*` CSS custom properties at `:root` |
|
|
98
|
+
|
|
99
|
+
Theme packs are plain CSS imports — they emit token variables, not utility
|
|
100
|
+
rules. Importing a pack alone does **not** enable the scanner; setting
|
|
101
|
+
`css.shadowMode: 'none'` alone does **not** import a pack. Use both for a
|
|
102
|
+
complete look; either independently is fine.
|
|
103
|
+
|
|
104
|
+
### Utility vocabulary
|
|
105
|
+
|
|
106
|
+
The engine is **inspired by Tailwind v4, not a fork** — the supported set is
|
|
107
|
+
hand-curated in [`crates/aihu-css-core/src/tokens.rs`](crates/aihu-css-core/src/tokens.rs).
|
|
108
|
+
|
|
109
|
+
**Fixed long-tail utilities** (literal class names):
|
|
110
|
+
|
|
111
|
+
- **Display** — `block`, `inline-block`, `inline`, `flex`, `inline-flex`, `grid`, `inline-grid`, `hidden`, `contents`
|
|
112
|
+
- **Flexbox** — `flex-row`, `flex-col`, `flex-wrap`, `flex-nowrap`, `flex-1`, `flex-auto`, `flex-none`
|
|
113
|
+
- **Alignment** — `items-{start,center,end,stretch,baseline}`, `justify-{start,center,end,between,around,evenly}`
|
|
114
|
+
- **Position** — `static`, `relative`, `absolute`, `fixed`, `sticky`
|
|
115
|
+
- **Overflow** — `overflow-{hidden,auto,scroll,visible}`
|
|
116
|
+
- **Typography** — `text-{left,center,right,justify}`, `italic`, `not-italic`, `underline`, `line-through`, `no-underline`, `uppercase`, `lowercase`, `capitalize`, `truncate`
|
|
117
|
+
- **Font weight** — `font-{thin,normal,medium,semibold,bold,black}`
|
|
118
|
+
- **Borders / radius** — `border`, `rounded`, `rounded-{none,sm,md,lg,xl,2xl,full}`
|
|
119
|
+
- **Shadows** — `shadow`, `shadow-{sm,md,lg,none}`
|
|
120
|
+
- **Sizing keywords** — `w-{full,screen,auto}`, `h-{full,screen,auto}`
|
|
121
|
+
|
|
122
|
+
**Parameterized utilities** (`<prefix>-<value>`):
|
|
123
|
+
|
|
124
|
+
| Prefix family | Examples | Notes |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| Spacing — `p`, `px`, `py`, `pt`, `pr`, `pb`, `pl`, `m`, `mx`, `my`, `mt`, `mr`, `mb`, `ml`, `gap`, `gap-x`, `gap-y` | `p-4`, `mx-auto`, `gap-6` | value × `0.25rem` |
|
|
127
|
+
| Sizing — `w`, `h`, `min-w`, `max-w`, `min-h`, `max-h` | `w-64`, `max-w-7xl` | spacing scale or fractions |
|
|
128
|
+
| Font size — `text-{xs,sm,base,lg,xl,2xl,3xl,…}` | `text-lg` | includes paired line-height |
|
|
129
|
+
| z-index — `z-{N}` | `z-10`, `z-50` | numeric only |
|
|
130
|
+
| Opacity — `opacity-{N}` | `opacity-75` | 0–100 |
|
|
131
|
+
| Palette colors — `bg`, `text`, `border`, `fill`, `stroke`, `ring`, `outline` followed by a named keyword | `bg-red-500`, `text-slate-700` | |
|
|
132
|
+
|
|
133
|
+
**Arbitrary values** (`<prefix>-[<value>]` — emitted verbatim into the mapped
|
|
134
|
+
property): `bg-[#1a1d24]`, `w-[34ch]`, `text-[14px]`, `px-[2rem]`, etc.
|
|
135
|
+
Supported prefixes: `bg`, `text`, `w`, `h`, `min-w`, `max-w`, `min-h`, `max-h`,
|
|
136
|
+
`p`, `px`, `py`, `m`, `mx`, `my`, `gap`, `rounded`, `border`, `leading`,
|
|
137
|
+
`tracking`, `z`, `top`/`right`/`bottom`/`left`/`inset`, `fill`, `stroke`,
|
|
138
|
+
`shadow`. Underscores in the bracket become spaces (Tailwind convention).
|
|
139
|
+
|
|
140
|
+
**Brand color utilities** — `bg-primary`, `text-accent`, `border-muted`, etc.
|
|
141
|
+
resolve to `var(--color-*)` registered by a `@theme` block or a theme pack.
|
|
142
|
+
|
|
143
|
+
#### Not supported (common misses — call out to avoid surprises)
|
|
144
|
+
|
|
145
|
+
The vocabulary is intentionally narrower than full Tailwind. The following are
|
|
146
|
+
**not** in the table; use them and you'll get the class string in your HTML
|
|
147
|
+
with **no matching CSS rule** (the scanner returns `None` and emits nothing):
|
|
148
|
+
|
|
149
|
+
- `grid-cols-N` / `grid-rows-N` / `col-span-N` / `row-span-N` — **no grid template utilities**. Use `display: grid` via the `grid` class, then write a hand-authored `@style` block or an arbitrary `style="grid-template-columns:…"` attribute.
|
|
150
|
+
- `mx-auto` / `my-auto` — the spacing scale is `value × 0.25rem` and doesn't include the `auto` keyword. Use `style="margin-inline: auto"` or an arbitrary value (`mx-[auto]`) instead.
|
|
151
|
+
- `max-w-Nxl` / `max-w-screen` / etc. — `max-w` parameterized values follow the spacing/sizing scale, not Tailwind's named breakpoint scale. `max-w-[1280px]` (arbitrary value) works; `max-w-7xl` does not.
|
|
152
|
+
- Tailwind v4 colors *outside* the named keyword set (e.g. `text-zinc-200` — depends on whether `zinc` is in `named_keyword_color`)
|
|
153
|
+
- Responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`, `2xl:`) — **not yet handled** by the scanner
|
|
154
|
+
- State variants outside the WC-native set (`host:`, `slotted:`, `part-*:` are supported per the variants section above; arbitrary `hover:`, `focus:`, `dark:` etc. depend on what the current `parse_variant` accepts)
|
|
155
|
+
- Container queries, `@layer` ordering, JIT-style arbitrary properties (`[mask-image:url(...)]`)
|
|
156
|
+
|
|
157
|
+
**The authoritative source is [`crates/aihu-css-core/src/tokens.rs`](crates/aihu-css-core/src/tokens.rs)** — the lists above are a summary of that file's match arms and may drift; when in doubt, grep that file or write a one-liner test against `utility_to_css(class_name)`.
|
|
158
|
+
|
|
159
|
+
#### Production caveat — native binary must resolve
|
|
160
|
+
|
|
161
|
+
The scanner runs through the prebuilt `aihu-css-compile` Rust binary. For npm
|
|
162
|
+
consumers the binary ships as a per-platform `optionalDependencies` package
|
|
163
|
+
(e.g. `@aihu/css-engine-darwin-arm64`) and is auto-resolved by your package
|
|
164
|
+
manager. If resolution fails (offline install, unsupported platform), the
|
|
165
|
+
compiler emits a one-shot console warning, the build succeeds, and utility
|
|
166
|
+
rules are silently omitted from the bundle CSS. In monorepo dev clones the
|
|
167
|
+
fallback is `target/release/aihu-css-compile` (run `cargo build --release -p
|
|
168
|
+
aihu-css-core`).
|
|
65
169
|
|
|
66
170
|
See [`examples/css-engine-utility/`](../../examples/css-engine-utility) for a
|
|
67
|
-
minimal end-to-end demonstration, including
|
|
68
|
-
the built CSS for the expected `.flex
|
|
171
|
+
minimal end-to-end demonstration, including `scripts/check-utility-css.ts`
|
|
172
|
+
which greps the built CSS asset for the expected `.flex{display:flex}` rule —
|
|
173
|
+
a useful pattern to copy into your own project's CI.
|
|
69
174
|
|
|
70
175
|
### Local development
|
|
71
176
|
|
|
@@ -93,7 +198,7 @@ npm install @aihu/css-engine
|
|
|
93
198
|
bun add @aihu/css-engine
|
|
94
199
|
```
|
|
95
200
|
|
|
96
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
201
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.3.0`.</i></sub>
|
|
97
202
|
|
|
98
203
|
<!-- END_AUTOGEN: install -->
|
|
99
204
|
|
|
@@ -104,12 +209,12 @@ bun add @aihu/css-engine
|
|
|
104
209
|
|
|
105
210
|
| | |
|
|
106
211
|
|---|---|
|
|
107
|
-
| **Version** | `0.
|
|
212
|
+
| **Version** | `0.3.0` |
|
|
108
213
|
| **Tier** | D — Compiler — CSS engine (Tailwind v4 hard fork, WC-native scoped output) |
|
|
109
214
|
| **Published files** | 5 entries |
|
|
110
215
|
| **License** | MIT |
|
|
111
216
|
|
|
112
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
217
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.3.0`.</i></sub>
|
|
113
218
|
|
|
114
219
|
<!-- END_AUTOGEN: stats -->
|
|
115
220
|
|
|
@@ -128,7 +233,7 @@ bun add @aihu/css-engine
|
|
|
128
233
|
| `./runtime/cn` | `./dist/runtime/cn.js` | `—` |
|
|
129
234
|
| `./runtime/progressive` | `./dist/runtime/progressive.js` | `—` |
|
|
130
235
|
|
|
131
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
236
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.3.0`.</i></sub>
|
|
132
237
|
|
|
133
238
|
<!-- END_AUTOGEN: exports -->
|
|
134
239
|
|
|
@@ -139,7 +244,7 @@ bun add @aihu/css-engine
|
|
|
139
244
|
|
|
140
245
|
**Dependencies:**
|
|
141
246
|
|
|
142
|
-
- `@aihu/compiler` — `workspace
|
|
247
|
+
- `@aihu/compiler` — `workspace:^`
|
|
143
248
|
|
|
144
249
|
**Optional dependencies (platform-specific):**
|
|
145
250
|
|
|
@@ -148,7 +253,7 @@ bun add @aihu/css-engine
|
|
|
148
253
|
- `@aihu/css-engine-linux-x64-gnu` — `0.1.2`
|
|
149
254
|
- `@aihu/css-engine-win32-x64-msvc` — `0.1.2`
|
|
150
255
|
|
|
151
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
256
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.3.0`.</i></sub>
|
|
152
257
|
|
|
153
258
|
<!-- END_AUTOGEN: deps -->
|
|
154
259
|
|
|
@@ -161,7 +266,7 @@ bun add @aihu/css-engine
|
|
|
161
266
|
- [@aihu/compiler](../compiler)
|
|
162
267
|
- [Aihu framework root](../../README.md)
|
|
163
268
|
|
|
164
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
269
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.3.0`.</i></sub>
|
|
165
270
|
|
|
166
271
|
<!-- END_AUTOGEN: see-also -->
|
|
167
272
|
|
|
@@ -172,6 +277,6 @@ bun add @aihu/css-engine
|
|
|
172
277
|
|
|
173
278
|
MIT — see [LICENSE](../../LICENSE).
|
|
174
279
|
|
|
175
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
280
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.3.0`.</i></sub>
|
|
176
281
|
|
|
177
282
|
<!-- END_AUTOGEN: license -->
|
|
@@ -20,8 +20,8 @@ use crate::ast::{SfcAst, SfcStyleScope};
|
|
|
20
20
|
use crate::progressive::ProgressiveRegistry;
|
|
21
21
|
use crate::scanner::{scan, ScanResult};
|
|
22
22
|
use crate::theme::{extract_theme_blocks, ThemeRegistry};
|
|
23
|
-
use crate::tokens::utility_to_css;
|
|
24
|
-
use crate::variants::{split_variants, Variant};
|
|
23
|
+
use crate::tokens::{animation_keyframes, utility_to_css};
|
|
24
|
+
use crate::variants::{split_variants, AttrMatch, Variant};
|
|
25
25
|
|
|
26
26
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
27
27
|
pub enum OutputMode {
|
|
@@ -33,7 +33,10 @@ pub enum OutputMode {
|
|
|
33
33
|
fn escape_class(class: &str) -> String {
|
|
34
34
|
let mut out = String::with_capacity(class.len() + 4);
|
|
35
35
|
for c in class.chars() {
|
|
36
|
-
if matches!(
|
|
36
|
+
if matches!(
|
|
37
|
+
c,
|
|
38
|
+
'[' | ']' | '#' | '(' | ')' | '.' | '%' | '/' | ':' | ',' | '@' | '=' | '"'
|
|
39
|
+
) {
|
|
37
40
|
out.push('\\');
|
|
38
41
|
}
|
|
39
42
|
out.push(c);
|
|
@@ -71,7 +74,11 @@ fn emit_token(token: &str, theme: &ThemeRegistry, prog: &ProgressiveRegistry) ->
|
|
|
71
74
|
|
|
72
75
|
// The base (innermost) selector and declaration body.
|
|
73
76
|
let mut selector = class_sel;
|
|
74
|
-
|
|
77
|
+
// Wrapping at-rule (e.g. `@media (min-width: …)` for breakpoints or
|
|
78
|
+
// `@container (min-width: …)` for container queries). Generalized from the
|
|
79
|
+
// old `media: Option<String>` slot so both `@media` and `@container` wrap
|
|
80
|
+
// the rule uniformly: `<at-rule> { <rule> }`.
|
|
81
|
+
let mut at_rule: Option<String> = None;
|
|
75
82
|
let mut dark_cascade = false;
|
|
76
83
|
|
|
77
84
|
for v in &variants {
|
|
@@ -85,9 +92,43 @@ fn emit_token(token: &str, theme: &ThemeRegistry, prog: &ProgressiveRegistry) ->
|
|
|
85
92
|
// `[&>div]:` → substitute `&` for the base selector.
|
|
86
93
|
selector = sel.replace('&', &selector);
|
|
87
94
|
}
|
|
95
|
+
Variant::Group(Some(state)) => {
|
|
96
|
+
// `group-hover:bg-x` → `.group:hover .group-hover\:bg-x`.
|
|
97
|
+
// Prepend a descendant-combinator ancestor selector: the rule
|
|
98
|
+
// applies to the element bearing this class when an ancestor
|
|
99
|
+
// marked `class="group"` is in `:<state>`. Within a shadow root
|
|
100
|
+
// both the marker and the styled element live in the same tree,
|
|
101
|
+
// so the class selectors match per spec §6.3 scoping.
|
|
102
|
+
selector = format!(".group:{state} {selector}");
|
|
103
|
+
}
|
|
104
|
+
Variant::Peer(Some(state)) => {
|
|
105
|
+
// `peer-checked:bg-x` → `.peer:checked ~ .peer-checked\:bg-x`.
|
|
106
|
+
// Prepend a subsequent-sibling-combinator selector: the rule
|
|
107
|
+
// applies when a PRIOR sibling marked `class="peer"` is in
|
|
108
|
+
// `:<state>`. CSS can only look backward to earlier siblings,
|
|
109
|
+
// so `peer` must appear before the styled element in source.
|
|
110
|
+
selector = format!(".peer:{state} ~ {selector}");
|
|
111
|
+
}
|
|
112
|
+
// Bare `group`/`peer` never reach here (they are marker utilities,
|
|
113
|
+
// not variant prefixes); a `None` state is unreachable but handled
|
|
114
|
+
// defensively as a no-op so the base selector is emitted unchanged.
|
|
115
|
+
Variant::Group(None) | Variant::Peer(None) => {}
|
|
116
|
+
// aria-*/data-* attribute variants compile to an attribute selector
|
|
117
|
+
// appended to the base: `aria-checked:` → `.cls[aria-checked="true"]`,
|
|
118
|
+
// `data-[state=open]:` → `.cls[data-state="open"]`. A keyword data-*
|
|
119
|
+
// (`data-active:`) emits a presence selector `[data-active]`.
|
|
120
|
+
Variant::Aria(m) => selector = format!("{selector}{}", attr_selector("aria", m)),
|
|
121
|
+
Variant::Data(m) => selector = format!("{selector}{}", attr_selector("data", m)),
|
|
88
122
|
Variant::Breakpoint(bp) => {
|
|
89
123
|
if let Some(min) = theme.breakpoint(bp) {
|
|
90
|
-
|
|
124
|
+
at_rule = Some(format!("@media (min-width: {min})"));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Container queries wrap the rule in an `@container` at-rule keyed on
|
|
128
|
+
// the container breakpoint scale (mirrors `breakpoint()`).
|
|
129
|
+
Variant::Container(bp) => {
|
|
130
|
+
if let Some(min) = theme.container_breakpoint(bp) {
|
|
131
|
+
at_rule = Some(format!("@container (min-width: {min})"));
|
|
91
132
|
}
|
|
92
133
|
}
|
|
93
134
|
Variant::Dark | Variant::HostContextDark => {
|
|
@@ -112,12 +153,41 @@ fn emit_token(token: &str, theme: &ThemeRegistry, prog: &ProgressiveRegistry) ->
|
|
|
112
153
|
format!("{selector} {{ {body} }}\n")
|
|
113
154
|
};
|
|
114
155
|
|
|
115
|
-
|
|
116
|
-
Some(
|
|
156
|
+
let rule = match at_rule {
|
|
157
|
+
Some(at) => format!("{at} {{\n{rule}}}\n"),
|
|
158
|
+
None => rule,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Hoist the @keyframes an `animate-*` utility depends on as a top-level
|
|
162
|
+
// sibling rule (it cannot live nested inside the selector body). Re-emitting
|
|
163
|
+
// an identical block is idempotent in CSS, so per-occurrence emission is
|
|
164
|
+
// safe. `base` is the variant-stripped class (e.g. `animate-spin`).
|
|
165
|
+
Some(match animation_keyframes(&base) {
|
|
166
|
+
Some(kf) => format!("{rule}{kf}\n"),
|
|
117
167
|
None => rule,
|
|
118
168
|
})
|
|
119
169
|
}
|
|
120
170
|
|
|
171
|
+
/// Build an attribute-selector fragment for an `aria-*`/`data-*` variant.
|
|
172
|
+
///
|
|
173
|
+
/// `attr_selector("aria", Name{checked, true})` → `[aria-checked="true"]`;
|
|
174
|
+
/// `attr_selector("data", NameValue{state, open})` → `[data-state="open"]`;
|
|
175
|
+
/// `attr_selector("data", Name{active, false})` → `[data-active]` (presence).
|
|
176
|
+
fn attr_selector(family: &str, m: &AttrMatch) -> String {
|
|
177
|
+
match m {
|
|
178
|
+
AttrMatch::Name { name, imply_true } => {
|
|
179
|
+
if *imply_true {
|
|
180
|
+
format!("[{family}-{name}=\"true\"]")
|
|
181
|
+
} else {
|
|
182
|
+
format!("[{family}-{name}]")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
AttrMatch::NameValue { name, value } => {
|
|
186
|
+
format!("[{family}-{name}=\"{value}\"]")
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
121
191
|
/// Emit CSS for a scanned utility set in the given mode.
|
|
122
192
|
pub fn emit(result: &ScanResult, theme: &ThemeRegistry, mode: OutputMode) -> String {
|
|
123
193
|
emit_with_progressive(result, theme, &ProgressiveRegistry::with_builtins(), mode)
|
|
@@ -138,6 +208,10 @@ pub fn emit_with_progressive(
|
|
|
138
208
|
// Flat back-compat: only plain utilities, no variant wrapping.
|
|
139
209
|
if let Some(body) = utility_to_css(token) {
|
|
140
210
|
out.push_str(&format!(".{token} {{ {body} }}\n"));
|
|
211
|
+
if let Some(kf) = animation_keyframes(token) {
|
|
212
|
+
out.push_str(kf);
|
|
213
|
+
out.push('\n');
|
|
214
|
+
}
|
|
141
215
|
}
|
|
142
216
|
}
|
|
143
217
|
OutputMode::Scoped => {
|
|
@@ -172,7 +246,12 @@ pub fn emit_sfc_scoped(ast: &SfcAst) -> String {
|
|
|
172
246
|
out.push_str(&theme.emit_host_tokens());
|
|
173
247
|
|
|
174
248
|
// 2. Scanned utility rules (scoped) — progressive prefixes routed via `prog`.
|
|
175
|
-
out.push_str(&emit_with_progressive(
|
|
249
|
+
out.push_str(&emit_with_progressive(
|
|
250
|
+
&result,
|
|
251
|
+
&theme,
|
|
252
|
+
&prog,
|
|
253
|
+
OutputMode::Scoped,
|
|
254
|
+
));
|
|
176
255
|
|
|
177
256
|
// 3. Fold the authored @style block (minus @theme directives).
|
|
178
257
|
if let Some(style) = &ast.style {
|
|
@@ -42,6 +42,11 @@ pub fn compile_classes(classes: &[String]) -> String {
|
|
|
42
42
|
output.push_str(" { ");
|
|
43
43
|
output.push_str(&body);
|
|
44
44
|
output.push_str(" }\n");
|
|
45
|
+
// Hoist the matching @keyframes as a sibling rule (animate-* only).
|
|
46
|
+
if let Some(kf) = tokens::animation_keyframes(class) {
|
|
47
|
+
output.push_str(kf);
|
|
48
|
+
output.push('\n');
|
|
49
|
+
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
output
|
|
@@ -70,6 +70,20 @@ impl ThemeRegistry {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/// Resolve a container-query breakpoint (`@sm`/`@md`/…) to its min-width.
|
|
74
|
+
/// Mirrors [`breakpoint`] but uses Tailwind's container-query scale (which
|
|
75
|
+
/// differs from the viewport breakpoint scale): `@sm`=24rem … `@2xl`=42rem.
|
|
76
|
+
pub fn container_breakpoint(&self, name: &str) -> Option<&'static str> {
|
|
77
|
+
match name {
|
|
78
|
+
"sm" => Some("24rem"),
|
|
79
|
+
"md" => Some("28rem"),
|
|
80
|
+
"lg" => Some("32rem"),
|
|
81
|
+
"xl" => Some("36rem"),
|
|
82
|
+
"2xl" => Some("42rem"),
|
|
83
|
+
_ => None,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
/// Merge an `@theme { ... }` block's tokens over the current registry.
|
|
74
88
|
/// Returns the number of tokens registered/overridden.
|
|
75
89
|
pub fn apply_theme_block(&mut self, theme_body: &str) -> usize {
|