@a-type/ui 0.3.2 → 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.
- package/package.json +3 -2
- package/src/components/actions/ActionBar.tsx +38 -0
- package/src/components/actions/ActionButton.tsx +59 -0
- package/src/components/actions/index.ts +2 -0
- package/src/components/actions.ts +1 -0
- package/src/components/avatar/Avatar.tsx +62 -0
- package/src/components/avatar/AvatarList.tsx +71 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/avatar.ts +1 -0
- package/src/components/button/Button.stories.tsx +20 -0
- package/src/components/button/Button.tsx +66 -0
- package/src/components/button/ConfirmedButton.tsx +66 -0
- package/src/components/button/classes.tsx +56 -0
- package/src/components/button/index.ts +3 -0
- package/src/components/button.ts +1 -0
- package/src/components/camera/Camera.stories.tsx +40 -0
- package/src/components/camera/Camera.tsx +215 -0
- package/src/components/camera/index.ts +1 -0
- package/src/components/camera.ts +1 -0
- package/src/components/card/Card.stories.tsx +41 -0
- package/src/components/card/Card.tsx +68 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/card.ts +1 -0
- package/src/components/checkbox/Checkbox.tsx +46 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/checkbox.ts +1 -0
- package/src/components/chip/Chip.tsx +29 -0
- package/src/components/chip/index.ts +1 -0
- package/src/components/chip.ts +1 -0
- package/src/components/collapsible/Collapsible.tsx +48 -0
- package/src/components/collapsible/index.ts +1 -0
- package/src/components/collapsible.ts +1 -0
- package/src/components/colorPicker/ColorPicker.tsx +82 -0
- package/src/components/colorPicker/index.ts +1 -0
- package/src/components/colorPicker.ts +1 -0
- package/src/components/contextMenu/contextMenu.tsx +43 -0
- package/src/components/contextMenu.ts +1 -0
- package/src/components/dialog/Dialog.stories.tsx +38 -0
- package/src/components/dialog/Dialog.tsx +267 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/dialog.ts +1 -0
- package/src/components/divider/Divider.tsx +26 -0
- package/src/components/divider/index.ts +1 -0
- package/src/components/divider.ts +1 -0
- package/src/components/dropdownMenu/DropdownMenu.stories.tsx +47 -0
- package/src/components/dropdownMenu/DropdownMenu.tsx +89 -0
- package/src/components/dropdownMenu/index.ts +1 -0
- package/src/components/dropdownMenu.ts +1 -0
- package/src/components/errorBoundary/ErrorBoundary.tsx +23 -0
- package/src/components/errorBoundary/index.ts +1 -0
- package/src/components/errorBoundary.ts +1 -0
- package/src/components/forms/Form.tsx +9 -0
- package/src/components/forms/FormikForm.tsx +41 -0
- package/src/components/forms/SubmitButton.tsx +15 -0
- package/src/components/forms/TextField.tsx +112 -0
- package/src/components/forms/index.tsx +4 -0
- package/src/components/forms.ts +1 -0
- package/src/components/icon/Icon.tsx +28 -0
- package/src/components/icon/generated/IconSpritesheet.tsx +442 -0
- package/src/components/icon/generated/iconNames.ts +44 -0
- package/src/components/icon/index.ts +3 -0
- package/src/components/icon.ts +1 -0
- package/src/components/imageUploader/ImageUploader.stories.tsx +39 -0
- package/src/components/imageUploader/ImageUploader.tsx +203 -0
- package/src/components/imageUploader/UploadIcon.tsx +23 -0
- package/src/components/imageUploader/index.ts +1 -0
- package/src/components/imageUploader.ts +1 -0
- package/src/components/infiniteLoadTrigger/InfiniteLoadTrigger.tsx +38 -0
- package/src/components/infiniteLoadTrigger.ts +1 -0
- package/src/components/input/Input.stories.tsx +17 -0
- package/src/components/input/Input.tsx +32 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input.ts +1 -0
- package/src/components/layouts/PageContent.tsx +51 -0
- package/src/components/layouts/PageFixedArea.tsx +17 -0
- package/src/components/layouts/PageNav.tsx +23 -0
- package/src/components/layouts/PageNowPlaying.tsx +24 -0
- package/src/components/layouts/PageRoot.tsx +29 -0
- package/src/components/layouts/PageSection.tsx +23 -0
- package/src/components/layouts/index.tsx +6 -0
- package/src/components/layouts.ts +1 -0
- package/src/components/liveUpdateTextField/LiveUpdateTextField.tsx +132 -0
- package/src/components/liveUpdateTextField/index.ts +1 -0
- package/src/components/liveUpdateTextField.ts +1 -0
- package/src/components/navBar/NavBar.tsx +59 -0
- package/src/components/navBar/index.ts +1 -0
- package/src/components/navBar.ts +1 -0
- package/src/components/note/Note.tsx +21 -0
- package/src/components/note/index.ts +1 -0
- package/src/components/note.ts +1 -0
- package/src/components/numberStepper/NumberStepper.stories.tsx +21 -0
- package/src/components/numberStepper/NumberStepper.tsx +74 -0
- package/src/components/numberStepper/index.ts +1 -0
- package/src/components/numberStepper.ts +1 -0
- package/src/components/particles/ParticleContext.tsx +11 -0
- package/src/components/particles/ParticleLayer.stories.tsx +46 -0
- package/src/components/particles/ParticleLayer.tsx +28 -0
- package/src/components/particles/index.ts +7 -0
- package/src/components/particles/particlesState.ts +502 -0
- package/src/components/particles.ts +1 -0
- package/src/components/peek/Peek.tsx +74 -0
- package/src/components/peek/index.ts +1 -0
- package/src/components/peek.ts +1 -0
- package/src/components/popover/Popover.tsx +84 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover.ts +1 -0
- package/src/components/relativeTime/RelativeTime.tsx +43 -0
- package/src/components/relativeTime/index.ts +1 -0
- package/src/components/relativeTime.ts +1 -0
- package/src/components/richEditor/EditorContent.tsx +4 -0
- package/src/components/richEditor/RichEditor.tsx +38 -0
- package/src/components/richEditor/index.ts +1 -0
- package/src/components/richEditor.ts +1 -0
- package/src/components/select/Select.tsx +247 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select.ts +1 -0
- package/src/components/skeletons/skeletons.tsx +27 -0
- package/src/components/skeletons.ts +1 -0
- package/src/components/spinner/Spinner.tsx +59 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner.ts +1 -0
- package/src/components/switch/Switch.tsx +23 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch.ts +1 -0
- package/src/components/tabs/tabs.tsx +18 -0
- package/src/components/tabs.ts +1 -0
- package/src/components/textArea/TextArea.stories.tsx +21 -0
- package/src/components/textArea/TextArea.tsx +58 -0
- package/src/components/textArea/index.ts +1 -0
- package/src/components/textArea.ts +1 -0
- package/src/components/toggleGroup/toggleGroup.tsx +11 -0
- package/src/components/toggleGroup.ts +1 -0
- package/src/components/tooltip/Tooltip.tsx +56 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip.ts +1 -0
- package/src/components/typography/index.ts +1 -0
- package/src/components/typography/typography.tsx +18 -0
- package/src/components/typography.ts +1 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useMergedRef.ts +14 -0
- package/src/hooks/useOnUnmount.ts +20 -0
- package/src/hooks/useSize.ts +164 -0
- package/src/hooks/useStableCallback.ts +11 -0
- package/src/hooks/useToggle.tsx +9 -0
- package/src/hooks/useVisualViewportOffset.ts +35 -0
- package/src/hooks/withClassName.tsx +21 -0
- package/src/hooks.ts +1 -0
- 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
|
+
}
|