neba 1.2.0 → 1.4.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 (107) hide show
  1. package/README.md +8 -8
  2. package/dist/components/alert/Alert.d.ts +7 -1
  3. package/dist/components/alert/Alert.js +1 -1
  4. package/dist/components/animate-appear/AnimateAppear.d.ts +55 -0
  5. package/dist/components/animate-appear/AnimateAppear.js +1 -0
  6. package/dist/components/animate-appear/index.d.ts +2 -0
  7. package/dist/components/animate-appear/index.js +1 -0
  8. package/dist/components/animate-blink/AnimateBlink.d.ts +31 -0
  9. package/dist/components/animate-blink/AnimateBlink.js +1 -0
  10. package/dist/components/animate-blink/index.d.ts +2 -0
  11. package/dist/components/animate-blink/index.js +1 -0
  12. package/dist/components/animate-fade/AnimateFade.d.ts +31 -0
  13. package/dist/components/animate-fade/AnimateFade.js +1 -0
  14. package/dist/components/animate-fade/index.d.ts +2 -0
  15. package/dist/components/animate-fade/index.js +1 -0
  16. package/dist/components/animate-grow/AnimateGrow.d.ts +41 -0
  17. package/dist/components/animate-grow/AnimateGrow.js +1 -0
  18. package/dist/components/animate-grow/index.d.ts +2 -0
  19. package/dist/components/animate-grow/index.js +1 -0
  20. package/dist/components/animate-headline/AnimateHeadline.d.ts +55 -0
  21. package/dist/components/animate-headline/AnimateHeadline.js +1 -0
  22. package/dist/components/animate-headline/index.d.ts +2 -0
  23. package/dist/components/animate-headline/index.js +1 -0
  24. package/dist/components/animate-lighting/AnimateLighting.d.ts +62 -0
  25. package/dist/components/animate-lighting/AnimateLighting.js +1 -0
  26. package/dist/components/animate-lighting/index.d.ts +2 -0
  27. package/dist/components/animate-lighting/index.js +1 -0
  28. package/dist/components/animate-marquee/AnimateMarquee.d.ts +65 -0
  29. package/dist/components/animate-marquee/AnimateMarquee.js +1 -0
  30. package/dist/components/animate-marquee/index.d.ts +2 -0
  31. package/dist/components/animate-marquee/index.js +1 -0
  32. package/dist/components/animate-rotate/AnimateRotate.d.ts +48 -0
  33. package/dist/components/animate-rotate/AnimateRotate.js +1 -0
  34. package/dist/components/animate-rotate/index.d.ts +2 -0
  35. package/dist/components/animate-rotate/index.js +1 -0
  36. package/dist/components/animate-slide/AnimateSlide.d.ts +46 -0
  37. package/dist/components/animate-slide/AnimateSlide.js +1 -0
  38. package/dist/components/animate-slide/index.d.ts +2 -0
  39. package/dist/components/animate-slide/index.js +1 -0
  40. package/dist/components/animate-typing/AnimateTyping.d.ts +58 -0
  41. package/dist/components/animate-typing/AnimateTyping.js +1 -0
  42. package/dist/components/animate-typing/index.d.ts +2 -0
  43. package/dist/components/animate-typing/index.js +1 -0
  44. package/dist/components/animate-zoom/AnimateZoom.d.ts +37 -0
  45. package/dist/components/animate-zoom/AnimateZoom.js +1 -0
  46. package/dist/components/animate-zoom/index.d.ts +2 -0
  47. package/dist/components/animate-zoom/index.js +1 -0
  48. package/dist/components/aspect-ratio/AspectRatio.d.ts +63 -0
  49. package/dist/components/aspect-ratio/AspectRatio.js +1 -0
  50. package/dist/components/aspect-ratio/index.d.ts +2 -0
  51. package/dist/components/aspect-ratio/index.js +1 -0
  52. package/dist/components/avatar/Avatar.d.ts +7 -1
  53. package/dist/components/avatar/Avatar.js +1 -1
  54. package/dist/components/blockquote/Blockquote.d.ts +7 -1
  55. package/dist/components/blockquote/Blockquote.js +1 -1
  56. package/dist/components/box/Box.d.ts +7 -1
  57. package/dist/components/box/Box.js +1 -1
  58. package/dist/components/chip/Chip.d.ts +7 -1
  59. package/dist/components/chip/Chip.js +1 -1
  60. package/dist/components/color-picker/ColorPicker.d.ts +102 -0
  61. package/dist/components/color-picker/ColorPicker.js +1 -0
  62. package/dist/components/color-picker/index.d.ts +2 -0
  63. package/dist/components/color-picker/index.js +1 -0
  64. package/dist/components/data-table/DataTable.d.ts +323 -0
  65. package/dist/components/data-table/DataTable.js +1 -0
  66. package/dist/components/data-table/index.d.ts +2 -0
  67. package/dist/components/data-table/index.js +1 -0
  68. package/dist/components/drawer/Drawer.d.ts +145 -0
  69. package/dist/components/drawer/Drawer.js +1 -0
  70. package/dist/components/drawer/index.d.ts +2 -0
  71. package/dist/components/drawer/index.js +1 -0
  72. package/dist/components/empty/Empty.d.ts +69 -0
  73. package/dist/components/empty/Empty.js +1 -0
  74. package/dist/components/empty/index.d.ts +2 -0
  75. package/dist/components/empty/index.js +1 -0
  76. package/dist/components/icon/Icon.d.ts +8 -1
  77. package/dist/components/icon/Icon.js +1 -1
  78. package/dist/components/mockup/Mockup.d.ts +151 -0
  79. package/dist/components/mockup/Mockup.js +1 -0
  80. package/dist/components/mockup/index.d.ts +2 -0
  81. package/dist/components/mockup/index.js +1 -0
  82. package/dist/components/popover/Popover.d.ts +109 -0
  83. package/dist/components/popover/Popover.js +1 -0
  84. package/dist/components/popover/index.d.ts +2 -0
  85. package/dist/components/popover/index.js +1 -0
  86. package/dist/components/skeleton/Skeleton.d.ts +76 -0
  87. package/dist/components/skeleton/Skeleton.js +1 -0
  88. package/dist/components/skeleton/index.d.ts +2 -0
  89. package/dist/components/skeleton/index.js +1 -0
  90. package/dist/components/typography/Typography.d.ts +7 -1
  91. package/dist/components/typography/Typography.js +1 -1
  92. package/dist/index.d.ts +19 -0
  93. package/dist/index.js +1 -1
  94. package/dist/internal/animate.d.ts +175 -0
  95. package/dist/internal/animate.js +1 -0
  96. package/dist/internal/color.d.ts +108 -0
  97. package/dist/internal/color.js +1 -0
  98. package/dist/internal/data-table.d.ts +170 -0
  99. package/dist/internal/data-table.js +1 -0
  100. package/dist/internal/i18n.d.ts +76 -0
  101. package/dist/internal/i18n.js +1 -1
  102. package/dist/internal/mockup.d.ts +193 -0
  103. package/dist/internal/mockup.js +1 -0
  104. package/dist/styles.css +1 -1
  105. package/dist/tailwind.css +405 -11
  106. package/dist/types.d.ts +112 -0
  107. package/package.json +12 -12
