@kbach/react 0.6.12 → 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-WYOKBFSK.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 }) => {
@@ -1803,6 +1847,28 @@ var RESOLVERS = {
1803
1847
  const presets = { none: "none" };
1804
1848
  return value in presets ? { content: presets[value] } : null;
1805
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
+ },
1806
1872
  // ── Outline extended (web-only) ───────────────────────────────────────────
1807
1873
  outline: ({ value, isArbitrary }, { colors }) => {
1808
1874
  if (!getEffectiveIsWeb()) return null;
@@ -1996,30 +2062,12 @@ var RESOLVERS = {
1996
2062
  // Each variant injects a @keyframes rule once via the __keyframe marker.
1997
2063
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
1998
2064
  // so it never reaches element inline styles.
1999
- animate: ({ value, isArbitrary }) => {
2000
- if (!getEffectiveIsWeb()) return null;
2001
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
2002
- const defs = {
2003
- none: { animation: "none" },
2004
- spin: {
2005
- animation: "kb-spin 1s linear infinite",
2006
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
2007
- },
2008
- ping: {
2009
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
2010
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
2011
- },
2012
- pulse: {
2013
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
2014
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
2015
- },
2016
- bounce: {
2017
- animation: "kb-bounce 1s infinite",
2018
- __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) } }"
2019
- }
2020
- };
2021
- const def = defs[value];
2022
- 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;
2023
2071
  },
2024
2072
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
2025
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 }) => {
@@ -1964,6 +2008,28 @@ var RESOLVERS = {
1964
2008
  const presets = { none: "none" };
1965
2009
  return value in presets ? { content: presets[value] } : null;
1966
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
+ },
1967
2033
  // ── Outline extended (web-only) ───────────────────────────────────────────
1968
2034
  outline: ({ value, isArbitrary }, { colors }) => {
1969
2035
  if (!getEffectiveIsWeb()) return null;
@@ -2157,30 +2223,12 @@ var RESOLVERS = {
2157
2223
  // Each variant injects a @keyframes rule once via the __keyframe marker.
2158
2224
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
2159
2225
  // so it never reaches element inline styles.
2160
- animate: ({ value, isArbitrary }) => {
2226
+ animate: ({ value, isArbitrary }, theme) => {
2161
2227
  if (!getEffectiveIsWeb()) return null;
2162
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
2163
- const defs = {
2164
- none: { animation: "none" },
2165
- spin: {
2166
- animation: "kb-spin 1s linear infinite",
2167
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
2168
- },
2169
- ping: {
2170
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
2171
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
2172
- },
2173
- pulse: {
2174
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
2175
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
2176
- },
2177
- bounce: {
2178
- animation: "kb-bounce 1s infinite",
2179
- __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) } }"
2180
- }
2181
- };
2182
- const def = defs[value];
2183
- 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;
2184
2232
  },
2185
2233
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
2186
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-WYOKBFSK.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 }) => {
@@ -1922,6 +1966,28 @@ var RESOLVERS = {
1922
1966
  const presets = { none: "none" };
1923
1967
  return value in presets ? { content: presets[value] } : null;
1924
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
+ },
1925
1991
  // ── Outline extended (web-only) ───────────────────────────────────────────
1926
1992
  outline: ({ value, isArbitrary }, { colors }) => {
1927
1993
  if (!getEffectiveIsWeb()) return null;
@@ -2115,30 +2181,12 @@ var RESOLVERS = {
2115
2181
  // Each variant injects a @keyframes rule once via the __keyframe marker.
2116
2182
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
2117
2183
  // so it never reaches element inline styles.
2118
- animate: ({ value, isArbitrary }) => {
2119
- if (!getEffectiveIsWeb()) return null;
2120
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
2121
- const defs = {
2122
- none: { animation: "none" },
2123
- spin: {
2124
- animation: "kb-spin 1s linear infinite",
2125
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
2126
- },
2127
- ping: {
2128
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
2129
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
2130
- },
2131
- pulse: {
2132
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
2133
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
2134
- },
2135
- bounce: {
2136
- animation: "kb-bounce 1s infinite",
2137
- __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) } }"
2138
- }
2139
- };
2140
- const def = defs[value];
2141
- 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;
2142
2190
  },
