@a-type/ui 0.3.1 → 0.3.3

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 (178) hide show
  1. package/dist/cjs/components/button/Button.js +6 -1
  2. package/dist/cjs/components/button/Button.js.map +1 -1
  3. package/dist/cjs/components/camera/Camera.d.ts +1 -0
  4. package/dist/cjs/components/camera/Camera.js +24 -18
  5. package/dist/cjs/components/camera/Camera.js.map +1 -1
  6. package/dist/cjs/components/icon/generated/IconSpritesheet.js +1 -1
  7. package/dist/cjs/components/icon/generated/IconSpritesheet.js.map +1 -1
  8. package/dist/cjs/components/icon/generated/iconNames.d.ts +1 -1
  9. package/dist/cjs/components/icon/generated/iconNames.js +10 -0
  10. package/dist/cjs/components/icon/generated/iconNames.js.map +1 -1
  11. package/dist/cjs/components/imageUploader/ImageUploader.js +28 -2
  12. package/dist/cjs/components/imageUploader/ImageUploader.js.map +1 -1
  13. package/dist/cjs/components/imageUploader/ImageUploader.stories.d.ts +22 -0
  14. package/dist/cjs/components/imageUploader/ImageUploader.stories.js +28 -0
  15. package/dist/cjs/components/imageUploader/ImageUploader.stories.js.map +1 -0
  16. package/dist/esm/components/button/Button.js +6 -1
  17. package/dist/esm/components/button/Button.js.map +1 -1
  18. package/dist/esm/components/camera/Camera.d.ts +1 -0
  19. package/dist/esm/components/camera/Camera.js +24 -18
  20. package/dist/esm/components/camera/Camera.js.map +1 -1
  21. package/dist/esm/components/icon/generated/IconSpritesheet.js +1 -1
  22. package/dist/esm/components/icon/generated/IconSpritesheet.js.map +1 -1
  23. package/dist/esm/components/icon/generated/iconNames.d.ts +1 -1
  24. package/dist/esm/components/icon/generated/iconNames.js +10 -0
  25. package/dist/esm/components/icon/generated/iconNames.js.map +1 -1
  26. package/dist/esm/components/imageUploader/ImageUploader.js +29 -3
  27. package/dist/esm/components/imageUploader/ImageUploader.js.map +1 -1
  28. package/dist/esm/components/imageUploader/ImageUploader.stories.d.ts +22 -0
  29. package/dist/esm/components/imageUploader/ImageUploader.stories.js +25 -0
  30. package/dist/esm/components/imageUploader/ImageUploader.stories.js.map +1 -0
  31. package/package.json +3 -2
  32. package/src/components/actions/ActionBar.tsx +38 -0
  33. package/src/components/actions/ActionButton.tsx +59 -0
  34. package/src/components/actions/index.ts +2 -0
  35. package/src/components/actions.ts +1 -0
  36. package/src/components/avatar/Avatar.tsx +62 -0
  37. package/src/components/avatar/AvatarList.tsx +71 -0
  38. package/src/components/avatar/index.ts +2 -0
  39. package/src/components/avatar.ts +1 -0
  40. package/src/components/button/Button.stories.tsx +20 -0
  41. package/src/components/button/Button.tsx +66 -0
  42. package/src/components/button/ConfirmedButton.tsx +66 -0
  43. package/src/components/button/classes.tsx +56 -0
  44. package/src/components/button/index.ts +3 -0
  45. package/src/components/button.ts +1 -0
  46. package/src/components/camera/Camera.stories.tsx +40 -0
  47. package/src/components/camera/Camera.tsx +215 -0
  48. package/src/components/camera/index.ts +1 -0
  49. package/src/components/camera.ts +1 -0
  50. package/src/components/card/Card.stories.tsx +41 -0
  51. package/src/components/card/Card.tsx +68 -0
  52. package/src/components/card/index.ts +1 -0
  53. package/src/components/card.ts +1 -0
  54. package/src/components/checkbox/Checkbox.tsx +46 -0
  55. package/src/components/checkbox/index.ts +1 -0
  56. package/src/components/checkbox.ts +1 -0
  57. package/src/components/chip/Chip.tsx +29 -0
  58. package/src/components/chip/index.ts +1 -0
  59. package/src/components/chip.ts +1 -0
  60. package/src/components/collapsible/Collapsible.tsx +48 -0
  61. package/src/components/collapsible/index.ts +1 -0
  62. package/src/components/collapsible.ts +1 -0
  63. package/src/components/colorPicker/ColorPicker.tsx +82 -0
  64. package/src/components/colorPicker/index.ts +1 -0
  65. package/src/components/colorPicker.ts +1 -0
  66. package/src/components/contextMenu/contextMenu.tsx +43 -0
  67. package/src/components/contextMenu.ts +1 -0
  68. package/src/components/dialog/Dialog.stories.tsx +38 -0
  69. package/src/components/dialog/Dialog.tsx +267 -0
  70. package/src/components/dialog/index.ts +1 -0
  71. package/src/components/dialog.ts +1 -0
  72. package/src/components/divider/Divider.tsx +26 -0
  73. package/src/components/divider/index.ts +1 -0
  74. package/src/components/divider.ts +1 -0
  75. package/src/components/dropdownMenu/DropdownMenu.stories.tsx +47 -0
  76. package/src/components/dropdownMenu/DropdownMenu.tsx +89 -0
  77. package/src/components/dropdownMenu/index.ts +1 -0
  78. package/src/components/dropdownMenu.ts +1 -0
  79. package/src/components/errorBoundary/ErrorBoundary.tsx +23 -0
  80. package/src/components/errorBoundary/index.ts +1 -0
  81. package/src/components/errorBoundary.ts +1 -0
  82. package/src/components/forms/Form.tsx +9 -0
  83. package/src/components/forms/FormikForm.tsx +41 -0
  84. package/src/components/forms/SubmitButton.tsx +15 -0
  85. package/src/components/forms/TextField.tsx +112 -0
  86. package/src/components/forms/index.tsx +4 -0
  87. package/src/components/forms.ts +1 -0
  88. package/src/components/icon/Icon.tsx +28 -0
  89. package/src/components/icon/generated/IconSpritesheet.tsx +442 -0
  90. package/src/components/icon/generated/iconNames.ts +44 -0
  91. package/src/components/icon/index.ts +3 -0
  92. package/src/components/icon.ts +1 -0
  93. package/src/components/imageUploader/ImageUploader.stories.tsx +39 -0
  94. package/src/components/imageUploader/ImageUploader.tsx +203 -0
  95. package/src/components/imageUploader/UploadIcon.tsx +23 -0
  96. package/src/components/imageUploader/index.ts +1 -0
  97. package/src/components/imageUploader.ts +1 -0
  98. package/src/components/infiniteLoadTrigger/InfiniteLoadTrigger.tsx +38 -0
  99. package/src/components/infiniteLoadTrigger.ts +1 -0
  100. package/src/components/input/Input.stories.tsx +17 -0
  101. package/src/components/input/Input.tsx +32 -0
  102. package/src/components/input/index.ts +1 -0
  103. package/src/components/input.ts +1 -0
  104. package/src/components/layouts/PageContent.tsx +51 -0
  105. package/src/components/layouts/PageFixedArea.tsx +17 -0
  106. package/src/components/layouts/PageNav.tsx +23 -0
  107. package/src/components/layouts/PageNowPlaying.tsx +24 -0
  108. package/src/components/layouts/PageRoot.tsx +29 -0
  109. package/src/components/layouts/PageSection.tsx +23 -0
  110. package/src/components/layouts/index.tsx +6 -0
  111. package/src/components/layouts.ts +1 -0
  112. package/src/components/liveUpdateTextField/LiveUpdateTextField.tsx +132 -0
  113. package/src/components/liveUpdateTextField/index.ts +1 -0
  114. package/src/components/liveUpdateTextField.ts +1 -0
  115. package/src/components/navBar/NavBar.tsx +59 -0
  116. package/src/components/navBar/index.ts +1 -0
  117. package/src/components/navBar.ts +1 -0
  118. package/src/components/note/Note.tsx +21 -0
  119. package/src/components/note/index.ts +1 -0
  120. package/src/components/note.ts +1 -0
  121. package/src/components/numberStepper/NumberStepper.stories.tsx +21 -0
  122. package/src/components/numberStepper/NumberStepper.tsx +74 -0
  123. package/src/components/numberStepper/index.ts +1 -0
  124. package/src/components/numberStepper.ts +1 -0
  125. package/src/components/particles/ParticleContext.tsx +11 -0
  126. package/src/components/particles/ParticleLayer.stories.tsx +46 -0
  127. package/src/components/particles/ParticleLayer.tsx +28 -0
  128. package/src/components/particles/index.ts +7 -0
  129. package/src/components/particles/particlesState.ts +502 -0
  130. package/src/components/particles.ts +1 -0
  131. package/src/components/peek/Peek.tsx +74 -0
  132. package/src/components/peek/index.ts +1 -0
  133. package/src/components/peek.ts +1 -0
  134. package/src/components/popover/Popover.tsx +84 -0
  135. package/src/components/popover/index.ts +1 -0
  136. package/src/components/popover.ts +1 -0
  137. package/src/components/relativeTime/RelativeTime.tsx +43 -0
  138. package/src/components/relativeTime/index.ts +1 -0
  139. package/src/components/relativeTime.ts +1 -0
  140. package/src/components/richEditor/EditorContent.tsx +4 -0
  141. package/src/components/richEditor/RichEditor.tsx +38 -0
  142. package/src/components/richEditor/index.ts +1 -0
  143. package/src/components/richEditor.ts +1 -0
  144. package/src/components/select/Select.tsx +247 -0
  145. package/src/components/select/index.ts +1 -0
  146. package/src/components/select.ts +1 -0
  147. package/src/components/skeletons/skeletons.tsx +27 -0
  148. package/src/components/skeletons.ts +1 -0
  149. package/src/components/spinner/Spinner.tsx +59 -0
  150. package/src/components/spinner/index.ts +1 -0
  151. package/src/components/spinner.ts +1 -0
  152. package/src/components/switch/Switch.tsx +23 -0
  153. package/src/components/switch/index.ts +1 -0
  154. package/src/components/switch.ts +1 -0
  155. package/src/components/tabs/tabs.tsx +18 -0
  156. package/src/components/tabs.ts +1 -0
  157. package/src/components/textArea/TextArea.stories.tsx +21 -0
  158. package/src/components/textArea/TextArea.tsx +58 -0
  159. package/src/components/textArea/index.ts +1 -0
  160. package/src/components/textArea.ts +1 -0
  161. package/src/components/toggleGroup/toggleGroup.tsx +11 -0
  162. package/src/components/toggleGroup.ts +1 -0
  163. package/src/components/tooltip/Tooltip.tsx +56 -0
  164. package/src/components/tooltip/index.ts +1 -0
  165. package/src/components/tooltip.ts +1 -0
  166. package/src/components/typography/index.ts +1 -0
  167. package/src/components/typography/typography.tsx +18 -0
  168. package/src/components/typography.ts +1 -0
  169. package/src/hooks/index.ts +7 -0
  170. package/src/hooks/useMergedRef.ts +14 -0
  171. package/src/hooks/useOnUnmount.ts +20 -0
  172. package/src/hooks/useSize.ts +164 -0
  173. package/src/hooks/useStableCallback.ts +11 -0
  174. package/src/hooks/useToggle.tsx +9 -0
  175. package/src/hooks/useVisualViewportOffset.ts +35 -0
  176. package/src/hooks/withClassName.tsx +21 -0
  177. package/src/hooks.ts +1 -0
  178. package/src/uno.preset.ts +767 -0