package/dist/tailwind.css CHANGED
@@ -47,9 +47,18 @@
47
47
 
48
48
  /* ---------------------------------------------------------------------------
49
49
  * Light theme (default)
50
+ *
51
+ * `.light` and `[data-theme='light']` carry the same block rather than only
52
+ * `:root`, because forcing light has to work on a nested element too — a
53
+ * preview box inside a dark page has nowhere else to get these values from.
54
+ * Everything here that is not a colour (the radii, the motion, the grain) is
55
+ * identical in both themes, so re-declaring it costs a duplicate that resolves
56
+ * to what was inherited anyway.
50
57
  * ------------------------------------------------------------------------- */
51
58
 
52
- :root {
59
+ :root,
60
+ .light,
61
+ [data-theme='light'] {
53
62
  /* Neutral surfaces and content */
54
63
  --neba-surface: oklch(100% 0 0);
55
64
  --neba-fg: oklch(24% 0.015 262);
@@ -86,8 +95,6 @@
86
95
  bottom bevel — that is what made the surface read as moulded plastic. */
87
96
  --neba-plate-edge: rgb(255 255 255 / 0.18);
88
97
  --neba-plate-top: rgb(255 255 255 / 0.26);
89
- --neba-plate-solid: inset 0 1px 0 var(--neba-plate-top), inset 0 0 0 1px var(--neba-plate-edge);
90
- --neba-plate-glass: inset 0 1px 0 var(--neba-plate-top);
91
98
  /* Off-vertical on purpose: light falling across a sheet at an angle, rather
92
99
  than the symmetric top-down gradient every framework ships. */
93
100
  --neba-sheen: linear-gradient(148deg, rgb(255 255 255 / 0.13), rgb(255 255 255 / 0) 52%);
@@ -138,14 +145,6 @@
138
145
  surface in space, not to advertise it. Level 4 is only reachable by
139
146
  hovering a level-3 surface. */
140
147
  --neba-shadow-ambient: rgb(16 24 40 / 0.13);
141
- --neba-shadow-0: 0 0 #0000;
142
- --neba-shadow-1: 0 1px 2px -1px var(--neba-shadow-ambient);
143
- --neba-shadow-2:
144
- 0 2px 4px -2px var(--neba-shadow-ambient), 0 5px 12px -7px var(--neba-shadow-ambient);
145
- --neba-shadow-3:
146
- 0 4px 8px -3px var(--neba-shadow-ambient), 0 12px 24px -11px var(--neba-shadow-ambient);
147
- --neba-shadow-4:
148
- 0 6px 12px -4px var(--neba-shadow-ambient), 0 20px 40px -15px var(--neba-shadow-ambient);
149
148
 
150
149
  /* Corner radius: ~45% of the control height at every step. Held just short of
151
150
  the 50% that would make it a pill — the flat run along the top and bottom
@@ -380,6 +379,22 @@
380
379
  .light,
381
380
  [data-theme='dark'],
382
381
  [data-theme='light'] {
382
+ /* The plate and the elevation ladder are two base tokens spread into shadow
383
+ lists, so they belong here rather than next to the values they read: left
384
+ in the light block they would keep the light hairline and the light ambient
385
+ inside a `.dark` that is not the document root. */
386
+ --neba-plate-solid: inset 0 1px 0 var(--neba-plate-top), inset 0 0 0 1px var(--neba-plate-edge);
387
+ --neba-plate-glass: inset 0 1px 0 var(--neba-plate-top);
388
+
389
+ --neba-shadow-0: 0 0 #0000;
390
+ --neba-shadow-1: 0 1px 2px -1px var(--neba-shadow-ambient);
391
+ --neba-shadow-2:
392
+ 0 2px 4px -2px var(--neba-shadow-ambient), 0 5px 12px -7px var(--neba-shadow-ambient);
393
+ --neba-shadow-3:
394
+ 0 4px 8px -3px var(--neba-shadow-ambient), 0 12px 24px -11px var(--neba-shadow-ambient);
395
+ --neba-shadow-4:
396
+ 0 6px 12px -4px var(--neba-shadow-ambient), 0 20px 40px -15px var(--neba-shadow-ambient);
397
+
383
398
  --neba-primary-fill: color-mix(
384
399
  in srgb,
385
400
  var(--neba-primary-solid) var(--neba-fill-alpha),
@@ -962,6 +977,48 @@
962
977
  animation-delay: calc(var(--n-i, 0) * 160ms);
963
978
  }
964
979
 
980
+ /*
981
+ * The highlight travelling across a placeholder.
982
+ *
983
+ * `inset-inline-start` rather than a transform, the same trade `neba-sweep`
984
+ * makes and for the same reason: it runs the other way under RTL without being
985
+ * told, and the layout pass it costs is confined to a box with nothing in it.
986
+ *
987
+ * A pseudo-element rather than a child, so a Skeleton is one element per shape
988
+ * and a stack of ten lines is ten nodes rather than twenty. The gradient is
989
+ * symmetric, so it needs no direction of its own.
990
+ */
991
+ @keyframes neba-skeleton-sweep {
992
+ from {
993
+ inset-inline-start: -60%;
994
+ }
995
+ to {
996
+ inset-inline-start: 100%;
997
+ }
998
+ }
999
+
1000
+ .neba-skeleton::after {
1001
+ content: '';
1002
+ position: absolute;
1003
+ inset-block: 0;
1004
+ inline-size: 60%;
1005
+ background-image: linear-gradient(90deg, transparent, var(--n-panel-press), transparent);
1006
+ animation: neba-skeleton-sweep 1.5s var(--neba-ease) infinite;
1007
+ }
1008
+
1009
+ /* The reduced-motion stand-in: the highlight stops travelling and the whole
1010
+ placeholder breathes instead. Colour, which is the axis every other state in
1011
+ the library already uses. */
1012
+ @keyframes neba-skeleton-pulse {
1013
+ 0%,
1014
+ 100% {
1015
+ background-color: transparent;
1016
+ }
1017
+ 50% {
1018
+ background-color: var(--n-panel-press);
1019
+ }
1020
+ }
1021
+
965
1022
  /* The reduced-motion stand-in for the sweep: the segment stops travelling and
966
1023
  the whole groove pulses instead. Colour, which is the axis every other state
967
1024
  in the library already uses. */
@@ -975,6 +1032,308 @@
975
1032
  }
976
1033
  }
