tailwind-styled-v4 5.0.8 → 5.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/README.md CHANGED
@@ -142,9 +142,32 @@ function Alert({ type, children }) {
142
142
  }
143
143
  ```
144
144
 
145
- ### 6. Sub-components (Inline)
145
+ ### 6. Sub-components
146
146
 
147
- Definisikan sub-components langsung di template literal dengan syntax `[name] { classes }` — **TypeScript otomatis infer nama tanpa perlu `.withSub<>()`**.
147
+ Ada **dua cara** mendefinisikan sub-components:
148
+
149
+ #### A. Config Object — Direkomendasikan (Autocomplete + Type Safe)
150
+
151
+ ```tsx
152
+ const Card = tw.div({
153
+ base: "flex flex-col p-4 rounded-xl bg-white shadow",
154
+ sub: {
155
+ header: "font-bold text-lg border-b pb-2",
156
+ body: "text-gray-600 py-2",
157
+ footer: "border-t pt-2 text-sm text-gray-400",
158
+ },
159
+ })
160
+
161
+ // TypeScript infer keys dari object literal → autocomplete penuh
162
+ <Card>
163
+ <Card.header>Judul</Card.header> // ✅ autocomplete
164
+ <Card.body>Konten</Card.body> // ✅ autocomplete
165
+ <Card.footer>Footer</Card.footer> // ✅ autocomplete
166
+ <Card.xyz>?</Card.xyz> // ❌ TypeScript error
167
+ </Card>
168
+ ```
169
+
170
+ #### B. Template Literal Inline — Ringkas (tanpa autocomplete)
148
171
 
149
172
  ```tsx
150
173
  const Card = tw.div`
@@ -154,24 +177,18 @@ const Card = tw.div`
154
177
  [footer] { border-t pt-2 text-sm text-gray-400 }
155
178
  `
156
179
 
157
- // TypeScript otomatis tahu: Card.header, Card.body, Card.footer
180
+ // Runtime benar, tapi TypeScript tidak bisa infer nama dari multiline template —
181
+ // ini limitasi TypeScript, bukan bug library. Gunakan config object untuk type safety.
158
182
  <Card>
159
183
  <Card.header>Judul</Card.header>
160
184
  <Card.body>Konten</Card.body>
161
185
  <Card.footer>Footer</Card.footer>
162
186
  </Card>
163
-
164
- // Autocomplete ✓ — Card.nonexistent akan error TypeScript
165
187
  ```
166
188
 
167
- > **Catatan:** Sub-components **tidak mewarisi** style base `Card`. Mereka adalah komponen terpisah dengan classes dari block `[name] { ... }`. Untuk mewarisi, gunakan `.extend()` (Pattern B di bawah).
189
+ > Sub-components **tidak mewarisi** style base. Untuk mewarisi, pakai `.extend()` (lihat Pattern B di bawah).
168
190
 
169
- **`.withSub<>()`** hanya diperlukan jika nama sub-component tidak muncul di template literal (misalnya di-register secara dinamis):
170
-
171
- ```tsx
172
- // Kasus khusus — nama tidak ada di template
173
- const Modal = tw.div`fixed inset-0`.withSub<"overlay" | "content">()
174
- ```
191
+ > **Sub-component tidak terdefinisi** tidak akan crash library otomatis fallback ke `<span>` passthrough. Tapi tetap gunakan config object untuk catch typo di TypeScript.
175
192
 
176
193
  ### 7. State Engine — Zero-JS State Management
177
194
 
@@ -248,9 +265,22 @@ const IconButton = Button.extend`p-2 rounded-full`
248
265
 
249
266
  ### Pattern Sub-components
250
267
 
251
- **Pattern A: Inline `[name] { }` Direkomendasikan**
268
+ **Pattern A: Config Object Direkomendasikan (autocomplete + type safe)**
269
+
270
+ ```tsx
271
+ const Card = tw.div({
272
+ base: "flex flex-col p-4 rounded-xl bg-white shadow",
273
+ sub: {
274
+ header: "font-bold text-lg border-b pb-2",
275
+ body: "text-gray-600 py-2",
276
+ footer: "border-t pt-2 text-sm",
277
+ },
278
+ })
279
+ // TypeScript infer: Card.header, Card.body, Card.footer ✅ autocomplete
280
+ // Card.xyz → TypeScript error ✅
281
+ ```
252
282
 
253
- Cocok kalau sub-components punya style sendiri yang *tidak perlu* mewarisi base:
283
+ **Pattern A2: Inline Template `[name] { }` Ringkas (tanpa autocomplete)**
254
284
 