2143
2191
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
2144
2192
  "line-clamp": ({ value }) => {
@@ -3,8 +3,8 @@ import {
3
3
  Fragment,
4
4
  jsx,
5
5
  jsxs
6
- } from "./chunk-GTDMKS7C.mjs";
7
- import "./chunk-WYOKBFSK.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 }) => {
@@ -1923,6 +1967,28 @@ var RESOLVERS = {
1923
1967
  const presets = { none: "none" };
1924
1968
  return value in presets ? { content: presets[value] } : null;
1925
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
+ },
1926
1992
  // ── Outline extended (web-only) ───────────────────────────────────────────
1927
1993
  outline: ({ value, isArbitrary }, { colors }) => {
1928
1994
  if (!getEffectiveIsWeb()) return null;
@@ -2116,30 +2182,12 @@ var RESOLVERS = {
2116
2182
  // Each variant injects a @keyframes rule once via the __keyframe marker.
2117
2183
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
2118
2184
  // so it never reaches element inline styles.
2119
- animate: ({ value, isArbitrary }) => {
2120
- if (!getEffectiveIsWeb()) return null;
2121
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
2122
- const defs = {
2123
- none: { animation: "none" },
2124
- spin: {
2125
- animation: "kb-spin 1s linear infinite",
2126
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
2127
- },
2128
- ping: {
2129
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
2130
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
2131
- },
2132
- pulse: {
2133
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
2134
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
2135
- },
2136
- bounce: {
2137
- animation: "kb-bounce 1s infinite",
2138
- __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) } }"
2139
- }
2140
- };
2141
- const def = defs[value];
2142
- 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;
2143
2191
  },
2144
2192
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
2145
2193
  "line-clamp": ({ value }) => {
@@ -4,10 +4,10 @@ import {
4
4
  _invalidateDefaultFontCache,
5
5
  jsx,
6
6
  jsxs
7
- } from "./chunk-GTDMKS7C.mjs";
7
+ } from "./chunk-7DHBNCL5.mjs";
8
8
  import {
9
9
  registerWebElement
10
- } from "./chunk-WYOKBFSK.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 }) => {
@@ -1737,6 +1781,28 @@ var RESOLVERS = {
1737
1781
  const presets = { none: "none" };
1738
1782
  return value in presets ? { content: presets[value] } : null;
1739
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
+ },
1740
1806
  // ── Outline extended (web-only) ───────────────────────────────────────────
1741
1807
  outline: ({ value, isArbitrary }, { colors }) => {
1742
1808
  if (!getEffectiveIsWeb()) return null;
@@ -1930,30 +1996,12 @@ var RESOLVERS = {
1930
1996
  // Each variant injects a @keyframes rule once via the __keyframe marker.
1931
1997
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
1932
1998
  // so it never reaches element inline styles.
1933
- animate: ({ value, isArbitrary }) => {
1934
- if (!getEffectiveIsWeb()) return null;
1935
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
1936
- const defs = {
1937
- none: { animation: "none" },
1938
- spin: {
1939
- animation: "kb-spin 1s linear infinite",
1940
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1941
- },
1942
- ping: {
1943
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1944
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1945
- },
1946
- pulse: {
1947
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1948
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1949
- },
1950
- bounce: {
1951
- animation: "kb-bounce 1s infinite",
1952
- __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) } }"
1953
- }
1954
- };
1955
- const def = defs[value];
1956
- 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;
1957
2005
  },
1958
2006
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
1959
2007
  "line-clamp": ({ value }) => {
@@ -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 }) => {
@@ -1703,6 +1747,28 @@ var RESOLVERS = {
1703
1747
  const presets = { none: "none" };
1704
1748
  return value in presets ? { content: presets[value] } : null;
1705
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
+ },
1706
1772
  // ── Outline extended (web-only) ───────────────────────────────────────────
1707
1773
  outline: ({ value, isArbitrary }, { colors }) => {
1708
1774
  if (!getEffectiveIsWeb()) return null;
@@ -1896,30 +1962,12 @@ var RESOLVERS = {
1896
1962
  // Each variant injects a @keyframes rule once via the __keyframe marker.
1897
1963
  // resolver.ts detects __keyframe, injects the rule, then strips the marker
1898
1964
  // so it never reaches element inline styles.
1899
- animate: ({ value, isArbitrary }) => {
1900
- if (!getEffectiveIsWeb()) return null;
1901
- if (isArbitrary) return { animation: value.replace(/_/g, " ") };
1902
- const defs = {
1903
- none: { animation: "none" },
1904
- spin: {
1905
- animation: "kb-spin 1s linear infinite",
1906
- __keyframe: "@keyframes kb-spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"
1907
- },
1908
- ping: {
1909
- animation: "kb-ping 1s cubic-bezier(0,0,0.2,1) infinite",
1910
- __keyframe: "@keyframes kb-ping { 75%, 100% { transform: scale(2); opacity: 0 } }"
1911
- },
1912
- pulse: {
1913
- animation: "kb-pulse 2s cubic-bezier(0.4,0,0.6,1) infinite",
1914
- __keyframe: "@keyframes kb-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .5 } }"
1915
- },
1916
- bounce: {
1917
- animation: "kb-bounce 1s infinite",
1918
- __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) } }"
1919
- }
1920
- };
1921
- const def = defs[value];
1922
- 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;
1923
1971
  },
1924
1972
  // ── Line-clamp (web-only) ─────────────────────────────────────────────────
1925
1973
  "line-clamp": ({ value }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react",
3
- "version": "0.6.12",
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",