977
1034
 
1035
+ /* ---------------------------------------------------------------------------
1036
+ * Animation
1037
+ *
1038
+ * The `Animate*` components, and the `transition` prop that shares their
1039
+ * effects. Everything below is real CSS for the reason the loading indicators
1040
+ * above are: a keyframe is not a Tailwind variant, so there is nothing to
1041
+ * express one with.
1042
+ *
1043
+ * The whole set is built on one idea. An effect is a single `@keyframes` that
1044
+ * runs from a state written entirely in `--n-anim-*` custom properties to the
1045
+ * element's natural one — so `src/internal/animate.ts` never generates CSS, it
1046
+ * only fills slots, and running an effect backwards is `animation-direction:
1047
+ * reverse` rather than a second keyframe. That is what makes `mode="out"` cost
1048
+ * nothing on all six.
1049
+ *
1050
+ * `fill-mode` is `both` throughout, which is what lets an animation that has
1051
+ * not been triggered yet simply sit paused on its own first frame. Without it,
1052
+ * an element waiting to be scrolled into view would be fully drawn and then
1053
+ * blink out the moment it arrived.
1054
+ *
1055
+ * This is also where the library's rule against transforms is at its narrowest.
1056
+ * A *control* never moves — that rule is about a button resampling its label
1057
+ * under the pointer, and it still holds without exception. An `Animate*` wraps
1058
+ * content the caller asked to have moved, and it moves it with the independent
1059
+ * `translate`, `scale` and `rotate` properties rather than the `transform`
1060
+ * shorthand, so a caller's own transform on the same element survives.
1061
+ * ------------------------------------------------------------------------- */
1062
+
1063
+ .neba-anim {
1064
+ animation-duration: var(--n-anim-duration, 320ms);
1065
+ animation-delay: var(--n-anim-delay, 0ms);
1066
+ animation-timing-function: var(--n-anim-ease, var(--neba-ease));
1067
+ animation-iteration-count: var(--n-anim-repeat, 1);
1068
+ animation-direction: var(--n-anim-direction, normal);
1069
+ animation-fill-mode: both;
1070
+ animation-play-state: var(--n-anim-state, running);
1071
+ }
1072
+
1073
+ @keyframes neba-anim-fade {
1074
+ from {
1075
+ opacity: var(--n-anim-opacity, 0);
1076
+ }
1077
+ to {
1078
+ opacity: 1;
1079
+ }
1080
+ }
1081
+
1082
+ .neba-anim-fade {
1083
+ animation-name: neba-anim-fade;
1084
+ }
1085
+
1086
+ /* Grow and Zoom share this one: they are the same arithmetic at two strengths,
1087
+ separated by their defaults and by their origin rather than by their maths. */
1088
+ @keyframes neba-anim-scale {
1089
+ from {
1090
+ opacity: var(--n-anim-opacity, 0);
1091
+ scale: var(--n-anim-scale, 0.8);
1092
+ }
1093
+ to {
1094
+ opacity: 1;
1095
+ scale: 1;
1096
+ }
1097
+ }
1098
+
1099
+ .neba-anim-scale {
1100
+ animation-name: neba-anim-scale;
1101
+ }
1102
+
1103
+ @keyframes neba-anim-slide {
1104
+ from {
1105
+ opacity: var(--n-anim-opacity, 0);
1106
+ translate: var(--n-anim-x, 0px) var(--n-anim-y, 0px);
1107
+ }
1108
+ to {
1109
+ opacity: 1;
1110
+ translate: 0 0;
1111
+ }
1112
+ }
1113
+
1114
+ .neba-anim-slide {
1115
+ animation-name: neba-anim-slide;
1116
+ }
1117
+
1118
+ /* Two angles rather than one, so the same keyframe covers both the quarter turn
1119
+ something arrives on and the endless one a badge spins through. */
1120
+ @keyframes neba-anim-rotate {
1121
+ from {
1122
+ opacity: var(--n-anim-opacity, 0);
1123
+ rotate: var(--n-anim-angle, -180deg);
1124
+ }
1125
+ to {
1126
+ opacity: 1;
1127
+ rotate: var(--n-anim-angle-to, 0deg);
1128
+ }
1129
+ }
1130
+
1131
+ .neba-anim-rotate {
1132
+ animation-name: neba-anim-rotate;
1133
+ }
1134
+
1135
+ /* Symmetric on purpose: a blink that ended anywhere but where it started would
1136
+ leave the element half drawn whenever the count ran out. */
1137
+ @keyframes neba-anim-blink {
1138
+ 0%,
1139
+ 100% {
1140
+ opacity: 1;
1141
+ }
1142
+ 50% {
1143
+ opacity: var(--n-anim-opacity, 0);
1144
+ }
1145
+ }
1146
+
1147
+ .neba-anim-blink {
1148
+ animation-name: neba-anim-blink;
1149
+ }
1150
+
1151
+ /*
1152
+ * Lighting: a light travelling around the outside of something.
1153
+ *
1154
+ * The arc is a conic gradient and what moves is its `from` angle, which is only
1155
+ * animatable because the angle is a registered custom property with a type. The
1156
+ * alternative — rotating the pseudo-element itself — swings its corners out
1157
+ * past the element on every quarter turn.
1158
+ *
1159
+ * `z-index: -1` inside an isolated stacking context puts the glow behind the
1160
+ * content but still inside the component, so it reads as light escaping from
1161
+ * under a sheet rather than as a border drawn around one.
1162
+ */
1163
+ @property --neba-glow-angle {
1164
+ syntax: '<angle>';
1165
+ initial-value: 0deg;
1166
+ inherits: false;
1167
+ }
1168
+
1169
+ @keyframes neba-anim-lighting {
1170
+ to {
1171
+ --neba-glow-angle: 360deg;
1172
+ }
1173
+ }
1174
+
1175
+ .neba-anim-lighting {
1176
+ position: relative;
1177
+ isolation: isolate;
1178
+ }
1179
+
1180
+ .neba-anim-lighting::before {
1181
+ content: '';
1182
+ position: absolute;
1183
+ z-index: -1;
1184
+ inset: calc(-1 * var(--n-anim-glow-width, 3px));
1185
+ border-radius: inherit;
1186
+ background: conic-gradient(
1187
+ from var(--neba-glow-angle),
1188
+ transparent 0deg,
1189
+ var(--n-anim-glow, var(--n-accent)) var(--n-anim-glow-arc, 50deg),
1190
+ transparent calc(var(--n-anim-glow-arc, 50deg) * 2),
1191
+ transparent 360deg
1192
+ );
1193
+ filter: blur(var(--n-anim-glow-blur, 4px));
1194
+ animation-name: neba-anim-lighting;
1195
+ animation-duration: var(--n-anim-duration, 3s);
1196
+ animation-delay: var(--n-anim-delay, 0ms);
1197
+ animation-timing-function: linear;
1198
+ animation-iteration-count: var(--n-anim-repeat, infinite);
1199
+ animation-direction: var(--n-anim-direction, normal);
1200
+ animation-fill-mode: both;
1201
+ animation-play-state: var(--n-anim-state, running);
1202
+ }
1203
+
1204
+ /*
1205
+ * Marquee: two identical copies, both moving by exactly one copy plus the gap.
1206
+ *
1207
+ * Which is the whole trick — when the first copy has travelled its own width,
1208
+ * the second is standing precisely where the first began, so there is no seam
1209
+ * and no frame where the strip is empty. A percentage `translate` resolves
1210
+ * against the element's own box, so neither copy has to be measured for this
1211
+ * to hold; measuring only ever decides how *fast* it goes.
1212
+ */
1213
+ @keyframes neba-anim-marquee-x {
1214
+ from {
1215
+ translate: 0 0;
1216
+ }
1217
+ to {
1218
+ translate: calc(-100% - var(--n-anim-gap, 0px)) 0;
1219
+ }
1220
+ }
1221
+
1222
+ @keyframes neba-anim-marquee-y {
1223
+ from {
1224
+ translate: 0 0;
1225
+ }
1226
+ to {
1227
+ translate: 0 calc(-100% - var(--n-anim-gap, 0px));
1228
+ }
1229
+ }
1230
+
1231
+ .neba-marquee {
1232
+ display: flex;
1233
+ overflow: hidden;
1234
+ gap: var(--n-anim-gap, 0px);
1235
+ }
1236
+
1237
+ .neba-marquee-vertical {
1238
+ flex-direction: column;
1239
+ }
1240
+
1241
+ .neba-marquee-track {
1242
+ display: flex;
1243
+ flex: 0 0 auto;
1244
+ align-items: center;
1245
+ gap: var(--n-anim-gap, 0px);
1246
+ animation-name: neba-anim-marquee-x;
1247
+ animation-duration: var(--n-anim-duration, 12s);
1248
+ animation-delay: var(--n-anim-delay, 0ms);
1249
+ /* Linear unless a caller insists otherwise. An eased marquee slows at both
1250
+ ends of a loop that has no ends, which reads as the page stuttering. */
1251
+ animation-timing-function: var(--n-anim-ease, linear);
1252
+ animation-iteration-count: var(--n-anim-repeat, infinite);
1253
+ animation-direction: var(--n-anim-direction, normal);
1254
+ animation-fill-mode: both;
1255
+ animation-play-state: var(--n-anim-state, running);
1256
+ }
1257
+
1258
+ .neba-marquee-vertical .neba-marquee-track {
1259
+ flex-direction: column;
1260
+ animation-name: neba-anim-marquee-y;
1261
+ }
1262
+
1263
+ .neba-marquee[data-pause-on-hover]:hover .neba-marquee-track {
1264
+ animation-play-state: paused;
1265
+ }
1266
+
1267
+ /*
1268
+ * Headline: one line replacing another from below.
1269
+ *
1270
+ * Every item is in the same grid cell, so the box is as tall as the tallest of
1271
+ * them and never resizes as the reel turns. The ones not on screen keep their
1272
+ * space with `visibility` rather than `display`, which is what makes that true.
1273
+ */
1274
+ @keyframes neba-anim-headline-in {
1275
+ from {
1276
+ translate: 0 var(--n-anim-rise, 100%);
1277
+ opacity: 0;
1278
+ }
1279
+ to {
1280
+ translate: 0 0;
1281
+ opacity: 1;
1282
+ }
1283
+ }
1284
+
1285
+ @keyframes neba-anim-headline-out {
1286
+ from {
1287
+ translate: 0 0;
1288
+ opacity: 1;
1289
+ }
1290
+ to {
1291
+ translate: 0 calc(-1 * var(--n-anim-rise, 100%));
1292
+ opacity: 0;
1293
+ }
1294
+ }
1295
+
1296
+ .neba-headline {
1297
+ display: grid;
1298
+ overflow: hidden;
1299
+ }
1300
+
1301
+ .neba-headline-item {
1302
+ grid-area: 1 / 1;
1303
+ visibility: hidden;
1304
+ animation-duration: var(--n-anim-duration, 480ms);
1305
+ animation-timing-function: var(--n-anim-ease, var(--neba-ease));
1306
+ animation-fill-mode: both;
1307
+ }
1308
+
1309
+ .neba-headline-item[data-state='active'] {
1310
+ visibility: visible;
1311
+ animation-name: neba-anim-headline-in;
1312
+ }
1313
+
1314
+ .neba-headline-item[data-state='leaving'] {
1315
+ visibility: visible;
1316
+ animation-name: neba-anim-headline-out;
1317
+ }
1318
+
1319
+ /* The block after a typewriter. `step-end` rather than a fade, because a caret
1320
+ that eases is a caret that looks like it is being rendered slowly. */
1321
+ @keyframes neba-anim-caret {
1322
+ 0%,
1323
+ 45% {
1324
+ opacity: 1;
1325
+ }
1326
+ 55%,
1327
+ 100% {
1328
+ opacity: 0;
1329
+ }
1330
+ }
1331
+
1332
+ .neba-typing-caret {
1333
+ display: inline-block;
1334
+ animation: neba-anim-caret 1s step-end infinite;
1335
+ }
1336
+
978
1337
  /* Respect a reduced-motion preference. Both layers track the pointer, so they
979
1338
  are motion in the sense that matters here — drop them entirely. */
