@praxis-kit/tailwind 0.7.0-beta.1
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 +85 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +518 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 K Huehn
|
|
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
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @praxis-kit/tailwind
|
|
2
|
+
|
|
3
|
+
Tailwind CSS integration for praxis-kit — layout-aware class pipeline with variant composition.
|
|
4
|
+
|
|
5
|
+
Wraps the core class pipeline with a display-aware resolver that manages all CSS display values as
|
|
6
|
+
reserved boolean props, strips conflicting layout utilities, and ensures a deterministic class
|
|
7
|
+
order.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @praxis-kit/tailwind
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
`styling.plugin` expects a `ClassPluginFactory` — a function the runtime calls internally with the
|
|
22
|
+
component's resolved pipeline options and strict mode. Pass `createTailwindPipeline` as a **function
|
|
23
|
+
reference**. Do not call it yourself.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createContractComponent } from '@praxis-kit/react'
|
|
27
|
+
import { createTailwindPipeline } from '@praxis-kit/tailwind'
|
|
28
|
+
|
|
29
|
+
// CORRECT — pass the reference; the runtime calls it
|
|
30
|
+
const Button = createContractComponent({
|
|
31
|
+
tag: 'button',
|
|
32
|
+
styling: {
|
|
33
|
+
base: 'items-center rounded',
|
|
34
|
+
variants: {
|
|
35
|
+
size: { sm: 'px-2 py-1 text-sm', lg: 'px-4 py-2 text-base' },
|
|
36
|
+
},
|
|
37
|
+
defaults: { size: 'sm' },
|
|
38
|
+
plugin: createTailwindPipeline,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// WRONG — calling it manually produces a ClassPlugin where a ClassPluginFactory is expected
|
|
43
|
+
const Button = createContractComponent({
|
|
44
|
+
tag: 'button',
|
|
45
|
+
styling: {
|
|
46
|
+
plugin: createTailwindPipeline({ base: 'items-center' }, false), // ❌
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With the plugin active, pass any display prop as a boolean to control the display mode. All CSS
|
|
52
|
+
display values are reserved — the pipeline injects the display class automatically and strips
|
|
53
|
+
conflicting utilities based on the active family:
|
|
54
|
+
|
|
55
|
+
| Prop | CSS class injected | Strips |
|
|
56
|
+
| -------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------- |
|
|
57
|
+
| `flex` | `flex` | `grid-*`, `col-*`, `row-*`, `auto-cols-*`, `auto-rows-*` |
|
|
58
|
+
| `inline-flex` | `inline-flex` | same as `flex` |
|
|
59
|
+
| `grid` | `grid` | `flex-*`, `grow`, `shrink`, `basis-*` |
|
|
60
|
+
| `inline-grid` | `inline-grid` | same as `grid` |
|
|
61
|
+
| `block`, `inline-block`, `inline`, `hidden`, `contents`, `flow-root`, `list-item`, `table-*` | (matching CSS class) | both flex-family and grid-family utilities |
|
|
62
|
+
| _(no prop)_ | nothing | both flex-family and grid-family utilities |
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<Button flex>Click me</Button> // injects "flex", strips grid-*
|
|
66
|
+
<Button inline-flex>Click me</Button> // injects "inline-flex", strips grid-*
|
|
67
|
+
<Button grid>Click me</Button> // injects "grid", strips flex-*
|
|
68
|
+
<Button block>Click me</Button> // injects "block", strips flex-* and grid-*
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Do not pass display classes as literal strings in `base`, `variants`, or `className` — the pipeline
|
|
72
|
+
will warn (when `strict` is enabled) and strip them in favour of the prop-controlled value.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Exports
|
|
77
|
+
|
|
78
|
+
| Export | Description |
|
|
79
|
+
| ------------------------ | --------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `createTailwindPipeline` | `ClassPluginFactory` — pass as `styling.plugin` reference; the runtime calls it |
|
|
81
|
+
| `ClassBuilder` | Assembles the final class string from classified tokens; sorts layout tokens before utilities |
|
|
82
|
+
| `ClassClassifier` | Parses a class token into one of: `layout`, `conditional`, `gap`, or `utility` |
|
|
83
|
+
| `DependencyEvaluator` | Decides whether a token survives the active layout mode using configurable regex rules |
|
|
84
|
+
| `defaultDependencyRules` | Built-in rules: strips `flex-*`/`grow`/`shrink`/`basis-*` in grid mode and vice versa |
|
|
85
|
+
| `LayoutState` | Tracks the active display mode and its filtering family for a single pipeline invocation |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { VariantMap, ClassPipelineOptions, StrictMode, ClassPlugin } from '@praxis-kit/core';
|
|
2
|
+
import { Simplify } from 'type-fest';
|
|
3
|
+
import { EmptyRecord } from '@praxis-kit/shared';
|
|
4
|
+
|
|
5
|
+
declare const LAYOUT_FAMILY_MAP: {
|
|
6
|
+
readonly flex: "flex";
|
|
7
|
+
readonly 'inline-flex': "flex";
|
|
8
|
+
readonly grid: "grid";
|
|
9
|
+
readonly 'inline-grid': "grid";
|
|
10
|
+
readonly block: "none";
|
|
11
|
+
readonly 'inline-block': "none";
|
|
12
|
+
readonly inline: "none";
|
|
13
|
+
readonly hidden: "none";
|
|
14
|
+
readonly contents: "none";
|
|
15
|
+
readonly 'flow-root': "none";
|
|
16
|
+
readonly 'list-item': "none";
|
|
17
|
+
readonly table: "none";
|
|
18
|
+
readonly 'table-caption': "none";
|
|
19
|
+
readonly 'table-cell': "none";
|
|
20
|
+
readonly 'table-column': "none";
|
|
21
|
+
readonly 'table-column-group': "none";
|
|
22
|
+
readonly 'table-footer-group': "none";
|
|
23
|
+
readonly 'table-header-group': "none";
|
|
24
|
+
readonly 'table-row-group': "none";
|
|
25
|
+
readonly 'table-row': "none";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
declare const layoutKeys: readonly ["flex", "inline-flex", "grid", "inline-grid", "block", "inline-block", "inline", "hidden", "contents", "flow-root", "list-item", "table", "table-caption", "table-cell", "table-column", "table-column-group", "table-footer-group", "table-header-group", "table-row-group", "table-row"];
|
|
29
|
+
|
|
30
|
+
type LayoutKey = (typeof layoutKeys)[number];
|
|
31
|
+
type LayoutFamily = (typeof LAYOUT_FAMILY_MAP)[LayoutKey];
|
|
32
|
+
type LayoutMode = LayoutKey | 'none';
|
|
33
|
+
type ExclusiveTrueProp<K extends PropertyKey> = {
|
|
34
|
+
[P in K]: Simplify<Record<P, true> & Partial<Record<Exclude<K, P>, never>>>;
|
|
35
|
+
}[K];
|
|
36
|
+
/**
|
|
37
|
+
* Mutually exclusive display shorthand props.
|
|
38
|
+
*
|
|
39
|
+
* At most one key may be `true`. Passing multiple is a compile-time error; the
|
|
40
|
+
* runtime also warns and lets the first key in declaration order take precedence.
|
|
41
|
+
*/
|
|
42
|
+
type LayoutProps = ExclusiveTrueProp<LayoutKey> | Partial<Record<LayoutKey, never>>;
|
|
43
|
+
|
|
44
|
+
type ClassToken = string;
|
|
45
|
+
type Token<TKind extends string, TData extends object = EmptyRecord> = {
|
|
46
|
+
kind: TKind;
|
|
47
|
+
raw: string;
|
|
48
|
+
} & TData;
|
|
49
|
+
type LayoutToken = Token<'layout', {
|
|
50
|
+
value: LayoutKey;
|
|
51
|
+
}>;
|
|
52
|
+
type ConditionalToken = Token<'conditional', {
|
|
53
|
+
requires: Exclude<LayoutFamily, 'none'>;
|
|
54
|
+
}>;
|
|
55
|
+
type GapToken = Token<'gap'>;
|
|
56
|
+
type UtilityToken = Token<'utility', {
|
|
57
|
+
base: string;
|
|
58
|
+
}>;
|
|
59
|
+
type ClassifiedToken = LayoutToken | ConditionalToken | GapToken | UtilityToken;
|
|
60
|
+
|
|
61
|
+
type VariantSelection = Record<string, string>;
|
|
62
|
+
type CompoundVariant = {
|
|
63
|
+
class?: unknown;
|
|
64
|
+
} & Record<string, unknown>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Layout-aware class pipeline for Tailwind CSS utility class strings.
|
|
68
|
+
*
|
|
69
|
+
* This is a `ClassPluginFactory` — the runtime calls it with the component's
|
|
70
|
+
* resolved pipeline options and strict mode. Do NOT call it yourself; pass the
|
|
71
|
+
* function reference as `styling.plugin` and let the runtime invoke it.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* // CORRECT — pass the reference; the runtime calls it
|
|
76
|
+
* createContractComponent({
|
|
77
|
+
* tag: 'div',
|
|
78
|
+
* styling: {
|
|
79
|
+
* base: 'items-center',
|
|
80
|
+
* plugin: createTailwindPipeline,
|
|
81
|
+
* },
|
|
82
|
+
* })
|
|
83
|
+
*
|
|
84
|
+
* // WRONG — calling it manually produces a ClassPlugin where a ClassPluginFactory is expected
|
|
85
|
+
* createContractComponent({
|
|
86
|
+
* tag: 'div',
|
|
87
|
+
* styling: {
|
|
88
|
+
* plugin: createTailwindPipeline({ base: 'items-center' }, false),
|
|
89
|
+
* },
|
|
90
|
+
* })
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* With the plugin active, pass any display prop (`flex`, `inline-flex`, `grid`,
|
|
94
|
+
* `inline-grid`, `block`, `hidden`, etc.) as a boolean to control the display
|
|
95
|
+
* mode. The pipeline injects the display class and strips conflicting utilities:
|
|
96
|
+
* flex-family modes strip `grid-*`; grid-family modes strip `flex-*`; all other
|
|
97
|
+
* display values (and no prop) strip both `flex-*` and `grid-*`.
|
|
98
|
+
*/
|
|
99
|
+
declare function createTailwindPipeline<V extends VariantMap = VariantMap>(options: ClassPipelineOptions<V>, strict: StrictMode): ClassPlugin<LayoutProps>;
|
|
100
|
+
|
|
101
|
+
declare class ClassBuilder {
|
|
102
|
+
#private;
|
|
103
|
+
build(tokens: ClassifiedToken[]): string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
declare class ClassClassifier {
|
|
107
|
+
#private;
|
|
108
|
+
classify(token: ClassToken): ClassifiedToken;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type DependencyRules = Record<Exclude<LayoutFamily, 'none'>, readonly RegExp[]>;
|
|
112
|
+
declare const defaultDependencyRules: DependencyRules;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The resolved display mode for a single render.
|
|
116
|
+
*
|
|
117
|
+
* Mode is owned by the display props; a display class literal in the resolved
|
|
118
|
+
* class string is a reserved-literal authoring mistake. Defaults to `'none'`
|
|
119
|
+
* when no prop is set.
|
|
120
|
+
*
|
|
121
|
+
* `family` is derived from mode: `'flex'` for flex/inline-flex, `'grid'` for
|
|
122
|
+
* grid/inline-grid, `'none'` for all other values (and when no prop is set).
|
|
123
|
+
* The evaluator uses family — not mode — for utility and gap filtering.
|
|
124
|
+
*/
|
|
125
|
+
declare class LayoutState {
|
|
126
|
+
#private;
|
|
127
|
+
constructor(mode: LayoutMode);
|
|
128
|
+
get mode(): LayoutMode;
|
|
129
|
+
get family(): LayoutFamily;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
declare class DependencyEvaluator {
|
|
133
|
+
private readonly rules;
|
|
134
|
+
constructor(rules: DependencyRules);
|
|
135
|
+
evaluate(token: ClassifiedToken, state: LayoutState): boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { ClassBuilder, ClassClassifier, type ClassToken, type ClassifiedToken, type CompoundVariant, type ConditionalToken, DependencyEvaluator, type DependencyRules, type GapToken, type LayoutFamily, type LayoutKey, type LayoutMode, type LayoutProps, LayoutState, type LayoutToken, type UtilityToken, type VariantSelection, createTailwindPipeline, defaultDependencyRules };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
// ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
2
|
+
function r(e) {
|
|
3
|
+
var t, f, n = "";
|
|
4
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
5
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
6
|
+
var o = e.length;
|
|
7
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
8
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
function clsx() {
|
|
12
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../../lib/primitive/src/utils/assert-never.ts
|
|
17
|
+
function assertNever(value) {
|
|
18
|
+
throw new Error(`Unexpected value: ${String(value)}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ../../lib/primitive/src/utils/cn.ts
|
|
22
|
+
function cn(...inputs) {
|
|
23
|
+
return clsx(...inputs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ../../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/index.mjs
|
|
27
|
+
var falsyToString = (value) => typeof value === "boolean" ? `${value}` : value === 0 ? "0" : value;
|
|
28
|
+
var cx = clsx;
|
|
29
|
+
var cva = (base, config) => (props) => {
|
|
30
|
+
var _config_compoundVariants;
|
|
31
|
+
if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
|
32
|
+
const { variants, defaultVariants } = config;
|
|
33
|
+
const getVariantClassNames = Object.keys(variants).map((variant) => {
|
|
34
|
+
const variantProp = props === null || props === void 0 ? void 0 : props[variant];
|
|
35
|
+
const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];
|
|
36
|
+
if (variantProp === null) return null;
|
|
37
|
+
const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);
|
|
38
|
+
return variants[variant][variantKey];
|
|
39
|
+
});
|
|
40
|
+
const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param) => {
|
|
41
|
+
let [key, value] = param;
|
|
42
|
+
if (value === void 0) {
|
|
43
|
+
return acc;
|
|
44
|
+
}
|
|
45
|
+
acc[key] = value;
|
|
46
|
+
return acc;
|
|
47
|
+
}, {});
|
|
48
|
+
const getCompoundVariantClassNames = config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param) => {
|
|
49
|
+
let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;
|
|
50
|
+
return Object.entries(compoundVariantOptions).every((param2) => {
|
|
51
|
+
let [key, value] = param2;
|
|
52
|
+
return Array.isArray(value) ? value.includes({
|
|
53
|
+
...defaultVariants,
|
|
54
|
+
...propsWithoutUndefined
|
|
55
|
+
}[key]) : {
|
|
56
|
+
...defaultVariants,
|
|
57
|
+
...propsWithoutUndefined
|
|
58
|
+
}[key] === value;
|
|
59
|
+
}) ? [
|
|
60
|
+
...acc,
|
|
61
|
+
cvClass,
|
|
62
|
+
cvClassName
|
|
63
|
+
] : acc;
|
|
64
|
+
}, []);
|
|
65
|
+
return cx(base, getVariantClassNames, getCompoundVariantClassNames, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ../../lib/styling/src/cva.ts
|
|
69
|
+
function cva2(base, config) {
|
|
70
|
+
const fn = cva(base, config);
|
|
71
|
+
return (props) => cn(fn(props));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ../../lib/styling/src/static-class-resolver.ts
|
|
75
|
+
var StaticClassResolver = class {
|
|
76
|
+
#baseClass;
|
|
77
|
+
#cache = /* @__PURE__ */ new Map();
|
|
78
|
+
#resolveTag;
|
|
79
|
+
constructor(baseClass, tagMap) {
|
|
80
|
+
this.#baseClass = baseClass;
|
|
81
|
+
this.#resolveTag = tagMap ? (tag) => {
|
|
82
|
+
const extra = tagMap[tag];
|
|
83
|
+
return extra ? `${this.#baseClass} ${extra}` : this.#baseClass;
|
|
84
|
+
} : () => this.#baseClass;
|
|
85
|
+
}
|
|
86
|
+
resolve(tag, skipTagMap = false) {
|
|
87
|
+
if (typeof tag !== "string" || skipTagMap) return this.#baseClass;
|
|
88
|
+
const cached = this.#cache.get(tag);
|
|
89
|
+
if (cached !== void 0) {
|
|
90
|
+
this.#cache.delete(tag);
|
|
91
|
+
this.#cache.set(tag, cached);
|
|
92
|
+
return cached;
|
|
93
|
+
}
|
|
94
|
+
const result = this.#resolveTag(tag);
|
|
95
|
+
this.#cache.set(tag, result);
|
|
96
|
+
if (this.#cache.size > 200) {
|
|
97
|
+
const lru = this.#cache.keys().next().value;
|
|
98
|
+
if (lru !== void 0) this.#cache.delete(lru);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ../../lib/styling/src/variant-class-resolver.ts
|
|
105
|
+
var VariantClassResolver = class _VariantClassResolver {
|
|
106
|
+
#cvaFn;
|
|
107
|
+
#presetMap;
|
|
108
|
+
#variantKeys;
|
|
109
|
+
#precomputedClasses;
|
|
110
|
+
#cache = /* @__PURE__ */ new Map();
|
|
111
|
+
constructor(cvaFn, presetMap, variantKeys, precomputedClasses) {
|
|
112
|
+
this.#cvaFn = cvaFn ?? null;
|
|
113
|
+
this.#presetMap = Object.freeze(presetMap ?? {});
|
|
114
|
+
this.#variantKeys = variantKeys ?? null;
|
|
115
|
+
this.#precomputedClasses = precomputedClasses ?? null;
|
|
116
|
+
}
|
|
117
|
+
resolve({ props, variantKey }) {
|
|
118
|
+
const normalizedKey = variantKey ?? "__none__";
|
|
119
|
+
const cacheKey = this.#createCacheKey(props, normalizedKey);
|
|
120
|
+
if (this.#precomputedClasses !== null) {
|
|
121
|
+
const precomputed = this.#precomputedClasses[cacheKey];
|
|
122
|
+
if (precomputed !== void 0) return precomputed;
|
|
123
|
+
}
|
|
124
|
+
const cached = this.#cache.get(cacheKey);
|
|
125
|
+
if (cached !== void 0) {
|
|
126
|
+
this.#cache.delete(cacheKey);
|
|
127
|
+
this.#cache.set(cacheKey, cached);
|
|
128
|
+
return cached;
|
|
129
|
+
}
|
|
130
|
+
const result = this.#compute(props, variantKey);
|
|
131
|
+
this.#cache.set(cacheKey, result);
|
|
132
|
+
if (this.#cache.size > 1e3) {
|
|
133
|
+
const lru = this.#cache.keys().next().value;
|
|
134
|
+
if (lru !== void 0) this.#cache.delete(lru);
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
#compute(props, variantKey) {
|
|
139
|
+
if (!this.#cvaFn) return "";
|
|
140
|
+
if (!variantKey) return this.#cvaFn(props);
|
|
141
|
+
const preset = this.#presetMap[variantKey];
|
|
142
|
+
if (!preset) return this.#cvaFn(props);
|
|
143
|
+
return this.#cvaFn({ ...preset, ...props });
|
|
144
|
+
}
|
|
145
|
+
// When variantKeys is provided, only those keys are included in the cache key — non-variant
|
|
146
|
+
// props (className, id, etc.) produce identical CVA output and must not fragment the cache.
|
|
147
|
+
// Iterating #variantKeys directly (fixed Set insertion order) avoids Object.keys + filter + sort.
|
|
148
|
+
// String is built incrementally to avoid a parts[] array allocation on every render.
|
|
149
|
+
#createCacheKey(props, variantKey) {
|
|
150
|
+
if (this.#variantKeys !== null) {
|
|
151
|
+
let key2 = variantKey;
|
|
152
|
+
for (const k of this.#variantKeys) {
|
|
153
|
+
if (k in props) key2 += `|${k}:${_VariantClassResolver.#serializeValue(props[k])}`;
|
|
154
|
+
}
|
|
155
|
+
return key2;
|
|
156
|
+
}
|
|
157
|
+
let key = variantKey;
|
|
158
|
+
for (const k of Object.keys(props).sort()) {
|
|
159
|
+
key += `|${k}:${_VariantClassResolver.#serializeValue(props[k])}`;
|
|
160
|
+
}
|
|
161
|
+
return key;
|
|
162
|
+
}
|
|
163
|
+
static #serializeValue(value) {
|
|
164
|
+
if (value === void 0) return "u";
|
|
165
|
+
if (value === null) return "n";
|
|
166
|
+
if (typeof value === "boolean") return `b:${value}`;
|
|
167
|
+
if (typeof value === "string") return `s:${value}`;
|
|
168
|
+
return `x:${String(value)}`;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// ../../lib/styling/src/create-class-pipeline.ts
|
|
173
|
+
function createClassPipeline(resolved) {
|
|
174
|
+
const baseClass = resolved.baseClassName ?? "";
|
|
175
|
+
const cvaFn = resolved.variants ? cva2("", {
|
|
176
|
+
variants: resolved.variants,
|
|
177
|
+
defaultVariants: resolved.defaultVariants,
|
|
178
|
+
compoundVariants: resolved.compoundVariants
|
|
179
|
+
}) : null;
|
|
180
|
+
const variantKeys = resolved.variants ? new Set(Object.keys(resolved.variants)) : void 0;
|
|
181
|
+
const staticResolver = new StaticClassResolver(baseClass, resolved.tagMap);
|
|
182
|
+
const variantResolver = new VariantClassResolver(
|
|
183
|
+
cvaFn,
|
|
184
|
+
resolved.presetMap,
|
|
185
|
+
variantKeys,
|
|
186
|
+
resolved.precomputedClasses
|
|
187
|
+
);
|
|
188
|
+
return function resolveClasses(tag, props, className, variantKey) {
|
|
189
|
+
const staticClasses = staticResolver.resolve(tag, variantKey !== void 0);
|
|
190
|
+
const variantClasses = variantResolver.resolve({ props, variantKey });
|
|
191
|
+
if (!className)
|
|
192
|
+
return staticClasses && variantClasses ? `${staticClasses} ${variantClasses}` : staticClasses || variantClasses;
|
|
193
|
+
return cn(staticClasses, variantClasses, className);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ../core/src/utils/index.ts
|
|
198
|
+
import { COMPONENT_ID, isComponent, isTag } from "@praxis-kit/shared/guards/children";
|
|
199
|
+
|
|
200
|
+
// src/class-builder.ts
|
|
201
|
+
var ClassBuilder = class {
|
|
202
|
+
build(tokens) {
|
|
203
|
+
const layout = [];
|
|
204
|
+
const normal = [];
|
|
205
|
+
for (const token of tokens) {
|
|
206
|
+
switch (token.kind) {
|
|
207
|
+
case "layout": {
|
|
208
|
+
layout.push(token.raw);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case "utility":
|
|
212
|
+
case "gap":
|
|
213
|
+
case "conditional": {
|
|
214
|
+
normal.push(token.raw);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
default:
|
|
218
|
+
throw assertNever(token);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return [...this.#dedupe(layout).toSorted(), ...this.#dedupe(normal)].join(" ");
|
|
222
|
+
}
|
|
223
|
+
#dedupe(arr) {
|
|
224
|
+
return [...new Set(arr)];
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/layout-keys.ts
|
|
229
|
+
var layoutKeys = [
|
|
230
|
+
"flex",
|
|
231
|
+
"inline-flex",
|
|
232
|
+
"grid",
|
|
233
|
+
"inline-grid",
|
|
234
|
+
"block",
|
|
235
|
+
"inline-block",
|
|
236
|
+
"inline",
|
|
237
|
+
"hidden",
|
|
238
|
+
"contents",
|
|
239
|
+
"flow-root",
|
|
240
|
+
"list-item",
|
|
241
|
+
"table",
|
|
242
|
+
"table-caption",
|
|
243
|
+
"table-cell",
|
|
244
|
+
"table-column",
|
|
245
|
+
"table-column-group",
|
|
246
|
+
"table-footer-group",
|
|
247
|
+
"table-header-group",
|
|
248
|
+
"table-row-group",
|
|
249
|
+
"table-row"
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
// src/constants.ts
|
|
253
|
+
var LAYOUT_OWNED_KEYS = new Set(layoutKeys);
|
|
254
|
+
var LAYOUT_FAMILY_MAP = {
|
|
255
|
+
flex: "flex",
|
|
256
|
+
"inline-flex": "flex",
|
|
257
|
+
grid: "grid",
|
|
258
|
+
"inline-grid": "grid",
|
|
259
|
+
block: "none",
|
|
260
|
+
"inline-block": "none",
|
|
261
|
+
inline: "none",
|
|
262
|
+
hidden: "none",
|
|
263
|
+
contents: "none",
|
|
264
|
+
"flow-root": "none",
|
|
265
|
+
"list-item": "none",
|
|
266
|
+
table: "none",
|
|
267
|
+
"table-caption": "none",
|
|
268
|
+
"table-cell": "none",
|
|
269
|
+
"table-column": "none",
|
|
270
|
+
"table-column-group": "none",
|
|
271
|
+
"table-footer-group": "none",
|
|
272
|
+
"table-header-group": "none",
|
|
273
|
+
"table-row-group": "none",
|
|
274
|
+
"table-row": "none"
|
|
275
|
+
};
|
|
276
|
+
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
277
|
+
var COMPOUND_META_KEYS = /* @__PURE__ */ new Set(["class"]);
|
|
278
|
+
|
|
279
|
+
// src/class-classifier.ts
|
|
280
|
+
var CONDITIONALS = {
|
|
281
|
+
"[&.flex": "flex",
|
|
282
|
+
"[&.grid": "grid"
|
|
283
|
+
};
|
|
284
|
+
var ClassClassifier = class _ClassClassifier {
|
|
285
|
+
static #getBaseUtility(token) {
|
|
286
|
+
let depth = 0;
|
|
287
|
+
for (let i = token.length - 1; i >= 0; i--) {
|
|
288
|
+
const char = token[i];
|
|
289
|
+
if (char === "]") depth++;
|
|
290
|
+
else if (char === "[") depth--;
|
|
291
|
+
else if (char === ":" && depth === 0 && token[i - 1] !== "\\") {
|
|
292
|
+
return token.slice(i + 1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return token;
|
|
296
|
+
}
|
|
297
|
+
classify(token) {
|
|
298
|
+
const base = _ClassClassifier.#getBaseUtility(token);
|
|
299
|
+
if (LAYOUT_OWNED_KEYS.has(base)) {
|
|
300
|
+
return {
|
|
301
|
+
kind: "layout",
|
|
302
|
+
value: base,
|
|
303
|
+
raw: token
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
for (const [prefix, requires] of Object.entries(CONDITIONALS)) {
|
|
307
|
+
if (token.startsWith(prefix)) {
|
|
308
|
+
return {
|
|
309
|
+
kind: "conditional",
|
|
310
|
+
requires,
|
|
311
|
+
raw: token
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return base === "gap" || base.startsWith("gap-") ? {
|
|
316
|
+
kind: "gap",
|
|
317
|
+
raw: token
|
|
318
|
+
} : {
|
|
319
|
+
kind: "utility",
|
|
320
|
+
base,
|
|
321
|
+
raw: token
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/dependency-rules.ts
|
|
327
|
+
var defaultDependencyRules = {
|
|
328
|
+
flex: [/^flex-/, /^grow/, /^shrink/, /^basis-/],
|
|
329
|
+
grid: [/^grid-/, /^col-/, /^row-/, /^auto-cols-/, /^auto-rows-/]
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/dependency-evaluator.ts
|
|
333
|
+
var DependencyEvaluator = class {
|
|
334
|
+
constructor(rules) {
|
|
335
|
+
this.rules = rules;
|
|
336
|
+
}
|
|
337
|
+
rules;
|
|
338
|
+
evaluate(token, state) {
|
|
339
|
+
switch (token.kind) {
|
|
340
|
+
case "layout": {
|
|
341
|
+
return token.value === state.mode;
|
|
342
|
+
}
|
|
343
|
+
case "conditional": {
|
|
344
|
+
return token.requires === state.family;
|
|
345
|
+
}
|
|
346
|
+
case "utility": {
|
|
347
|
+
for (const layout of Object.keys(this.rules)) {
|
|
348
|
+
if (this.rules[layout].some((r2) => r2.test(token.base))) {
|
|
349
|
+
return state.family === layout;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
case "gap": {
|
|
355
|
+
return state.family !== "none";
|
|
356
|
+
}
|
|
357
|
+
default:
|
|
358
|
+
throw assertNever(token);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/layout-state.ts
|
|
364
|
+
var LayoutState = class {
|
|
365
|
+
#mode;
|
|
366
|
+
#family;
|
|
367
|
+
constructor(mode) {
|
|
368
|
+
this.#mode = mode;
|
|
369
|
+
this.#family = mode === "none" ? "none" : LAYOUT_FAMILY_MAP[mode];
|
|
370
|
+
Object.freeze(this);
|
|
371
|
+
}
|
|
372
|
+
get mode() {
|
|
373
|
+
return this.#mode;
|
|
374
|
+
}
|
|
375
|
+
get family() {
|
|
376
|
+
return this.#family;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/create-tailwind-pipeline.ts
|
|
381
|
+
import { isString } from "@praxis-kit/shared";
|
|
382
|
+
var DEV = process.env.NODE_ENV !== "production";
|
|
383
|
+
var pendingAsyncWarns = /* @__PURE__ */ new Set();
|
|
384
|
+
var asyncWarnScheduled = false;
|
|
385
|
+
function flushAsyncWarns() {
|
|
386
|
+
asyncWarnScheduled = false;
|
|
387
|
+
const messages = [...pendingAsyncWarns];
|
|
388
|
+
pendingAsyncWarns.clear();
|
|
389
|
+
for (const msg of messages) {
|
|
390
|
+
console.warn(msg);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function pipelineWarn(strict, message) {
|
|
394
|
+
if (!strict) return;
|
|
395
|
+
if (strict === "async-warn") {
|
|
396
|
+
if (pendingAsyncWarns.has(message)) return;
|
|
397
|
+
pendingAsyncWarns.add(message);
|
|
398
|
+
if (!asyncWarnScheduled) {
|
|
399
|
+
asyncWarnScheduled = true;
|
|
400
|
+
queueMicrotask(flushAsyncWarns);
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
console.warn(message);
|
|
405
|
+
}
|
|
406
|
+
var classifier = new ClassClassifier();
|
|
407
|
+
var evaluator = new DependencyEvaluator(defaultDependencyRules);
|
|
408
|
+
var builder = new ClassBuilder();
|
|
409
|
+
function normalizeVariantValue(value) {
|
|
410
|
+
if (isString(value)) return value;
|
|
411
|
+
return value.join(" ");
|
|
412
|
+
}
|
|
413
|
+
function resolveLayout(props) {
|
|
414
|
+
const active = [];
|
|
415
|
+
for (const key of layoutKeys) {
|
|
416
|
+
if (props[key]) active.push(key);
|
|
417
|
+
}
|
|
418
|
+
if (DEV && active.length > 1) {
|
|
419
|
+
console.warn(
|
|
420
|
+
`[createTailwindPipeline] Multiple display props set (${active.join(", ")}); "${active[0]}" takes precedence.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return active[0] ?? "none";
|
|
424
|
+
}
|
|
425
|
+
function warnReservedLayoutLiterals(strict, tokens) {
|
|
426
|
+
if (!strict) return;
|
|
427
|
+
const reserved = [];
|
|
428
|
+
for (const token of tokens) {
|
|
429
|
+
if (token.kind === "layout") reserved.push(token.raw);
|
|
430
|
+
}
|
|
431
|
+
if (reserved.length === 0) return;
|
|
432
|
+
pipelineWarn(
|
|
433
|
+
strict,
|
|
434
|
+
`[createTailwindPipeline] Reserved display class(es) ${reserved.map((r2) => `"${r2}"`).join(", ")} found in resolved classes. The display mode is controlled by the display props (flex, inline-flex, grid, block, hidden, etc.), not by class strings.`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
function getVariantConfig(options) {
|
|
438
|
+
return options.variants;
|
|
439
|
+
}
|
|
440
|
+
function getCompoundVariants(options) {
|
|
441
|
+
return options.compoundVariants ?? [];
|
|
442
|
+
}
|
|
443
|
+
function classifyTokens(className) {
|
|
444
|
+
return className.split(/\s+/).filter(Boolean).map(classifier.classify);
|
|
445
|
+
}
|
|
446
|
+
function compoundDimensions(compounds) {
|
|
447
|
+
if (compounds.length === 0) return EMPTY_SET;
|
|
448
|
+
const dims = /* @__PURE__ */ new Set();
|
|
449
|
+
for (const compound of compounds) {
|
|
450
|
+
for (const key in compound) {
|
|
451
|
+
if (!COMPOUND_META_KEYS.has(key)) dims.add(key);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return dims;
|
|
455
|
+
}
|
|
456
|
+
function getDefaultVariants(options) {
|
|
457
|
+
return options.defaultVariants;
|
|
458
|
+
}
|
|
459
|
+
function resolveActiveSelection(options, variants, props, variantKey) {
|
|
460
|
+
const preset = variantKey ? options.presetMap?.[variantKey] : void 0;
|
|
461
|
+
const defaults = getDefaultVariants(options);
|
|
462
|
+
const selection = {};
|
|
463
|
+
for (const dim in variants) {
|
|
464
|
+
const value = props[dim] ?? preset?.[dim] ?? defaults?.[dim];
|
|
465
|
+
if (value !== void 0 && value !== null) selection[dim] = String(value);
|
|
466
|
+
}
|
|
467
|
+
return selection;
|
|
468
|
+
}
|
|
469
|
+
function warnDeadVariants(strict, options, compoundDims, props, variantKey, state) {
|
|
470
|
+
if (!strict) return;
|
|
471
|
+
const variants = getVariantConfig(options);
|
|
472
|
+
if (!variants) return;
|
|
473
|
+
const selection = resolveActiveSelection(options, variants, props, variantKey);
|
|
474
|
+
for (const dim in selection) {
|
|
475
|
+
if (compoundDims.has(dim)) continue;
|
|
476
|
+
const value = selection[dim];
|
|
477
|
+
const raw = variants[dim]?.[value];
|
|
478
|
+
if (raw == null) continue;
|
|
479
|
+
const classStr = normalizeVariantValue(raw);
|
|
480
|
+
const tokens = classifyTokens(classStr);
|
|
481
|
+
if (tokens.length === 0) continue;
|
|
482
|
+
if (tokens.every((t) => !evaluator.evaluate(t, state))) {
|
|
483
|
+
pipelineWarn(
|
|
484
|
+
strict,
|
|
485
|
+
`[createTailwindPipeline] Variant "${dim}=${value}" contributes only classes stripped under layout mode "${state.mode}" ("${classStr}") \u2014 it produces nothing in this mode.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function createTailwindPipeline(options, strict) {
|
|
491
|
+
const pipeline = createClassPipeline(options);
|
|
492
|
+
const compoundDims = compoundDimensions(getCompoundVariants(options));
|
|
493
|
+
return {
|
|
494
|
+
ownedKeys: LAYOUT_OWNED_KEYS,
|
|
495
|
+
pipeline(tag, props, className, variantKey) {
|
|
496
|
+
const mode = resolveLayout(props);
|
|
497
|
+
const raw = pipeline(tag, props, className, variantKey);
|
|
498
|
+
const tokens = classifyTokens(raw);
|
|
499
|
+
const state = new LayoutState(mode);
|
|
500
|
+
if (DEV) {
|
|
501
|
+
warnReservedLayoutLiterals(strict, tokens);
|
|
502
|
+
warnDeadVariants(strict, options, compoundDims, props, variantKey, state);
|
|
503
|
+
}
|
|
504
|
+
const filtered = tokens.filter((token) => evaluator.evaluate(token, state));
|
|
505
|
+
const built = builder.build(filtered);
|
|
506
|
+
if (mode === "none") return built;
|
|
507
|
+
return filtered.some((t) => t.kind === "layout" && t.value === mode) ? built : cn(mode, built);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
export {
|
|
512
|
+
ClassBuilder,
|
|
513
|
+
ClassClassifier,
|
|
514
|
+
DependencyEvaluator,
|
|
515
|
+
LayoutState,
|
|
516
|
+
createTailwindPipeline,
|
|
517
|
+
defaultDependencyRules
|
|
518
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@praxis-kit/tailwind",
|
|
3
|
+
"version": "0.7.0-beta.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"type-fest": "^5.6.0",
|
|
14
|
+
"@praxis-kit/shared": "3.0.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"tsup": "^8.5.1",
|
|
18
|
+
"vitest": "^4.1.8",
|
|
19
|
+
"@praxis-kit/core": "3.0.1"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/slowebworkz/praxis-kit.git",
|
|
27
|
+
"directory": "packages/tailwind"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/slowebworkz/praxis-kit/tree/main/packages/tailwind#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/slowebworkz/praxis-kit/issues"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"author": "K Huehn <slowebworkz@gmail.com>",
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"description": "Praxis Kit Tailwind CSS integration — layout-aware class pipeline with variant composition",
|
|
40
|
+
"keywords": [
|
|
41
|
+
"praxis-kit",
|
|
42
|
+
"polymorphic",
|
|
43
|
+
"ui",
|
|
44
|
+
"components",
|
|
45
|
+
"accessibility",
|
|
46
|
+
"aria",
|
|
47
|
+
"typescript",
|
|
48
|
+
"framework-agnostic",
|
|
49
|
+
"contract",
|
|
50
|
+
"variants",
|
|
51
|
+
"tailwind",
|
|
52
|
+
"tailwindcss",
|
|
53
|
+
"css"
|
|
54
|
+
],
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup",
|
|
60
|
+
"dev": "tsup --watch",
|
|
61
|
+
"lint": "eslint . --fix --config eslint.config.ts",
|
|
62
|
+
"lint:check": "eslint . --config eslint.config.ts",
|
|
63
|
+
"typecheck": "tsc --noEmit",
|
|
64
|
+
"test": "vitest run",
|
|
65
|
+
"test:related": "vitest run --changed",
|
|
66
|
+
"lint:pkg": "publint"
|
|
67
|
+
}
|
|
68
|
+
}
|