255
285
  ```tsx
256
286
  const Card = tw.div`
@@ -259,7 +289,7 @@ const Card = tw.div`
259
289
  [body] { text-gray-600 py-2 }
260
290
  [footer] { border-t pt-2 text-sm }
261
291
  `
262
- // TypeScript otomatis: Card.header, Card.body, Card.footer
292
+ // Runtime benar — TypeScript tidak bisa infer nama dari multiline template literal
263
293
  ```
264
294
 
265
295
  **Pattern B: `.extend()` — Sub-components Mewarisi Style Base**
package/dist/index.d.mts CHANGED
@@ -26,7 +26,12 @@ interface ComponentConfig {
26
26
  state?: Record<string, Record<string, string>>;
27
27
  container?: Record<string, string>;
28
28
  containerName?: string;
29
+ /** Sub-component definitions — keys di-infer otomatis oleh TypeScript */
30
+ sub?: Record<string, string>;
29
31
  }
32
+ type InferSubFromConfig<C extends ComponentConfig> = C extends {
33
+ sub: Record<infer K extends string, string>;
34
+ } ? K : never;
30
35
  interface ContainerConfig {
31
36
  base?: string;
32
37
  queries?: Record<string, string>;
@@ -54,14 +59,14 @@ type TwSubComponentAccessor = React.FC<{
54
59
  children?: React.ReactNode;
55
60
  className?: string;
56
61
  }>;
57
- type TrimLeft<S extends string> = S extends ` ${infer R}` | `
58
- ${infer R}` | ` ${infer R}` | `
62
+ type TrimLeft<S extends string> = S extends ` ${infer R}` | `
63
+ ${infer R}` | ` ${infer R}` | `
59
64
  ${infer R}` ? TrimLeft<R> : S;
60
- type TrimRight<S extends string> = S extends `${infer L} ` | `${infer L}
61
- ` | `${infer L} ` | `${infer L}
65
+ type TrimRight<S extends string> = S extends `${infer L} ` | `${infer L}
66
+ ` | `${infer L} ` | `${infer L}
62
67
  ` ? TrimRight<L> : S;
63
68
  type Trim<S extends string> = TrimLeft<TrimRight<S>>;
64
- type ExtractSubNames<T extends string> = T extends `${infer _}[${infer Name}]${infer Rest}` ? (Trim<Name> extends string ? Trim<Name> : never) | ExtractSubNames<Rest> : never;
69
+ type ExtractSubNames<T extends string> = T extends `${string}[${infer Name}]${string}{${string}}${infer Rest}` ? Trim<Name> | ExtractSubNames<Rest> : T extends `${string}\n${infer Name}{${string}}${infer Rest}` ? (Trim<Name> extends "" ? never : Trim<Name>) | ExtractSubNames<Rest> : never;
65
70
  type SubComponentKeys<S extends string> = string extends S ? {
66
71
  [key: string]: TwSubComponentAccessor;
67
72
  } : {
@@ -80,7 +85,20 @@ type TwStyledComponent<Config extends ComponentConfig = ComponentConfig, S exten
80
85
  }): TwStyledComponent<Config, S>;
81
86
  };
82
87
  withVariants: (config: Partial<Config>) => TwStyledComponent<Config, S>;
83
- withSub<NewS extends string>(): TwStyledComponent<Config, S | NewS>;
88
+ /**
89
+ * Declare sub-component names secara eksplisit untuk autocomplete + type safety.
90
+ *
91
+ * @example
92
+ * export const Button = tw.button`
93
+ * flex h-12 ...
94
+ * icon { flex h-4 }
95
+ * `.withSub<"icon" | "footer">()
96
+ *
97
+ * Button.icon // ✅ autocomplete
98
+ * Button.footer // ✅ autocomplete
99
+ * Button.xyz // ❌ TypeScript error
100
+ */
101
+ withSub<NewS extends string>(): TwStyledComponent<Config, NewS>;
84
102
  animate: (opts: AnimateOptions) => Promise<TwStyledComponent<Config, S>>;
85
103
  } & SubComponentKeys<S>;
86
104
  interface TwSubComponent<P = unknown> {
@@ -88,9 +106,9 @@ interface TwSubComponent<P = unknown> {
88
106
  displayName?: string;
89
107
  }
90
108
  interface TwTemplateFactory<Config extends ComponentConfig = ComponentConfig> {
91
- <const T extends string>(strings: readonly [T, ...unknown[]], ...exprs: unknown[]): TwStyledComponent<Config, ExtractSubNames<T>>;
109
+ <const T extends string>(strings: readonly [T], ...exprs: []): TwStyledComponent<Config, ExtractSubNames<T>>;
92
110
  (strings: TemplateStringsArray, ...exprs: unknown[]): TwStyledComponent<Config, string>;
93
- <C extends ComponentConfig>(config: C): TwStyledComponent<C, string>;
111
+ <C extends ComponentConfig>(config: C): TwStyledComponent<C, InferSubFromConfig<C>>;
94
112
  }
95
113
  type TwTagFactory = {
96
114
  [K in HtmlTagName]: TwTemplateFactory;
package/dist/index.d.ts CHANGED
@@ -26,7 +26,12 @@ interface ComponentConfig {
26
26
  state?: Record<string, Record<string, string>>;
27
27
  container?: Record<string, string>;
28
28
  containerName?: string;
29
+ /** Sub-component definitions — keys di-infer otomatis oleh TypeScript */
30
+ sub?: Record<string, string>;
29
31
  }
32
+ type InferSubFromConfig<C extends ComponentConfig> = C extends {
33
+ sub: Record<infer K extends string, string>;
34
+ } ? K : never;
30
35
  interface ContainerConfig {
31
36
  base?: string;
32
37
  queries?: Record<string, string>;
@@ -54,14 +59,14 @@ type TwSubComponentAccessor = React.FC<{
54
59
  children?: React.ReactNode;
55
60
  className?: string;
56
61
  }>;
57
- type TrimLeft<S extends string> = S extends ` ${infer R}` | `
58
- ${infer R}` | ` ${infer R}` | `
62
+ type TrimLeft<S extends string> = S extends ` ${infer R}` | `
63
+ ${infer R}` | ` ${infer R}` | `
59
64
  ${infer R}` ? TrimLeft<R> : S;