980
1339
  @media (prefers-reduced-motion: reduce) {
@@ -1014,4 +1373,39 @@
1014
1373
  .neba-typing-dot {
1015
1374
  animation-duration: 2.6s;
1016
1375
  }
1376
+
1377
+ /* The highlight stops travelling and the placeholder pulses in place. Kept
1378
+ running for the same reason: a skeleton that holds still is indistinguishable
1379
+ from an empty box that finished loading with nothing in it. */
1380
+ .neba-skeleton::after {
1381
+ inset-inline-start: 0;
1382
+ inline-size: 100%;
1383
+ background-image: none;
1384
+ animation: neba-skeleton-pulse 2.4s var(--neba-ease) infinite;
1385
+ }
1386
+
1387
+ /*
1388
+ * The Animate components go the other way from the indicators above, and the
1389
+ * difference is what each of them is saying. A spinner that stops is lying
1390
+ * about whether anything is happening; an entrance that does not play has
1391
+ * still delivered its content, which is the only thing it was ever carrying.
1392
+ *
1393
+ * `animation: none` rather than a zero duration, because the shorthand also
1394
+ * clears `fill-mode` — and it is the fill that was holding an untriggered
1395
+ * element on its own first frame at `opacity: 0`. Left in place, respecting a
1396
+ * reader's preference would blank the page.
1397
+ */
1398
+ .neba-anim,
1399
+ .neba-marquee-track,
1400
+ .neba-headline-item,
1401
+ .neba-typing-caret {
1402
+ animation: none;
1403
+ }
1404
+
1405
+ /* The arc stops travelling and becomes an even glow. The same trade the
1406
+ skeleton makes: the decoration survives, the motion does not. */
1407
+ .neba-anim-lighting::before {
1408
+ background: var(--n-anim-glow, var(--n-accent));
1409
+ animation: none;
1410
+ }
1017
1411
  }
