@hulla/style 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,541 @@
1
+ # @hulla/style
2
+
3
+ > **Styling made easy** 🎨
4
+ > A unified, type-safe styling library that works with any CSS framework or methodology.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/@hulla/style.svg)](https://www.npmjs.com/package/@hulla/style)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## Why @hulla/style?
10
+
11
+ **@hulla/style** is a tiny (~1KB), zero-dependency library that unifies class name composition with powerful variant management. Unlike other solutions, it works consistently across any composer (clsx, tailwind-merge, etc.) and provides first-class TypeScript support.
12
+
13
+ ### The Problem
14
+
15
+ When building component libraries, you often need to:
16
+ - Compose class names conditionally
17
+ - Define component variants (sizes, colors, states)
18
+ - Combine multiple variants together
19
+ - Use different CSS frameworks (Tailwind, vanilla CSS, CSS modules)
20
+ - Ensure type safety for all variants
21
+
22
+ Most libraries solve only part of this puzzle, forcing you to combine multiple tools or compromise on features.
23
+
24
+ ### The Solution
25
+
26
+ **@hulla/style** provides a unified API that:
27
+ - ✅ **Works with any composer** - Use with clsx, tailwind-merge, or vanilla strings
28
+ - ✅ **Handles complex types** - Objects, arrays, nested structures work everywhere
29
+ - ✅ **Type-safe variants** - Get autocomplete and type checking for all variants
30
+ - ✅ **Composable architecture** - Mix variants, groups, and raw strings seamlessly
31
+ - ✅ **Framework agnostic** - Works with React, Vue, Astro, Svelte, or plain HTML
32
+ - ✅ **Zero dependencies** - Tiny bundle size, no external deps required
33
+ - ✅ **Extensible** - Customize serialization and composition behavior
34
+
35
+ ## Comparison with Alternatives
36
+
37
+ | Feature | @hulla/style | clsx/classnames | cva | tailwind-variants |
38
+ |---------|--------------|-----------------|-----|-------------------|
39
+ | Class composition | ✅ | ✅ | ❌ | ❌ |
40
+ | Variant management | ✅ | ❌ | ✅ | ✅ |
41
+ | Variant groups | ✅ | ❌ | ❌ | Limited |
42
+ | Object syntax support | ✅ Everywhere | ✅ Only cn | ❌ | ❌ |
43
+ | Works with any composer | ✅ | N/A | ❌ tw only | ❌ tw only |
44
+ | Customizable serialization | ✅ | ❌ | ❌ | ❌ |
45
+ | Bundle size | ~1KB | ~1KB | ~2.5KB | ~5KB |
46
+ | TypeScript support | ✅ Full | Partial | ✅ Full | ✅ Full |
47
+ | Framework agnostic | ✅ | ✅ | ✅ | ❌ React only |
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ npm install @hulla/style
53
+ # or
54
+ pnpm add @hulla/style
55
+ # or
56
+ yarn add @hulla/style
57
+ # or
58
+ bun add @hulla/style
59
+ ```
60
+
61
+ ## Quick Start
62
+
63
+ ```typescript
64
+ import { style, type VariantProps } from '@hulla/style'
65
+
66
+ // Create your style utilities
67
+ const { cn, variant, variantGroup } = style()
68
+
69
+ // Use cn for simple class composition
70
+ const buttonClass = cn('px-4 py-2', 'rounded', 'bg-blue-500')
71
+ // => "px-4 py-2 rounded bg-blue-500"
72
+
73
+ // Define variants for reusable component styles
74
+ const button = variant({
75
+ name: 'variant',
76
+ classes: {
77
+ primary: 'bg-blue-500 text-white',
78
+ secondary: 'bg-gray-500 text-white',
79
+ },
80
+ base: 'px-4 py-2 rounded font-semibold',
81
+ default: 'primary'
82
+ })
83
+
84
+ button.css() // => "px-4 py-2 rounded font-semibold bg-blue-500 text-white"
85
+ button.css('secondary') // => "px-4 py-2 rounded font-semibold bg-gray-500 text-white"
86
+
87
+ type Props = VariantProps<typeof button> // { variant?: 'primary' | 'secondary' }
88
+ ```
89
+
90
+ ## Core Concepts
91
+
92
+ ### 1. Class Name Composition (`cn`)
93
+
94
+ The `cn` function composes class names, supporting strings, arrays, objects, Sets, and Maps:
95
+
96
+ ```typescript
97
+ const { cn } = style()
98
+
99
+ // Strings
100
+ cn('foo', 'bar') // => "foo bar"
101
+
102
+ // Arrays
103
+ cn(['foo', 'bar']) // => "foo bar"
104
+
105
+ // Objects (keys with truthy values)
106
+ cn({ foo: true, bar: false, baz: true }) // => "foo baz"
107
+
108
+ // Mixed
109
+ cn('base', ['hover:bg-blue'], { active: true, disabled: false })
110
+ // => "base hover:bg-blue active"
111
+
112
+ // Nested
113
+ cn('base', ['text-lg', { bold: true, italic: false }])
114
+ // => "base text-lg bold"
115
+ ```
116
+
117
+ ### 2. Variants
118
+
119
+ Variants define reusable component styles with different states:
120
+
121
+ ```typescript
122
+ const button = variant({
123
+ name: 'size',
124
+ classes: {
125
+ sm: 'text-sm px-2 py-1',
126
+ md: 'text-base px-4 py-2',
127
+ lg: 'text-lg px-6 py-3',
128
+ },
129
+ base: 'rounded font-semibold transition-colors',
130
+ default: 'md'
131
+ })
132
+
133
+ button.css('sm') // => "rounded font-semibold transition-colors text-sm px-2 py-1"
134
+ button.css('md') // => "rounded font-semibold transition-colors text-base px-4 py-2"
135
+ button.css() // => "rounded font-semibold transition-colors text-base px-4 py-2" (default)
136
+ ```
137
+
138
+ #### Array Classes
139
+
140
+ ```typescript
141
+ const button = variant({
142
+ name: 'variant',
143
+ classes: {
144
+ primary: ['bg-blue-500', 'text-white', 'hover:bg-blue-600'],
145
+ secondary: ['bg-gray-500', 'text-white', 'hover:bg-gray-600'],
146
+ },
147
+ default: 'primary'
148
+ })
149
+ ```
150
+
151
+ #### Object Classes
152
+
153
+ ```typescript
154
+ const button = variant({
155
+ name: 'state',
156
+ classes: {
157
+ active: { 'bg-blue-500': true, 'text-white': true, 'opacity-50': false },
158
+ disabled: { 'bg-gray-300': true, 'cursor-not-allowed': true },
159
+ },
160
+ default: 'active'
161
+ })
162
+ ```
163
+
164
+ #### TypeScript Integration
165
+
166
+ ```typescript
167
+ import type { VariantProps } from '@hulla/style'
168
+
169
+ const button = variant({
170
+ name: 'variant',
171
+ classes: {
172
+ primary: 'bg-blue-500',
173
+ secondary: 'bg-gray-500',
174
+ },
175
+ default: 'primary'
176
+ })
177
+
178
+ type ButtonProps = VariantProps<typeof button>
179
+ // ButtonProps = { variant?: 'primary' | 'secondary' }
180
+
181
+ function Button({ variant }: ButtonProps) {
182
+ return <button className={button.css(variant)} />
183
+ }
184
+ ```
185
+
186
+ ### 3. Variant Groups
187
+
188
+ Combine multiple variants for more complex component APIs:
189
+
190
+ ```typescript
191
+ const size = variant({
192
+ name: 'size',
193
+ classes: {
194
+ sm: 'text-sm px-2 py-1',
195
+ md: 'text-base px-4 py-2',
196
+ lg: 'text-lg px-6 py-3',
197
+ },
198
+ default: 'md'
199
+ })
200
+
201
+ const variant = variant({
202
+ name: 'variant',
203
+ classes: {
204
+ primary: 'bg-blue-500 text-white',
205
+ secondary: 'bg-gray-500 text-white',
206
+ danger: 'bg-red-500 text-white',
207
+ },
208
+ default: 'primary'
209
+ })
210
+
211
+ const buttonStyles = variantGroup(size, variant)
212
+
213
+ // Use with defaults
214
+ buttonStyles.css({})
215
+ // => "text-base px-4 py-2 bg-blue-500 text-white"
216
+
217
+ // Override specific variants
218
+ buttonStyles.css({ size: 'lg', variant: 'danger' })
219
+ // => "text-lg px-6 py-3 bg-red-500 text-white"
220
+
221
+ // TypeScript support
222
+ type ButtonProps = VariantProps<typeof buttonStyles>
223
+ // ButtonProps = { size?: 'sm' | 'md' | 'lg', variant?: 'primary' | 'secondary' | 'danger' }
224
+ ```
225
+
226
+ ### 4. Composing Everything Together
227
+
228
+ Mix `cn`, variants, and variant groups seamlessly:
229
+
230
+ ```typescript
231
+ const { cn, variant, variantGroup } = style()
232
+
233
+ const size = variant({
234
+ name: 'size',
235
+ classes: { sm: 'text-sm', lg: 'text-lg' },
236
+ default: 'sm'
237
+ })
238
+
239
+ const color = variant({
240
+ name: 'color',
241
+ classes: { blue: 'text-blue-500', red: 'text-red-500' },
242
+ default: 'blue'
243
+ })
244
+
245
+ const styles = variantGroup(size, color)
246
+
247
+ // Compose with additional classes
248
+ const finalClass = cn(
249
+ 'base-class',
250
+ styles.css({ size: 'lg', color: 'red' }),
251
+ 'hover:opacity-80',
252
+ { active: true }
253
+ )
254
+ // => "base-class text-lg text-red-500 hover:opacity-80 active"
255
+ ```
256
+
257
+ ## Advanced Usage
258
+
259
+ ### Custom Composers
260
+
261
+ Use @hulla/style with your preferred class name library:
262
+
263
+ ```typescript
264
+ import { style } from '@hulla/style'
265
+ import { twMerge } from 'tailwind-merge'
266
+ import { clsx } from 'clsx'
267
+
268
+ // With tailwind-merge (handles Tailwind class conflicts)
269
+ const { cn, variant, variantGroup } = style({ composer: twMerge })
270
+
271
+ // With clsx
272
+ const { cn, variant, variantGroup } = style({ composer: clsx })
273
+
274
+ // Objects, arrays, and nested structures work with ANY composer!
275
+ cn({ 'text-blue-500': true, 'bg-white': false }, ['px-4', 'py-2'])
276
+ ```
277
+
278
+ ### Custom Serialization
279
+
280
+ Override how class names are serialized:
281
+
282
+ ```typescript
283
+ import { style, defaultComposer } from '@hulla/style'
284
+
285
+ const { cn, variant, variantGroup } = style({
286
+ serializer: (input) => {
287
+ // Custom logic to convert input to string
288
+ if (typeof input === 'string') return input
289
+ // ... your custom serialization
290
+ return ''
291
+ },
292
+ composer: defaultComposer
293
+ })
294
+ ```
295
+
296
+ ### Variants Without Defaults
297
+
298
+ For more explicit APIs, create variants without defaults:
299
+
300
+ ```typescript
301
+ const button = variant({
302
+ name: 'variant',
303
+ classes: {
304
+ primary: 'bg-blue-500',
305
+ secondary: 'bg-gray-500',
306
+ },
307
+ // No default specified
308
+ })
309
+
310
+ // TypeScript enforces passing a variant
311
+ button.css('primary') // ✅ OK
312
+ button.css() // ❌ TypeScript error: prop is required
313
+ ```
314
+
315
+ ## Real-World Examples
316
+
317
+ ### React Button Component
318
+
319
+ ```tsx
320
+ import { style } from '@hulla/style'
321
+ import type { VariantProps } from '@hulla/style'
322
+ import { twMerge } from 'tailwind-merge'
323
+
324
+ const { cn, variant, variantGroup } = style({ composer: twMerge })
325
+
326
+ const buttonSize = variant({
327
+ name: 'size',
328
+ classes: {
329
+ sm: 'text-sm px-3 py-1.5',
330
+ md: 'text-base px-4 py-2',
331
+ lg: 'text-lg px-6 py-3',
332
+ },
333
+ default: 'md'
334
+ })
335
+
336
+ const buttonVariant = variant({
337
+ name: 'variant',
338
+ classes: {
339
+ primary: 'bg-blue-500 hover:bg-blue-600 text-white',
340
+ secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900',
341
+ danger: 'bg-red-500 hover:bg-red-600 text-white',
342
+ },
343
+ default: 'primary'
344
+ })
345
+
346
+ const buttonStyles = variantGroup(buttonSize, buttonVariant)
347
+
348
+ type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
349
+ VariantProps<typeof buttonStyles>
350
+
351
+ export function Button({ size, variant, className, children, ...props }: ButtonProps) {
352
+ return (
353
+ <button
354
+ className={cn(
355
+ 'rounded font-semibold transition-colors disabled:opacity-50',
356
+ buttonStyles.css({ size, variant }),
357
+ className
358
+ )}
359
+ {...props}
360
+ >
361
+ {children}
362
+ </button>
363
+ )
364
+ }
365
+
366
+ // Usage
367
+ <Button size="lg" variant="danger" className="custom-class">
368
+ Delete
369
+ </Button>
370
+ ```
371
+
372
+ ### Astro Component
373
+
374
+ ```astro
375
+ ---
376
+ import { style } from '@hulla/style'
377
+ import type { VariantProps } from '@hulla/style'
378
+
379
+ const { variant, variantGroup } = style()
380
+
381
+ const size = variant({
382
+ name: 'size',
383
+ classes: {
384
+ sm: 'text-sm px-2 py-1',
385
+ md: 'text-base px-4 py-2',
386
+ },
387
+ default: 'md'
388
+ })
389
+
390
+ const color = variant({
391
+ name: 'color',
392
+ classes: {
393
+ primary: 'bg-blue-500 text-white',
394
+ secondary: 'bg-gray-500 text-white',
395
+ },
396
+ default: 'primary'
397
+ })
398
+
399
+ const buttonStyles = variantGroup(size, color)
400
+
401
+ type Props = VariantProps<typeof buttonStyles>
402
+ const props = Astro.props
403
+ ---
404
+
405
+ <button class={buttonStyles.css(props)}>
406
+ <slot />
407
+ </button>
408
+ ```
409
+
410
+ ### Vue Component
411
+
412
+ ```vue
413
+ <script setup lang="ts">
414
+ import { style } from '@hulla/style'
415
+ import type { VariantProps } from '@hulla/style'
416
+
417
+ const { cn, variant, variantGroup } = style()
418
+
419
+ const size = variant({
420
+ name: 'size',
421
+ classes: {
422
+ sm: 'text-sm px-2 py-1',
423
+ md: 'text-base px-4 py-2',
424
+ },
425
+ default: 'md'
426
+ })
427
+
428
+ const buttonVariant = variant({
429
+ name: 'variant',
430
+ classes: {
431
+ primary: 'bg-blue-500 text-white',
432
+ secondary: 'bg-gray-500 text-white',
433
+ },
434
+ default: 'primary'
435
+ })
436
+
437
+ const buttonStyles = variantGroup(size, buttonVariant)
438
+
439
+ type ButtonProps = VariantProps<typeof buttonStyles>
440
+
441
+ interface Props extends ButtonProps {
442
+ class?: string
443
+ }
444
+
445
+ const props = withDefaults(defineProps<Props>(), {})
446
+
447
+ const classes = computed(() =>
448
+ cn(
449
+ 'rounded transition-colors',
450
+ buttonStyles.css({ size: props.size, variant: props.variant }),
451
+ props.class
452
+ )
453
+ )
454
+ </script>
455
+
456
+ <template>
457
+ <button :class="classes">
458
+ <slot />
459
+ </button>
460
+ </template>
461
+ ```
462
+
463
+ ## API Reference
464
+
465
+ ### `style(config?)`
466
+
467
+ Creates style utilities with optional configuration.
468
+
469
+ ```typescript
470
+ const { cn, variant, variantGroup } = style({
471
+ serializer?: (input: ClassName) => string,
472
+ composer?: (...strings: string[]) => string
473
+ })
474
+ ```
475
+
476
+ **Parameters:**
477
+ - `config.serializer` - Custom function to serialize class name inputs to strings
478
+ - `config.composer` - Custom function to compose strings (e.g., `clsx`, `twMerge`)
479
+
480
+ **Returns:**
481
+ - `cn` - Function to compose class names
482
+ - `variant` - Function to create variants
483
+ - `variantGroup` - Function to create variant groups
484
+
485
+ ### `cn(...classes)`
486
+
487
+ Composes class names from various input types.
488
+
489
+ ```typescript
490
+ cn(
491
+ 'string',
492
+ ['array', 'of', 'strings'],
493
+ { objectKey: boolean },
494
+ nestedStructures
495
+ )
496
+ ```
497
+
498
+ ### `variant(definition)`
499
+
500
+ Creates a variant with multiple style options.
501
+
502
+ ```typescript
503
+ const myVariant = variant({
504
+ name: string, // Variant name (for variantGroup)
505
+ classes: Record<string, ClassName>, // Style definitions
506
+ base?: string, // Base classes applied to all variants
507
+ default?: keyof classes // Default variant (optional)
508
+ })
509
+
510
+ myVariant.css(key?) // Returns class string
511
+ myVariant.params // Access variant definition
512
+ ```
513
+
514
+ ### `variantGroup(...variants)`
515
+
516
+ Combines multiple variants into a single API.
517
+
518
+ ```typescript
519
+ const group = variantGroup(variant1, variant2, ...)
520
+
521
+ group.css(props) // Returns composed class string
522
+ group.params // Access all variant definitions
523
+ ```
524
+
525
+ ### Type Helpers
526
+
527
+ ```typescript
528
+ import type { VariantProps, ClassName, Serializer, Composer } from '@hulla/style'
529
+
530
+ // Extract props type from variant or variant group
531
+ type Props = VariantProps<typeof myVariantOrGroup>
532
+ ```
533
+
534
+ ## License
535
+
536
+ MIT © [Samuel Hulla](https://hulla.dev)
537
+
538
+ ## Contributing
539
+
540
+ Contributions are welcome! Please check out our [GitHub repository](https://github.com/hulladev/style).
541
+
@@ -1,90 +1,81 @@
1
- import { HTMLAttributes } from "astro/types"
1
+ type ClassNameNonRecursive = string | string[] | null | undefined | false | true | boolean | 0 | 0n | typeof NaN;
2
+ type ClassesDefinition = Record<string, ClassNameNonRecursive | Record<string, any> | any[]>;
3
+ type Simplify<T> = {
4
+ [K in keyof T]: T[K];
5
+ } & {};
6
+ type KeysWithDefault<V extends readonly VariantAPI<any>[]> = {
7
+ [P in V[number]["params"] as P["name"]]: P["default"] extends undefined ? P["name"] : never;
8
+ }[V[number]["params"]["name"]];
9
+ type KeysWithoutDefault<V extends readonly VariantAPI<any>[]> = {
10
+ [P in V[number]["params"] as P["name"]]: P["default"] extends undefined ? never : P["name"];
11
+ }[V[number]["params"]["name"]];
12
+ type GroupProps<V extends readonly VariantAPI<any>[]> = Simplify<{
13
+ [P in KeysWithDefault<V>]: keyof Extract<V[number]["params"], {
14
+ name: P;
15
+ }>["classes"];
16
+ } & {
17
+ [P in KeysWithoutDefault<V>]?: keyof Extract<V[number]["params"], {
18
+ name: P;
19
+ }>["classes"];
20
+ }>;
21
+ type APIPropsMapper<V extends VariantAPI<any>> = V extends VariantAPI<Variant<infer N, infer VN, infer D, any>> ? D extends NO_DEFAULT ? {
22
+ [K in N]: keyof VN;
23
+ } : {
24
+ [K in N]?: keyof VN;
25
+ } : {};
2
26
 
3
- type Variants = Record<string, Record<string, string>>
4
- type Compose = <const CS extends any[] = ClassName[]>(...classes: CS) => string
5
- type ClassName = string | null | undefined | false | 0 | 0n | typeof NaN
6
- type Config<
7
- V extends Variants,
8
- DV extends {
9
- [K in keyof V]?: keyof V[K]
10
- },
11
- B extends string,
12
- C extends Compose,
13
- > = {
14
- props: V
15
- defaults?: DV
16
- base?: B
17
- compose?: C
18
- transform?: Transform
19
- }
20
- type BaseProps = {
21
- class?: string | null
22
- className?: string | null
23
- "class:list"?: HTMLAttributes<"div">["class:list"]
24
- }
25
- type Props<V extends Variants, _DV extends Defaults<V>, Extra extends Record<string, unknown>> = BaseProps & {
26
- [K in keyof V]?: keyof V[K]
27
- } & Extra
28
- type Transform = (css: string) => string
29
- type Setup<C extends Compose> = {
30
- compose?: C
31
- transform?: (css: string) => string
32
- }
33
- type Defaults<V extends Variants> = {
34
- [K in keyof V]?: keyof V[K]
35
- }
36
- type VariantFunction<ExtraProps extends Record<string, unknown> = {}> = <
37
- const V extends Variants,
38
- const DV extends Defaults<V>,
39
- const B extends string,
40
- const C extends Compose,
41
- >(
42
- config: Config<V, DV, B, C>,
43
- modifier?: (data: {
44
- config: Config<V, DV, B, Compose>
45
- props: Props<V, DV, ExtraProps>
46
- }) => Partial<Config<V, DV, B, Compose>>
47
- ) => (props: Props<V, DV, ExtraProps>) => string
27
+ type ClassName = ClassNameNonRecursive | Record<string, ClassNameNonRecursive>;
28
+ type Serializer = (input: ClassName) => string;
29
+ type Composer = (...strings: string[]) => string;
30
+ declare const NO_DEFAULT: unique symbol;
31
+ type NO_DEFAULT = typeof NO_DEFAULT;
32
+ type Variant<N extends string, V extends ClassesDefinition, D extends keyof V | NO_DEFAULT = NO_DEFAULT, B extends string = ""> = {
33
+ name: N;
34
+ classes: V;
35
+ base?: B;
36
+ default?: D extends NO_DEFAULT ? never : D;
37
+ };
38
+ type StyleConfig = {
39
+ serializer?: Serializer;
40
+ composer?: Composer;
41
+ };
42
+ type VariantAPI<V extends Variant<any, any, any, any>> = {
43
+ params: V;
44
+ css: V extends Variant<any, any, NO_DEFAULT, any> ? (prop: keyof V["classes"]) => string : (prop?: keyof V["classes"]) => string;
45
+ };
46
+ type VariantGroupAPI<V extends readonly VariantAPI<any>[]> = {
47
+ params: Record<V[number]["params"]["name"], VariantAPI<any>>;
48
+ css: (props: GroupProps<V>) => string;
49
+ };
48
50
  type StyleFunctions = {
49
- cn: Compose
50
- vn: VariantFunction<{}>
51
- }
52
- type NotNeverKeys<T extends Record<string, unknown>> = {
53
- [K in keyof T]: T[K] extends never ? never : K
54
- }[keyof T]
55
- type HideNever<T extends Record<string, unknown>> = {
56
- [K in NotNeverKeys<T>]: T[K]
57
- }
58
- type VariantProps<S extends (props: Props<Variants, Defaults<Variants>, Record<string, unknown>>) => string> =
59
- Parameters<S>[0] extends Props<infer V, infer DV, Record<string, unknown>>
60
- ? Omit<
61
- {
62
- [K in keyof V]: K extends keyof DV ? never : keyof V[K]
63
- },
64
- {
65
- [K in keyof DV]: K extends keyof V ? K : never
66
- }[keyof DV]
67
- > & {
68
- [K in keyof DV]?: K extends keyof V ? keyof V[K] : never
69
- }
70
- : never
51
+ cn: <const CN extends ClassName[]>(...classes: CN) => string;
52
+ };
53
+ type VariantProps<V extends VariantAPI<any> | VariantGroupAPI<any>> = V extends VariantGroupAPI<infer G> ? GroupProps<G> : V extends VariantAPI<infer V> ? APIPropsMapper<VariantAPI<V>> : {};
71
54
 
72
- declare function style<C extends Compose>(setup?: Setup<C>): StyleFunctions
55
+ declare function variantGroupBuilder<const V extends readonly VariantAPI<any>[]>(...variants: V): VariantGroupAPI<V>;
73
56
 
74
- export {
75
- type BaseProps,
76
- type ClassName,
77
- type Compose,
78
- type Config,
79
- type Defaults,
80
- type HideNever,
81
- type NotNeverKeys,
82
- type Props,
83
- type Setup,
84
- type StyleFunctions,
85
- type Transform,
86
- type VariantFunction,
87
- type VariantProps,
88
- type Variants,
89
- style,
90
- }
57
+ declare function style(config?: StyleConfig): {
58
+ cn: <const CN extends ClassName[]>(...classes: CN) => string;
59
+ variantGroup: typeof variantGroupBuilder;
60
+ variant: {
61
+ <N extends string, V extends ClassesDefinition, D extends keyof V & string, B extends string = "">(variantDefinition: {
62
+ name: N;
63
+ classes: V;
64
+ default: D;
65
+ base?: B | undefined;
66
+ }): VariantAPI<Variant<N, V, D, B>>;
67
+ <N_1 extends string, V_1 extends ClassesDefinition, B_1 extends string = "">(variantDefinition: {
68
+ name: N_1;
69
+ classes: V_1;
70
+ base?: B_1 | undefined;
71
+ } & {
72
+ default?: undefined;
73
+ }): VariantAPI<Variant<N_1, V_1, typeof NO_DEFAULT, B_1>>;
74
+ };
75
+ };
76
+
77
+ declare function defaultSerializer(input: ClassName): string;
78
+
79
+ declare function defaultComposer(...strings: string[]): string;
80
+
81
+ export { type ClassName, type Composer, NO_DEFAULT, type Serializer, type StyleConfig, type StyleFunctions, type Variant, type VariantAPI, type VariantGroupAPI, type VariantProps, defaultComposer, defaultSerializer, style };
package/dist/cjs/index.js CHANGED
@@ -1,38 +1 @@
1
- function e(...t) {
2
- let r = []
3
- for (let e of t.filter(Boolean)) for (let t of e.split(" ")) r.push(t)
4
- return Array.from(new Set(r).values()).join(" ")
5
- }
6
- exports.style = function (t) {
7
- let { compose: r = e, transform: n } = t ?? {}
8
- return {
9
- vn: (e, t) => (o) => {
10
- let s = t ? { ...e, ...(t({ config: e, props: o }) ?? {}) } : e,
11
- f = ""
12
- for (let e in s.props) {
13
- let t = void 0 !== o[e] ? s.props[e][o[e]] : s.defaults?.[e] ? s.props[e][s.defaults[e]] : void 0
14
- t && (f += "" === f ? t : ` ${t}`)
15
- }
16
- let a = [
17
- o.class,
18
- o.className,
19
- ...((function (e) {
20
- switch (typeof e) {
21
- case "string":
22
- return [e]
23
- case "object":
24
- if (e instanceof Array) return e
25
- if (e instanceof Set || e instanceof Map) return Array.from(e)
26
- return Object.entries(e).map(([e, t]) => (t ? e : void 0))
27
- default:
28
- return
29
- }
30
- })(o["class:list"]) ?? []),
31
- ],
32
- i = (s.compose ?? r)(s.base, f, ...a),
33
- l = n ?? s.transform
34
- return l ? l(i) : i
35
- },
36
- cn: n ? (...e) => n(r(...e)) : r,
37
- }
38
- }
1
+ function r(...e){return{params:e.reduce((r,e)=>(r[e.params.name]=e.params,r),{}),css:r=>e.map(e=>e.css(r[e.params.name])).join(" ")}}function e(r){let e=function r(e){if("string"==typeof e||"number"==typeof e)return e;if("object"==typeof e){if(Array.isArray(e))return e.map(r).join(" ");if(e instanceof Set||e instanceof Map)return Array.from(e).map(r).join(" ");let n="";return Object.entries(e??{}).forEach(([r,e])=>{e&&(n+=(n?" ":"")+r)}),n}return null}(r);return e?String(e):""}function n(...r){return r.filter(Boolean).join(" ")}exports.defaultComposer=n,exports.defaultSerializer=e,exports.style=function(t){let{serializer:a=e,composer:o=n}=t??{},i=(...r)=>o(...r.map(r=>a(r)));return{cn:i,variantGroup:r,variant:function(r){return{params:r,css:e=>{if(void 0===r.default&&void 0===e)throw Error(`Missing prop key for css function in variant ${r.name}. Either provide a default or pass a prop please.`);return i(...r.base?[r.base]:[],r.classes[e??r.default])}}}}};
@@ -1,90 +1,81 @@
1
- import { HTMLAttributes } from "astro/types"
1
+ type ClassNameNonRecursive = string | string[] | null | undefined | false | true | boolean | 0 | 0n | typeof NaN;
2
+ type ClassesDefinition = Record<string, ClassNameNonRecursive | Record<string, any> | any[]>;
3
+ type Simplify<T> = {
4
+ [K in keyof T]: T[K];
5
+ } & {};
6
+ type KeysWithDefault<V extends readonly VariantAPI<any>[]> = {
7
+ [P in V[number]["params"] as P["name"]]: P["default"] extends undefined ? P["name"] : never;
8
+ }[V[number]["params"]["name"]];
9
+ type KeysWithoutDefault<V extends readonly VariantAPI<any>[]> = {
10
+ [P in V[number]["params"] as P["name"]]: P["default"] extends undefined ? never : P["name"];
11
+ }[V[number]["params"]["name"]];
12
+ type GroupProps<V extends readonly VariantAPI<any>[]> = Simplify<{
13
+ [P in KeysWithDefault<V>]: keyof Extract<V[number]["params"], {
14
+ name: P;
15
+ }>["classes"];
16
+ } & {
17
+ [P in KeysWithoutDefault<V>]?: keyof Extract<V[number]["params"], {
18
+ name: P;
19
+ }>["classes"];
20
+ }>;
21
+ type APIPropsMapper<V extends VariantAPI<any>> = V extends VariantAPI<Variant<infer N, infer VN, infer D, any>> ? D extends NO_DEFAULT ? {
22
+ [K in N]: keyof VN;
23
+ } : {
24
+ [K in N]?: keyof VN;
25
+ } : {};
2
26
 
3
- type Variants = Record<string, Record<string, string>>
4
- type Compose = <const CS extends any[] = ClassName[]>(...classes: CS) => string
5
- type ClassName = string | null | undefined | false | 0 | 0n | typeof NaN
6
- type Config<
7
- V extends Variants,
8
- DV extends {
9
- [K in keyof V]?: keyof V[K]
10
- },
11
- B extends string,
12
- C extends Compose,
13
- > = {
14
- props: V
15
- defaults?: DV
16
- base?: B
17
- compose?: C
18
- transform?: Transform
19
- }
20
- type BaseProps = {
21
- class?: string | null
22
- className?: string | null
23
- "class:list"?: HTMLAttributes<"div">["class:list"]
24
- }
25
- type Props<V extends Variants, _DV extends Defaults<V>, Extra extends Record<string, unknown>> = BaseProps & {
26
- [K in keyof V]?: keyof V[K]
27
- } & Extra
28
- type Transform = (css: string) => string
29
- type Setup<C extends Compose> = {
30
- compose?: C
31
- transform?: (css: string) => string
32
- }
33
- type Defaults<V extends Variants> = {
34
- [K in keyof V]?: keyof V[K]
35
- }
36
- type VariantFunction<ExtraProps extends Record<string, unknown> = {}> = <
37
- const V extends Variants,
38
- const DV extends Defaults<V>,
39
- const B extends string,
40
- const C extends Compose,
41
- >(
42
- config: Config<V, DV, B, C>,
43
- modifier?: (data: {
44
- config: Config<V, DV, B, Compose>
45
- props: Props<V, DV, ExtraProps>
46
- }) => Partial<Config<V, DV, B, Compose>>
47
- ) => (props: Props<V, DV, ExtraProps>) => string
27
+ type ClassName = ClassNameNonRecursive | Record<string, ClassNameNonRecursive>;
28
+ type Serializer = (input: ClassName) => string;
29
+ type Composer = (...strings: string[]) => string;
30
+ declare const NO_DEFAULT: unique symbol;
31
+ type NO_DEFAULT = typeof NO_DEFAULT;
32
+ type Variant<N extends string, V extends ClassesDefinition, D extends keyof V | NO_DEFAULT = NO_DEFAULT, B extends string = ""> = {
33
+ name: N;
34
+ classes: V;
35
+ base?: B;
36
+ default?: D extends NO_DEFAULT ? never : D;
37
+ };
38
+ type StyleConfig = {
39
+ serializer?: Serializer;
40
+ composer?: Composer;
41
+ };
42
+ type VariantAPI<V extends Variant<any, any, any, any>> = {
43
+ params: V;
44
+ css: V extends Variant<any, any, NO_DEFAULT, any> ? (prop: keyof V["classes"]) => string : (prop?: keyof V["classes"]) => string;
45
+ };
46
+ type VariantGroupAPI<V extends readonly VariantAPI<any>[]> = {
47
+ params: Record<V[number]["params"]["name"], VariantAPI<any>>;
48
+ css: (props: GroupProps<V>) => string;
49
+ };
48
50
  type StyleFunctions = {
49
- cn: Compose
50
- vn: VariantFunction<{}>
51
- }
52
- type NotNeverKeys<T extends Record<string, unknown>> = {
53
- [K in keyof T]: T[K] extends never ? never : K
54
- }[keyof T]
55
- type HideNever<T extends Record<string, unknown>> = {
56
- [K in NotNeverKeys<T>]: T[K]
57
- }
58
- type VariantProps<S extends (props: Props<Variants, Defaults<Variants>, Record<string, unknown>>) => string> =
59
- Parameters<S>[0] extends Props<infer V, infer DV, Record<string, unknown>>
60
- ? Omit<
61
- {
62
- [K in keyof V]: K extends keyof DV ? never : keyof V[K]
63
- },
64
- {
65
- [K in keyof DV]: K extends keyof V ? K : never
66
- }[keyof DV]
67
- > & {
68
- [K in keyof DV]?: K extends keyof V ? keyof V[K] : never
69
- }
70
- : never
51
+ cn: <const CN extends ClassName[]>(...classes: CN) => string;
52
+ };
53
+ type VariantProps<V extends VariantAPI<any> | VariantGroupAPI<any>> = V extends VariantGroupAPI<infer G> ? GroupProps<G> : V extends VariantAPI<infer V> ? APIPropsMapper<VariantAPI<V>> : {};
71
54
 
72
- declare function style<C extends Compose>(setup?: Setup<C>): StyleFunctions
55
+ declare function variantGroupBuilder<const V extends readonly VariantAPI<any>[]>(...variants: V): VariantGroupAPI<V>;
73
56
 
74
- export {
75
- type BaseProps,
76
- type ClassName,
77
- type Compose,
78
- type Config,
79
- type Defaults,
80
- type HideNever,
81
- type NotNeverKeys,
82
- type Props,
83
- type Setup,
84
- type StyleFunctions,
85
- type Transform,
86
- type VariantFunction,
87
- type VariantProps,
88
- type Variants,
89
- style,
90
- }
57
+ declare function style(config?: StyleConfig): {
58
+ cn: <const CN extends ClassName[]>(...classes: CN) => string;
59
+ variantGroup: typeof variantGroupBuilder;
60
+ variant: {
61
+ <N extends string, V extends ClassesDefinition, D extends keyof V & string, B extends string = "">(variantDefinition: {
62
+ name: N;
63
+ classes: V;
64
+ default: D;
65
+ base?: B | undefined;
66
+ }): VariantAPI<Variant<N, V, D, B>>;
67
+ <N_1 extends string, V_1 extends ClassesDefinition, B_1 extends string = "">(variantDefinition: {
68
+ name: N_1;
69
+ classes: V_1;
70
+ base?: B_1 | undefined;
71
+ } & {
72
+ default?: undefined;
73
+ }): VariantAPI<Variant<N_1, V_1, typeof NO_DEFAULT, B_1>>;
74
+ };
75
+ };
76
+
77
+ declare function defaultSerializer(input: ClassName): string;
78
+
79
+ declare function defaultComposer(...strings: string[]): string;
80
+
81
+ export { type ClassName, type Composer, NO_DEFAULT, type Serializer, type StyleConfig, type StyleFunctions, type Variant, type VariantAPI, type VariantGroupAPI, type VariantProps, defaultComposer, defaultSerializer, style };
package/dist/es/index.mjs CHANGED
@@ -1,39 +1 @@
1
- function e(...t) {
2
- let r = []
3
- for (let e of t.filter(Boolean)) for (let t of e.split(" ")) r.push(t)
4
- return Array.from(new Set(r).values()).join(" ")
5
- }
6
- function t(t) {
7
- let { compose: r = e, transform: n } = t ?? {}
8
- return {
9
- vn: (e, t) => (o) => {
10
- let s = t ? { ...e, ...(t({ config: e, props: o }) ?? {}) } : e,
11
- f = ""
12
- for (let e in s.props) {
13
- let t = void 0 !== o[e] ? s.props[e][o[e]] : s.defaults?.[e] ? s.props[e][s.defaults[e]] : void 0
14
- t && (f += "" === f ? t : ` ${t}`)
15
- }
16
- let a = [
17
- o.class,
18
- o.className,
19
- ...((function (e) {
20
- switch (typeof e) {
21
- case "string":
22
- return [e]
23
- case "object":
24
- if (e instanceof Array) return e
25
- if (e instanceof Set || e instanceof Map) return Array.from(e)
26
- return Object.entries(e).map(([e, t]) => (t ? e : void 0))
27
- default:
28
- return
29
- }
30
- })(o["class:list"]) ?? []),
31
- ],
32
- i = (s.compose ?? r)(s.base, f, ...a),
33
- l = n ?? s.transform
34
- return l ? l(i) : i
35
- },
36
- cn: n ? (...e) => n(r(...e)) : r,
37
- }
38
- }
39
- export { t as style }
1
+ function r(...e){return{params:e.reduce((r,e)=>(r[e.params.name]=e.params,r),{}),css:r=>e.map(e=>e.css(r[e.params.name])).join(" ")}}function e(r){let e=function r(e){if("string"==typeof e||"number"==typeof e)return e;if("object"==typeof e){if(Array.isArray(e))return e.map(r).join(" ");if(e instanceof Set||e instanceof Map)return Array.from(e).map(r).join(" ");let n="";return Object.entries(e??{}).forEach(([r,e])=>{e&&(n+=(n?" ":"")+r)}),n}return null}(r);return e?String(e):""}function n(...r){return r.filter(Boolean).join(" ")}function t(t){let{serializer:a=e,composer:i=n}=t??{},o=(...r)=>i(...r.map(r=>a(r)));return{cn:o,variantGroup:r,variant:function(r){return{params:r,css:e=>{if(void 0===r.default&&void 0===e)throw Error(`Missing prop key for css function in variant ${r.name}. Either provide a default or pass a prop please.`);return o(...r.base?[r.base]:[],r.classes[e??r.default])}}}}}export{n as defaultComposer,e as defaultSerializer,t as style};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hulla/style",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Styling made easy 🎨",
5
5
  "author": {
6
6
  "name": "Samuel Hulla",