nestable-tailwind-variants 0.0.1 → 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 +181 -37
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/ntv.d.ts +99 -0
- package/dist/ntv.d.ts.map +1 -0
- package/dist/ntv.js +148 -0
- package/dist/ntv.js.map +1 -0
- package/package.json +56 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuhei Yasuda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,45 +1,189 @@
|
|
|
1
1
|
# nestable-tailwind-variants
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A variant styling library for Tailwind CSS that supports nested condition definitions. Express complex style combinations intuitively through nested objects instead of flat `compoundVariants` patterns.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Inspired by [React Spectrum's `style` macro](https://react-spectrum.adobe.com/styling), with some ideas from [tailwind-variants](https://www.tailwind-variants.org/).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install nestable-tailwind-variants
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
Define variants with string union types:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { ntv } from 'nestable-tailwind-variants';
|
|
19
|
+
|
|
20
|
+
const button = ntv<{ variant?: 'primary' | 'secondary' }>({
|
|
21
|
+
default: 'px-4 py-2 rounded font-medium',
|
|
22
|
+
variant: {
|
|
23
|
+
primary: 'bg-blue-500 text-white',
|
|
24
|
+
secondary: 'bg-gray-200 text-gray-800',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
button({ variant: 'primary' });
|
|
29
|
+
// => 'px-4 py-2 rounded font-medium bg-blue-500 text-white'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Class conflicts are automatically resolved by [tailwind-merge](https://github.com/dcastil/tailwind-merge).
|
|
33
|
+
|
|
34
|
+
Boolean conditions starting with `is` or `allows` can be used directly without nesting:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
const button = ntv<{ isDisabled?: boolean }>({
|
|
38
|
+
default: 'bg-blue-500',
|
|
39
|
+
isDisabled: 'bg-gray-300 cursor-not-allowed',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
button({ isDisabled: true });
|
|
43
|
+
// => 'bg-gray-300 cursor-not-allowed'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Nested Conditions
|
|
47
|
+
|
|
48
|
+
Nest conditions to define styles that apply when multiple conditions are true.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
interface CardStyleProps {
|
|
52
|
+
variant?: 'elevated';
|
|
53
|
+
isHovered?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const card = ntv<CardStyleProps>({
|
|
57
|
+
variant: {
|
|
58
|
+
elevated: {
|
|
59
|
+
default: 'shadow-md',
|
|
60
|
+
isHovered: 'shadow-xl',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
card({ variant: 'elevated' });
|
|
66
|
+
// => 'shadow-md'
|
|
67
|
+
|
|
68
|
+
card({ variant: 'elevated', isHovered: true });
|
|
69
|
+
// => 'shadow-xl'
|
|
70
|
+
```
|
|
6
71
|
|
|
7
|
-
|
|
72
|
+
When a condition matches at the same level, `default` is skipped. Conditions can be nested to any depth.
|
|
8
73
|
|
|
9
|
-
##
|
|
74
|
+
## Comparison with tailwind-variants
|
|
10
75
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `nestable-tailwind-variants`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
76
|
+
With tailwind-variants, compound conditions require `compoundVariants`.
|
|
15
77
|
|
|
16
|
-
|
|
78
|
+
**tailwind-variants:**
|
|
17
79
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
80
|
+
```tsx
|
|
81
|
+
const button = tv({
|
|
82
|
+
base: 'rounded font-medium',
|
|
83
|
+
variants: {
|
|
84
|
+
variant: {
|
|
85
|
+
primary: 'bg-blue-500 text-white',
|
|
86
|
+
secondary: 'bg-gray-200 text-gray-800',
|
|
87
|
+
},
|
|
88
|
+
isHovered: { true: '' },
|
|
89
|
+
isPressed: { true: '' },
|
|
90
|
+
},
|
|
91
|
+
compoundVariants: [
|
|
92
|
+
{ variant: 'primary', isHovered: true, class: 'bg-blue-600' },
|
|
93
|
+
{ variant: 'primary', isPressed: true, class: 'bg-blue-700' },
|
|
94
|
+
{ variant: 'secondary', isHovered: true, class: 'bg-gray-300' },
|
|
95
|
+
{ variant: 'secondary', isPressed: true, class: 'bg-gray-400' },
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**nestable-tailwind-variants:**
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
const button = ntv<ButtonStyleProps>({
|
|
104
|
+
default: 'rounded font-medium',
|
|
105
|
+
variant: {
|
|
106
|
+
primary: {
|
|
107
|
+
default: 'bg-blue-500 text-white',
|
|
108
|
+
isHovered: 'bg-blue-600',
|
|
109
|
+
isPressed: 'bg-blue-700',
|
|
110
|
+
},
|
|
111
|
+
secondary: {
|
|
112
|
+
default: 'bg-gray-200 text-gray-800',
|
|
113
|
+
isHovered: 'bg-gray-300',
|
|
114
|
+
isPressed: 'bg-gray-400',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Nesting groups related styles together, reflecting the logical hierarchy of conditions in your code.
|
|
121
|
+
|
|
122
|
+
## Composing Styles
|
|
123
|
+
|
|
124
|
+
Combine multiple style functions using `composeNtv`:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { ntv, composeNtv } from 'nestable-tailwind-variants';
|
|
128
|
+
|
|
129
|
+
const baseButton = ntv<{ size?: 'sm' | 'lg' }>({
|
|
130
|
+
default: 'rounded font-medium',
|
|
131
|
+
size: {
|
|
132
|
+
sm: 'px-2 py-1 text-sm',
|
|
133
|
+
lg: 'px-4 py-2 text-lg',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const coloredButton = ntv<{ variant?: 'primary' | 'secondary' }>({
|
|
138
|
+
variant: {
|
|
139
|
+
primary: 'bg-blue-500 text-white',
|
|
140
|
+
secondary: 'bg-gray-200 text-gray-800',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const button = composeNtv(baseButton, coloredButton);
|
|
145
|
+
|
|
146
|
+
button({ size: 'lg', variant: 'primary' });
|
|
147
|
+
// => 'rounded font-medium px-4 py-2 text-lg bg-blue-500 text-white'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## API
|
|
151
|
+
|
|
152
|
+
### `ntv<Props>(style)`
|
|
153
|
+
|
|
154
|
+
Creates a style function from a nested style definition.
|
|
155
|
+
|
|
156
|
+
- `style` - Style definition object
|
|
157
|
+
- `default` - Base styles (skipped when other conditions match at the same level)
|
|
158
|
+
- `[variantKey]` - Style definitions for each variant value
|
|
159
|
+
- `is*` / `allows*` - Boolean condition styles
|
|
160
|
+
- Returns `(props: Partial<Props>) => string`
|
|
161
|
+
|
|
162
|
+
### `composeNtv(...fns)`
|
|
163
|
+
|
|
164
|
+
Composes multiple style functions into a single function.
|
|
165
|
+
|
|
166
|
+
- `fns` - Style functions to compose
|
|
167
|
+
- Returns `(props: Partial<Props>) => string`
|
|
168
|
+
|
|
169
|
+
### `createNTV(options)`
|
|
170
|
+
|
|
171
|
+
Creates a customized `ntv` function.
|
|
172
|
+
|
|
173
|
+
- `options.twMerge` (`boolean`, default: `true`) - Enable tailwind-merge
|
|
174
|
+
- `options.twMergeConfig` (`object`) - Custom tailwind-merge configuration
|
|
175
|
+
- Returns customized `ntv` function
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
const ntvNoMerge = createNTV({ twMerge: false });
|
|
179
|
+
|
|
180
|
+
const customNTV = createNTV({
|
|
181
|
+
twMergeConfig: {
|
|
182
|
+
extend: {
|
|
183
|
+
theme: {
|
|
184
|
+
shadow: ['100', '200', '300'],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtD,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/ntv.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
2
|
+
export type TWMergeConfig = Parameters<typeof extendTailwindMerge>[0];
|
|
3
|
+
export interface NTVConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Whether to use `tailwind-merge` to resolve conflicting Tailwind classes.
|
|
6
|
+
* @see https://github.com/dcastil/tailwind-merge
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
twMerge?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Custom configuration for `tailwind-merge`.
|
|
12
|
+
* @see https://github.com/dcastil/tailwind-merge/blob/main/docs/configuration.md
|
|
13
|
+
*/
|
|
14
|
+
twMergeConfig?: TWMergeConfig;
|
|
15
|
+
}
|
|
16
|
+
type BooleanConditionKey = `is${Capitalize<string>}` | `allows${Capitalize<string>}`;
|
|
17
|
+
type ExtractBooleanKeys<Props> = {
|
|
18
|
+
[K in keyof Props]: K extends BooleanConditionKey ? K : never;
|
|
19
|
+
}[keyof Props];
|
|
20
|
+
type ExtractVariantKeys<Props> = Exclude<keyof Props, ExtractBooleanKeys<Props>>;
|
|
21
|
+
type NestedStyleValue<Props> = string | StyleDefinition<Props>;
|
|
22
|
+
type StyleDefinition<Props> = {
|
|
23
|
+
default?: string;
|
|
24
|
+
} & {
|
|
25
|
+
[K in ExtractBooleanKeys<Props>]?: NestedStyleValue<Props>;
|
|
26
|
+
} & {
|
|
27
|
+
[K in ExtractVariantKeys<Props>]?: {
|
|
28
|
+
[V in Extract<Props[K], string>]?: NestedStyleValue<Props>;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
type StyleFunction<P = Record<string, unknown>> = (props: Partial<P>) => string;
|
|
32
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
33
|
+
type ExtractProps<T> = T extends StyleFunction<infer P> ? P : never;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a customized `ntv` function with the specified options.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // Disable tailwind-merge
|
|
40
|
+
* const ntvNoMerge = createNTV({ twMerge: false });
|
|
41
|
+
*
|
|
42
|
+
* // With custom tailwind-merge config
|
|
43
|
+
* const customNTV = createNTV({
|
|
44
|
+
* twMergeConfig: {
|
|
45
|
+
* extend: {
|
|
46
|
+
* theme: {
|
|
47
|
+
* shadow: ['100', '200', '300'],
|
|
48
|
+
* },
|
|
49
|
+
* },
|
|
50
|
+
* },
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function createNTV(options?: NTVConfig): <Props extends Record<string, unknown>>(style: StyleDefinition<Props>) => StyleFunction<Props>;
|
|
55
|
+
/**
|
|
56
|
+
* Creates a style function from a nested style definition.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const button = ntv<{ variant?: 'primary' | 'secondary'; isDisabled?: boolean }>({
|
|
61
|
+
* default: 'px-4 py-2 rounded',
|
|
62
|
+
* variant: {
|
|
63
|
+
* primary: 'bg-blue-500 text-white',
|
|
64
|
+
* secondary: 'bg-gray-200 text-gray-800',
|
|
65
|
+
* },
|
|
66
|
+
* isDisabled: 'opacity-50 cursor-not-allowed',
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* button({ variant: 'primary' });
|
|
70
|
+
* // => 'px-4 py-2 rounded bg-blue-500 text-white'
|
|
71
|
+
*
|
|
72
|
+
* button({ variant: 'primary', isDisabled: true });
|
|
73
|
+
* // => 'px-4 py-2 rounded bg-blue-500 text-white opacity-50 cursor-not-allowed'
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare const ntv: <Props extends Record<string, unknown>>(style: StyleDefinition<Props>) => StyleFunction<Props>;
|
|
77
|
+
/**
|
|
78
|
+
* Composes multiple style functions into a single function.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const baseButton = ntv<{ size?: 'sm' | 'lg' }>({
|
|
83
|
+
* default: 'rounded font-medium',
|
|
84
|
+
* size: { sm: 'px-2 py-1 text-sm', lg: 'px-4 py-2 text-lg' },
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* const coloredButton = ntv<{ variant?: 'primary' | 'secondary' }>({
|
|
88
|
+
* variant: { primary: 'bg-blue-500 text-white', secondary: 'bg-gray-200' },
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* const button = composeNtv(baseButton, coloredButton);
|
|
92
|
+
*
|
|
93
|
+
* button({ size: 'lg', variant: 'primary' });
|
|
94
|
+
* // => 'rounded font-medium px-4 py-2 text-lg bg-blue-500 text-white'
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function composeNtv<T extends StyleFunction[]>(...fns: T): StyleFunction<UnionToIntersection<ExtractProps<T[number]>>>;
|
|
98
|
+
export {};
|
|
99
|
+
//# sourceMappingURL=ntv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ntv.d.ts","sourceRoot":"","sources":["../src/ntv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE9D,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAEtE,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,KAAK,mBAAmB,GAAG,KAAK,UAAU,CAAC,MAAM,CAAC,EAAE,GAAG,SAAS,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;AAErF,KAAK,kBAAkB,CAAC,KAAK,IAAI;KAC9B,CAAC,IAAI,MAAM,KAAK,GAAG,CAAC,SAAS,mBAAmB,GAAG,CAAC,GAAG,KAAK;CAC9D,CAAC,MAAM,KAAK,CAAC,CAAC;AAEf,KAAK,kBAAkB,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;AAEjF,KAAK,gBAAgB,CAAC,KAAK,IAAI,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;AAE/D,KAAK,eAAe,CAAC,KAAK,IAAI;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG;KACD,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,gBAAgB,CAAC,KAAK,CAAC;CAC3D,GAAG;KACD,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;SAChC,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,gBAAgB,CAAC,KAAK,CAAC;KAC3D;CACF,CAAC;AAEF,KAAK,aAAa,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;AAEhF,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,GAAG,KAAK,CAAC,SAAS,CACjF,CAAC,EAAE,MAAM,CAAC,KACP,IAAI,GACL,CAAC,GACD,KAAK,CAAC;AAEV,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAoFpE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,CACvB,OAAO,GAAE,SAAc,GACtB,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC,KAAK,aAAa,CAAC,KAAK,CAAC,CAwBhG;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,GAAG,GA/CZ,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,eAAe,CAAC,KAAK,CAAC,KAAK,aAAa,CAAC,KAAK,CA+ClE,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,aAAa,EAAE,EAClD,GAAG,GAAG,EAAE,CAAC,GACR,aAAa,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAE7D"}
|
package/dist/ntv.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { twMerge, extendTailwindMerge } from 'tailwind-merge';
|
|
2
|
+
function isStyleDefinition(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function evaluateDefinition(definition, context) {
|
|
6
|
+
if (typeof definition === 'string') {
|
|
7
|
+
context.classes.push(definition);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!isStyleDefinition(definition)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const entries = Object.entries(definition);
|
|
14
|
+
let defaultValue = undefined;
|
|
15
|
+
const conditions = [];
|
|
16
|
+
for (const [key, value] of entries) {
|
|
17
|
+
if (key === 'default') {
|
|
18
|
+
defaultValue = value;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
conditions.push([key, value]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const allConditionKeys = conditions.map(([key]) => key);
|
|
25
|
+
const subSkipConditions = new Set([...context.skipConditions, ...allConditionKeys]);
|
|
26
|
+
if (defaultValue !== undefined) {
|
|
27
|
+
evaluateDefinition(defaultValue, {
|
|
28
|
+
...context,
|
|
29
|
+
skipConditions: subSkipConditions,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (const [key, value] of conditions) {
|
|
33
|
+
if (context.skipConditions.has(key)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
subSkipConditions.delete(key);
|
|
37
|
+
const propValue = context.props[key];
|
|
38
|
+
if (/^(is|allows)[A-Z]/.test(key) && propValue) {
|
|
39
|
+
const nestedSkipConditions = new Set([...subSkipConditions, key]);
|
|
40
|
+
evaluateDefinition(value, {
|
|
41
|
+
props: context.props,
|
|
42
|
+
skipConditions: nestedSkipConditions,
|
|
43
|
+
classes: context.classes,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (key !== 'default' && propValue !== undefined) {
|
|
47
|
+
if (isStyleDefinition(value)) {
|
|
48
|
+
const variantValue = value[propValue];
|
|
49
|
+
if (variantValue !== undefined) {
|
|
50
|
+
const nestedSkipConditions = new Set([...subSkipConditions, key]);
|
|
51
|
+
evaluateDefinition(variantValue, {
|
|
52
|
+
props: context.props,
|
|
53
|
+
skipConditions: nestedSkipConditions,
|
|
54
|
+
classes: context.classes,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function joinClasses(...classes) {
|
|
62
|
+
return classes.filter(Boolean).join(' ');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Creates a customized `ntv` function with the specified options.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* // Disable tailwind-merge
|
|
70
|
+
* const ntvNoMerge = createNTV({ twMerge: false });
|
|
71
|
+
*
|
|
72
|
+
* // With custom tailwind-merge config
|
|
73
|
+
* const customNTV = createNTV({
|
|
74
|
+
* twMergeConfig: {
|
|
75
|
+
* extend: {
|
|
76
|
+
* theme: {
|
|
77
|
+
* shadow: ['100', '200', '300'],
|
|
78
|
+
* },
|
|
79
|
+
* },
|
|
80
|
+
* },
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function createNTV(options = {}) {
|
|
85
|
+
const { twMerge: useTwMerge = true, twMergeConfig } = options;
|
|
86
|
+
const mergeClasses = useTwMerge
|
|
87
|
+
? twMergeConfig
|
|
88
|
+
? extendTailwindMerge(twMergeConfig)
|
|
89
|
+
: twMerge
|
|
90
|
+
: joinClasses;
|
|
91
|
+
return function ntv(style) {
|
|
92
|
+
return (props) => {
|
|
93
|
+
const context = {
|
|
94
|
+
props,
|
|
95
|
+
skipConditions: new Set(),
|
|
96
|
+
classes: [],
|
|
97
|
+
};
|
|
98
|
+
evaluateDefinition(style, context);
|
|
99
|
+
return mergeClasses(...context.classes);
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Creates a style function from a nested style definition.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* const button = ntv<{ variant?: 'primary' | 'secondary'; isDisabled?: boolean }>({
|
|
109
|
+
* default: 'px-4 py-2 rounded',
|
|
110
|
+
* variant: {
|
|
111
|
+
* primary: 'bg-blue-500 text-white',
|
|
112
|
+
* secondary: 'bg-gray-200 text-gray-800',
|
|
113
|
+
* },
|
|
114
|
+
* isDisabled: 'opacity-50 cursor-not-allowed',
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* button({ variant: 'primary' });
|
|
118
|
+
* // => 'px-4 py-2 rounded bg-blue-500 text-white'
|
|
119
|
+
*
|
|
120
|
+
* button({ variant: 'primary', isDisabled: true });
|
|
121
|
+
* // => 'px-4 py-2 rounded bg-blue-500 text-white opacity-50 cursor-not-allowed'
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export const ntv = createNTV();
|
|
125
|
+
/**
|
|
126
|
+
* Composes multiple style functions into a single function.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const baseButton = ntv<{ size?: 'sm' | 'lg' }>({
|
|
131
|
+
* default: 'rounded font-medium',
|
|
132
|
+
* size: { sm: 'px-2 py-1 text-sm', lg: 'px-4 py-2 text-lg' },
|
|
133
|
+
* });
|
|
134
|
+
*
|
|
135
|
+
* const coloredButton = ntv<{ variant?: 'primary' | 'secondary' }>({
|
|
136
|
+
* variant: { primary: 'bg-blue-500 text-white', secondary: 'bg-gray-200' },
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* const button = composeNtv(baseButton, coloredButton);
|
|
140
|
+
*
|
|
141
|
+
* button({ size: 'lg', variant: 'primary' });
|
|
142
|
+
* // => 'rounded font-medium px-4 py-2 text-lg bg-blue-500 text-white'
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export function composeNtv(...fns) {
|
|
146
|
+
return (props) => twMerge(...fns.map((fn) => fn(props)));
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=ntv.js.map
|
package/dist/ntv.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ntv.js","sourceRoot":"","sources":["../src/ntv.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestable-tailwind-variants",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A variant styling library for Tailwind CSS that supports nested condition definitions.",
|
|
5
|
+
"homepage": "https://github.com/yuheiy/nestable-tailwind-variants#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/yuheiy/nestable-tailwind-variants/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Yuhei Yasuda <yuhei.yasuda1003@gmail.com> (https://yuheiy.com/)",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/yuheiy/nestable-tailwind-variants.git"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/index.js",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"tailwind-merge": "^3.4.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@commitlint/cli": "^20.3.1",
|
|
28
|
+
"@commitlint/config-conventional": "^20.3.1",
|
|
29
|
+
"bumpp": "^10.3.2",
|
|
30
|
+
"lint-staged": "^16.2.7",
|
|
31
|
+
"oxfmt": "^0.23.0",
|
|
32
|
+
"oxlint": "^1.38.0",
|
|
33
|
+
"typescript": "^5.9.3",
|
|
34
|
+
"vitest": "^4.0.16",
|
|
35
|
+
"node": "runtime:^24.12.0"
|
|
36
|
+
},
|
|
37
|
+
"lint-staged": {
|
|
38
|
+
"*": "oxfmt --no-error-on-unmatched-pattern",
|
|
39
|
+
"*.{js,jsx,ts,tsx,mjs,cjs}": "pnpm run lint"
|
|
40
|
+
},
|
|
41
|
+
"devEngines": {
|
|
42
|
+
"runtime": {
|
|
43
|
+
"name": "node",
|
|
44
|
+
"version": "^24.12.0",
|
|
45
|
+
"onFail": "download"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc",
|
|
50
|
+
"format": "oxfmt",
|
|
51
|
+
"format:check": "oxfmt --check",
|
|
52
|
+
"lint": "oxlint",
|
|
53
|
+
"lint:fix": "oxlint --fix",
|
|
54
|
+
"release": "bumpp -r",
|
|
55
|
+
"test": "vitest",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
}
|
|
58
|
+
}
|