package/dist/types.d.ts CHANGED
@@ -129,6 +129,118 @@ export type NebaVariant = 'solid' | 'outline' | 'text';
129
129
  * pressing removes one, so a flat control still answers a press without moving.
130
130
  */
131
131
  export type NebaElevation = 0 | 1 | 2 | 3;
132
+ /**
133
+ * The six effects the `Animate*` components are built out of, and the six
134
+ * values the `transition` prop takes.
135
+ *
136
+ * They are named after what a reader sees rather than after the CSS property
137
+ * underneath: `zoom` and `grow` are both a change of scale, and they are two
138
+ * words because they are two *gestures* — one arrives from the middle of where
139
+ * it will end up, the other unfolds from nothing.
140
+ *
141
+ * Everything past these six is a component rather than a value. Typing, a
142
+ * marquee and a headline reel need to know what their children *are*, and a
143
+ * prop that only sets class names cannot.
144
+ */
145
+ export type NebaAnimation = 'fade' | 'grow' | 'slide' | 'zoom' | 'rotate' | 'blink';
146
+ /**
147
+ * What makes an animation run.
148
+ *
149
+ * - `mount` — as soon as it is on the page. The default, and the only one that
150
+ * needs nothing from the caller.
151
+ * - `visible` — when it is scrolled into view. Once, unless `once` is off.
152
+ * - `hover` — while the pointer is on it, restarting from the beginning on each
153
+ * entry. Keyboard focus counts as a pointer here, or the effect would be
154
+ * unreachable without a mouse.
155
+ * - `manual` — never on its own. `play` is what runs it.
156
+ */
157
+ export type NebaAnimateTrigger = 'mount' | 'visible' | 'hover' | 'manual';
158
+ /** Whether an effect brings its child in or takes it away. */
159
+ export type NebaAnimateMode = 'in' | 'out';
160
+ /**
161
+ * How many times an animation runs. `'infinite'` rather than `Infinity`,
162
+ * because it is written into CSS as that word and a caller who types the
163
+ * number would be surprised by which one worked.
164
+ */
165
+ export type NebaAnimateRepeat = number | 'infinite';
166
+ /**
167
+ * The settings every `Animate*` component takes, and the reason they are one
168
+ * interface: a `delay` of 200 has to mean the same thing on a fade and on a
169
+ * marquee, exactly as a `size` of `md` means one height everywhere.
170
+ *
171
+ * Durations and delays are milliseconds — numbers, not CSS strings. A prop
172
+ * whose type is `string` invites `'0.4s'`, and then two components in the same
173
+ * screen are written in two units.
174
+ */
175
+ export interface NebaAnimateProps {
176
+ /** How long one run takes, in milliseconds. */
177
+ duration?: number;
178
+ /** How long before it starts, in milliseconds. @default 0 */
179
+ delay?: number;
180
+ /** The easing curve, as CSS writes it. @default the house curve */
181
+ easing?: string;
182
+ /** How many times it runs. @default 1 */
183
+ repeat?: NebaAnimateRepeat;
184
+ /** Runs every other pass backwards, so a repeat returns instead of jumping. */
185
+ alternate?: boolean;
186
+ /** Holds the animation where it is. @default false */
187
+ paused?: boolean;
188
+ /** What starts it. @default 'mount' */
189
+ trigger?: NebaAnimateTrigger;
190
+ /** Runs it, when `trigger` is `manual`. Each `false` → `true` starts it over. */
191
+ play?: boolean;
192
+ /**
193
+ * With `trigger="visible"`, whether it runs only the first time. Off, it runs
194
+ * again every time the element comes back into view.
195
+ * @default true
196
+ */
197
+ once?: boolean;
198
+ /**
199
+ * With `trigger="visible"`, how much of the element has to be on screen
200
+ * before it counts as visible, from `0` to `1`.
201
+ * @default 0.2
202
+ */
203
+ threshold?: number;
204
+ }
205
+ /** The options a `transition` prop takes when a bare effect name is not enough. */
206
+ export interface NebaTransitionOptions {
207
+ /** Which effect. */
208
+ type: NebaAnimation;
209
+ /** Milliseconds. */
210
+ duration?: number;
211
+ /** Milliseconds. @default 0 */
212
+ delay?: number;
213
+ /** CSS easing. @default the house curve */
214
+ easing?: string;
215
+ /** @default 1 */
216
+ repeat?: NebaAnimateRepeat;
217
+ /** Runs every other pass backwards. */
218
+ alternate?: boolean;
219
+ /** Which edge a `slide` comes from. @default 'bottom' */
220
+ from?: NebaSide;
221
+ /** How far a `slide` travels — a CSS length, or a number in pixels. */
222
+ distance?: number | string;
223
+ /** Where a `grow` or a `zoom` starts, as a multiple of its final size. */
224
+ scale?: number;
225
+ /** How far a `rotate` turns from, in degrees. */
226
+ angle?: number;
227
+ }
228
+ /**
229
+ * An entrance animation on a component that displays something.
230
+ *
231
+ * A bare effect name is the whole of what most callers want —
232
+ * `transition="fade"` — and the object form is there for the rest.
233
+ *
234
+ * It runs on mount and once. Anything else — replaying on scroll, on hover, or
235
+ * under your own control — is what the `Animate*` components are, and any
236
+ * component can be wrapped in one. This prop exists so the common case does not
237
+ * need an extra element in the tree.
238
+ *
239
+ * It is offered on components that *display* something and on none that are
240
+ * pressed. A control that moves under the pointer is the one thing the design
241
+ * language rules out, and a `transition` on a Button would be exactly that.
242
+ */
243
+ export type NebaTransition = NebaAnimation | NebaTransitionOptions;
132
244
  /** Style props shared by most components; spread into their own prop types. */
