@mochi-css/vanilla 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,16 @@
1
- # Mochi-CSS/vanilla
1
+ # 🧁 Mochi-CSS/vanilla
2
2
 
3
3
  This package is part of the [Mochi-CSS project](https://github.com/Niikelion/mochi-css).
4
4
  It provides type-safe CSS-in-JS styling functions with static extraction support, allowing you to write styles in TypeScript that get extracted to plain CSS at build time.
5
5
 
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm i @mochi-css/vanilla
10
+ ```
11
+
12
+ ---
13
+
6
14
  ## Functions
7
15
 
8
16
  ### `css(...styles)`
@@ -35,6 +43,20 @@ const buttonStyles = css(textStyles, {
35
43
  })
36
44
  ```
37
45
 
46
+ ### `globalCss(styles)`
47
+
48
+ `globalCss` injects styles into the global scope — they are not scoped to any class and apply to all matching elements. Use it for resets, base typography, or any styles that must target plain HTML elements.
49
+
50
+ ```ts
51
+ import { globalCss } from "@mochi-css/vanilla"
52
+
53
+ globalCss({
54
+ "*, *::before, *::after": { boxSizing: "border-box" },
55
+ body: { margin: 0, fontFamily: "sans-serif" },
56
+ "h1, h2, h3": { lineHeight: 1.2 },
57
+ })
58
+ ```
59
+
38
60
  ### `styled(component, ...styles)`
39
61
 
40
62
  `styled` creates a styled component by combining a base element or component with style definitions. It automatically applies the generated class names and forwards variant props.
@@ -48,6 +70,52 @@ const Button = styled("button", {
48
70
  })
49
71
  ```
50
72
 
73
+ ### `keyframes(stops)`
74
+
75
+ The `keyframes` function lets you define CSS animations using the same type-safe syntax as style definitions.
76
+
77
+ Define animation stops using `from`/`to` or percentage keys:
78
+
79
+ ```ts
80
+ import { keyframes, css } from "@mochi-css/vanilla"
81
+
82
+ const fadeIn = keyframes({
83
+ from: { opacity: 0 },
84
+ to: { opacity: 1 }
85
+ })
86
+
87
+ const fadeInStyle = css({
88
+ animation: `${fadeIn} 0.3s ease`
89
+ })
90
+ ```
91
+
92
+ For more control over animation timing, use percentage-based stops:
93
+
94
+ ```ts
95
+ const bounce = keyframes({
96
+ "0%": { transform: "translateY(0)" },
97
+ "50%": { transform: "translateY(-20px)" },
98
+ "100%": { transform: "translateY(0)" }
99
+ })
100
+ ```
101
+
102
+ Each stop can contain multiple CSS properties with auto-units:
103
+
104
+ ```ts
105
+ const grow = keyframes({
106
+ from: {
107
+ opacity: 0,
108
+ transform: "scale(0.5)",
109
+ fontSize: 12 // becomes 12px
110
+ },
111
+ to: {
112
+ opacity: 1,
113
+ transform: "scale(1)",
114
+ fontSize: 24 // becomes 24px
115
+ }
116
+ })
117
+ ```
118
+
51
119
  ## Style definitions
52
120
 
53
121
  A style definition is either a bundle of styles returned by `css`, or an object containing:
@@ -132,76 +200,119 @@ const linkStyle = css({
132
200
 
133
201
  ## Media Selectors
134
202
 
135
- Media selectors allow you to apply styles conditionally based on viewport size, color scheme preferences, and other media features. Media selectors start with the `@` symbol and are compiled into CSS media queries:
203
+ Mochi-CSS supports `@media`, `@container`, `@supports`, and `@layer` at-rules as style object keys.
204
+ You can write them as plain strings or use the typed helper functions exported from the package.
205
+
206
+ ### `media` helper
136
207
 
137
208
  ```ts
138
- import { css } from "@mochi-css/vanilla"
209
+ import { css, media } from "@mochi-css/vanilla"
139
210
 
140
211
  const responsiveContainer = css({
141
212
  display: "flex",
142
213
  flexDirection: "row",
143
214
  padding: 32,
144
215
 
145
- // Responsive breakpoints
146
- "@max-width: 768px": {
216
+ // media(condition) — wraps condition in parens automatically
217
+ [media("max-width: 768px")]: {
147
218
  flexDirection: "column",
148
219
  padding: 16
149
220
  },
150
221
 
151
- "@max-width: 480px": {
152
- padding: 8
153
- },
154
-
155
222
  // Modern range syntax
156
- "@width <= 1024px": {
223
+ [media("width <= 1024px")]: {
157
224
  gap: 16
158
225
  },
159
226
 
160
- // Color scheme preferences
161
- "@prefers-color-scheme: dark": {
227
+ // Shorthand properties for common queries
228
+ [media.dark]: {
162
229
  backgroundColor: "#1a1a1a",
163
230
  color: "white"
164
231
  },
165
232
 
166
- // Reduced motion
167
- "@prefers-reduced-motion: reduce": {
233
+ [media.motion]: {
168
234
  transition: "none"
235
+ },
236
+
237
+ [media.print]: {
238
+ display: "block"
169
239
  }
170
240
  })
171
241
  ```
172
242
 
173
- ### Combined Media Selectors
174
-
175
- You can combine multiple media conditions using `and` and `not` operators:
243
+ Combine conditions with `media.and` / `media.or`:
176
244
 
177
245
  ```ts
178
- const responsiveLayout = css({
246
+ const layout = css({
179
247
  display: "grid",
180
248
  gridTemplateColumns: "1fr",
181
249
 
182
- // Screen media type with width condition
183
- "@screen and (width > 1000px)": {
184
- gridTemplateColumns: "1fr 1fr"
185
- },
186
-
187
- // Multiple conditions with 'and'
188
- "@screen and (min-width: 768px) and (max-width: 1024px)": {
250
+ [media.and("min-width: 768px", "max-width: 1024px")]: {
189
251
  gridTemplateColumns: "1fr 1fr",
190
252
  gap: 16
191
253
  },
192
254
 
193
- // Print styles
194
- "@print": {
255
+ [media.or("max-width: 480px", "print")]: {
195
256
  display: "block"
257
+ }
258
+ })
259
+ ```
260
+
261
+ ### `container` helper
262
+
263
+ ```ts
264
+ import { css, container } from "@mochi-css/vanilla"
265
+
266
+ const card = css({
267
+ fontSize: 16,
268
+
269
+ // Anonymous container query
270
+ [container("min-width: 400px")]: {
271
+ fontSize: 20
196
272
  },
197
273
 
198
- // Combining media type with preference
199
- "@screen and (not (prefers-color-scheme: dark))": {
200
- backgroundColor: "#121212"
274
+ // Named container query
275
+ [container.named("sidebar", "min-width: 200px")]: {
276
+ display: "none"
201
277
  }
202
278
  })
203
279
  ```
204
280
 
281
+ ### `supports` helper
282
+
283
+ ```ts
284
+ import { css, supports } from "@mochi-css/vanilla"
285
+
286
+ const grid = css({
287
+ display: "flex",
288
+
289
+ [supports("display: grid")]: {
290
+ display: "grid"
291
+ },
292
+
293
+ [supports.not("display: grid")]: {
294
+ display: "flex"
295
+ },
296
+
297
+ [supports.and("display: grid", "gap: 1px")]: {
298
+ gap: 16
299
+ }
300
+ })
301
+ ```
302
+
303
+ ### Raw strings
304
+
305
+ You can also use raw at-rule strings directly without the helpers:
306
+
307
+ ```ts
308
+ const styles = css({
309
+ "@media (max-width: 768px)": { padding: 8 },
310
+ "@container (min-width: 300px)": { fontSize: 20 },
311
+ "@supports (display: grid)": { display: "grid" },
312
+ "@layer utilities": { color: "red" }
313
+ })
314
+ ```
315
+
205
316
  ### Combining Nested Selectors with Media Selectors
206
317
 
207
318
  Nested selectors and media selectors can be combined for fine-grained control:
@@ -214,14 +325,13 @@ const buttonStyle = css({
214
325
  backgroundColor: "darkblue",
215
326
 
216
327
  // Media selector inside nested selector
217
- "@width <= 480px": {
218
- // Disable hover effects on mobile (touch devices)
328
+ [media("max-width: 480px")]: {
219
329
  backgroundColor: "blue"
220
330
  }
221
331
  },
222
332
 
223
333
  // Media selector with nested selectors inside
224
- "@max-width: 768px": {
334
+ [media("max-width: 768px")]: {
225
335
  padding: 8,
226
336
 
227
337
  "& > span": {
@@ -244,13 +354,13 @@ const cardStyle = css({
244
354
  size: {
245
355
  small: {
246
356
  padding: 8,
247
- "@max-width: 480px": {
357
+ [media("max-width: 480px")]: {
248
358
  padding: 4
249
359
  }
250
360
  },
251
361
  large: {
252
362
  padding: 32,
253
- "@max-width: 480px": {
363
+ [media("max-width: 480px")]: {
254
364
  padding: 16
255
365
  }
256
366
  }
@@ -360,62 +470,3 @@ Tokens can be used in two ways:
360
470
  - **As values**: Use the token directly (e.g., `backgroundColor: buttonColor`) to reference the CSS variable
361
471
  - **As keys**: Use `token.variable` (e.g., `[buttonColor.variable]: primaryColor`) to assign a value to the CSS variable
362
472
 
363
- ## Keyframes
364
-
365
- The `keyframes` function lets you define CSS animations using the same type-safe syntax as style definitions.
366
-
367
- ### Basic Usage
368
-
369
- Define animation stops using `from`/`to` or percentage keys:
370
-
371
- ```ts
372
- import { keyframes, css } from "@mochi-css/vanilla"
373
-
374
- const fadeIn = keyframes({
375
- from: { opacity: 0 },
376
- to: { opacity: 1 }
377
- })
378
-
379
- const fadeInStyle = css({
380
- animation: `${fadeIn} 0.3s ease`
381
- })
382
- ```
383
-
384
- ### Percentage Stops
385
-
386
- For more control over animation timing, use percentage-based stops:
387
-
388
- ```ts
389
- import { keyframes, css } from "@mochi-css/vanilla"
390
-
391
- const bounce = keyframes({
392
- "0%": { transform: "translateY(0)" },
393
- "50%": { transform: "translateY(-20px)" },
394
- "100%": { transform: "translateY(0)" }
395
- })
396
-
397
- const bouncingElement = css({
398
- animation: `${bounce} 1s ease-in-out infinite`
399
- })
400
- ```
401
-
402
- ### Multiple Properties
403
-
404
- Each stop can contain multiple CSS properties with auto-units:
405
-
406
- ```ts
407
- import { keyframes } from "@mochi-css/vanilla"
408
-
409
- const grow = keyframes({
410
- from: {
411
- opacity: 0,
412
- transform: "scale(0.5)",
413
- fontSize: 12 // becomes 12px
414
- },
415
- to: {
416
- opacity: 1,
417
- transform: "scale(1)",
418
- fontSize: 24 // becomes 24px
419
- }
420
- })
421
- ```
package/dist/index.d.mts CHANGED
@@ -55,38 +55,33 @@ declare function createToken(name: string): Token;
55
55
  //#region src/selector.d.ts
56
56
  /**
57
57
  * CSS selector building and manipulation utilities.
58
- * Handles nested selectors (using `&` placeholder) and media queries.
58
+ * Handles nested selectors (using `&` placeholder) and CSS at-rules.
59
59
  * @module selector
60
60
  */
61
61
  /**
62
- * Immutable CSS selector builder that handles nested selectors and media queries.
62
+ * Immutable CSS selector builder that handles nested selectors and CSS at-rules.
63
63
  * Uses the `&` character as a placeholder for parent selector substitution.
64
64
  *
65
65
  * @example
66
66
  * const selector = new MochiSelector(['.button'])
67
67
  * selector.extend('&:hover').cssSelector // '.button:hover'
68
- * selector.wrap('@media (min-width: 768px)').mediaQuery // '@media (min-width: 768px)'
68
+ * selector.wrap('@media (min-width: 768px)').atRules // ['@media (min-width: 768px)']
69
69
  */
70
70
  declare class MochiSelector {
71
71
  private readonly cssSelectors;
72
- private readonly mediaSelectors;
72
+ readonly atRules: string[];
73
73
  /**
74
74
  * Creates a new MochiSelector instance.
75
75
  * @param cssSelectors - Array of CSS selectors (may contain `&` placeholders)
76
- * @param mediaSelectors - Array of media query conditions (without `@media` prefix)
76
+ * @param atRules - Array of full CSS at-rule strings e.g. `"@media (min-width: 768px)"`
77
77
  */
78
- constructor(cssSelectors?: string[], mediaSelectors?: string[]);
78
+ constructor(cssSelectors?: string[], atRules?: string[]);
79
79
  /**
80
80
  * Gets the combined CSS selector string.
81
81
  * Multiple selectors are joined with commas.
82
82
  * @returns The CSS selector, or "*" if no selectors are defined
83
83
  */
84
84
  get cssSelector(): string;
85
- /**
86
- * Gets the combined media query string, if any.
87
- * @returns The full `@media` query string, or undefined if no media conditions
88
- */
89
- get mediaQuery(): string | undefined;
90
85
  /**
91
86
  * Substitutes all `&` placeholders with the given root selector.
92
87
  * @param root - The selector to replace `&` with
@@ -104,13 +99,14 @@ declare class MochiSelector {
104
99
  */
105
100
  extend(child: string): MochiSelector;
106
101
  /**
107
- * Wraps this selector with a media query condition.
108
- * @param mediaQuery - The media query string (starting with `@`)
109
- * @returns A new MochiSelector with the added media condition
102
+ * Wraps this selector with a CSS at-rule.
103
+ * @param atRule - The full at-rule string (e.g. `"@media (min-width: 768px)"`)
104
+ * @returns A new MochiSelector with the added at-rule, or unchanged if not a known at-rule
110
105
  * @example
111
- * selector.wrap('@min-width: 768px') // Adds media query condition
106
+ * selector.wrap('@media (min-width: 768px)') // Adds media query
107
+ * selector.wrap('@container sidebar (min-width: 300px)') // Adds container query
112
108
  */
113
- wrap(mediaQuery: string): MochiSelector;
109
+ wrap(atRule: string): MochiSelector;
114
110
  /**
115
111
  * Splits a comma-separated selector string into individual selectors.
116
112
  * @param selector - The selector string to split
@@ -455,11 +451,15 @@ type PropsWithUnit = PropertyWithUnit & keyof Props;
455
451
  * Converts a kebab-case string to camelCase.
456
452
  */
457
453
 
454
+ /**
455
+ * Checks if a key represents a CSS at-rule (media, container, supports, layer).
456
+ */
457
+ declare function isAtRuleKey(key: string): key is AtRuleKey;
458
458
  /** A nested CSS selector pattern containing the parent reference `&` */
459
459
  type NestedCssSelector = `${string}&${string}`;
460
- /** A CSS media query starting with `@` */
461
- type MediaSelector = `@${string}`;
462
- type NestedStyleKeys = MediaSelector | NestedCssSelector;
460
+ /** A CSS at-rule key for media, container, supports, or layer queries */
461
+ type AtRuleKey = `@media ${string}` | `@container ${string}` | `@supports ${string}` | `@layer ${string}`;
462
+ type NestedStyleKeys = AtRuleKey | NestedCssSelector;
463
463
  /**
464
464
  * Style properties without nesting support.
465
465
  * Includes all standard CSS properties with type-safe value converters,
@@ -468,7 +468,7 @@ type NestedStyleKeys = MediaSelector | NestedCssSelector;
468
468
  * Properties with known units (e.g., width, height, padding) accept numbers
469
469
  * that are automatically converted with their default unit (e.g., px, ms).
470
470
  */
471
- type SimpleStyleProps = { [K in PropsWithUnit]?: CssLike<number | Props[K]> } & { [K in Exclude<keyof Props, PropsWithUnit>]?: CssLike<Props[K]> } & Partial<Record<CssVar, CssLike<string | number>>>;
471
+ type SimpleStyleProps = { [K in PropsWithUnit]?: CssLike<number | Props[K]> } & { [K in Exclude<keyof Props, PropsWithUnit>]?: CssLike<Props[K]> };
472
472
  /**
473
473
  * Full style properties type with support for nested selectors and media queries.
474
474
  * Extends SimpleStyleProps to allow recursive style definitions.
@@ -478,10 +478,10 @@ type SimpleStyleProps = { [K in PropsWithUnit]?: CssLike<number | Props[K]> } &
478
478
  * color: 'blue',
479
479
  * padding: 16,
480
480
  * '&:hover': { color: 'red' },
481
- * '@min-width: 768px': { padding: 24 }
481
+ * '@media (min-width: 768px)': { padding: 24 }
482
482
  * }
483
483
  */
484
- type StyleProps = SimpleStyleProps & { [K in NestedStyleKeys]?: StyleProps | CssLike<string | number> };
484
+ type StyleProps = SimpleStyleProps & { [K in NestedStyleKeys]?: StyleProps | CssLike<string | number> } & Record<string, unknown>;
485
485
  /**
486
486
  * Converts a SimpleStyleProps object to a CSS properties record.
487
487
  * Transforms camelCase property names to kebab-case and applies value converters.
@@ -491,7 +491,7 @@ type StyleProps = SimpleStyleProps & { [K in NestedStyleKeys]?: StyleProps | Css
491
491
  * cssFromProps({ backgroundColor: 'blue', padding: 16 })
492
492
  * // { 'background-color': 'blue', 'padding': '16px' }
493
493
  */
494
- declare function cssFromProps(props: SimpleStyleProps): Record<string, string>;
494
+ declare function cssFromProps(props: SimpleStyleProps & Partial<Record<CssVar, CssLike<number | string>>>): Record<string, string>;
495
495
  //#endregion
496
496
  //#region src/cssObject.d.ts
497
497
  /**
@@ -514,7 +514,8 @@ declare class CssObjectSubBlock {
514
514
  get hash(): string;
515
515
  /**
516
516
  * Converts this block to a CSS string.
517
- * Handles media query wrapping if the selector has media conditions.
517
+ * Handles at-rule wrapping (media, container, supports, layer) if present.
518
+ * Multiple at-rules are nested in order.
518
519
  * @param root - The root selector to substitute for `&`
519
520
  * @returns Formatted CSS string
520
521
  */
@@ -589,7 +590,7 @@ type VariantProps<V extends AllVariants> = {
589
590
  compoundVariants?: CompoundVariant<V>[];
590
591
  };
591
592
  /** Combined type for style props with optional variants */
592
- type MochiCSSProps<V extends AllVariants> = StyleProps & VariantProps<V>;
593
+ type MochiCSSProps<V extends AllVariants> = Omit<StyleProps, "variants" | "compoundVariants" | "defaultVariants"> & VariantProps<V>;
593
594
  /** Utility type to override properties of A with properties of B */
594
595
  type Override<A extends object, B extends object> = B & Omit<A, keyof B>;
595
596
  /** Recursively merges variant types from a tuple, with later types overriding earlier */
@@ -792,5 +793,44 @@ declare class GlobalCssObject {
792
793
  */
793
794
  declare function globalCss(styles: GlobalCssStyles): void;
794
795
  //#endregion
795
- export { AllVariants, CSSObject, CompoundVariant, CssLike, CssLikeObject, CssObjectBlock, CssObjectSubBlock, CssVar, CssVarVal, DefaultVariants, GlobalCssObject, type GlobalCssStyles, type KeyframeStops, KeyframesObject, MergeCSSVariants, MochiCSS, MochiCSSProps, MochiKeyframes, MochiSelector, RefineVariants, type StyleProps, Token, VariantProps, createToken, css, cssFromProps, globalCss, keyframes, mergeMochiCss, styled };
796
- //# sourceMappingURL=index.d.mts.map
796
+ //#region src/query.d.ts
797
+ interface MediaHelper {
798
+ /** `@media (condition)` */
799
+ (condition: string): AtRuleKey & `@media ${string}`;
800
+ /** `@media (a) and (b) and …` */
801
+ and(...conditions: [string, string, ...string[]]): AtRuleKey & `@media ${string}`;
802
+ /** `@media (a), (b), …` */
803
+ or(...conditions: [string, string, ...string[]]): AtRuleKey & `@media ${string}`;
804
+ /** `@media (prefers-color-scheme: dark)` */
805
+ readonly dark: AtRuleKey & `@media ${string}`;
806
+ /** `@media (prefers-color-scheme: light)` */
807
+ readonly light: AtRuleKey & `@media ${string}`;
808
+ /** `@media (prefers-reduced-motion: no-preference)` */
809
+ readonly motion: AtRuleKey & `@media ${string}`;
810
+ /** `@media print` */
811
+ readonly print: AtRuleKey & `@media ${string}`;
812
+ }
813
+ /** Helper for constructing `@media` at-rule keys. */
814
+ declare const media: MediaHelper;
815
+ interface ContainerHelper {
816
+ /** `@container (condition)` — anonymous container */
817
+ (condition: string): AtRuleKey & `@container ${string}`;
818
+ /** `@container name (condition)` — named container */
819
+ named(name: string, condition: string): AtRuleKey & `@container ${string}`;
820
+ }
821
+ /** Helper for constructing `@container` at-rule keys. */
822
+ declare const container: ContainerHelper;
823
+ interface SupportsHelper {
824
+ /** `@supports (declaration)` */
825
+ (condition: string): AtRuleKey & `@supports ${string}`;
826
+ /** `@supports not (declaration)` */
827
+ not(condition: string): AtRuleKey & `@supports ${string}`;
828
+ /** `@supports (a) and (b) and …` */
829
+ and(...conditions: [string, string, ...string[]]): AtRuleKey & `@supports ${string}`;
830
+ /** `@supports (a) or (b) or …` */
831
+ or(...conditions: [string, string, ...string[]]): AtRuleKey & `@supports ${string}`;
832
+ }
833
+ /** Helper for constructing `@supports` at-rule keys. */
834
+ declare const supports: SupportsHelper;
835
+ //#endregion
836
+ export { AllVariants, type AtRuleKey, CSSObject, CompoundVariant, CssLike, CssLikeObject, CssObjectBlock, CssObjectSubBlock, CssVar, CssVarVal, DefaultVariants, GlobalCssObject, type GlobalCssStyles, type KeyframeStops, KeyframesObject, MergeCSSVariants, MochiCSS, MochiCSSProps, MochiKeyframes, MochiSelector, RefineVariants, type StyleProps, Token, VariantProps, container, createToken, css, cssFromProps, globalCss, isAtRuleKey, keyframes, media, mergeMochiCss, styled, supports };