@kbach/react 0.6.11 → 0.6.13

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
@@ -362,6 +362,18 @@ Nested groups need names, or the inner element reacts to whichever `.group` is n
362
362
  <div className="text-[18px]" />
363
363
  ```
364
364
 
365
+ ### Arbitrary properties (web only)
366
+
367
+ For the rare CSS property with no named utility, write `[property:value]` directly — no utility prefix:
368
+
369
+ ```jsx
370
+ <div className="[mask-type:luminance]" />
371
+ <div className="[-webkit-line-clamp:3]" />
372
+ <div className="[--my-var:10px]" /> {/* CSS custom property */}
373
+ ```
374
+
375
+ Underscores become spaces, same as any other arbitrary value: `[background:url(/a.png)_no-repeat]`. Only the first `:` splits the property from the value, so a value that itself contains a colon (a URL's `http://`) still comes through intact.
376
+
365
377
  ## Color system
366
378
 
367
379
  12-shade scale — 1 lightest, 12 darkest. Use as `bg-blue-6`, `text-gray-10`, `border-red-4/50`.
@@ -406,6 +418,15 @@ module.exports = {
406
418
  sans: 'Inter, sans-serif',
407
419
  'sans-bold': 'Inter_700Bold, sans-serif',
408
420
  },
421
+ // Custom @keyframes (web only) — reference by name from `animation`,
422
+ // then use that name as animate-{name}. Declaration values are plain
423
+ // CSS strings, same idea as a style object but for a keyframe step.
424
+ keyframes: {
425
+ wiggle: { '0%, 100%': { transform: 'rotate(-3deg)' }, '50%': { transform: 'rotate(3deg)' } },
426
+ },
427
+ animation: {
428
+ wiggle: 'wiggle 1s ease-in-out infinite',
429
+ },
409
430
  },
410
431
 