133
245
  export interface NebaStyleProps {
134
246
  /** @default 'solid' */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neba",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "A comprehensive React UI component library — buttons, fields, menus, dialogs, tables and much more — accessible and themeable, sharing one prop vocabulary and a translucent acrylic design language. ESM only, types included, dark mode built in.",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
@@ -23,9 +23,9 @@
23
23
  "format": "prettier .",
24
24
  "format:fix": "prettier . --write",
25
25
  "docs:changelog": "node scripts/copy-changelog.mjs",
26
- "docs:dev": "npm run build && npm run docs:changelog && vitepress dev docs",
27
- "docs:build": "npm run build && npm run docs:changelog && vitepress build docs",
28
- "docs:serve": "npm run build && npm run docs:build && vitepress serve docs",
26
+ "docs:dev": "npm run docs:changelog && vitepress dev docs",
27
+ "docs:build": "npm run docs:changelog && vitepress build docs",
28
+ "docs:serve": "npm run docs:build && vitepress serve docs",
29
29
  "test": "vitest run",
30
30
  "test:watch": "vitest",
31
31
  "typecheck": "tsc --project tsconfig.prod.json --noEmit && tsc --project test/tsconfig.json && tsc --project docs/tsconfig.json"
@@ -103,14 +103,14 @@
103
103
  "@types/node": "^26.1.2",
104
104
  "@types/react": "^19.2.18",
105
105
  "@types/react-dom": "^19.2.4",
106
- "@typescript-eslint/parser": "^8.65.0",
107
- "@vitejs/plugin-react-swc": "^4.3.3",
106
+ "@typescript-eslint/parser": "^8.66.0",
107
+ "@vitejs/plugin-react": "^6.0.5",
108
108
  "@vitest/browser": "^4.1.10",
109
109
  "@vitest/browser-playwright": "^4.1.10",
110
110
  "eslint": "^9.39.1",
111
111
  "eslint-config-prettier": "^10.1.8",
112
112
  "eslint-plugin-n": "^18.2.2",
113
- "globals": "^17.8.0",
113
+ "globals": "^17.9.0",
114
114
  "jiti": "^2.7.0",
115
115
  "playwright": "^1.62.1",
116
116
  "postcss": "^8.5.25",
@@ -120,18 +120,18 @@
120
120
  "react-dom": "^19.2.8",
121
121
  "tailwindcss": "^4.3.3",
122
122
  "terser-glob": "^1.2.1",
123
- "tsx": "^4.23.1",
124
- "typescript": "^5.9.3",
125
- "typescript-eslint": "^8.65.0",
123
+ "tsx": "^4.23.8",
124
+ "typescript": "^6.0.3",
125
+ "typescript-eslint": "^8.66.0",
126
126
  "vite": "^8.2.0",
127
127
  "vitepress": "^1.6.4",
128
128
  "vitepress-i18n": "^1.3.5",
129
- "vitepress-sidebar": "^1.36.1",
129
+ "vitepress-sidebar": "^1.38.0",
130
130
  "vitest": "^4.1.10",
131
131
  "vitest-browser-react": "^2.2.0"
132
132
  },
133
133
  "dependencies": {
134
- "@base-ui/react": "^1.6.0"
134
+ "@base-ui/react": "^1.7.0"
135
135
  },
136
136
  "peerDependencies": {
137
137
  "@types/react": "^18.0.0 || ^19.0.0",