@yewdiel21/motion-kit 0.17.0 → 0.18.0

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.
Files changed (146) hide show
  1. package/README.md +23 -0
  2. package/dist/auto/index.d.ts +18 -3
  3. package/dist/auto/index.js +32 -2
  4. package/dist/auto/index.js.map +1 -1
  5. package/dist/{chunk-VJFPTLAK.js → chunk-AWDEKDEJ.js} +2 -2
  6. package/dist/{chunk-BZLMCHUD.js → chunk-EDMXWUW3.js} +2 -2
  7. package/dist/{chunk-7IQTMF7Y.js → chunk-Q7IW4FV3.js} +2580 -58
  8. package/dist/chunk-Q7IW4FV3.js.map +1 -0
  9. package/dist/css/components/badge-button.css +16 -0
  10. package/dist/css/components/bento-grid.css +13 -0
  11. package/dist/css/components/drilldown-menu.css +11 -7
  12. package/dist/css/components/equalizer-slider.css +26 -0
  13. package/dist/css/components/handwriting-text.css +15 -0
  14. package/dist/css/components/magnetize-button.css +18 -0
  15. package/dist/css/components/morphing-page-dots.css +13 -0
  16. package/dist/css/components/not-found.css +22 -0
  17. package/dist/css/components/notification-stack.css +42 -0
  18. package/dist/css/components/particle-button.css +14 -0
  19. package/dist/css/components/radio-group.css +29 -0
  20. package/dist/css/components/rating-scale.css +23 -0
  21. package/dist/css/components/scroll-preview-dock.css +18 -0
  22. package/dist/css/components/select.css +16 -0
  23. package/dist/css/components/signature-pad.css +130 -103
  24. package/dist/css/components/stepper.css +36 -0
  25. package/dist/css/components/sticky-header.css +10 -0
  26. package/dist/css/components/temperature-slider.css +29 -0
  27. package/dist/css/components/voice-message-bubble.css +62 -45
  28. package/dist/css/components/waveform-player.css +49 -34
  29. package/dist/css/components/wheel-picker.css +16 -15
  30. package/dist/css/tokens.css +4 -0
  31. package/dist/elements/index.d.ts +206 -4
  32. package/dist/elements/index.js +581 -6
  33. package/dist/elements/index.js.map +1 -1
  34. package/dist/{engine-Bn6hC87X.d.ts → engine-t8d7brjn.d.ts} +341 -1
  35. package/dist/index.css +578 -170
  36. package/dist/index.d.ts +23 -23
  37. package/dist/index.js +33 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/react/index.d.ts +113 -7
  40. package/dist/react/index.js +669 -3
  41. package/dist/react/index.js.map +1 -1
  42. package/dist/{reduced-YS6TZjpa.d.ts → reduced-dPsZYqtI.d.ts} +1 -1
  43. package/dist/{store-CZ6vdtIE.d.ts → store-6hZNKiee.d.ts} +1 -1
  44. package/package.json +1 -1
  45. package/registry.json +180 -0
  46. package/src/auto/auto-init.ts +31 -0
  47. package/src/components/badge-button/README.md +28 -0
  48. package/src/components/badge-button/badge-button.css +15 -0
  49. package/src/components/badge-button/element.ts +16 -0
  50. package/src/components/badge-button/engine.ts +85 -0
  51. package/src/components/badge-button/react.tsx +31 -0
  52. package/src/components/bento-grid/README.md +26 -0
  53. package/src/components/bento-grid/bento-grid.css +12 -0
  54. package/src/components/bento-grid/element.ts +22 -0
  55. package/src/components/bento-grid/engine.ts +153 -0
  56. package/src/components/bento-grid/react.tsx +25 -0
  57. package/src/components/drilldown-menu/drilldown-menu.css +11 -7
  58. package/src/components/drilldown-menu/engine.ts +44 -4
  59. package/src/components/equalizer-slider/README.md +43 -0
  60. package/src/components/equalizer-slider/element.ts +41 -0
  61. package/src/components/equalizer-slider/engine.ts +32 -0
  62. package/src/components/equalizer-slider/equalizer-slider.css +26 -0
  63. package/src/components/equalizer-slider/react.tsx +60 -0
  64. package/src/components/equalizer-slider/shared.ts +161 -0
  65. package/src/components/handwriting-text/README.md +29 -0
  66. package/src/components/handwriting-text/element.ts +14 -0
  67. package/src/components/handwriting-text/engine.ts +108 -0
  68. package/src/components/handwriting-text/glyphs.ts +71 -0
  69. package/src/components/handwriting-text/handwriting-text.css +14 -0
  70. package/src/components/handwriting-text/react.tsx +23 -0
  71. package/src/components/magnetize-button/README.md +26 -0
  72. package/src/components/magnetize-button/element.ts +13 -0
  73. package/src/components/magnetize-button/engine.ts +84 -0
  74. package/src/components/magnetize-button/magnetize-button.css +17 -0
  75. package/src/components/magnetize-button/react.tsx +22 -0
  76. package/src/components/morphing-page-dots/README.md +28 -0
  77. package/src/components/morphing-page-dots/element.ts +16 -0
  78. package/src/components/morphing-page-dots/engine.ts +89 -0
  79. package/src/components/morphing-page-dots/morphing-page-dots.css +12 -0
  80. package/src/components/morphing-page-dots/react.tsx +32 -0
  81. package/src/components/not-found/README.md +26 -0
  82. package/src/components/not-found/element.ts +15 -0
  83. package/src/components/not-found/engine.ts +57 -0
  84. package/src/components/not-found/not-found.css +21 -0
  85. package/src/components/not-found/react.tsx +23 -0
  86. package/src/components/notification-stack/README.md +24 -0
  87. package/src/components/notification-stack/element.ts +26 -0
  88. package/src/components/notification-stack/engine.ts +329 -0
  89. package/src/components/notification-stack/notification-stack.css +41 -0
  90. package/src/components/notification-stack/react.tsx +28 -0
  91. package/src/components/particle-button/README.md +26 -0
  92. package/src/components/particle-button/element.ts +13 -0
  93. package/src/components/particle-button/engine.ts +52 -0
  94. package/src/components/particle-button/particle-button.css +13 -0
  95. package/src/components/particle-button/react.tsx +22 -0
  96. package/src/components/radio-group/README.md +43 -0
  97. package/src/components/radio-group/element.ts +41 -0
  98. package/src/components/radio-group/engine.ts +11 -0
  99. package/src/components/radio-group/radio-group.css +28 -0
  100. package/src/components/radio-group/react.tsx +60 -0
  101. package/src/components/radio-group/shared.ts +118 -0
  102. package/src/components/rating-scale/README.md +43 -0
  103. package/src/components/rating-scale/element.ts +41 -0
  104. package/src/components/rating-scale/engine.ts +63 -0
  105. package/src/components/rating-scale/rating-scale.css +22 -0
  106. package/src/components/rating-scale/react.tsx +60 -0
  107. package/src/components/scroll-preview-dock/README.md +24 -0
  108. package/src/components/scroll-preview-dock/element.ts +20 -0
  109. package/src/components/scroll-preview-dock/engine.ts +228 -0
  110. package/src/components/scroll-preview-dock/react.tsx +26 -0
  111. package/src/components/scroll-preview-dock/scroll-preview-dock.css +17 -0
  112. package/src/components/select/README.md +23 -9
  113. package/src/components/select/select.css +16 -0
  114. package/src/components/signature-pad/engine.ts +550 -437
  115. package/src/components/signature-pad/signature-pad.css +130 -103
  116. package/src/components/stepper/README.md +46 -0
  117. package/src/components/stepper/element.ts +42 -0
  118. package/src/components/stepper/engine.ts +103 -0
  119. package/src/components/stepper/react.tsx +60 -0
  120. package/src/components/stepper/stepper.css +35 -0
  121. package/src/components/sticky-header/README.md +30 -0
  122. package/src/components/sticky-header/element.ts +19 -0
  123. package/src/components/sticky-header/engine.ts +95 -0
  124. package/src/components/sticky-header/react.tsx +25 -0
  125. package/src/components/sticky-header/sticky-header.css +9 -0
  126. package/src/components/temperature-slider/README.md +42 -0
  127. package/src/components/temperature-slider/element.ts +53 -0
  128. package/src/components/temperature-slider/engine.ts +44 -0
  129. package/src/components/temperature-slider/react.tsx +66 -0
  130. package/src/components/temperature-slider/temperature-slider.css +29 -0
  131. package/src/components/voice-message-bubble/engine.ts +11 -0
  132. package/src/components/voice-message-bubble/voice-message-bubble.css +14 -12
  133. package/src/components/waveform-player/README.md +1 -1
  134. package/src/components/waveform-player/engine.ts +344 -261
  135. package/src/components/waveform-player/waveform-player.css +50 -35
  136. package/src/components/wheel-picker/README.md +3 -1
  137. package/src/components/wheel-picker/element.ts +12 -5
  138. package/src/components/wheel-picker/engine.ts +37 -8
  139. package/src/components/wheel-picker/wheel-picker.css +17 -16
  140. package/src/css/tokens.css +4 -0
  141. package/src/elements/index.ts +45 -0
  142. package/src/index.ts +311 -295
  143. package/src/react/index.ts +16 -0
  144. package/dist/chunk-7IQTMF7Y.js.map +0 -1
  145. /package/dist/{chunk-VJFPTLAK.js.map → chunk-AWDEKDEJ.js.map} +0 -0
  146. /package/dist/{chunk-BZLMCHUD.js.map → chunk-EDMXWUW3.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import { c4 as MenuOptions, c3 as MenuInstance, bi as DialogOptions, bg as DialogInstance, cD as RevealSpeed } from './engine-Bn6hC87X.js';
1
+ import { ct as MenuOptions, cs as MenuInstance, bB as DialogOptions, bz as DialogInstance, df as RevealSpeed } from './engine-t8d7brjn.js';
2
2
  import { I as Instance } from './types-BkS_raL8.js';
3
3
  import { SplitText } from 'gsap/SplitText';
4
4
 
@@ -1,6 +1,6 @@
1
1
  import { I as Instance } from './types-BkS_raL8.js';
2
2
  import { Flip } from 'gsap/Flip';
3
- import { a8 as RevealHandle, a9 as DateInput } from './engine-Bn6hC87X.js';
3
+ import { an as RevealHandle, ao as DateInput } from './engine-t8d7brjn.js';
4
4
 
5
5
  interface ThemePickerOptions {
6
6
  /** Theme ids to show, in order. Default: visible presets, or all with showHidden. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yewdiel21/motion-kit",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Motion toolkit for vanilla, Astro and React: GSAP recipes, accessible components, 19 themes, CSS controls and optional WebGL effects.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/registry.json CHANGED
@@ -1155,6 +1155,186 @@
1155
1155
  "element": "components/wheel-picker/element.ts",
1156
1156
  "react": "components/wheel-picker/react.tsx",
1157
1157
  "since": "0.17.0"
1158
+ },
1159
+ "stepper": {
1160
+ "kind": "component",
1161
+ "description": "Indicadores de pasos horizontales, verticales, compactos y por puntos con progreso animado.",
1162
+ "files": [
1163
+ "components/stepper/engine.ts",
1164
+ "components/stepper/stepper.css",
1165
+ "components/stepper/README.md"
1166
+ ],
1167
+ "react": "components/stepper/react.tsx",
1168
+ "element": "components/stepper/element.ts",
1169
+ "since": "0.18.0"
1170
+ },
1171
+ "radio-group": {
1172
+ "kind": "component",
1173
+ "description": "Selección única con radios nativos, tarjetas o píldoras y resaltado elástico.",
1174
+ "files": [
1175
+ "components/radio-group/engine.ts",
1176
+ "components/radio-group/radio-group.css",
1177
+ "components/radio-group/README.md"
1178
+ ],
1179
+ "react": "components/radio-group/react.tsx",
1180
+ "element": "components/radio-group/element.ts",
1181
+ "since": "0.18.0"
1182
+ },
1183
+ "equalizer-slider": {
1184
+ "kind": "component",
1185
+ "description": "Control numérico de volumen con barras tipo ecualizador, respuesta al arrastre y cifras animadas.",
1186
+ "files": [
1187
+ "components/equalizer-slider/engine.ts",
1188
+ "components/equalizer-slider/equalizer-slider.css",
1189
+ "components/equalizer-slider/README.md"
1190
+ ],
1191
+ "react": "components/equalizer-slider/react.tsx",
1192
+ "element": "components/equalizer-slider/element.ts",
1193
+ "since": "0.18.0"
1194
+ },
1195
+ "temperature-slider": {
1196
+ "kind": "component",
1197
+ "description": "Temperatura con control nativo, transiciones de estado y cifra que rueda.",
1198
+ "files": [
1199
+ "components/temperature-slider/engine.ts",
1200
+ "components/temperature-slider/temperature-slider.css",
1201
+ "components/temperature-slider/README.md"
1202
+ ],
1203
+ "react": "components/temperature-slider/react.tsx",
1204
+ "element": "components/temperature-slider/element.ts",
1205
+ "since": "0.18.0"
1206
+ },
1207
+ "rating-scale": {
1208
+ "kind": "component",
1209
+ "description": "Escala de valoración numérica o de satisfacción con radios accesibles y selección animada.",
1210
+ "files": [
1211
+ "components/rating-scale/engine.ts",
1212
+ "components/rating-scale/rating-scale.css",
1213
+ "components/rating-scale/README.md"
1214
+ ],
1215
+ "react": "components/rating-scale/react.tsx",
1216
+ "element": "components/rating-scale/element.ts",
1217
+ "since": "0.18.0"
1218
+ },
1219
+ "notification-stack": {
1220
+ "kind": "component",
1221
+ "description": "Notificaciones superpuestas que se expanden, permiten acciones y se descartan con movimiento.",
1222
+ "files": [
1223
+ "components/notification-stack/engine.ts",
1224
+ "components/notification-stack/notification-stack.css",
1225
+ "components/notification-stack/README.md"
1226
+ ],
1227
+ "react": "components/notification-stack/react.tsx",
1228
+ "element": "components/notification-stack/element.ts",
1229
+ "since": "0.18.0"
1230
+ },
1231
+ "bento-grid": {
1232
+ "kind": "component",
1233
+ "description": "Grid adaptable con spans, filtros y reordenamiento animado mediante Flip.",
1234
+ "files": [
1235
+ "components/bento-grid/engine.ts",
1236
+ "components/bento-grid/bento-grid.css",
1237
+ "components/bento-grid/README.md"
1238
+ ],
1239
+ "react": "components/bento-grid/react.tsx",
1240
+ "element": "components/bento-grid/element.ts",
1241
+ "since": "0.18.0"
1242
+ },
1243
+ "scroll-preview-dock": {
1244
+ "kind": "component",
1245
+ "description": "Dock de navegación con vistas previas de secciones y seguimiento del scroll.",
1246
+ "files": [
1247
+ "components/scroll-preview-dock/engine.ts",
1248
+ "components/scroll-preview-dock/scroll-preview-dock.css",
1249
+ "components/scroll-preview-dock/README.md"
1250
+ ],
1251
+ "react": "components/scroll-preview-dock/react.tsx",
1252
+ "element": "components/scroll-preview-dock/element.ts",
1253
+ "since": "0.18.0"
1254
+ },
1255
+ "sticky-header": {
1256
+ "kind": "component",
1257
+ "description": "Cabecera que se compacta al desplazar sin cambiar el espacio del contenido.",
1258
+ "files": [
1259
+ "components/sticky-header/engine.ts",
1260
+ "components/sticky-header/sticky-header.css",
1261
+ "components/sticky-header/README.md"
1262
+ ],
1263
+ "react": "components/sticky-header/react.tsx",
1264
+ "element": "components/sticky-header/element.ts",
1265
+ "since": "0.18.0"
1266
+ },
1267
+ "handwriting-text": {
1268
+ "kind": "component",
1269
+ "description": "Texto trazado con glifos vectoriales originales, animación de escritura y repetición.",
1270
+ "files": [
1271
+ "components/handwriting-text/engine.ts",
1272
+ "components/handwriting-text/handwriting-text.css",
1273
+ "components/handwriting-text/README.md"
1274
+ ],
1275
+ "react": "components/handwriting-text/react.tsx",
1276
+ "element": "components/handwriting-text/element.ts",
1277
+ "since": "0.18.0"
1278
+ },
1279
+ "particle-button": {
1280
+ "kind": "component",
1281
+ "description": "Botón con pulsación elástica y partículas al completar una acción.",
1282
+ "files": [
1283
+ "components/particle-button/engine.ts",
1284
+ "components/particle-button/particle-button.css",
1285
+ "components/particle-button/README.md"
1286
+ ],
1287
+ "react": "components/particle-button/react.tsx",
1288
+ "element": "components/particle-button/element.ts",
1289
+ "since": "0.18.0"
1290
+ },
1291
+ "magnetize-button": {
1292
+ "kind": "component",
1293
+ "description": "Botón que atrae partículas al señalar o enfocar y las libera al salir.",
1294
+ "files": [
1295
+ "components/magnetize-button/engine.ts",
1296
+ "components/magnetize-button/magnetize-button.css",
1297
+ "components/magnetize-button/README.md"
1298
+ ],
1299
+ "react": "components/magnetize-button/react.tsx",
1300
+ "element": "components/magnetize-button/element.ts",
1301
+ "since": "0.18.0"
1302
+ },
1303
+ "badge-button": {
1304
+ "kind": "component",
1305
+ "description": "Botón con insignia, contador que rueda y respuesta al añadir o quitar.",
1306
+ "files": [
1307
+ "components/badge-button/engine.ts",
1308
+ "components/badge-button/badge-button.css",
1309
+ "components/badge-button/README.md"
1310
+ ],
1311
+ "react": "components/badge-button/react.tsx",
1312
+ "element": "components/badge-button/element.ts",
1313
+ "since": "0.18.0"
1314
+ },
1315
+ "morphing-page-dots": {
1316
+ "kind": "component",
1317
+ "description": "Paginación de carrusel con indicador que se estira entre páginas y teclado.",
1318
+ "files": [
1319
+ "components/morphing-page-dots/engine.ts",
1320
+ "components/morphing-page-dots/morphing-page-dots.css",
1321
+ "components/morphing-page-dots/README.md"
1322
+ ],
1323
+ "react": "components/morphing-page-dots/react.tsx",
1324
+ "element": "components/morphing-page-dots/element.ts",
1325
+ "since": "0.18.0"
1326
+ },
1327
+ "not-found": {
1328
+ "kind": "component",
1329
+ "description": "Estado 404 reutilizable con ilustración animada, regreso y enlace al inicio.",
1330
+ "files": [
1331
+ "components/not-found/engine.ts",
1332
+ "components/not-found/not-found.css",
1333
+ "components/not-found/README.md"
1334
+ ],
1335
+ "react": "components/not-found/react.tsx",
1336
+ "element": "components/not-found/element.ts",
1337
+ "since": "0.18.0"
1158
1338
  }
1159
1339
  }
1160
1340
  }
@@ -1,3 +1,18 @@
1
+ import { createNotFound } from '../components/not-found/engine';
2
+ import { createMorphingPageDots } from '../components/morphing-page-dots/engine';
3
+ import { createBadgeButton } from '../components/badge-button/engine';
4
+ import { createMagnetizeButton } from '../components/magnetize-button/engine';
5
+ import { createParticleButton } from '../components/particle-button/engine';
6
+ import { createHandwritingText } from '../components/handwriting-text/engine';
7
+ import { createStickyHeader } from '../components/sticky-header/engine';
8
+ import { createScrollPreviewDock } from '../components/scroll-preview-dock/engine';
9
+ import { createBentoGrid } from '../components/bento-grid/engine';
10
+ import { createNotificationStack } from '../components/notification-stack/engine';
11
+ import { createRatingScale } from '../components/rating-scale/engine';
12
+ import { createTemperatureSlider } from '../components/temperature-slider/engine';
13
+ import { createEqualizerSlider } from '../components/equalizer-slider/engine';
14
+ import { createRadioGroup } from '../components/radio-group/engine';
15
+ import { createStepper } from '../components/stepper/engine';
1
16
  import { createQuantity } from '../components/quantity/engine';
2
17
  import { createPagination } from '../components/pagination/engine';
3
18
  import { createHoverCard } from '../components/hover-card/engine';
@@ -120,6 +135,22 @@ import { createWheelPicker } from '../components/wheel-picker/engine';
120
135
  type Factory = (el: HTMLElement, options: Record<string, unknown>) => Instance;
121
136
 
122
137
  const factories = {
138
+ 'stepper': createStepper,
139
+ 'radio-group': createRadioGroup,
140
+ 'equalizer-slider': createEqualizerSlider,
141
+ 'temperature-slider': createTemperatureSlider,
142
+ 'rating-scale': createRatingScale,
143
+ 'notification-stack': createNotificationStack,
144
+ 'bento-grid': createBentoGrid,
145
+ 'scroll-preview-dock': createScrollPreviewDock,
146
+ 'sticky-header': createStickyHeader,
147
+ 'handwriting-text': createHandwritingText,
148
+ 'particle-button': createParticleButton,
149
+ 'magnetize-button': createMagnetizeButton,
150
+ 'badge-button': createBadgeButton,
151
+ 'morphing-page-dots': createMorphingPageDots,
152
+ 'not-found': createNotFound,
153
+
123
154
  'wheel-picker': createWheelPicker,
124
155
  'drilldown-menu': createDrilldownMenu,
125
156
  'voice-message-bubble': createVoiceMessageBubble,
@@ -0,0 +1,28 @@
1
+ # Badge button
2
+
3
+ Botón con contador: el primer elemento hace aparecer el badge con `pop`; las cifras cambian con `rollDigits` y una pequeña pulsación `swatch`. El valor y el nombre accesible se actualizan en el momento de la acción. El badge visual usa `99+` por defecto, mientras el nombre conserva la cantidad real.
4
+
5
+ ```ts
6
+ import { createBadgeButton } from '@yewdiel21/motion-kit';
7
+ import '@yewdiel21/motion-kit/css/badge-button';
8
+ const button = createBadgeButton(document.querySelector('#pending'), {
9
+ count: 0, onChange: (count) => updateLocalList(count),
10
+ });
11
+ button.setCount(10); // sin notificar
12
+ button.setCount(0, true);
13
+ // button.destroy();
14
+ ```
15
+
16
+ `BadgeButtonOptions`: `label` (Guardar, cuando falta contenido), `count` (0), `step` (1, incremento por activación; admite 0 y valores negativos), `hideZero` (true), `max` (99), `onChange(count)`. Los valores se redondean y limitan a enteros no negativos. `BadgeButtonInstance`: `setCount(count, notify = false)`, `count`, `destroy()`. Emite `mk:change` con `{count}` al notificar. Un paso 0 conserva el contador y permite que la app gestione su acción con un listener nativo.
17
+
18
+ ```tsx
19
+ <BadgeButton label="Añadir pendiente" count={count} onChange={setCount} />
20
+ ```
21
+
22
+ React admite `count` controlado o `defaultCount`, atributos de botón e `instanceRef`. El valor controlado sigue siendo la autoridad si el padre rechaza un cambio. Usa `label`; los hijos y `dangerouslySetInnerHTML` se filtran en ejecución. Cambiar presentación conserva el estado no controlado.
23
+
24
+ ```html
25
+ <mk-badge-button count="0" hide-zero="true"><button type="button">Añadir pendiente</button></mk-badge-button>
26
+ ```
27
+
28
+ `defineBadgeButton()` registra `MkBadgeButtonElement`; atributos `label`, `count`, `step`, `hide-zero`, `max`. Conserva el contador vivo en cambios de presentación y reconexiones; el atributo `count` explícito lo reemplaza. El botón nativo tiene type button por defecto y respeta disabled. Movimiento reducido actualiza directamente. `destroy()` restaura el contenido y estilos escritos por el consumidor y libera las animaciones y eventos.
@@ -0,0 +1,15 @@
1
+ .mk-badge-button,
2
+ mk-badge-button { display: inline-flex; }
3
+ [data-mk-badge-button] {
4
+ display: inline-flex; align-items: center; justify-content: center; gap: .7rem; position: relative;
5
+ min-height: 48px; padding: .65rem 1rem .65rem 1.25rem; border: 1px solid var(--mk-outline-soft); border-radius: 16px;
6
+ background: var(--mk-surface-3); color: var(--mk-text); font: 500 .9375rem var(--mk-font); cursor: pointer;
7
+ transition: background 160ms, border-radius 320ms var(--mk-ease-swatch);
8
+ }
9
+ [data-mk-badge-button]:hover:not(:disabled) { border-radius: 24px; background: var(--mk-surface-4); }
10
+ [data-mk-badge-button]:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 4px; }
11
+ [data-mk-badge-button]:disabled,
12
+ [data-mk-badge-button][aria-disabled='true'] { opacity: .4; cursor: not-allowed; }
13
+ [data-mk-badge-count] { display: inline-grid; place-items: center; min-width: 26px; height: 26px; padding: 0 6px; border-radius: 999px; background: var(--mk-accent); color: var(--mk-on-accent); font-size: .75rem; font-weight: 650; font-variant-numeric: tabular-nums; }
14
+ [data-mk-badge-count][hidden] { display: none; }
15
+ @media (prefers-reduced-motion: reduce) { [data-mk-badge-button] { transition: none; } }
@@ -0,0 +1,16 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createBadgeButton, type BadgeButtonInstance } from './engine';
3
+
4
+ export class MkBadgeButtonElement extends MkElement<BadgeButtonInstance> {
5
+ static options: Record<string, OptionType> = { label: 'string', count: 'number', step: 'number', hideZero: 'boolean', max: 'number' };
6
+ private retained: number | undefined;
7
+ protected create(options: Record<string, unknown>) { return createBadgeButton(this, { ...options, ...(this.retained === undefined ? {} : { count: this.retained }) }); }
8
+ disconnectedCallback() { this.retained = this.instance?.count ?? this.retained; super.disconnectedCallback(); }
9
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
10
+ const focused = this.contains(document.activeElement);
11
+ if (previous !== next) this.retained = name === 'count' ? undefined : this.instance?.count ?? this.retained;
12
+ super.attributeChangedCallback(name, previous, next);
13
+ if (focused) this.querySelector<HTMLButtonElement>('button')?.focus({ preventScroll: true });
14
+ }
15
+ }
16
+ export const defineBadgeButton = () => defineElement('mk-badge-button', MkBadgeButtonElement);
@@ -0,0 +1,85 @@
1
+ import { gsap, EASE, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { inert, type Instance } from '../../core/types';
5
+ import { press, rollDigits } from '../../motion/micro';
6
+
7
+ export interface BadgeButtonOptions {
8
+ label?: string;
9
+ count?: number;
10
+ /** Increment requested by an activation; use 0 for a host-owned count. Default 1. */
11
+ step?: number;
12
+ /** Hide the badge when its count is zero (default true). */
13
+ hideZero?: boolean;
14
+ max?: number;
15
+ onChange?: (count: number) => void;
16
+ }
17
+ export interface BadgeButtonInstance extends Instance {
18
+ setCount(count: number, notify?: boolean): void;
19
+ readonly count: number;
20
+ }
21
+ const normalize = (value: number) => Number.isFinite(value) ? Math.max(0, Math.min(Number.MAX_SAFE_INTEGER, Math.round(value))) : 0;
22
+ let sequence = 0;
23
+ export function createBadgeButton(root: HTMLElement | null, options: BadgeButtonOptions = {}): BadgeButtonInstance {
24
+ if (!root) return inert({ setCount() {}, count: 0 });
25
+ registerMotion();
26
+ const scope = createScope();
27
+ let button = root.matches('button') ? root as HTMLButtonElement : root.querySelector<HTMLButtonElement>('button');
28
+ if (!button) { button = document.createElement('button'); root.append(button); const made = button; scope.add(() => made.remove()); }
29
+ const control = button;
30
+ if (!control.textContent?.trim() && !control.querySelector('[aria-label], svg, img')) {
31
+ const label = document.createElement('span'); label.textContent = options.label ?? 'Guardar'; control.append(label); scope.add(() => label.remove());
32
+ }
33
+ const originalStyle = control.getAttribute('style');
34
+ const originalTransform = control.style.transform;
35
+ scope.add(() => { if (originalStyle === null) control.removeAttribute('style'); else control.setAttribute('style', originalStyle); });
36
+ const badge = document.createElement('span'); badge.dataset.mkBadgeCount = ''; badge.setAttribute('aria-hidden', 'true'); control.append(badge); scope.add(() => badge.remove());
37
+ // Keep the actual quantity in the accessible name even when the visual badge reads "99+".
38
+ const spoken = document.createElement('span'); spoken.className = 'mk-sr-only'; control.append(spoken); scope.add(() => spoken.remove());
39
+ const authoredLabel = control.getAttribute('aria-label');
40
+ const labelledBy = control.getAttribute('aria-labelledby');
41
+ if (labelledBy?.trim()) {
42
+ spoken.id = `mk-badge-quantity-${++sequence}`;
43
+ scope.attr(control, 'aria-labelledby', `${labelledBy} ${spoken.id}`);
44
+ }
45
+ scope.attr(control, 'type', control.getAttribute('type') ?? 'button');
46
+ scope.attr(control, 'data-mk-badge-button', '');
47
+ scope.attr(root, 'data-mk-ready', '');
48
+ let count = normalize(options.count ?? 0);
49
+ const max = Math.max(1, normalize(options.max ?? 99));
50
+ const step = Number.isFinite(options.step) ? Math.round(options.step!) : 1;
51
+ let rolling: gsap.core.Animation | null = null;
52
+ let bump: gsap.core.Animation | null = null;
53
+ let pressing: gsap.core.Animation | null = null;
54
+ const format = () => count > max ? `${max}+` : String(count);
55
+ const announce = () => {
56
+ spoken.textContent = ` (${count})`;
57
+ if (!labelledBy?.trim() && authoredLabel?.trim()) scope.attr(control, 'aria-label', `${authoredLabel} (${count})`);
58
+ };
59
+ const paint = () => { badge.hidden = options.hideZero !== false && count === 0; badge.textContent = format(); announce(); };
60
+ const setCount = (next: number, notify = false) => {
61
+ if (scope.disposed || !Number.isFinite(next)) return;
62
+ const previous = count;
63
+ count = normalize(next);
64
+ if (count === previous) return;
65
+ const wasHidden = badge.hidden;
66
+ badge.hidden = options.hideZero !== false && count === 0;
67
+ announce();
68
+ rolling?.kill(); bump?.kill();
69
+ if (reduced() || badge.hidden) paint();
70
+ else {
71
+ rolling = rollDigits(badge, format(), count >= previous ? 1 : -1, .34);
72
+ bump = gsap.fromTo(badge, { scale: wasHidden ? .35 : .8, y: wasHidden ? 7 : 0 }, { scale: 1, y: 0, duration: .5, ease: wasHidden ? EASE.pop : EASE.swatch });
73
+ }
74
+ if (notify) { options.onChange?.(count); root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { count } })); }
75
+ };
76
+ scope.on(control, 'click', (event) => {
77
+ if (control.disabled || control.getAttribute('aria-disabled') === 'true') { event.preventDefault(); return; }
78
+ pressing?.kill(); pressing = press(control);
79
+ pressing?.eventCallback('onComplete', () => { gsap.set(control, { clearProps: 'transform,scale' }); control.style.transform = originalTransform; if (!control.getAttribute('style')) control.removeAttribute('style'); });
80
+ setCount(count + step, true);
81
+ });
82
+ paint();
83
+ scope.add(() => { rolling?.kill(); bump?.kill(); pressing?.kill(); gsap.killTweensOf(badge); });
84
+ return { setCount, get count() { return count; }, destroy: () => scope.dispose() };
85
+ }
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { useRef, type ButtonHTMLAttributes, type Ref } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createBadgeButton, type BadgeButtonInstance, type BadgeButtonOptions } from './engine';
5
+
6
+ export interface BadgeButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'dangerouslySetInnerHTML' | 'onChange'>, BadgeButtonOptions {
7
+ defaultCount?: number;
8
+ instanceRef?: Ref<BadgeButtonInstance>;
9
+ }
10
+ export function BadgeButton({ label = 'Guardar', count, defaultCount = 0, step, hideZero, max, onChange, instanceRef, type = 'button', className, ...props }: BadgeButtonProps) {
11
+ const { children: _children, dangerouslySetInnerHTML: _html, ...safe } = props as ButtonHTMLAttributes<HTMLButtonElement>;
12
+ const root = useRef<HTMLButtonElement>(null);
13
+ const instance = useRef<BadgeButtonInstance | null>(null);
14
+ const current = useLatest({ count, onChange });
15
+ const retained = useRef(defaultCount);
16
+ useIsomorphicLayoutEffect(() => {
17
+ let created: BadgeButtonInstance;
18
+ created = createBadgeButton(root.current, {
19
+ label, count: current.current.count ?? retained.current, step, hideZero, max,
20
+ onChange: (next) => { current.current.onChange?.(next); if (current.current.count !== undefined) created.setCount(current.current.count); },
21
+ });
22
+ instance.current = created;
23
+ return () => { retained.current = created.count; created.destroy(); instance.current = null; };
24
+ }, [label, step, hideZero, max]);
25
+ useIsomorphicLayoutEffect(() => { if (count !== undefined) instance.current?.setCount(count); }, [count]);
26
+ useIsomorphicLayoutEffect(() => {
27
+ if (typeof instanceRef === 'function') instanceRef(instance.current); else if (instanceRef) instanceRef.current = instance.current;
28
+ return () => { if (typeof instanceRef === 'function') instanceRef(null); else if (instanceRef) instanceRef.current = null; };
29
+ }, [instanceRef, label, step, hideZero, max]);
30
+ return <button {...safe} type={type} ref={root} className={['mk-badge-button', className].filter(Boolean).join(' ')} />;
31
+ }
@@ -0,0 +1,26 @@
1
+ # Bento grid
2
+
3
+ A reusable responsive authored grid. Existing cards travel with Flip when reordered or filtered; entering cards pop through a short blur. No hover-repulsion recipe is required.
4
+
5
+ ```html
6
+ <div id="grid">
7
+ <article data-mk-bento-item="activity" data-mk-col-span="2" data-mk-category="work">…</article>
8
+ <article data-mk-bento-item="notes" data-mk-row-span="2" data-mk-category="work files">…</article>
9
+ </div>
10
+ ```
11
+
12
+ ```ts
13
+ const grid = createBentoGrid(document.querySelector('#grid'), { columns: 3, gap: 12 });
14
+ grid.setFilter('files'); // null shows everything
15
+ grid.reorder(['notes', 'activity']);
16
+ grid.mutate(() => document.querySelector('#grid').append(newCard));
17
+ grid.destroy();
18
+ ```
19
+
20
+ Exports: `createBentoGrid`, `BentoGridOptions`, `BentoGridInstance`; React `BentoGrid`, `BentoGridProps`; elements `MkBentoGridElement`, `defineBentoGrid` (`mk-bento-grid`). CSS: `bento-grid.css` plus kit tokens/theme/fonts.
21
+
22
+ Options: `columns` (default 3, bounded 1–12), `gap` (pixels, default 12), `filter` (category or null), `duration` (seconds, default .55), `onChange(keys)`. Direct children opt in with unique `data-mk-bento-item` keys; column/row spans default to one. The grid reduces to two columns below 440px of its own width and one below 280px; spans clamp to available columns.
23
+
24
+ Instance: `refresh()`, `setFilter(filter)`, `reorder(keys)`, `mutate(change)`, `setOptions(partial)`, readonly `filter`/`keys` (visible order), `destroy()`. `mk:change` carries `{keys,filter}`. `mutate` measures before authored DOM edits; a MutationObserver also handles framework additions and authored category/span changes. Unknown/duplicate reorder keys are ignored; unlisted keys follow in their existing order. Focus survives reordering; filtering a focused card returns focus to the grid. Filtered cards are hidden and inert. Customize the empty copy with `data-mk-empty-label`.
25
+
26
+ React deliberately accepts authored children with stable keys and the same data attributes. Change their array order to reorder declaratively. Elements accept scalar option attributes and expose the instance methods. Destroy restores authored attributes and original order, retaining externally added children. Reduced motion keeps filtering and layout immediate.
@@ -0,0 +1,12 @@
1
+ .mk-bento-grid, mk-bento-grid { container-type: inline-size; display: grid; grid-template-columns: repeat(var(--mk-bento-columns, 3), minmax(0, 1fr)); grid-auto-rows: minmax(112px, auto); gap: var(--mk-bento-gap, 12px); color: var(--mk-text); font-family: var(--mk-font); }
2
+ .mk-bento-grid > [data-mk-bento-item], mk-bento-grid > [data-mk-bento-item] { box-sizing: border-box; min-width: 0; grid-column: span var(--mk-bento-col-span, 1); grid-row: span var(--mk-bento-row-span, 1); padding: 20px; border-radius: 24px; background: var(--mk-surface); transition: background .2s, box-shadow .25s; }
3
+ .mk-bento-grid > [data-mk-bento-item][hidden], mk-bento-grid > [data-mk-bento-item][hidden] { display: none !important; }
4
+ .mk-bento-grid > [data-mk-bento-item]:focus-within, mk-bento-grid > [data-mk-bento-item]:focus-within { background: var(--mk-surface-2); }
5
+ .mk-bento-grid :is(a,button):focus-visible, mk-bento-grid :is(a,button):focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 4px; }
6
+ .mk-bento-grid :is(button,a)[aria-disabled=true], .mk-bento-grid button:disabled { opacity: .4; cursor: not-allowed; }
7
+ .mk-bento-grid[data-mk-empty]::after, mk-bento-grid[data-mk-empty]::after { content: attr(data-mk-empty-label); grid-column: 1 / -1; min-height: 112px; display: grid; place-items: center; border-radius: 24px; color: var(--mk-muted); background: var(--mk-surface); font-size: 13px; }
8
+ @media (hover: hover) { :is(.mk-bento-grid,mk-bento-grid) > [data-mk-bento-item]:hover { background: var(--mk-surface-2); box-shadow: 0 10px 24px -24px var(--mk-text); } }
9
+ /* The container can be a 640px playground card, independent of viewport width. */
10
+ @container (max-width: 440px) { :is(.mk-bento-grid,mk-bento-grid) > [data-mk-bento-item] { grid-column: span min(var(--mk-bento-col-span, 1), 2); padding: 16px; border-radius: 16px; } }
11
+ @media (max-width: 480px) { :is(.mk-bento-grid,mk-bento-grid) > [data-mk-bento-item] { padding: 16px; border-radius: 16px; } }
12
+ @media (prefers-reduced-motion: reduce) { :is(.mk-bento-grid,mk-bento-grid) * { transition: none !important; } }
@@ -0,0 +1,22 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createBentoGrid, type BentoGridInstance, type BentoGridOptions } from './engine';
3
+
4
+ export class MkBentoGridElement extends MkElement<BentoGridInstance> {
5
+ static options: Record<string, OptionType> = {"columns":"number","gap":"number","filter":"string","duration":"number"};
6
+ protected create(options: Record<string, unknown>) { return createBentoGrid(this, options); }
7
+ setOptions(options: Partial<BentoGridOptions>) { this.instance?.setOptions(options); }
8
+
9
+ refresh() { this.instance?.refresh(); }
10
+ setFilter(filter: string | null) { this.instance?.setFilter(filter); }
11
+ reorder(keys: string[]) { this.instance?.reorder(keys); }
12
+ mutate(change: () => void) { this.instance?.mutate(change); }
13
+ get filter() { return this.instance?.filter ?? null; }
14
+ get keys() { return this.instance?.keys ?? []; }
15
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
16
+ if (previous === next || !this.isConnected || !this.instance) return;
17
+ const option = name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
18
+ const defaults: Record<string, unknown> = {"columns":3,"gap":12,"filter":null,"duration":0.55};
19
+ this.instance.setOptions({ [option]: this.options[option] ?? defaults[option] });
20
+ }
21
+ }
22
+ export const defineBentoGrid = () => defineElement('mk-bento-grid', MkBentoGridElement);