411
432
  plugins: [
@@ -422,6 +443,12 @@ module.exports = {
422
443
 
423
444
  Setting `fontFamily.sans` to anything other than `'System'` auto-injects `body { font-family: … }`.
424
445
 
446
+ ```jsx
447
+ <div className="animate-wiggle" />
448
+ {/* Override duration/timing/repeat inline, keeps the same wiggle @keyframes: */}
449
+ <div className="animate-[wiggle_2s_ease-in-out]" />
450
+ ```
451
+
425
452
  Color aliases:
426
453
 
427
454
  ```js
@@ -23,7 +23,7 @@ import {
23
23
  transformToWebProps,
24
24
  useConditionalWidth,
25
25
  useGlobalDarkMode
26
- } from "./chunk-OWJUMH2V.mjs";
26
+ } from "./chunk-HCRWLOJE.mjs";
27
27
 
28
28
  // src/jsx-runtime.tsx
29
29
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -540,7 +540,13 @@ var defaultTheme = {
540
540
  xl: 1280,
541
541
  // desktop
542
542
  "2xl": 1536
543
- }
543
+ },
544
+ // Empty by default — the 4 built-in presets (spin/ping/pulse/bounce) are handled
545
+ // directly in the `animate` resolver, not through this theme table. This exists so
546
+ // user-defined ones (kbach.config.js theme.extend.keyframes/animation) merge in
547
+ // alongside them without needing to redeclare the built-ins.
548
+ keyframes: {},
549
+ animation: {}
544
550
  };
545
551
 
546
552
  // src/core/platform.ts
@@ -1240,6 +1246,44 @@ var BORDER_SIDE_PROPS = {
1240
1246
  "border-b": ["borderBottomWidth", "borderBottomColor"],
1241
1247
  "border-l": ["borderLeftWidth", "borderLeftColor"]
1242
1248
  };
1249
+ function keyframeDeclToCSS(decl) {
1250
+ return Object.entries(decl).map(([prop, val]) => `${prop.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${val}`).join("; ");
1251
+ }
1252
+ function buildKeyframeCSS(name, steps) {
1253
+ const body = Object.entries(steps).map(([selector, decl]) => `${selector} { ${keyframeDeclToCSS(decl)} }`).join(" ");
1254
+ return `@keyframes ${name} { ${body} }`;
1255
+ }
1256
+ var BUILTIN_ANIMATIONS = {
1257
+ none: { animation: "none" },
1258
+ spin: {
1259
+ animation: "kb-spin 1s linear infinite",
1260
+ __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1261
+ },
1262
+ ping: {
1263
+ animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1264
+ __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1265
+ },
1266
+ pulse: {
1267
+ animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1268
+ __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1269
+ },
1270
+ bounce: {
1271
+ animation: "kb-bounce 1s infinite",
1272
+ __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
1273
+ }
1274
+ };
1275
+ function buildAnimationValue(animation, keyframes) {
1276
+ const words = animation.trim().split(/\s+/);
1277
+ const name = words[0];
1278
+ if (!name) return { animation };
1279
+ const builtin = BUILTIN_ANIMATIONS[name];
1280
+ if (builtin?.__keyframe) {
1281
+ const kbName = builtin.animation.split(/\s+/, 1)[0];
1282
+ return { animation: [kbName, ...words.slice(1)].join(" "), __keyframe: builtin.__keyframe };
1283
+ }
1284
+ const steps = keyframes?.[name];
1285
+ return steps ? { animation, __keyframe: buildKeyframeCSS(name, steps) } : { animation };
1286
+ }
1243
1287
  var RESOLVERS = {
1244
1288
  // ── Background ─────────────────────────────────────────────────────────────
1245
1289
  bg: ({ value, isArbitrary }, { colors }) => {
@@ -1793,6 +1837,38 @@ var RESOLVERS = {
1793
1837
  };
1794
1838
  return offsets[value] ? { textUnderlineOffset: offsets[value] } : null;
1795
1839
  },
1840
+ // ── Content (web-only — for before:/after: pseudo-elements, which don't exist on native) ──
1841
+ // Arbitrary value carries its own quotes from the bracket syntax (content-['*'] parses to
1842
+ // the literal string 'you can see it' quotes included), so it passes straight through as a
1843
+ // valid `content: '*'` CSS value with no extra wrapping needed.
1844
+ content: ({ value, isArbitrary }) => {
1845
+ if (!getEffectiveIsWeb()) return null;
1846
+ if (isArbitrary) return { content: value };
1847
+ const presets = { none: "none" };
1848
+ return value in presets ? { content: presets[value] } : null;
1849
+ },
1850
+ // ── Arbitrary CSS property (web-only): [property:value] with no utility prefix ──
1851
+ // e.g. [mask-type:luminance], [--my-var:10px]. parser.ts already isolates this case
1852
+ // as utility: '' — it's ALSO how a stray, prefix-less negative bracket ("-[10px]",
1853
+ // presumably a typo) parses, which is why this only fires when the arbitrary value
1854
+ // itself contains a "property:value" pair; anything else (no colon) still resolves
1855
+ // to null exactly as before this resolver existed, rather than guessing a property.
1856
+ //
1857
+ // Only the FIRST colon splits property from value — the value itself may contain
1858
+ // more colons (a URL's "http://…") and must stay intact: [background:url(http://x/a.png)].
1859
+ // camelToKebab() in resolver.ts is a no-op on an already-kebab-case (or "--"-prefixed
1860
+ // custom property) key, so it can be used as the style object key as-written with no
1861
+ // extra case conversion — it only needs to look like a plausible CSS property name.
1862
+ "": ({ value, isArbitrary, negative }) => {
1863
+ if (!getEffectiveIsWeb() || !isArbitrary || !value) return null;
1864
+ if (negative) return null;
1865
+ const colonIdx = value.indexOf(":");
1866
+ if (colonIdx <= 0) return null;
1867
+ const prop = value.slice(0, colonIdx).trim();
1868
+ const cssValue = value.slice(colonIdx + 1).trim();
1869
+ if (!cssValue || !/^(--[\w-]+|[a-zA-Z-]+)$/.test(prop)) return null;
1870
+ return { [prop]: cssValue };
1871
+ },
1796
1872
  // ── Outline extended (web-only) ───────────────────────────────────────────
1797
1873
  outline: ({ value, isArbitrary }, { colors }) => {
1798
1874
  if (!getEffectiveIsWeb()) return null;
@@ -1986,30 +2062,12 @@ var RESOLVERS = {
1986
2062
  // Each variant injects a @keyframes rule once via the __keyframe marker.
1987
2063
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
1988
2064
  // so it never reaches element inline styles.
1989
- animate: ({ value, isArbitrary }) => {
1990
- if (!getEffectiveIsWeb()) return null;
1991
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
1992
- const defs = {
1993
- none: { animation: "none" },
1994
- spin: {
1995
- animation: "kb-spin 1s linear infinite",
1996
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1997
- },
1998
- ping: {
1999
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
2000
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
2001
- },
2002
- pulse: {
2003
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
2004
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
2005
- },
2006
- bounce: {
2007
- animation: "kb-bounce 1s infinite",
2008
- __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
2009
- }
2010
- };
2011
- const def = defs[value];
2012
- return def ? def : null;
2065
+ animate: ({ value, isArbitrary }, theme) => {
2066
+ if (!getEffectiveIsWeb()) return null;
2067
+ if (isArbitrary) return buildAnimationValue(value.replace(/_/g, " "), theme.keyframes);
2068
+ if (value in BUILTIN_ANIMATIONS) return BUILTIN_ANIMATIONS[value];
2069
+ const custom = theme.animation?.[value];
2070
+ return custom ? buildAnimationValue(custom, theme.keyframes) : null;
2013
2071
  },
2014
2072
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
2015
2073
  "line-clamp": ({ value }) => {
package/dist/index.d.mts CHANGED
@@ -90,6 +90,23 @@ interface ThemeConfig {
90
90
  flex: Record<string, number | string>;
91
91
  shadow: Record<string, StyleValue>;
92
92
  screens: Record<string, string | number>;
93
+ /**
94
+ * Custom @keyframes, web only. Each key is a keyframe name, its value maps
95
+ * percentage/from/to selectors to a plain CSS declaration object (camelCase
96
+ * properties, same shape as an inline style object):
97
+ * keyframes: { wiggle: { '0%, 100%': { transform: 'rotate(-3deg)' }, '50%': { transform: 'rotate(3deg)' } } }
98
+ * Referenced from `animation` below, or directly via animate-[wiggle_1s_ease-in-out].
99
+ */
100
+ keyframes: Record<string, Record<string, StyleValue>>;
101
+ /**
102
+ * Named animation shorthands built on `keyframes` above, referenced via
103
+ * animate-{name} (e.g. animate-wiggle):
104
+ * animation: { wiggle: 'wiggle 1s ease-in-out infinite' }
105
+ * The first word must match a `keyframes` key so its @keyframes rule can be
106
+ * injected alongside the animation — a name with no matching keyframes entry
107
+ * still sets the `animation` CSS property, it just won't animate anything.
108
+ */
109
+ animation: Record<string, string>;
93
110
  [key: string]: unknown;
94
111
  }
95
112
  /**
package/dist/index.d.ts CHANGED
@@ -90,6 +90,23 @@ interface ThemeConfig {
90
90
  flex: Record<string, number | string>;
91
91
  shadow: Record<string, StyleValue>;
92
92
  screens: Record<string, string | number>;
93
+ /**
94
+ * Custom @keyframes, web only. Each key is a keyframe name, its value maps
95
+ * percentage/from/to selectors to a plain CSS declaration object (camelCase
96
+ * properties, same shape as an inline style object):
97
+ * keyframes: { wiggle: { '0%, 100%': { transform: 'rotate(-3deg)' }, '50%': { transform: 'rotate(3deg)' } } }
98
+ * Referenced from `animation` below, or directly via animate-[wiggle_1s_ease-in-out].
99
+ */
100
+ keyframes: Record<string, Record<string, StyleValue>>;
101
+ /**
102
+ * Named animation shorthands built on `keyframes` above, referenced via
103
+ * animate-{name} (e.g. animate-wiggle):
104
+ * animation: { wiggle: 'wiggle 1s ease-in-out infinite' }
105
+ * The first word must match a `keyframes` key so its @keyframes rule can be
106
+ * injected alongside the animation — a name with no matching keyframes entry
107
+ * still sets the `animation` CSS property, it just won't animate anything.
108
+ */
109
+ animation: Record<string, string>;
93
110
  [key: string]: unknown;
94
111
  }
95
112
  /**
package/dist/index.js CHANGED
@@ -625,7 +625,13 @@ var defaultTheme = {
625
625
  xl: 1280,
626
626
  // desktop
627
627
  "2xl": 1536
628
- }
628
+ },
629
+ // Empty by default — the 4 built-in presets (spin/ping/pulse/bounce) are handled
630
+ // directly in the `animate` resolver, not through this theme table. This exists so
631
+ // user-defined ones (kbach.config.js theme.extend.keyframes/animation) merge in
632
+ // alongside them without needing to redeclare the built-ins.
633
+ keyframes: {},
634
+ animation: {}
629
635
  };
630
636
 
631
637
  // src/core/reset.ts
@@ -1401,6 +1407,44 @@ var BORDER_SIDE_PROPS = {
1401
1407
  "border-b": ["borderBottomWidth", "borderBottomColor"],
1402
1408
  "border-l": ["borderLeftWidth", "borderLeftColor"]
1403
1409
  };
1410
+ function keyframeDeclToCSS(decl) {
1411
+ return Object.entries(decl).map(([prop, val]) => `${prop.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${val}`).join("; ");
1412
+ }
1413
+ function buildKeyframeCSS(name, steps) {
1414
+ const body = Object.entries(steps).map(([selector, decl]) => `${selector} { ${keyframeDeclToCSS(decl)} }`).join(" ");
1415
+ return `@keyframes ${name} { ${body} }`;
1416
+ }
1417
+ var BUILTIN_ANIMATIONS = {
1418
+ none: { animation: "none" },
1419
+ spin: {
1420
+ animation: "kb-spin 1s linear infinite",
1421
+ __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1422
+ },
1423
+ ping: {
1424
+ animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1425
+ __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1426
+ },
1427
+ pulse: {
1428
+ animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1429
+ __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1430
+ },
1431
+ bounce: {
1432
+ animation: "kb-bounce 1s infinite",
1433
+ __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
1434
+ }
1435
+ };
1436
+ function buildAnimationValue(animation, keyframes) {
1437
+ const words = animation.trim().split(/\s+/);
1438
+ const name = words[0];
1439
+ if (!name) return { animation };
1440
+ const builtin = BUILTIN_ANIMATIONS[name];
1441
+ if (builtin?.__keyframe) {
1442
+ const kbName = builtin.animation.split(/\s+/, 1)[0];
1443
+ return { animation: [kbName, ...words.slice(1)].join(" "), __keyframe: builtin.__keyframe };
1444
+ }
1445
+ const steps = keyframes?.[name];
1446
+ return steps ? { animation, __keyframe: buildKeyframeCSS(name, steps) } : { animation };
1447
+ }
1404
1448
  var RESOLVERS = {
1405
1449
  // ── Background ─────────────────────────────────────────────────────────────
1406
1450
  bg: ({ value, isArbitrary }, { colors }) => {
@@ -1954,6 +1998,38 @@ var RESOLVERS = {
1954
1998
  };
1955
1999
  return offsets[value] ? { textUnderlineOffset: offsets[value] } : null;
1956
2000
  },
2001
+ // ── Content (web-only — for before:/after: pseudo-elements, which don't exist on native) ──
2002
+ // Arbitrary value carries its own quotes from the bracket syntax (content-['*'] parses to
2003
+ // the literal string 'you can see it' quotes included), so it passes straight through as a
2004
+ // valid `content: '*'` CSS value with no extra wrapping needed.
2005
+ content: ({ value, isArbitrary }) => {
2006
+ if (!getEffectiveIsWeb()) return null;
2007
+ if (isArbitrary) return { content: value };
2008
+ const presets = { none: "none" };
2009
+ return value in presets ? { content: presets[value] } : null;
2010
+ },
2011
+ // ── Arbitrary CSS property (web-only): [property:value] with no utility prefix ──
2012
+ // e.g. [mask-type:luminance], [--my-var:10px]. parser.ts already isolates this case
2013
+ // as utility: '' — it's ALSO how a stray, prefix-less negative bracket ("-[10px]",
2014
+ // presumably a typo) parses, which is why this only fires when the arbitrary value
2015
+ // itself contains a "property:value" pair; anything else (no colon) still resolves
2016
+ // to null exactly as before this resolver existed, rather than guessing a property.
2017
+ //
2018
+ // Only the FIRST colon splits property from value — the value itself may contain
2019
+ // more colons (a URL's "http://…") and must stay intact: [background:url(http://x/a.png)].
2020
+ // camelToKebab() in resolver.ts is a no-op on an already-kebab-case (or "--"-prefixed
2021
+ // custom property) key, so it can be used as the style object key as-written with no
2022
+ // extra case conversion — it only needs to look like a plausible CSS property name.
2023
+ "": ({ value, isArbitrary, negative }) => {
2024
+ if (!getEffectiveIsWeb() || !isArbitrary || !value) return null;
2025
+ if (negative) return null;
2026
+ const colonIdx = value.indexOf(":");
2027
+ if (colonIdx <= 0) return null;
2028
+ const prop = value.slice(0, colonIdx).trim();
2029
+ const cssValue = value.slice(colonIdx + 1).trim();
2030
+ if (!cssValue || !/^(--[\w-]+|[a-zA-Z-]+)$/.test(prop)) return null;
2031
+ return { [prop]: cssValue };
2032
+ },
1957
2033
  // ── Outline extended (web-only) ───────────────────────────────────────────
1958
2034
  outline: ({ value, isArbitrary }, { colors }) => {
1959
2035
  if (!getEffectiveIsWeb()) return null;
@@ -2147,30 +2223,12 @@ var RESOLVERS = {
2147
2223
  // Each variant injects a @keyframes rule once via the __keyframe marker.
2148
2224
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
2149
2225
  // so it never reaches element inline styles.
2150
- animate: ({ value, isArbitrary }) => {
2226
+ animate: ({ value, isArbitrary }, theme) => {
2151
2227
  if (!getEffectiveIsWeb()) return null;
2152
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
2153
- const defs = {
2154
- none: { animation: "none" },
2155
- spin: {
2156
- animation: "kb-spin 1s linear infinite",
2157
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
2158
- },
2159
- ping: {
2160
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
2161
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
2162
- },
2163
- pulse: {
2164
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
2165
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
2166
- },
2167
- bounce: {
2168
- animation: "kb-bounce 1s infinite",
2169
- __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
2170
- }
2171
- };
2172
- const def = defs[value];
2173
- return def ? def : null;
2228
+ if (isArbitrary) return buildAnimationValue(value.replace(/_/g, " "), theme.keyframes);
2229
+ if (value in BUILTIN_ANIMATIONS) return BUILTIN_ANIMATIONS[value];
2230
+ const custom = theme.animation?.[value];
2231
+ return custom ? buildAnimationValue(custom, theme.keyframes) : null;
2174
2232
  },
2175
2233
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
2176
2234
  "line-clamp": ({ value }) => {
package/dist/index.mjs CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  useGlobalDarkMode,
40
40
  useGlobalWidth,
41
41
  useSyncExternalStore
42
- } from "./chunk-OWJUMH2V.mjs";
42
+ } from "./chunk-HCRWLOJE.mjs";
43
43
 
44
44
  // src/context.tsx
45
45
  import { createContext, useContext } from "react";
@@ -580,7 +580,13 @@ var defaultTheme = {
580
580
  xl: 1280,
581
581
  // desktop
582
582
  "2xl": 1536
583
- }
583
+ },
584
+ // Empty by default — the 4 built-in presets (spin/ping/pulse/bounce) are handled
585
+ // directly in the `animate` resolver, not through this theme table. This exists so
586
+ // user-defined ones (kbach.config.js theme.extend.keyframes/animation) merge in
587
+ // alongside them without needing to redeclare the built-ins.
588
+ keyframes: {},
589
+ animation: {}
584
590
  };
585
591
 
586
592
  // src/core/reset.ts
@@ -1359,6 +1365,44 @@ var BORDER_SIDE_PROPS = {
1359
1365
  "border-b": ["borderBottomWidth", "borderBottomColor"],
1360
1366
  "border-l": ["borderLeftWidth", "borderLeftColor"]
1361
1367
  };
1368
+ function keyframeDeclToCSS(decl) {
1369
+ return Object.entries(decl).map(([prop, val]) => `${prop.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${val}`).join("; ");
1370
+ }
1371
+ function buildKeyframeCSS(name, steps) {
1372
+ const body = Object.entries(steps).map(([selector, decl]) => `${selector} { ${keyframeDeclToCSS(decl)} }`).join(" ");
1373
+ return `@keyframes ${name} { ${body} }`;
1374
+ }
1375
+ var BUILTIN_ANIMATIONS = {
1376
+ none: { animation: "none" },
1377
+ spin: {
1378
+ animation: "kb-spin 1s linear infinite",
1379
+ __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1380
+ },
1381
+ ping: {
1382
+ animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1383
+ __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1384
+ },
1385
+ pulse: {
1386
+ animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1387
+ __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1388
+ },
1389
+ bounce: {
1390
+ animation: "kb-bounce 1s infinite",
1391
+ __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
1392
+ }
1393
+ };
1394
+ function buildAnimationValue(animation, keyframes) {
1395
+ const words = animation.trim().split(/\s+/);
1396
+ const name = words[0];
1397
+ if (!name) return { animation };
1398
+ const builtin = BUILTIN_ANIMATIONS[name];
1399
+ if (builtin?.__keyframe) {
1400
+ const kbName = builtin.animation.split(/\s+/, 1)[0];
1401
+ return { animation: [kbName, ...words.slice(1)].join(" "), __keyframe: builtin.__keyframe };
1402
+ }
1403
+ const steps = keyframes?.[name];
1404
+ return steps ? { animation, __keyframe: buildKeyframeCSS(name, steps) } : { animation };
1405
+ }
1362
1406
  var RESOLVERS = {
1363
1407
  // ── Background ─────────────────────────────────────────────────────────────
1364
1408
  bg: ({ value, isArbitrary }, { colors }) => {
@@ -1912,6 +1956,38 @@ var RESOLVERS = {
1912
1956
  };
1913
1957
  return offsets[value] ? { textUnderlineOffset: offsets[value] } : null;
1914
1958
  },
1959
+ // ── Content (web-only — for before:/after: pseudo-elements, which don't exist on native) ──
1960
+ // Arbitrary value carries its own quotes from the bracket syntax (content-['*'] parses to
1961
+ // the literal string 'you can see it' quotes included), so it passes straight through as a
1962
+ // valid `content: '*'` CSS value with no extra wrapping needed.
1963
+ content: ({ value, isArbitrary }) => {
1964
+ if (!getEffectiveIsWeb()) return null;
1965
+ if (isArbitrary) return { content: value };
1966
+ const presets = { none: "none" };
1967
+ return value in presets ? { content: presets[value] } : null;
1968
+ },
1969
+ // ── Arbitrary CSS property (web-only): [property:value] with no utility prefix ──
1970
+ // e.g. [mask-type:luminance], [--my-var:10px]. parser.ts already isolates this case
1971
+ // as utility: '' — it's ALSO how a stray, prefix-less negative bracket ("-[10px]",
1972
+ // presumably a typo) parses, which is why this only fires when the arbitrary value
1973
+ // itself contains a "property:value" pair; anything else (no colon) still resolves
1974
+ // to null exactly as before this resolver existed, rather than guessing a property.
1975
+ //
1976
+ // Only the FIRST colon splits property from value — the value itself may contain
1977
+ // more colons (a URL's "http://…") and must stay intact: [background:url(http://x/a.png)].
1978
+ // camelToKebab() in resolver.ts is a no-op on an already-kebab-case (or "--"-prefixed
1979
+ // custom property) key, so it can be used as the style object key as-written with no
1980
+ // extra case conversion — it only needs to look like a plausible CSS property name.
1981
+ "": ({ value, isArbitrary, negative }) => {
1982
+ if (!getEffectiveIsWeb() || !isArbitrary || !value) return null;
1983
+ if (negative) return null;
1984
+ const colonIdx = value.indexOf(":");
1985
+ if (colonIdx <= 0) return null;
1986
+ const prop = value.slice(0, colonIdx).trim();
1987
+ const cssValue = value.slice(colonIdx + 1).trim();
1988
+ if (!cssValue || !/^(--[\w-]+|[a-zA-Z-]+)$/.test(prop)) return null;
1989
+ return { [prop]: cssValue };
1990
+ },
1915
1991
  // ── Outline extended (web-only) ───────────────────────────────────────────
1916
1992
  outline: ({ value, isArbitrary }, { colors }) => {
1917
1993
  if (!getEffectiveIsWeb()) return null;
@@ -2105,30 +2181,12 @@ var RESOLVERS = {
2105
2181
  // Each variant injects a @keyframes rule once via the __keyframe marker.
2106
2182
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
2107
2183
  // so it never reaches element inline styles.
2108
- animate: ({ value, isArbitrary }) => {
2109
- if (!getEffectiveIsWeb()) return null;
2110
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
2111
- const defs = {
2112
- none: { animation: "none" },
2113
- spin: {
2114
- animation: "kb-spin 1s linear infinite",
2115
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
2116
- },
2117
- ping: {
2118
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
2119
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
2120
- },
2121
- pulse: {
2122
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
2123
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
2124
- },
2125
- bounce: {
2126
- animation: "kb-bounce 1s infinite",
2127
- __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
2128
- }
2129
- };
2130
- const def = defs[value];
2131
- return def ? def : null;
2184
+ animate: ({ value, isArbitrary }, theme) => {
2185
+ if (!getEffectiveIsWeb()) return null;
2186
+ if (isArbitrary) return buildAnimationValue(value.replace(/_/g, " "), theme.keyframes);
2187
+ if (value in BUILTIN_ANIMATIONS) return BUILTIN_ANIMATIONS[value];
2188
+ const custom = theme.animation?.[value];
2189
+ return custom ? buildAnimationValue(custom, theme.keyframes) : null;
2132
2190
  },
2133
2191
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
2134
2192
  "line-clamp": ({ value }) => {
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-Z5SUFJ53.mjs";
7
- import "./chunk-OWJUMH2V.mjs";
6
+ } from "./chunk-7DHBNCL5.mjs";
7
+ import "./chunk-HCRWLOJE.mjs";
8
8
 
9
9
  // src/jsx-dev-runtime.tsx
10
10
  function jsxDEV(type, props, key, isStaticChildren, source, self) {
@@ -581,7 +581,13 @@ var defaultTheme = {
581
581
  xl: 1280,
582
582
  // desktop
583
583
  "2xl": 1536
584
- }
584
+ },
585
+ // Empty by default — the 4 built-in presets (spin/ping/pulse/bounce) are handled
586
+ // directly in the `animate` resolver, not through this theme table. This exists so
587
+ // user-defined ones (kbach.config.js theme.extend.keyframes/animation) merge in
588
+ // alongside them without needing to redeclare the built-ins.
589
+ keyframes: {},
590
+ animation: {}
585
591
  };
586
592
 
587
593
  // src/core/reset.ts
@@ -1360,6 +1366,44 @@ var BORDER_SIDE_PROPS = {
1360
1366
  "border-b": ["borderBottomWidth", "borderBottomColor"],
1361
1367
  "border-l": ["borderLeftWidth", "borderLeftColor"]
1362
1368
  };
1369
+ function keyframeDeclToCSS(decl) {
1370
+ return Object.entries(decl).map(([prop, val]) => `${prop.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${val}`).join("; ");
1371
+ }
1372
+ function buildKeyframeCSS(name, steps) {
1373
+ const body = Object.entries(steps).map(([selector, decl]) => `${selector} { ${keyframeDeclToCSS(decl)} }`).join(" ");
1374
+ return `@keyframes ${name} { ${body} }`;
1375
+ }
1376
+ var BUILTIN_ANIMATIONS = {
1377
+ none: { animation: "none" },
1378
+ spin: {
1379
+ animation: "kb-spin 1s linear infinite",
1380
+ __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1381
+ },
1382
+ ping: {
1383
+ animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1384
+ __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1385
+ },
1386
+ pulse: {
1387
+ animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1388
+ __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1389
+ },
1390
+ bounce: {
1391
+ animation: "kb-bounce 1s infinite",
1392
+ __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
1393
+ }
1394
+ };
1395
+ function buildAnimationValue(animation, keyframes) {
1396
+ const words = animation.trim().split(/\s+/);
1397
+ const name = words[0];
1398
+ if (!name) return { animation };
1399
+ const builtin = BUILTIN_ANIMATIONS[name];
1400
+ if (builtin?.__keyframe) {
1401
+ const kbName = builtin.animation.split(/\s+/, 1)[0];
1402
+ return { animation: [kbName, ...words.slice(1)].join(" "), __keyframe: builtin.__keyframe };
1403
+ }
1404
+ const steps = keyframes?.[name];
1405
+ return steps ? { animation, __keyframe: buildKeyframeCSS(name, steps) } : { animation };
1406
+ }
1363
1407
  var RESOLVERS = {
1364
1408
  // ── Background ─────────────────────────────────────────────────────────────
1365
1409
  bg: ({ value, isArbitrary }, { colors }) => {
@@ -1913,6 +1957,38 @@ var RESOLVERS = {
1913
1957
  };
1914
1958
  return offsets[value] ? { textUnderlineOffset: offsets[value] } : null;
1915
1959
  },
1960
+ // ── Content (web-only — for before:/after: pseudo-elements, which don't exist on native) ──
1961
+ // Arbitrary value carries its own quotes from the bracket syntax (content-['*'] parses to
1962
+ // the literal string 'you can see it' quotes included), so it passes straight through as a
1963
+ // valid `content: '*'` CSS value with no extra wrapping needed.
1964
+ content: ({ value, isArbitrary }) => {
1965
+ if (!getEffectiveIsWeb()) return null;
1966
+ if (isArbitrary) return { content: value };
1967
+ const presets = { none: "none" };
1968
+ return value in presets ? { content: presets[value] } : null;
1969
+ },
1970
+ // ── Arbitrary CSS property (web-only): [property:value] with no utility prefix ──
1971
+ // e.g. [mask-type:luminance], [--my-var:10px]. parser.ts already isolates this case
1972
+ // as utility: '' — it's ALSO how a stray, prefix-less negative bracket ("-[10px]",
1973
+ // presumably a typo) parses, which is why this only fires when the arbitrary value
1974
+ // itself contains a "property:value" pair; anything else (no colon) still resolves
1975
+ // to null exactly as before this resolver existed, rather than guessing a property.
1976
+ //
1977
+ // Only the FIRST colon splits property from value — the value itself may contain
1978
+ // more colons (a URL's "http://…") and must stay intact: [background:url(http://x/a.png)].
1979
+ // camelToKebab() in resolver.ts is a no-op on an already-kebab-case (or "--"-prefixed
1980
+ // custom property) key, so it can be used as the style object key as-written with no
1981
+ // extra case conversion — it only needs to look like a plausible CSS property name.
1982
+ "": ({ value, isArbitrary, negative }) => {
1983
+ if (!getEffectiveIsWeb() || !isArbitrary || !value) return null;
1984
+ if (negative) return null;
1985
+ const colonIdx = value.indexOf(":");
1986
+ if (colonIdx <= 0) return null;
1987
+ const prop = value.slice(0, colonIdx).trim();
1988
+ const cssValue = value.slice(colonIdx + 1).trim();
1989
+ if (!cssValue || !/^(--[\w-]+|[a-zA-Z-]+)$/.test(prop)) return null;
1990
+ return { [prop]: cssValue };
1991
+ },
1916
1992
  // ── Outline extended (web-only) ───────────────────────────────────────────
1917
1993
  outline: ({ value, isArbitrary }, { colors }) => {
1918
1994
  if (!getEffectiveIsWeb()) return null;
@@ -2106,30 +2182,12 @@ var RESOLVERS = {
2106
2182
  // Each variant injects a @keyframes rule once via the __keyframe marker.
2107
2183
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
2108
2184
  // so it never reaches element inline styles.
2109
- animate: ({ value, isArbitrary }) => {
2110
- if (!getEffectiveIsWeb()) return null;
2111
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
2112
- const defs = {
2113
- none: { animation: "none" },
2114
- spin: {
2115
- animation: "kb-spin 1s linear infinite",
2116
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
2117
- },
2118
- ping: {
2119
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
2120
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
2121
- },
2122
- pulse: {
2123
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
2124
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
2125
- },
2126
- bounce: {
2127
- animation: "kb-bounce 1s infinite",
2128
- __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
2129
- }
2130
- };
2131
- const def = defs[value];
2132
- return def ? def : null;
2185
+ animate: ({ value, isArbitrary }, theme) => {
2186
+ if (!getEffectiveIsWeb()) return null;
2187
+ if (isArbitrary) return buildAnimationValue(value.replace(/_/g, " "), theme.keyframes);
2188
+ if (value in BUILTIN_ANIMATIONS) return BUILTIN_ANIMATIONS[value];
2189
+ const custom = theme.animation?.[value];
2190
+ return custom ? buildAnimationValue(custom, theme.keyframes) : null;
2133
2191
  },
2134
2192
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
2135
2193
  "line-clamp": ({ value }) => {
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-Z5SUFJ53.mjs";
7
+ } from "./chunk-7DHBNCL5.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-OWJUMH2V.mjs";
10
+ } from "./chunk-HCRWLOJE.mjs";
11
11
  export {
12
12
  Fragment,
13
13
  _invalidateDefaultFontCache,
@@ -65,6 +65,23 @@ interface ThemeConfig {
65
65
  flex: Record<string, number | string>;
66
66
  shadow: Record<string, StyleValue>;
67
67
  screens: Record<string, string | number>;
68
+ /**
69
+ * Custom @keyframes, web only. Each key is a keyframe name, its value maps
70
+ * percentage/from/to selectors to a plain CSS declaration object (camelCase
71
+ * properties, same shape as an inline style object):
72
+ * keyframes: { wiggle: { '0%, 100%': { transform: 'rotate(-3deg)' }, '50%': { transform: 'rotate(3deg)' } } }
73
+ * Referenced from `animation` below, or directly via animate-[wiggle_1s_ease-in-out].
74
+ */
75
+ keyframes: Record<string, Record<string, StyleValue>>;
76
+ /**
77
+ * Named animation shorthands built on `keyframes` above, referenced via
78
+ * animate-{name} (e.g. animate-wiggle):
79
+ * animation: { wiggle: 'wiggle 1s ease-in-out infinite' }
80
+ * The first word must match a `keyframes` key so its @keyframes rule can be
81
+ * injected alongside the animation — a name with no matching keyframes entry
82
+ * still sets the `animation` CSS property, it just won't animate anything.
83
+ */
84
+ animation: Record<string, string>;
68
85
  [key: string]: unknown;
69
86
  }
70
87
  /**
@@ -65,6 +65,23 @@ interface ThemeConfig {
65
65
  flex: Record<string, number | string>;
66
66
  shadow: Record<string, StyleValue>;
67
67
  screens: Record<string, string | number>;
68
+ /**
69
+ * Custom @keyframes, web only. Each key is a keyframe name, its value maps
70
+ * percentage/from/to selectors to a plain CSS declaration object (camelCase
71
+ * properties, same shape as an inline style object):
72
+ * keyframes: { wiggle: { '0%, 100%': { transform: 'rotate(-3deg)' }, '50%': { transform: 'rotate(3deg)' } } }
73
+ * Referenced from `animation` below, or directly via animate-[wiggle_1s_ease-in-out].
74
+ */
75
+ keyframes: Record<string, Record<string, StyleValue>>;
76
+ /**
77
+ * Named animation shorthands built on `keyframes` above, referenced via
78
+ * animate-{name} (e.g. animate-wiggle):
79
+ * animation: { wiggle: 'wiggle 1s ease-in-out infinite' }
80
+ * The first word must match a `keyframes` key so its @keyframes rule can be
81
+ * injected alongside the animation — a name with no matching keyframes entry
82
+ * still sets the `animation` CSS property, it just won't animate anything.
83
+ */
84
+ animation: Record<string, string>;
68
85
  [key: string]: unknown;
69
86
  }
70
87
  /**
@@ -640,7 +640,13 @@ var defaultTheme = {
640
640
  xl: 1280,
641
641
  // desktop
642
642
  "2xl": 1536
643
- }
643
+ },
644
+ // Empty by default — the 4 built-in presets (spin/ping/pulse/bounce) are handled
645
+ // directly in the `animate` resolver, not through this theme table. This exists so
646
+ // user-defined ones (kbach.config.js theme.extend.keyframes/animation) merge in
647
+ // alongside them without needing to redeclare the built-ins.
648
+ keyframes: {},
649
+ animation: {}
644
650
  };
645
651
 
646
652
  // src/core/utilities.ts
@@ -1174,6 +1180,44 @@ var BORDER_SIDE_PROPS = {
1174
1180
  "border-b": ["borderBottomWidth", "borderBottomColor"],
1175
1181
  "border-l": ["borderLeftWidth", "borderLeftColor"]
1176
1182
  };
1183
+ function keyframeDeclToCSS(decl) {
1184
+ return Object.entries(decl).map(([prop, val]) => `${prop.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${val}`).join("; ");
1185
+ }
1186
+ function buildKeyframeCSS(name, steps) {
1187
+ const body = Object.entries(steps).map(([selector, decl]) => `${selector} { ${keyframeDeclToCSS(decl)} }`).join(" ");
1188
+ return `@keyframes ${name} { ${body} }`;
1189
+ }
1190
+ var BUILTIN_ANIMATIONS = {
1191
+ none: { animation: "none" },
1192
+ spin: {
1193
+ animation: "kb-spin 1s linear infinite",
1194
+ __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1195
+ },
1196
+ ping: {
1197
+ animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1198
+ __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1199
+ },
1200
+ pulse: {
1201
+ animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1202
+ __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1203
+ },
1204
+ bounce: {
1205
+ animation: "kb-bounce 1s infinite",
1206
+ __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
1207
+ }
1208
+ };
1209
+ function buildAnimationValue(animation, keyframes) {
1210
+ const words = animation.trim().split(/\s+/);
1211
+ const name = words[0];
1212
+ if (!name) return { animation };
1213
+ const builtin = BUILTIN_ANIMATIONS[name];
1214
+ if (builtin?.__keyframe) {
1215
+ const kbName = builtin.animation.split(/\s+/, 1)[0];
1216
+ return { animation: [kbName, ...words.slice(1)].join(" "), __keyframe: builtin.__keyframe };
1217
+ }
1218
+ const steps = keyframes?.[name];
1219
+ return steps ? { animation, __keyframe: buildKeyframeCSS(name, steps) } : { animation };
1220
+ }
1177
1221
  var RESOLVERS = {
1178
1222
  // ── Background ─────────────────────────────────────────────────────────────
1179
1223
  bg: ({ value, isArbitrary }, { colors }) => {
@@ -1727,6 +1771,38 @@ var RESOLVERS = {
1727
1771
  };
1728
1772
  return offsets[value] ? { textUnderlineOffset: offsets[value] } : null;
1729
1773
  },
1774
+ // ── Content (web-only — for before:/after: pseudo-elements, which don't exist on native) ──
1775
+ // Arbitrary value carries its own quotes from the bracket syntax (content-['*'] parses to
1776
+ // the literal string 'you can see it' quotes included), so it passes straight through as a
1777
+ // valid `content: '*'` CSS value with no extra wrapping needed.
1778
+ content: ({ value, isArbitrary }) => {
1779
+ if (!getEffectiveIsWeb()) return null;
1780
+ if (isArbitrary) return { content: value };
1781
+ const presets = { none: "none" };
1782
+ return value in presets ? { content: presets[value] } : null;
1783
+ },
1784
+ // ── Arbitrary CSS property (web-only): [property:value] with no utility prefix ──
1785
+ // e.g. [mask-type:luminance], [--my-var:10px]. parser.ts already isolates this case
1786
+ // as utility: '' — it's ALSO how a stray, prefix-less negative bracket ("-[10px]",
1787
+ // presumably a typo) parses, which is why this only fires when the arbitrary value
1788
+ // itself contains a "property:value" pair; anything else (no colon) still resolves
1789
+ // to null exactly as before this resolver existed, rather than guessing a property.
1790
+ //
1791
+ // Only the FIRST colon splits property from value — the value itself may contain
1792
+ // more colons (a URL's "http://…") and must stay intact: [background:url(http://x/a.png)].
1793
+ // camelToKebab() in resolver.ts is a no-op on an already-kebab-case (or "--"-prefixed
1794
+ // custom property) key, so it can be used as the style object key as-written with no
1795
+ // extra case conversion — it only needs to look like a plausible CSS property name.
1796
+ "": ({ value, isArbitrary, negative }) => {
1797
+ if (!getEffectiveIsWeb() || !isArbitrary || !value) return null;
1798
+ if (negative) return null;
1799
+ const colonIdx = value.indexOf(":");
1800
+ if (colonIdx <= 0) return null;
1801
+ const prop = value.slice(0, colonIdx).trim();
1802
+ const cssValue = value.slice(colonIdx + 1).trim();
1803
+ if (!cssValue || !/^(--[\w-]+|[a-zA-Z-]+)$/.test(prop)) return null;
1804
+ return { [prop]: cssValue };
1805
+ },
1730
1806
  // ── Outline extended (web-only) ───────────────────────────────────────────
1731
1807
  outline: ({ value, isArbitrary }, { colors }) => {
1732
1808
  if (!getEffectiveIsWeb()) return null;
@@ -1920,30 +1996,12 @@ var RESOLVERS = {
1920
1996
  // Each variant injects a @keyframes rule once via the __keyframe marker.
1921
1997
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
1922
1998
  // so it never reaches element inline styles.
1923
- animate: ({ value, isArbitrary }) => {
1924
- if (!getEffectiveIsWeb()) return null;
1925
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
1926
- const defs = {
1927
- none: { animation: "none" },
1928
- spin: {
1929
- animation: "kb-spin 1s linear infinite",
1930
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1931
- },
1932
- ping: {
1933
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1934
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1935
- },
1936
- pulse: {
1937
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1938
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1939
- },
1940
- bounce: {
1941
- animation: "kb-bounce 1s infinite",
1942
- __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
1943
- }
1944
- };
1945
- const def = defs[value];
1946
- return def ? def : null;
1999
+ animate: ({ value, isArbitrary }, theme) => {
2000
+ if (!getEffectiveIsWeb()) return null;
2001
+ if (isArbitrary) return buildAnimationValue(value.replace(/_/g, " "), theme.keyframes);
2002
+ if (value in BUILTIN_ANIMATIONS) return BUILTIN_ANIMATIONS[value];
2003
+ const custom = theme.animation?.[value];
2004
+ return custom ? buildAnimationValue(custom, theme.keyframes) : null;
1947
2005
  },
1948
2006
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
1949
2007
  "line-clamp": ({ value }) => {
@@ -3232,7 +3290,7 @@ function pushTemplateLiteralBody(body, into) {
3232
3290
  while ((im = interpolationRe.exec(body)) !== null) {
3233
3291
  pushClassLikeStrings(im[0], into);
3234
3292
  }
3235
- pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, " "), into);
3293
+ pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, () => "$"), into);
3236
3294
  }
3237
3295
  function pushClassLikeStrings(text, into) {
3238
3296
  const quotedStringRe = /(["'`])((?:(?!\1).)*)\1/g;
@@ -3245,9 +3303,9 @@ function pushClassLikeStrings(text, into) {
3245
3303
  }
3246
3304
  function extractClassStrings(code) {
3247
3305
  const found = /* @__PURE__ */ new Set();
3248
- const simpleRe = /(?:className|kb)=["']([^"']+)["']/g;
3306
+ const simpleRe = /(?:className|kb)=(["'])((?:(?!\1).)*)\1/g;
3249
3307
  let m;
3250
- while ((m = simpleRe.exec(code)) !== null) pushTokens(m[1], found);
3308
+ while ((m = simpleRe.exec(code)) !== null) pushTokens(m[2], found);
3251
3309
  const jsxExprRe = /(?:className|kb)=\{/g;
3252
3310
  while ((m = jsxExprRe.exec(code)) !== null) {
3253
3311
  let depth = 1;
@@ -606,7 +606,13 @@ var defaultTheme = {
606
606
  xl: 1280,
607
607
  // desktop
608
608
  "2xl": 1536
609
- }
609
+ },
610
+ // Empty by default — the 4 built-in presets (spin/ping/pulse/bounce) are handled
611
+ // directly in the `animate` resolver, not through this theme table. This exists so
612
+ // user-defined ones (kbach.config.js theme.extend.keyframes/animation) merge in
613
+ // alongside them without needing to redeclare the built-ins.
614
+ keyframes: {},
615
+ animation: {}
610
616
  };
611
617
 
612
618
  // src/core/utilities.ts
@@ -1140,6 +1146,44 @@ var BORDER_SIDE_PROPS = {
1140
1146
  "border-b": ["borderBottomWidth", "borderBottomColor"],
1141
1147
  "border-l": ["borderLeftWidth", "borderLeftColor"]
1142
1148
  };
1149
+ function keyframeDeclToCSS(decl) {
1150
+ return Object.entries(decl).map(([prop, val]) => `${prop.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${val}`).join("; ");
1151
+ }
1152
+ function buildKeyframeCSS(name, steps) {
1153
+ const body = Object.entries(steps).map(([selector, decl]) => `${selector} { ${keyframeDeclToCSS(decl)} }`).join(" ");
1154
+ return `@keyframes ${name} { ${body} }`;
1155
+ }
1156
+ var BUILTIN_ANIMATIONS = {
1157
+ none: { animation: "none" },
1158
+ spin: {
1159
+ animation: "kb-spin 1s linear infinite",
1160
+ __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1161
+ },
1162
+ ping: {
1163
+ animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1164
+ __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1165
+ },
1166
+ pulse: {
1167
+ animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1168
+ __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1169
+ },
1170
+ bounce: {
1171
+ animation: "kb-bounce 1s infinite",
1172
+ __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
1173
+ }
1174
+ };
1175
+ function buildAnimationValue(animation, keyframes) {
1176
+ const words = animation.trim().split(/\s+/);
1177
+ const name = words[0];
1178
+ if (!name) return { animation };
1179
+ const builtin = BUILTIN_ANIMATIONS[name];
1180
+ if (builtin?.__keyframe) {
1181
+ const kbName = builtin.animation.split(/\s+/, 1)[0];
1182
+ return { animation: [kbName, ...words.slice(1)].join(" "), __keyframe: builtin.__keyframe };
1183
+ }
1184
+ const steps = keyframes?.[name];
1185
+ return steps ? { animation, __keyframe: buildKeyframeCSS(name, steps) } : { animation };
1186
+ }
1143
1187
  var RESOLVERS = {
1144
1188
  // ── Background ─────────────────────────────────────────────────────────────
1145
1189
  bg: ({ value, isArbitrary }, { colors }) => {
@@ -1693,6 +1737,38 @@ var RESOLVERS = {
1693
1737
  };
1694
1738
  return offsets[value] ? { textUnderlineOffset: offsets[value] } : null;
1695
1739
  },
1740
+ // ── Content (web-only — for before:/after: pseudo-elements, which don't exist on native) ──
1741
+ // Arbitrary value carries its own quotes from the bracket syntax (content-['*'] parses to
1742
+ // the literal string 'you can see it' quotes included), so it passes straight through as a
1743
+ // valid `content: '*'` CSS value with no extra wrapping needed.
1744
+ content: ({ value, isArbitrary }) => {
1745
+ if (!getEffectiveIsWeb()) return null;
1746
+ if (isArbitrary) return { content: value };
1747
+ const presets = { none: "none" };
1748
+ return value in presets ? { content: presets[value] } : null;
1749
+ },
1750
+ // ── Arbitrary CSS property (web-only): [property:value] with no utility prefix ──
1751
+ // e.g. [mask-type:luminance], [--my-var:10px]. parser.ts already isolates this case
1752
+ // as utility: '' — it's ALSO how a stray, prefix-less negative bracket ("-[10px]",
1753
+ // presumably a typo) parses, which is why this only fires when the arbitrary value
1754
+ // itself contains a "property:value" pair; anything else (no colon) still resolves
1755
+ // to null exactly as before this resolver existed, rather than guessing a property.
1756
+ //
1757
+ // Only the FIRST colon splits property from value — the value itself may contain
1758
+ // more colons (a URL's "http://…") and must stay intact: [background:url(http://x/a.png)].
1759
+ // camelToKebab() in resolver.ts is a no-op on an already-kebab-case (or "--"-prefixed
1760
+ // custom property) key, so it can be used as the style object key as-written with no
1761
+ // extra case conversion — it only needs to look like a plausible CSS property name.
1762
+ "": ({ value, isArbitrary, negative }) => {
1763
+ if (!getEffectiveIsWeb() || !isArbitrary || !value) return null;
1764
+ if (negative) return null;
1765
+ const colonIdx = value.indexOf(":");
1766
+ if (colonIdx <= 0) return null;
1767
+ const prop = value.slice(0, colonIdx).trim();
1768
+ const cssValue = value.slice(colonIdx + 1).trim();
1769
+ if (!cssValue || !/^(--[\w-]+|[a-zA-Z-]+)$/.test(prop)) return null;
1770
+ return { [prop]: cssValue };
1771
+ },
1696
1772
  // ── Outline extended (web-only) ───────────────────────────────────────────
1697
1773
  outline: ({ value, isArbitrary }, { colors }) => {
1698
1774
  if (!getEffectiveIsWeb()) return null;
@@ -1886,30 +1962,12 @@ var RESOLVERS = {
1886
1962
  // Each variant injects a @keyframes rule once via the __keyframe marker.
1887
1963
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
1888
1964
  // so it never reaches element inline styles.
1889
- animate: ({ value, isArbitrary }) => {
1890
- if (!getEffectiveIsWeb()) return null;
1891
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
1892
- const defs = {
1893
- none: { animation: "none" },
1894
- spin: {
1895
- animation: "kb-spin 1s linear infinite",
1896
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1897
- },
1898
- ping: {
1899
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1900
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1901
- },
1902
- pulse: {
1903
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1904
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1905
- },
1906
- bounce: {
1907
- animation: "kb-bounce 1s infinite",
1908
- __keyframe: "@keyframes kb-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1) } 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1) } }"
1909
- }
1910
- };
1911
- const def = defs[value];
1912
- return def ? def : null;
1965
+ animate: ({ value, isArbitrary }, theme) => {
1966
+ if (!getEffectiveIsWeb()) return null;
1967
+ if (isArbitrary) return buildAnimationValue(value.replace(/_/g, " "), theme.keyframes);
1968
+ if (value in BUILTIN_ANIMATIONS) return BUILTIN_ANIMATIONS[value];
1969
+ const custom = theme.animation?.[value];
1970
+ return custom ? buildAnimationValue(custom, theme.keyframes) : null;
1913
1971
  },
1914
1972
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
1915
1973
  "line-clamp": ({ value }) => {
@@ -3198,7 +3256,7 @@ function pushTemplateLiteralBody(body, into) {
3198
3256
  while ((im = interpolationRe.exec(body)) !== null) {
3199
3257
  pushClassLikeStrings(im[0], into);
3200
3258
  }
3201
- pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, " "), into);
3259
+ pushTokens(body.replace(/\$\{(?:[^{}]|\{[^{}]*\})*\}/g, () => "$"), into);
3202
3260
  }
3203
3261
  function pushClassLikeStrings(text, into) {
3204
3262
  const quotedStringRe = /(["'`])((?:(?!\1).)*)\1/g;
@@ -3211,9 +3269,9 @@ function pushClassLikeStrings(text, into) {
3211
3269
  }
3212
3270
  function extractClassStrings(code) {
3213
3271
  const found = /* @__PURE__ */ new Set();
3214
- const simpleRe = /(?:className|kb)=["']([^"']+)["']/g;
3272
+ const simpleRe = /(?:className|kb)=(["'])((?:(?!\1).)*)\1/g;
3215
3273
  let m;
3216
- while ((m = simpleRe.exec(code)) !== null) pushTokens(m[1], found);
3274
+ while ((m = simpleRe.exec(code)) !== null) pushTokens(m[2], found);
3217
3275
  const jsxExprRe = /(?:className|kb)=\{/g;
3218
3276
  while ((m = jsxExprRe.exec(code)) !== null) {
3219
3277
  let depth = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "React / React Native components and hooks for the Kbach framework",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./dist/index.js",