@@ -0,0 +1,502 @@
1
+ export class Particles {
2
+ private canvas: HTMLCanvasElement | null = null;
3
+ private ctx: CanvasRenderingContext2D | null = null;
4
+ private state: 'paused' | 'running' = 'paused';
5
+ private lastFrameTimestamp = 0;
6
+ private disabled = false;
7
+
8
+ // an object pool of Particles
9
+ private particles: Particle[];
10
+ private freeParticles: Particle[] = [];
11
+
12
+ // keep canvas render size the same as its actual size
13
+ private resizeObserver = new ResizeObserver((entries) => {
14
+ for (const entry of entries) {
15
+ const { width, height } = entry.contentRect;
16
+ if (this.canvas) {
17
+ this.canvas.width = width;
18
+ this.canvas.height = height;
19
+ }
20
+ }
21
+ });
22
+
23
+ constructor({ initialPoolSize }: { initialPoolSize: number }) {
24
+ // if prefers-reduced-motion is set, disable particles
25
+ this.disabled = window.matchMedia(
26
+ '(prefers-reduced-motion: reduce)',
27
+ ).matches;
28
+ if (this.disabled) {
29
+ initialPoolSize = 0;
30
+ }
31
+ this.particles = new Array(initialPoolSize);
32
+ for (let i = 0; i < initialPoolSize; i++) {
33
+ this.particles[i] = new Particle();
34
+ this.freeParticles.push(this.particles[i]);
35
+ }
36
+ }
37
+
38
+ setCanvas = (canvas: HTMLCanvasElement | null) => {
39
+ if (this.disabled) {
40
+ return;
41
+ }
42
+
43
+ if (this.canvas) {
44
+ this.resizeObserver.unobserve(this.canvas);
45
+ }
46
+ this.canvas = canvas;
47
+ this.ctx = canvas ? canvas.getContext('2d') : null;
48
+ if (!canvas) {
49
+ this.pause();
50
+ } else {
51
+ this.resume();
52
+ this.resizeObserver.observe(canvas);
53
+ }
54
+ };
55
+
56
+ private resume = () => {
57
+ if (this.disabled) {
58
+ return;
59
+ }
60
+
61
+ this.state = 'running';
62
+ this.lastFrameTimestamp = performance.now();
63
+ requestAnimationFrame(this.draw);
64
+ };
65
+
66
+ private pause = () => {
67
+ this.state = 'paused';
68
+ };
69
+
70
+ private draw = (timestamp: number) => {
71
+ if (this.state === 'paused') {
72
+ return;
73
+ }
74
+ const ctx = this.ctx;
75
+
76
+ if (!ctx || !this.canvas) {
77
+ console.warn('No canvas context');
78
+ this.pause();
79
+ return;
80
+ }
81
+
82
+ const delta = timestamp - this.lastFrameTimestamp;
83
+
84
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
85
+
86
+ this.renderParticles(ctx, delta);
87
+
88
+ requestAnimationFrame(this.draw);
89
+ this.lastFrameTimestamp = timestamp;
90
+ };
91
+
92
+ private renderParticles = (ctx: CanvasRenderingContext2D, delta: number) => {
93
+ let freed = 0;
94
+ for (let i = 0; i < this.particles.length; i++) {
95
+ const particle = this.particles[i];
96
+ if (particle.render(ctx, delta)) {
97
+ particle.dispose();
98
+ this.freeParticles.push(particle);
99
+ freed++;
100
+ }
101
+ }
102
+ if (freed) {
103
+ console.log('Freed particles', freed);
104
+ }
105
+ };
106
+
107
+ addParticles = (spawn: ParticleSpawn) => {
108
+ // wrap in RAF because initializers often use element dimensions
109
+ requestAnimationFrame(() => {
110
+ if (this.freeParticles.length < spawn.count) {
111
+ this.extendPool(spawn.count - this.freeParticles.length);
112
+ }
113
+ for (let i = 0; i < spawn.count; i++) {
114
+ const particle = this.freeParticles.pop();
115
+ const initials = spawn.initializer(i);
116
+ if (!particle) {
117
+ throw new Error('Particle allocation failed');
118
+ }
119
+ particle.allocate(
120
+ initials.x,
121
+ initials.y,
122
+ initials.velocityX,
123
+ initials.velocityY,
124
+ initials.drag,
125
+ initials.lifespan,
126
+ spawn.behavior,
127
+ );
128
+ }
129
+ console.log('Allocated particles', spawn.count);
130
+ });
131
+ };
132
+
133
+ private extendPool = (size: number) => {
134
+ for (let i = 0; i < size; i++) {
135
+ const particle = new Particle();
136
+ this.particles.push(particle);
137
+ this.freeParticles.push(particle);
138
+ }
139
+ };
140
+
141
+ elementExplosion = elementExplosion;
142
+ windowBorderExplosion = windowBorderExplosion;
143
+ }
144
+
145
+ class Particle {
146
+ disposed = true;
147
+ lifetime = 0;
148
+ lifespan = 0;
149
+ x = -1;
150
+ y = -1;
151
+ behavior: ParticleBehavior = nullBehavior;
152
+ velocityX = 0;
153
+ velocityY = 0;
154
+ drag = 0;
155
+
156
+ constructor() {}
157
+
158
+ allocate = (
159
+ x: number,
160
+ y: number,
161
+ velocityX: number,
162
+ velocityY: number,
163
+ drag: number,
164
+ lifespan: number,
165
+ behavior: ParticleBehavior,
166
+ ) => {
167
+ this.disposed = false;
168
+ this.lifetime = 0;
169
+ this.lifespan = lifespan;
170
+ this.x = x;
171
+ this.y = y;
172
+ this.velocityX = velocityX;
173
+ this.velocityY = velocityY;
174
+ this.drag = drag;
175
+ this.behavior = behavior;
176
+ };
177
+
178
+ dispose = () => {
179
+ this.disposed = true;
180
+ };
181
+
182
+ render = (ctx: CanvasRenderingContext2D, delta: number) => {
183
+ if (this.disposed) {
184
+ return;
185
+ }
186
+ this.lifetime += delta;
187
+ this.x += this.velocityX * delta;
188
+ this.y += this.velocityY * delta;
189
+ this.velocityX *= 1 - this.drag * delta;
190
+ this.velocityY *= 1 - this.drag * delta;
191
+ this.behavior(ctx, this.x, this.y, this.lifetime, this.lifespan);
192
+ return this.lifetime >= this.lifespan;
193
+ };
194
+ }
195
+
196
+ type ParticleBehavior = (
197
+ ctx: CanvasRenderingContext2D,
198
+ x: number,
199
+ y: number,
200
+ lifetime: number,
201
+ lifespan: number,
202
+ ) => void;
203
+ const nullBehavior = () => {};
204
+
205
+ type ParticleSpawn = {
206
+ count: number;
207
+ behavior: ParticleBehavior;
208
+ initializer: ParticleInitializer;
209
+ };
210
+ type ParticleInitializer = (index: number) => {
211
+ x: number;
212
+ y: number;
213
+ lifespan: number;
214
+ velocityX: number;
215
+ velocityY: number;
216
+ drag: number;
217
+ };
218
+
219
+ export function createCircleParticles({
220
+ count,
221
+ startRadius = 4,
222
+ endRadius = 0,
223
+ initializer,
224
+ color = YELLOW,
225
+ }: {
226
+ count: number;
227
+ startRadius?: number;
228
+ endRadius?: number;
229
+ initializer: ParticleInitializer;
230
+ color?: Color | [Color, Color];
231
+ }): ParticleSpawn {
232
+ const colorMixer = Array.isArray(color)
233
+ ? colorInterpolate(color)
234
+ : colorInterpolate([color, color]);
235
+ return {
236
+ count,
237
+ behavior: (ctx, x, y, lifetime, lifespan) => {
238
+ const lifetimePercentage = Math.max(0, Math.min(1, lifetime / lifespan));
239
+ const finalColor = colorMixer(lifetimePercentage);
240
+ const radius = Math.max(
241
+ 0,
242
+ startRadius + (endRadius - startRadius) * lifetimePercentage,
243
+ );
244
+ ctx.beginPath();
245
+ ctx.fillStyle = colorToString(finalColor);
246
+ ctx.arc(x, y, radius, 0, 2 * Math.PI);
247
+ ctx.fill();
248
+ },
249
+ initializer,
250
+ };
251
+ }
252
+
253
+ function fuzz(value: number, fuzz: number) {
254
+ return value + (Math.random() - 0.5) * fuzz;
255
+ }
256
+
257
+ type BorderName = 'top' | 'right' | 'bottom' | 'left';
258
+ export const createElementBorderInitializer = ({
259
+ element,
260
+ borders = ['top', 'right', 'bottom', 'left'],
261
+ force = 0.1,
262
+ drag = 0.001,
263
+ lifespan = 2000,
264
+ forceFuzz = 0.05,
265
+ angleFuzz = 0.02,
266
+ }: {
267
+ element: HTMLElement;
268
+ borders?: BorderName[];
269
+ force?: number;
270
+ drag?: number;
271
+ lifespan?: number;
272
+ forceFuzz?: number;
273
+ angleFuzz?: number;
274
+ }): ParticleInitializer => {
275
+ // randomly spawn particles around the border of the element by 'unwrapping' the selected borders as
276
+ // a single theoretical line, picking a random point on the line, and then converting that point
277
+ // back to a point on the border.
278
+ return (index: number) => {
279
+ const rect = element.getBoundingClientRect();
280
+
281
+ const borderLengths = borders.map((border) => {
282
+ switch (border) {
283
+ case 'top':
284
+ case 'bottom':
285
+ return rect.width;
286
+ case 'left':
287
+ case 'right':
288
+ return rect.height;
289
+ }
290
+ });
291
+ const totalBorderLength = borderLengths.reduce((a, b) => a + b, 0);
292
+ const randomPoint = Math.random() * totalBorderLength;
293
+ let borderIndex = 0;
294
+ let borderLength = borderLengths[0];
295
+ while (randomPoint > borderLength) {
296
+ borderIndex++;
297
+ borderLength += borderLengths[borderIndex];
298
+ }
299
+ const border = borders[borderIndex];
300
+ const borderOffset =
301
+ randomPoint - (borderLength - borderLengths[borderIndex]);
302
+
303
+ let x = 0;
304
+ let y = 0;
305
+ let velocityX = 0;
306
+ let velocityY = 0;
307
+ switch (border) {
308
+ case 'top':
309
+ x = rect.left + borderOffset;
310
+ y = rect.top;
311
+ break;
312
+ case 'right':
313
+ x = rect.right;
314
+ y = rect.top + borderOffset;
315
+ break;
316
+ case 'bottom':
317
+ x = rect.left + borderOffset;
318
+ y = rect.bottom;
319
+ break;
320
+ case 'left':
321
+ x = rect.left;
322
+ y = rect.top + borderOffset;
323
+ break;
324
+ }
325
+
326
+ // velocity is away from the center of the element
327
+ const center = {
328
+ x: rect.left + rect.width / 2,
329
+ y: rect.top + rect.height / 2,
330
+ };
331
+
332
+ // special case: width/height is 0
333
+ let angle =
334
+ rect.width === 0 || rect.height === 0
335
+ ? Math.random() * (Math.PI * 2)
336
+ : Math.atan2(y - center.y, x - center.x);
337
+ angle = fuzz(angle, angleFuzz);
338
+
339
+ const totalForce = fuzz(force, forceFuzz);
340
+ velocityX = Math.cos(angle) * totalForce;
341
+ velocityY = Math.sin(angle) * totalForce;
342
+
343
+ return { x, y, velocityX, velocityY, drag, lifespan };
344
+ };
345
+ };
346
+
347
+ export type Color = {
348
+ space: 'rgb';
349
+ values: [number, number, number];
350
+ opacity: number;
351
+ };
352
+
353
+ const YELLOW: Color = {
354
+ space: 'rgb',
355
+ values: [249, 231, 148],
356
+ opacity: 1,
357
+ };
358
+ const TRANSPARENT: Color = {
359
+ space: 'rgb',
360
+ values: [255, 255, 255],
361
+ opacity: 0,
362
+ };
363
+
364
+ export const elementExplosion = ({
365
+ element,
366
+ color = [YELLOW, TRANSPARENT],
367
+ borders,
368
+ lifespan,
369
+ force,
370
+ drag,
371
+ forceFuzz,
372
+ angleFuzz,
373
+ ...rest
374
+ }: {
375
+ element: HTMLElement;
376
+ color?: Color | [Color, Color];
377
+ count: number;
378
+ startRadius?: number;
379
+ endRadius?: number;
380
+ borders?: BorderName[];
381
+ lifespan?: number;
382
+ force?: number;
383
+ drag?: number;
384
+ forceFuzz?: number;
385
+ angleFuzz?: number;
386
+ }) =>
387
+ createCircleParticles({
388
+ initializer: createElementBorderInitializer({
389
+ element,
390
+ borders,
391
+ lifespan,
392
+ force,
393
+ drag,
394
+ forceFuzz,
395
+ angleFuzz,
396
+ }),
397
+ color,
398
+ ...rest,
399
+ });
400
+
401
+ export const createWindowBorderInitializer = ({
402
+ force = 0.1,
403
+ drag = 0.001,
404
+ border = 'top',
405
+ lifespan = 2000,
406
+ }: {
407
+ force?: number;
408
+ drag?: number;
409
+ border?: BorderName;
410
+ lifespan?: number;
411
+ }): ParticleInitializer => {
412
+ return (index: number) => {
413
+ const rect = document.body.getBoundingClientRect();
414
+ let x = 0;
415
+ let y = 0;
416
+ let velocityX = 0;
417
+ let velocityY = 0;
418
+ switch (border) {
419
+ case 'top':
420
+ velocityY = force * (Math.random() + 0.25);
421
+ x = rect.left + Math.random() * rect.width;
422
+ y = rect.top;
423
+ break;
424
+ case 'right':
425
+ velocityX = -force * (Math.random() + 0.25);
426
+ x = rect.right;
427
+ y = rect.top + Math.random() * rect.height;
428
+ break;
429
+ case 'bottom':
430
+ velocityY = -force * (Math.random() + 0.25);
431
+ x = rect.left + Math.random() * rect.width;
432
+ y = rect.bottom;
433
+ break;
434
+ case 'left':
435
+ velocityX = force * (Math.random() + 0.25);
436
+ x = rect.left;
437
+ y = rect.top + Math.random() * rect.height;
438
+ break;
439
+ }
440
+ return { x, y, velocityX, velocityY, drag, lifespan };
441
+ };
442
+ };
443
+
444
+ export const windowBorderExplosion = ({
445
+ color = [YELLOW, TRANSPARENT],
446
+ border = 'top',
447
+ lifespan,
448
+ ...rest
449
+ }: {
450
+ color?: Color | [Color, Color];
451
+ count: number;
452
+ startRadius?: number;
453
+ endRadius?: number;
454
+ border?: BorderName;
455
+ lifespan?: number;
456
+ }) =>
457
+ createCircleParticles({
458
+ initializer: createWindowBorderInitializer({
459
+ border,
460
+ lifespan,
461
+ }),
462
+ color,
463
+ ...rest,
464
+ });
465
+
466
+ function colorInterpolate(colors: [Color, Color]) {
467
+ return function interpolate(value: number): Color {
468
+ if (colors[0].space !== colors[1].space) {
469
+ throw new Error('Cannot interpolate between colors of different spaces');
470
+ }
471
+ if (colors[0].space === 'rgb') {
472
+ return interpolateRgb(colors[0], colors[1], value);
473
+ } else {
474
+ throw new Error(`Unsupported color space ${colors[0].space}`);
475
+ }
476
+ };
477
+ }
478
+
479
+ function interpolateRgb(color1: Color, color2: Color, value: number): Color {
480
+ const result: Color = {
481
+ space: 'rgb' as const,
482
+ values: [0, 0, 0],
483
+ opacity: 0,
484
+ };
485
+ for (let i = 0; i < 3; i++) {
486
+ result.values[i] =
487
+ color1.values[i] + (color2.values[i] - color1.values[i]) * value;
488
+ }
489
+ result.opacity = color1.opacity + (color2.opacity - color1.opacity) * value;
490
+ return result;
491
+ }
492
+
493
+ function colorToString(color: Color): string {
494
+ if (color.space === 'rgb') {
495
+ return colorRgbToString(color);
496
+ }
497
+ throw new Error(`Unsupported color space ${color.space}`);
498
+ }
499
+
500
+ function colorRgbToString(color: Color) {
501
+ return `rgb(${color.values[0]}, ${color.values[1]}, ${color.values[2]})`;
502
+ }
@@ -0,0 +1 @@
1
+ export * from './particles/index.js';
@@ -0,0 +1,74 @@
1
+ import { debounce } from '@a-type/utils';
2
+ import classNames from 'classnames';
3
+ import { ReactNode, useEffect, useId, useMemo, useRef, useState } from 'react';
4
+ import { useSize } from '../../hooks/useSize.js';
5
+ import { useToggle } from '../../hooks/useToggle.js';
6
+
7
+ export interface PeekProps {
8
+ peekHeight?: number;
9
+ children: ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function Peek({ peekHeight = 120, children, className }: PeekProps) {
14
+ const containerRef = useRef<HTMLDivElement>(null);
15
+ const [isPeekable, setIsPeekable] = useState(false);
16
+ const contentRef = useSize<HTMLDivElement>(
17
+ useMemo(
18
+ () =>
19
+ debounce(({ height }) => {
20
+ setIsPeekable(height > peekHeight);
21
+ if (containerRef.current) {
22
+ containerRef.current.style.setProperty(
23
+ '--collapsible-height',
24
+ `${height}px`,
25
+ );
26
+ }
27
+ }, 300),
28
+ [],
29
+ ),
30
+ );
31
+
32
+ const [open, toggle] = useToggle(false);
33
+
34
+ useEffect(() => {
35
+ if (containerRef.current) {
36
+ containerRef.current.style.setProperty(
37
+ '--peek-height',
38
+ `${peekHeight}px`,
39
+ );
40
+ }
41
+ }, [peekHeight]);
42
+
43
+ const id = useId();
44
+
45
+ return (
46
+ <div
47
+ className={classNames(
48
+ 'relative animate-ease-default animate-forwards overflow-hidden [&[data-state=closed]]:(animate-keyframes-peek-close animate-duration-300 max-h-[var(--peek-height,120px)]) [&[data-state=open]]:(animate-keyframes-peek-open animate-duration-600)',
49
+ className,
50
+ )}
51
+ ref={containerRef}
52
+ data-state={isPeekable ? (open ? 'open' : 'closed') : undefined}
53
+ >
54
+ <div ref={contentRef} id={id}>
55
+ {children}
56
+ </div>
57
+ {isPeekable && (
58
+ <button
59
+ data-state={open ? 'open' : 'closed'}
60
+ className={classNames(
61
+ 'h-80px absolute bottom-0 z-1 bg-transparent border-none w-full cursor-pointer border-b border-solid border-white',
62
+ 'focus-visible:(outline-none bg-gradient-to-b from-transparent to-primary-wash border-b border-solid border-primary',
63
+ 'after:(content-["-_tap_to_expand_-"] p-3 color-gray9 text-xs flex flex-col justify-end items-center absolute inset-0 top-auto h-80px bg-gradient-to-b from-transparent to-white)',
64
+ 'after:[&[data-state=open]]:content-["-_tap_to_collapse_-"]',
65
+ )}
66
+ onClick={toggle}
67
+ aria-label="Toggle show description"
68
+ aria-expanded={open}
69
+ aria-controls={id}
70
+ />
71
+ )}
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1 @@
1
+ export * from './Peek.js';
@@ -0,0 +1 @@
1
+ export * from './peek/index.js';
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ import * as PopoverPrimitive from '@radix-ui/react-popover';
4
+ import { ComponentPropsWithoutRef, forwardRef } from 'react';
5
+
6
+ import { withClassName } from '../../hooks/withClassName.js';
7
+ import classNames from 'classnames';
8
+
9
+ const StyledContent = withClassName(
10
+ PopoverPrimitive.Content,
11
+ 'layer-components:(rounded-xl min-w-120px bg-white z-menu shadow-lg border-default op-0 display-none max-w-90vw)',
12
+ 'will-change-transform',
13
+ 'layer-components:transform-origin-[var(--radix-popover-transform-origin)]',
14
+ 'layer-components:[&[data-state=open]]:animate-popover-in',
15
+ 'layer-components:[&[data-state=closed]]:animate-popover-out',
16
+ 'important:motion-reduce:animate-none',
17
+ 'layer-components:(max-h-[var(--radix-popover-content-available-height)] overflow-y-auto)',
18
+ 'layer-components:[&[data-state=open]]:(opacity-100 flex flex-col)',
19
+ 'layer-components:[&[data-state=closed]]:pointer-events-none',
20
+ );
21
+
22
+ const StyledArrow = withClassName(
23
+ PopoverPrimitive.Arrow,
24
+ 'fill-white stroke-black',
25
+ );
26
+
27
+ const StyledClose = withClassName(
28
+ PopoverPrimitive.Close,
29
+ '[all:unset] [font-family:inherit] rounded-full h-25px w-25px inline-flex items-center justify-center color-dark-blend absolute top-5px right-5px hover:bg-light-blend focus:shadow-focus',
30
+ );
31
+
32
+ // Exports
33
+ export const Popover = PopoverPrimitive.Root;
34
+ export const PopoverTrigger = PopoverPrimitive.Trigger;
35
+ export const PopoverArrow = StyledArrow;
36
+ export const PopoverClose = StyledClose;
37
+ export const PopoverAnchor = PopoverPrimitive.Anchor;
38
+
39
+ export const PopoverContent = forwardRef<
40
+ HTMLDivElement,
41
+ ComponentPropsWithoutRef<typeof StyledContent> & {
42
+ disableBlur?: boolean;
43
+ containerClassName?: string;
44
+ padding?: 'none' | 'default';
45
+ radius?: 'none' | 'default' | 'md';
46
+ }
47
+ >(function PopoverContent(
48
+ {
49
+ children,
50
+ forceMount,
51
+ disableBlur,
52
+ containerClassName,
53
+ className,
54
+ radius = 'default',
55
+ padding = 'default',
56
+ ...props
57
+ },
58
+ ref,
59
+ ) {
60
+ return (
61
+ <PopoverPrimitive.Portal
62
+ forceMount={forceMount}
63
+ className={containerClassName}
64
+ >
65
+ <StyledContent
66
+ {...props}
67
+ forceMount={forceMount}
68
+ ref={ref}
69
+ className={classNames(
70
+ {
71
+ 'layer-variants:important:p-0': padding === 'none',
72
+ 'layer-variants:p-5': padding === 'default',
73
+ 'layer-variants:rounded-none': radius === 'none',
74
+ 'layer-variants:rounded-lg': radius === 'default',
75
+ 'layer-variants:rounded-md': radius === 'md',
76
+ },
77
+ className,
78
+ )}
79
+ >
80
+ {children}
81
+ </StyledContent>
82
+ </PopoverPrimitive.Portal>
83
+ );
84
+ });
@@ -0,0 +1 @@
1
+ export * from './Popover.js';
@@ -0,0 +1 @@
1
+ export * from './popover/index.js';
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useMemo } from 'react';
4
+ import { formatDistanceToNowStrict } from 'date-fns';
5
+ import { shortenTimeUnits } from '@a-type/utils';
6
+
7
+ export interface RelativeTimeProps {
8
+ value: number;
9
+ abbreviate?: boolean;
10
+ }
11
+
12
+ function formatDistanceToNow(date: Date) {
13
+ const now = Date.now();
14
+ if (Math.abs(date.getTime() - now) < 1000) {
15
+ return 'just now';
16
+ }
17
+ return (
18
+ formatDistanceToNowStrict(date) +
19
+ (date.getTime() < now ? ' ago' : ' from now')
20
+ );
21
+ }
22
+
23
+ export function RelativeTime({ value, abbreviate }: RelativeTimeProps) {
24
+ const asDate = useMemo(() => new Date(value), [value]);
25
+ const [time, setTime] = useState(() =>
26
+ abbreviate
27
+ ? shortenTimeUnits(formatDistanceToNow(asDate))
28
+ : formatDistanceToNow(asDate),
29
+ );
30
+
31
+ useEffect(() => {
32
+ const interval = setInterval(() => {
33
+ setTime(
34
+ abbreviate
35
+ ? shortenTimeUnits(formatDistanceToNow(asDate))
36
+ : formatDistanceToNow(asDate),
37
+ );
38
+ }, 60 * 1000);
39
+ return () => clearInterval(interval);
40
+ }, [asDate, abbreviate]);
41
+
42
+ return <>{time}</>;
43
+ }