60
- type TrimRight<S extends string> = S extends `${infer L} ` | `${infer L}
61
- ` | `${infer L} ` | `${infer L}
65
+ type TrimRight<S extends string> = S extends `${infer L} ` | `${infer L}
66
+ ` | `${infer L} ` | `${infer L}
62
67
  ` ? TrimRight<L> : S;
63
68
  type Trim<S extends string> = TrimLeft<TrimRight<S>>;
64
- type ExtractSubNames<T extends string> = T extends `${infer _}[${infer Name}]${infer Rest}` ? (Trim<Name> extends string ? Trim<Name> : never) | ExtractSubNames<Rest> : never;
69
+ type ExtractSubNames<T extends string> = T extends `${string}[${infer Name}]${string}{${string}}${infer Rest}` ? Trim<Name> | ExtractSubNames<Rest> : T extends `${string}\n${infer Name}{${string}}${infer Rest}` ? (Trim<Name> extends "" ? never : Trim<Name>) | ExtractSubNames<Rest> : never;
65
70
  type SubComponentKeys<S extends string> = string extends S ? {
66
71
  [key: string]: TwSubComponentAccessor;
67
72
  } : {
@@ -80,7 +85,20 @@ type TwStyledComponent<Config extends ComponentConfig = ComponentConfig, S exten
80
85
  }): TwStyledComponent<Config, S>;
81
86
  };
82
87
  withVariants: (config: Partial<Config>) => TwStyledComponent<Config, S>;
83
- withSub<NewS extends string>(): TwStyledComponent<Config, S | NewS>;
88
+ /**
89
+ * Declare sub-component names secara eksplisit untuk autocomplete + type safety.
90
+ *
91
+ * @example
92
+ * export const Button = tw.button`
93
+ * flex h-12 ...
94
+ * icon { flex h-4 }
95
+ * `.withSub<"icon" | "footer">()
96
+ *
97
+ * Button.icon // ✅ autocomplete
98
+ * Button.footer // ✅ autocomplete
99
+ * Button.xyz // ❌ TypeScript error
100
+ */
101
+ withSub<NewS extends string>(): TwStyledComponent<Config, NewS>;
84
102
  animate: (opts: AnimateOptions) => Promise<TwStyledComponent<Config, S>>;
85
103
  } & SubComponentKeys<S>;
86
104
  interface TwSubComponent<P = unknown> {
@@ -88,9 +106,9 @@ interface TwSubComponent<P = unknown> {
88
106
  displayName?: string;
89
107
  }
90
108
  interface TwTemplateFactory<Config extends ComponentConfig = ComponentConfig> {
91
- <const T extends string>(strings: readonly [T, ...unknown[]], ...exprs: unknown[]): TwStyledComponent<Config, ExtractSubNames<T>>;
109
+ <const T extends string>(strings: readonly [T], ...exprs: []): TwStyledComponent<Config, ExtractSubNames<T>>;
92
110
  (strings: TemplateStringsArray, ...exprs: unknown[]): TwStyledComponent<Config, string>;
93
- <C extends ComponentConfig>(config: C): TwStyledComponent<C, string>;
111
+ <C extends ComponentConfig>(config: C): TwStyledComponent<C, InferSubFromConfig<C>>;
94
112
  }
95
113
  type TwTagFactory = {
96
114
  [K in HtmlTagName]: TwTemplateFactory;
package/dist/index.js CHANGED
@@ -341,28 +341,44 @@ function getStateRegistry() {
341
341
 
342
342
  // packages/domain/core/src/createComponent.ts
343
343
  var ALWAYS_BLOCKED = /* @__PURE__ */ new Set(["base", "_ref", "state", "container", "containerName"]);
344
- function parseSubComponentNames(template) {
345
- const matches = [...template.matchAll(/\[(\w+)\]/g)];
346
- return [...new Set(matches.map((m) => m[1]))];
344
+ function parseSubComponentBlocks(template) {
345
+ const map = /* @__PURE__ */ new Map();
346
+ const re = /(?:\[([a-zA-Z][a-zA-Z0-9_-]*)\]|([a-zA-Z][a-zA-Z0-9_-]*))\s*\{([^}]*)\}/g;
347
+ let m;
348
+ while ((m = re.exec(template)) !== null) {
349
+ const name = m[1] ?? m[2];
350
+ const classes = m[3].trim().replace(/\s+/g, " ");
351
+ if (classes) map.set(name, classes);
352
+ }
353
+ return map;
347
354
  }
348
355
  function extractBaseClasses(template) {
349
- return template.replace(/\[\w+\]\s*\{[^}]*\}/g, "").replace(/\s+/g, " ").trim();
356
+ return template.replace(/(?:\[[a-zA-Z][a-zA-Z0-9_-]*\]|[a-zA-Z][a-zA-Z0-9_-]*)\s*\{[^}]*\}/g, "").replace(/\s+/g, " ").trim();
350
357
  }
351
- function createSubComponentAccessor(parentDisplayName, name) {
358
+ function createSubComponentAccessor(parentDisplayName, name, classes) {
352
359
  const SubComponent = ({
353
360
  children,
354
361
  className
355
- }) => React3__default.default.createElement(React3__default.default.Fragment, null, children);
362
+ }) => React3__default.default.createElement(
363
+ "span",
364
+ { className: className ? `${classes} ${className}` : classes },
365
+ children
366
+ );
356
367
  SubComponent.displayName = `${parentDisplayName}[${name}]`;
357
368
  return SubComponent;
358
369
  }
359
- function registerSubComponents(component, template) {
360
- const names = parseSubComponentNames(template);
370
+ function registerSubComponents(component, template, configSub) {
361
371
  const displayName = component.displayName ?? "tw";
362
372
  const map = component;
363
- for (const name of names) {
373
+ if (configSub) {
374
+ for (const [name, classes] of Object.entries(configSub)) {
375
+ map[name] = createSubComponentAccessor(displayName, name, classes.trim().replace(/\s+/g, " "));
376
+ }
377
+ }
378
+ const blocks = parseSubComponentBlocks(template);
379
+ for (const [name, classes] of blocks) {
364
380
  if (!(name in map)) {
365
- map[name] = createSubComponentAccessor(displayName, name);
381
+ map[name] = createSubComponentAccessor(displayName, name, classes);
366
382
  }
367
383
  }
368
384
  }
@@ -413,13 +429,21 @@ function carryOverSubComponents(target, source) {
413
429
  function attachExtend(component, originalTag, base, config) {
414
430
  function extendWithClasses(stringsOrConfig) {
415
431
  if (Array.isArray(stringsOrConfig) && "raw" in stringsOrConfig) {
416
- const extra = stringsOrConfig.raw.join("").trim().replace(/\s+/g, " ");
417
- const merged2 = twMerge(extractBaseClasses(base), extra);
432
+ const rawExtra = stringsOrConfig.raw.join("").trim().replace(/\s+/g, " ");
433
+ const merged2 = twMerge(extractBaseClasses(base), extractBaseClasses(rawExtra));
418
434
  const extended2 = createComponent(
419
435
  originalTag,
420
436
  typeof config === "string" ? merged2 : { ...config, base: merged2 }
421
437
  );
422
438
  carryOverSubComponents(extended2, component);
439
+ const extendSubBlocks = parseSubComponentBlocks(rawExtra);
440
+ if (extendSubBlocks.size > 0) {
441
+ const extComp = extended2;
442
+ const displayName = extended2.displayName ?? "tw";
443
+ for (const [subName, subClasses] of extendSubBlocks) {
444
+ extComp[subName] = createSubComponentAccessor(displayName, subName, subClasses);
445
+ }
446
+ }
423
447
  return extended2;
424
448
  }
425
449
  const extCfg = stringsOrConfig;
@@ -477,6 +501,7 @@ function createComponent(tag, config) {
477
501
  const stateConfig = typeof config === "string" ? void 0 : config.state;
478
502
  const containerConfig = typeof config === "string" ? void 0 : config.container;
479
503
  const containerName = typeof config === "string" ? void 0 : config.containerName;
504
+ const configSub = typeof config === "string" ? void 0 : config.sub;
480
505
  const stateResult = stateConfig ? processState(typeof tag === "string" ? tag : "component", stateConfig) : null;
481
506
  const containerResult = containerConfig ? processContainer(typeof tag === "string" ? tag : "component", containerConfig, containerName) : null;
482
507
  const engineClasses = [stateResult?.stateClass, containerResult?.containerClass].filter(Boolean).join(" ");
@@ -495,8 +520,8 @@ function createComponent(tag, config) {
495
520
  const component2 = baseComponent2;
496
521
  component2.displayName = `tw.${tagLabel}`;
497
522
  const result2 = attachExtend(component2, tag, base, config);
498
- registerSubComponents(result2, base);
499
- return result2;
523
+ registerSubComponents(result2, base, configSub);
524
+ return wrapWithSubProxy(result2, tagLabel);
500
525
  }
501
526
  const baseComponent = React3__default.default.forwardRef((props, ref) => {
502
527
  const { className, ...rest } = props;
@@ -512,8 +537,36 @@ function createComponent(tag, config) {
512
537
  const component = baseComponent;
513
538
  component.displayName = `tw.${tagLabel}`;
514
539
  const result = attachExtend(component, tag, base, config);
515
- registerSubComponents(result, base);
516
- return result;
540
+ registerSubComponents(result, base, configSub);
541
+ return wrapWithSubProxy(result, tagLabel);
542
+ }
543
+ var SKIP_PROXY_KEYS = /* @__PURE__ */ new Set([
544
+ "extend",
545
+ "withVariants",
546
+ "animate",
547
+ "withSub",
548
+ "displayName",
549
+ "$$typeof",
550
+ "render",
551
+ "prototype",
552
+ "__esModule",
553
+ "then"
554
+ ]);
555
+ function wrapWithSubProxy(component, tagLabel) {
556
+ return new Proxy(component, {
557
+ get(target, prop) {
558
+ const value = target[prop];
559
+ if (value !== void 0) return value;
560
+ if (typeof prop === "symbol") return value;
561
+ if (SKIP_PROXY_KEYS.has(prop)) return value;
562
+ const Fallback = ({
563
+ children,
564
+ className
565
+ }) => React3__default.default.createElement("span", { className }, children);
566
+ Fallback.displayName = `tw.${tagLabel}.${prop}(fallback)`;
567
+ return Fallback;
568
+ }
569
+ });
517
570
  }
518
571
  var __generatedRegistry = {};
519
572
  function lookupGenerated(componentId, props, defaultVariants) {
@@ -1008,7 +1061,7 @@ function createStyledSystem(config) {
1008
1061
  tokens
1009
1062
  });
1010
1063
  }
1011
- var SUB_RE = /\[([a-zA-Z][a-zA-Z0-9_-]*)\]\s*\{([^}]*)\}/g;
1064
+ var SUB_RE = /(?:\[([a-zA-Z][a-zA-Z0-9_-]*)\]|([a-zA-Z][a-zA-Z0-9_-]*))\s*\{([^}]*)\}/g;
1012
1065
  var COMMENT_RE = /\/\/[^\n]*/g;
1013
1066
  function parseTemplate(strings, exprs) {
1014
1067
  const raw = strings.raw.reduce((acc, str, i) => {
@@ -1021,8 +1074,8 @@ function parseTemplate(strings, exprs) {
1021
1074
  let match;
1022
1075
  SUB_RE.lastIndex = 0;
1023
1076
  while ((match = SUB_RE.exec(raw)) !== null) {
1024
- const name = match[1];
1025
- const inner = match[2].replace(COMMENT_RE, "").split("\n").map((l) => l.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
1077
+ const name = match[1] ?? match[2];
1078
+ const inner = match[3].replace(COMMENT_RE, "").split("\n").map((l) => l.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
1026
1079
  subs[name] = inner;
1027
1080
  base = base.replace(match[0], "");
1028
1081
  }