@frontfriend/tailwind 4.0.8 → 4.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontfriend/tailwind",
3
- "version": "4.0.8",
3
+ "version": "4.0.9",
4
4
  "description": "Design token management for Tailwind CSS",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -77,6 +77,7 @@
77
77
  },
78
78
  "peerDependencies": {
79
79
  "tailwindcss": "^3.0.0 || ^4.0.0",
80
+ "tw-animate-css": "^1.3.0",
80
81
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
81
82
  "vue": ">=3.0.0"
82
83
  },
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Codemod: rewrite component-library consumption to the top-level `slots:{}`
3
+ * contract. Pairs with migrate-config-to-slots.js (data side).
4
+ *
5
+ * Rule (top-level only): a component's named child slots move under `slots`, so
6
+ * consumption gains exactly ONE `.slots.` hop at the first non-reserved key.
7
+ *
8
+ * Anchors handled:
9
+ * A) `ffdc(<args>).<component>.<slot>...` -> insert `.slots` before <slot>
10
+ * (the first hop after the component selector, if it's not a reserved key).
11
+ * B) component-root vars: `const V = ffdc(...).<component>;` (chain length 1)
12
+ * -> rewrite `V.<prop>` / `V?.<prop>` / `V[expr]` when <prop> is not reserved.
13
+ *
14
+ * Slot-subtree alias vars (`const x = ffdc(...).<comp>.<slot>;`, chain >= 2) are
15
+ * NOT treated as roots: their own accesses are already inside a slot (second
16
+ * hop) and stay direct. Only their RHS slot hop is rewritten by anchor A.
17
+ *
18
+ * Idempotent: `.slots` is reserved, so re-running is a no-op.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const { RESERVED } = require('./migrate-config-to-slots');
23
+
24
+ function isSlotKey(key) {
25
+ return !RESERVED.has(key); // RESERVED already includes 'slots'
26
+ }
27
+
28
+ function rewriteSource(src) {
29
+ let out = src;
30
+ const notes = [];
31
+
32
+ // --- Anchor A: ffdc(...).component.slot... ---
33
+ // Capture `ffdc(<args>).<component>` then the first `.prop`.
34
+ out = out.replace(
35
+ /(ffdc\([^)]*\)\s*\.\s*\w+)\.(\w+)/g,
36
+ (m, head, prop) => {
37
+ if (!isSlotKey(prop)) return m; // reserved or already 'slots'
38
+ notes.push(`A: ${head.replace(/\s+/g, '')}.${prop} -> .slots.${prop}`);
39
+ return `${head}.slots.${prop}`;
40
+ },
41
+ );
42
+
43
+ // --- Identify component-root vars (chain length exactly 1 after ffdc) ---
44
+ // Matches AFTER anchor A ran, so slot-subtree RHS now contain `.slots.` and
45
+ // will NOT match this single-hop pattern.
46
+ const roots = new Set();
47
+ const declRe = /\b(?:const|let|var)\s+(\w+)\s*=\s*ffdc\([^)]*\)\s*\.\s*\w+\s*;/g;
48
+ let mm;
49
+ while ((mm = declRe.exec(out)) !== null) roots.add(mm[1]);
50
+
51
+ // --- Anchor B: rewrite first-hop access on each root var ---
52
+ for (const V of roots) {
53
+ const esc = V.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
54
+ // dot / optional-chaining access: V.prop | V?.prop
55
+ out = out.replace(
56
+ new RegExp(`(?<![\\w$])${esc}(\\?\\.|\\.)(\\w+)`, 'g'),
57
+ (m, op, prop) => {
58
+ if (!isSlotKey(prop)) return m;
59
+ notes.push(`B: ${V}${op}${prop} -> ${V}${op}slots.${prop}`);
60
+ return `${V}${op}slots.${prop}`;
61
+ },
62
+ );
63
+ // computed access: V[expr] -> V.slots[expr] (top-level dynamic slot)
64
+ out = out.replace(new RegExp(`(?<![\\w$])${esc}\\[`, 'g'), (m) => {
65
+ notes.push(`B: ${V}[...] -> ${V}.slots[...]`);
66
+ return `${V}.slots[`;
67
+ });
68
+ }
69
+
70
+ return { out, roots: [...roots], notes, changed: out !== src };
71
+ }
72
+
73
+ module.exports = { rewriteSource };
74
+
75
+ // CLI: node migrate-component-slots-codemod.js [--write] <file...>
76
+ if (require.main === module) {
77
+ const args = process.argv.slice(2);
78
+ const write = args.includes('--write');
79
+ const files = args.filter((a) => a !== '--write');
80
+ let changedCount = 0;
81
+ for (const f of files) {
82
+ const src = fs.readFileSync(f, 'utf8');
83
+ const { out, notes, changed } = rewriteSource(src);
84
+ if (changed) {
85
+ changedCount++;
86
+ if (write) fs.writeFileSync(f, out);
87
+ console.log(`\n${changed ? '*' : ' '} ${f} (${notes.length} rewrites)`);
88
+ for (const n of notes) console.log(` ${n}`);
89
+ }
90
+ }
91
+ console.log(`\n${changedCount} file(s) ${write ? 'written' : 'would change'} of ${files.length}.`);
92
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Migrate a componentsConfig from the legacy "direct named slots" layout to the
3
+ * canonical v4 layout where a component's named child slots live under `slots:{}`.
4
+ *
5
+ * TOP-LEVEL ONLY (by design): only the component's own top-level keys are
6
+ * partitioned. Slot internals are left untouched, so consumption only ever
7
+ * gains a single `.slots.` hop (e.g. `tabs.list.line` -> `tabs.slots.list.line`,
8
+ * NOT `tabs.slots.list.slots.line`).
9
+ *
10
+ * Per component, top-level keys split into:
11
+ * - RESERVED (typed buckets, left in place): root, variants, variant, size,
12
+ * sizes, iconPosition, color, fullwidth, states, compoundVariants,
13
+ * defaultVariants, props (+ existing `slots`)
14
+ * - SLOTS (everything else): moved verbatim under `slots`.
15
+ *
16
+ * Pure + idempotent: re-running on already-migrated config is a no-op.
17
+ */
18
+
19
+ // Single source of truth lives in lib/core so apps/saas can import it at
20
+ // runtime (normalize-on-write). This script keeps the CLI wrapper.
21
+ const { migrateConfig, migrateEnvelope, RESERVED } = require('../lib/core/components-config-slots');
22
+
23
+ module.exports = { migrateConfig, migrateEnvelope, RESERVED };
24
+
25
+ // CLI: node migrate-config-to-slots.js <in.json> [out.json] (stdout if no out)
26
+ if (require.main === module) {
27
+ const fs = require('fs');
28
+ const [, , inPath, outPath] = process.argv;
29
+ if (!inPath) {
30
+ console.error('usage: migrate-config-to-slots.js <in.json> [out.json]');
31
+ process.exit(1);
32
+ }
33
+ const cfg = JSON.parse(fs.readFileSync(inPath, 'utf8'));
34
+ const migrated = migrateConfig(cfg);
35
+ const json = JSON.stringify(migrated, null, 2);
36
+ if (outPath) fs.writeFileSync(outPath, json + '\n');
37
+ else process.stdout.write(json + '\n');
38
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Re-split codemod: relocate the move-out keys from `.slots.X` to their bucket
3
+ * (`.props.X` / `.states.X` / `.variant.X`) in component consumption, matching
4
+ * the data re-bucketing in components-config-slots.js.
5
+ *
6
+ * Scoped to config-root vars (assigned from `ffdc(...)`) so unrelated `.slots`
7
+ * access is untouched. Handles dot access and bracket access (hyphenated keys
8
+ * like `inset-2col` are always bracket form).
9
+ *
10
+ * Idempotent: once a key is on its bucket it no longer matches `.slots.X`.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const { bucketFor, STATE_KEYS, PROP_KEYS, VARIANT_KEYS } = require('../lib/core/components-config-slots');
15
+
16
+ // Keys to relocate out of slots (everything that isn't routed to 'slots').
17
+ const MOVE_OUT = [...STATE_KEYS, ...PROP_KEYS, ...VARIANT_KEYS];
18
+
19
+ function rewriteSource(src) {
20
+ let out = src;
21
+ const notes = [];
22
+
23
+ // Detect config-root vars: `const V = ffdc(...).<component>;` (chain length 1).
24
+ const roots = new Set();
25
+ const declRe = /\b(?:const|let|var)\s+(\w+)\s*=\s*ffdc\([^)]*\)\s*\.\s*\w+\s*;/g;
26
+ let m;
27
+ while ((m = declRe.exec(out)) !== null) roots.add(m[1]);
28
+ if (roots.size === 0) return { out, notes, changed: false };
29
+
30
+ for (const V of roots) {
31
+ const v = V.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ for (const key of MOVE_OUT) {
33
+ const bucket = bucketFor(key);
34
+ const k = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
35
+ // dot: V.slots.key (key must be a valid identifier)
36
+ if (/^[A-Za-z_$][\w$]*$/.test(key)) {
37
+ out = out.replace(
38
+ new RegExp(`(?<![\\w$])${v}\\.slots\\.${k}(?![\\w$])`, 'g'),
39
+ () => { notes.push(`${V}.slots.${key} -> ${V}.${bucket}.${key}`); return `${V}.${bucket}.${key}`; },
40
+ );
41
+ }
42
+ // bracket: V.slots['key'] | V.slots["key"]
43
+ out = out.replace(
44
+ new RegExp(`(?<![\\w$])${v}\\.slots\\[(['"])${k}\\1\\]`, 'g'),
45
+ (mm, q) => { notes.push(`${V}.slots[${q}${key}${q}] -> ${V}.${bucket}[..]`); return `${V}.${bucket}[${q}${key}${q}]`; },
46
+ );
47
+ }
48
+ }
49
+ return { out, notes, changed: out !== src };
50
+ }
51
+
52
+ module.exports = { rewriteSource };
53
+
54
+ if (require.main === module) {
55
+ const args = process.argv.slice(2);
56
+ const write = args.includes('--write');
57
+ const files = args.filter((a) => a !== '--write');
58
+ let n = 0;
59
+ for (const f of files) {
60
+ const src = fs.readFileSync(f, 'utf8');
61
+ const { out, notes, changed } = rewriteSource(src);
62
+ if (changed) {
63
+ n++;
64
+ if (write) fs.writeFileSync(f, out);
65
+ console.log(`* ${f} (${notes.length})`);
66
+ }
67
+ }
68
+ console.log(`${n} file(s) ${write ? 'written' : 'would change'} of ${files.length}.`);
69
+ }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const { migrateConfig } = require('./migrate-config-to-slots');
4
5
 
5
6
  const packageRoot = path.join(__dirname, '..');
6
7
  const repoRoot = path.join(packageRoot, '..', '..');
@@ -670,7 +671,11 @@ function buildDefaultConfigData(rawMasterConfig) {
670
671
  throw new Error(`Default config still contains FrontFriend-only classes: ${forbidden.join(', ')}`);
671
672
  }
672
673
 
673
- return generated;
674
+ // The master source and the shadcn override maps are authored in the legacy
675
+ // "direct named slots" shape; the path-based neutralize/merge above relies on
676
+ // that. Migrate the finished config to the canonical top-level `slots:{}`
677
+ // layout so the bundled fallback matches the component library contract.
678
+ return migrateConfig(generated);
674
679
  }
675
680
 
676
681
  function generateDefaultConfigData() {