@dxos/brand 0.13.0 → 0.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/brand",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "DXOS brand assets.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -45,15 +45,15 @@
45
45
  "d3": "^7.9.0",
46
46
  "react": "~19.2.7",
47
47
  "react-dom": "~19.2.7",
48
- "@dxos/log": "0.13.0",
49
- "@dxos/react-ui": "0.13.0",
50
- "@dxos/ui-theme": "0.13.0"
48
+ "@dxos/log": "0.14.0",
49
+ "@dxos/react-ui": "0.14.0",
50
+ "@dxos/ui-theme": "0.14.0"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@phosphor-icons/react": "2.1.10",
54
54
  "react": "~19.2.7",
55
55
  "react-dom": "~19.2.7",
56
- "@dxos/ui-theme": "^0.13.0"
56
+ "@dxos/ui-theme": "^0.14.0"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"
@@ -5,36 +5,47 @@
5
5
  import { type IconWeight } from '@phosphor-icons/react';
6
6
  import React, { type ReactElement, forwardRef } from 'react';
7
7
 
8
- const weights = new Map<IconWeight, ReactElement>([
9
- [
10
- 'regular',
11
- <>
12
- <g transform='matrix(0.969697,0,0,1,-570.182,0)'>
13
- <rect x='588' y='0' width='264' height='256' style={{ fill: 'none' }} />
14
- <g transform='matrix(0.917198,0,0,1,-223.93,-876)'>
15
- <path
16
- d='M1065.83,1064L1029.14,1064C991.913,1064 961.684,1037.12 961.684,1004C961.684,971.197 991.282,944.542 1028.02,944.008L1028.02,944L1047.85,944L1039.21,968L1029.14,968C1006.79,968 988.669,984.118 988.669,1004C988.669,1023.87 1006.81,1040 1029.14,1040C1029.38,1040 1029.62,1040 1029.85,1040L1029.85,1040L1074.47,1040L1065.83,1064ZM1083.11,1040L1083.11,1040L1083.11,1064L1083.11,1064L1083.11,1040Z'
17
- style={{ fill: 'rgb(1,122,183)' }}
18
- />
19
- </g>
20
- <path
21
- d='M761.579,164L720,164C699.51,164 682.875,147.869 682.875,128C682.875,108.452 698.942,92.543 718.969,92.014L718.969,92L729.238,92L721.317,116L720,116C713.165,116 707.625,121.373 707.625,128C707.625,134.623 713.17,140 720,140C720.072,140 720.144,139.999 720.216,139.998L720.216,140L769.5,140L761.579,164Z'
22
- style={{ fill: 'rgb(6,197,253)' }}
23
- />
24
- <path
25
- d='M745.738,212L720.025,212L720,212C672.202,212 633.389,174.377 633.375,128.024C633.361,81.958 671.591,44.542 718.969,44.006L718.969,44L719.975,44L745.079,44L737.159,68L719.982,68C685.809,68.01 658.115,94.88 658.125,128.017C658.135,161.126 685.858,188 720,188L720.018,188C720.378,188 720.738,187.997 721.098,187.991L721.098,188L753.659,188L745.738,212Z'
26
- style={{ fill: 'rgb(10,75,105)' }}
27
- />
28
- <g transform='matrix(1.03125,0,0,1,588,0)'>
29
- <path
30
- d='M128,236C68.393,236 20,187.607 20,128C20,68.353 68.353,20 128,20L160,20L152.319,44L128,44C81.608,44 44,81.608 44,128C44,174.361 81.639,212 128,212C127.756,212.004 127.878,212.004 128,212.003C128.122,212.002 128.244,212 128,212L152.958,212L145.277,236L128,236ZM128,236C128.628,236 127.372,236.011 128,236Z'
31
- style={{ fill: 'rgb(5,40,61)' }}
32
- />
33
- </g>
34
- </g>
35
- </>,
36
- ],
37
- ]);
8
+ const OUTER_TRANSFORM = 'matrix(0.969697,0,0,1,-570.182,0)';
9
+
10
+ export type ComposerRing = {
11
+ /** Maps the path's source coordinates into the 256 x 256 viewBox. */
12
+ transform: string;
13
+ fill: string;
14
+ d: string;
15
+ };
16
+
17
+ /**
18
+ * The four rings of the mark, innermost first; each transform maps the ring onto a viewBox (0 0 256 256) centred on (128, 128).
19
+ * Exported separately so idents can animate each ring independently.
20
+ */
21
+ export const composerRingPaths: ComposerRing[] = [
22
+ {
23
+ transform: OUTER_TRANSFORM,
24
+ fill: 'rgb(6,197,253)',
25
+ d: 'M761.579,164L720,164C699.51,164 682.875,147.869 682.875,128C682.875,108.452 698.942,92.543 718.969,92.014L718.969,92L729.238,92L721.317,116L720,116C713.165,116 707.625,121.373 707.625,128C707.625,134.623 713.17,140 720,140C720.072,140 720.144,139.999 720.216,139.998L720.216,140L769.5,140L761.579,164Z',
26
+ },
27
+ {
28
+ transform: `${OUTER_TRANSFORM} matrix(0.917198,0,0,1,-223.93,-876)`,
29
+ fill: 'rgb(1,122,183)',
30
+ d: 'M1065.83,1064L1029.14,1064C991.913,1064 961.684,1037.12 961.684,1004C961.684,971.197 991.282,944.542 1028.02,944.008L1028.02,944L1047.85,944L1039.21,968L1029.14,968C1006.79,968 988.669,984.118 988.669,1004C988.669,1023.87 1006.81,1040 1029.14,1040C1029.38,1040 1029.62,1040 1029.85,1040L1029.85,1040L1074.47,1040L1065.83,1064ZM1083.11,1040L1083.11,1040L1083.11,1064L1083.11,1064L1083.11,1040Z',
31
+ },
32
+ {
33
+ transform: OUTER_TRANSFORM,
34
+ fill: 'rgb(10,75,105)',
35
+ d: 'M745.738,212L720.025,212L720,212C672.202,212 633.389,174.377 633.375,128.024C633.361,81.958 671.591,44.542 718.969,44.006L718.969,44L719.975,44L745.079,44L737.159,68L719.982,68C685.809,68.01 658.115,94.88 658.125,128.017C658.135,161.126 685.858,188 720,188L720.018,188C720.378,188 720.738,187.997 721.098,187.991L721.098,188L753.659,188L745.738,212Z',
36
+ },
37
+ {
38
+ transform: `${OUTER_TRANSFORM} matrix(1.03125,0,0,1,588,0)`,
39
+ fill: 'rgb(5,40,61)',
40
+ d: 'M128,236C68.393,236 20,187.607 20,128C20,68.353 68.353,20 128,20L160,20L152.319,44L128,44C81.608,44 44,81.608 44,128C44,174.361 81.639,212 128,212C127.756,212.004 127.878,212.004 128,212.003C128.122,212.002 128.244,212 128,212L152.958,212L145.277,236L128,236ZM128,236C128.628,236 127.372,236.011 128,236Z',
41
+ },
42
+ ];
43
+
44
+ export const composerRings: ReactElement[] = composerRingPaths.map(({ transform, fill, d }, index) => (
45
+ <path key={index} transform={transform} d={d} style={{ fill }} />
46
+ ));
47
+
48
+ const weights = new Map<IconWeight, ReactElement>([['regular', <>{composerRings}</>]]);
38
49
 
39
50
  export const Composer = forwardRef<SVGSVGElement, any>((props, forwardedRef) => {
40
51
  const weight = props.weight || 'regular';
@@ -0,0 +1,427 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useEffect, useRef, useState } from 'react';
7
+
8
+ import * as Button from '@dxos/react-ui/Button';
9
+ import { withTheme } from '@dxos/react-ui/testing';
10
+ import { mx } from '@dxos/ui-theme';
11
+
12
+ import { composerRingPaths } from './Composer.tsx';
13
+
14
+ const FONT_URL = 'https://fonts.googleapis.com/css2?family=Poiret+One&display=swap';
15
+
16
+ type RingStep = {
17
+ keyframes: Keyframe[];
18
+ easing: string;
19
+ };
20
+
21
+ type IdentVariant = {
22
+ label: string;
23
+ /** Ring animation; receives the ring index (0 = innermost) to vary direction per ring. */
24
+ ring: (index: number) => RingStep;
25
+ /** Ring order: innermost first unless reversed. */
26
+ outerFirst?: boolean;
27
+ /** Duration of each ring's animation (ms). */
28
+ duration: number;
29
+ /** Delay between successive rings (ms). */
30
+ stagger: number;
31
+ /** Fraction (0-1) of the last ring's animation after which the wordmark may start; long easing tails look settled before they end. */
32
+ settledAt?: number;
33
+ /** Start from the symmetric mark, then extend each bottom arm to its true length. */
34
+ grow?: {
35
+ /** Growth speed shared by every arm (viewBox units per ms), so shorter arms finish first. */
36
+ rate: number;
37
+ /** Pause after the rings land before the arms grow (ms); ignored when `finishAt` is set. */
38
+ delay: number;
39
+ /** Grow while the rings are still moving, fully grown at this fraction (0-1) of the last ring's animation. */
40
+ finishAt?: number;
41
+ };
42
+ };
43
+
44
+ /**
45
+ * Bottom-arm vertices of each ring paired with their mirror image of the top arm across y = 128 (in path coordinates),
46
+ * so the mark can start symmetric; the bottom arms of the real mark are longer.
47
+ */
48
+ const SYMMETRIC_ARMS: [string, string][][] = [
49
+ [
50
+ ['761.579,164', '729.238,164'],
51
+ ['769.5,140', '721.317,140'],
52
+ ],
53
+ [
54
+ ['1065.83,1064', '1047.85,1064'],
55
+ ['1074.47,1040', '1039.21,1040'],
56
+ ],
57
+ [
58
+ ['745.738,212', '745.079,212'],
59
+ ['753.659,188', '737.159,188'],
60
+ ],
61
+ [
62
+ ['152.958,212', '152.319,212'],
63
+ ['145.277,236', '160,236'],
64
+ ],
65
+ ];
66
+
67
+ const symmetricPaths = composerRingPaths.map(({ d }, index) =>
68
+ SYMMETRIC_ARMS[index].reduce((path, [from, to]) => path.replaceAll(from, to), d),
69
+ );
70
+
71
+ /** Furthest any bottom-arm vertex travels, in path x units. */
72
+ const growDistances = SYMMETRIC_ARMS.map((pairs) =>
73
+ Math.max(...pairs.map(([from, to]) => Math.abs(parseFloat(from) - parseFloat(to)))),
74
+ );
75
+
76
+ const VARIANT_NAMES = ['grow', 'ripple', 'focus', 'sweep', 'spin', 'turn', 'fade'] as const;
77
+
78
+ type VariantName = (typeof VARIANT_NAMES)[number];
79
+
80
+ const variants: Record<VariantName, IdentVariant> = {
81
+ grow: {
82
+ label: 'Grow',
83
+ duration: 700,
84
+ stagger: 180,
85
+ ring: () => ({
86
+ keyframes: [
87
+ { opacity: 0, transform: 'scale(0.85)' },
88
+ { opacity: 1, transform: 'scale(1)' },
89
+ ],
90
+ easing: 'ease-out',
91
+ }),
92
+ grow: { rate: 0.05, delay: 300 },
93
+ },
94
+ ripple: {
95
+ label: 'Ripple',
96
+ duration: 900,
97
+ stagger: 150,
98
+ ring: () => ({
99
+ keyframes: [
100
+ { opacity: 0, transform: 'scale(0)' },
101
+ { opacity: 1, transform: 'scale(1)' },
102
+ ],
103
+ easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
104
+ }),
105
+ },
106
+ focus: {
107
+ label: 'Focus',
108
+ outerFirst: true,
109
+ duration: 1100,
110
+ stagger: 200,
111
+ ring: () => ({
112
+ keyframes: [
113
+ { opacity: 0, filter: 'blur(24px)' },
114
+ { opacity: 1, filter: 'blur(0px)' },
115
+ ],
116
+ easing: 'ease-out',
117
+ }),
118
+ },
119
+ sweep: {
120
+ label: 'Sweep',
121
+ duration: 1000,
122
+ stagger: 160,
123
+ ring: () => ({
124
+ keyframes: [
125
+ { opacity: 0, transform: 'rotate(-90deg)' },
126
+ { opacity: 1, transform: 'rotate(0deg)' },
127
+ ],
128
+ easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
129
+ }),
130
+ },
131
+ spin: {
132
+ label: 'Spin',
133
+ outerFirst: true,
134
+ duration: 1200,
135
+ stagger: 120,
136
+ ring: (index) => ({
137
+ keyframes: [
138
+ { opacity: 0, transform: `scale(4) rotate(${index % 2 ? -270 : 270}deg)` },
139
+ { opacity: 1, transform: 'scale(1) rotate(0deg)' },
140
+ ],
141
+ easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
142
+ }),
143
+ grow: { rate: 0.05, delay: 200 },
144
+ },
145
+ turn: {
146
+ label: 'Turn',
147
+ outerFirst: true,
148
+ duration: 2000,
149
+ // Every ring stops at the same moment; inner rings turn further so they still move against each other.
150
+ stagger: 0,
151
+ ring: (index) => ({
152
+ keyframes: [
153
+ { opacity: 0, offset: 0, transform: `rotate(${(index % 2 ? -1 : 1) * (1080 - index * 180)}deg)` },
154
+ { opacity: 1, offset: 0.25 },
155
+ { opacity: 1, offset: 1, transform: 'rotate(0deg)' },
156
+ ],
157
+ easing: 'cubic-bezier(0.25, 1, 0.5, 1)',
158
+ }),
159
+ grow: { rate: 0.1, delay: 0, finishAt: 0.55 },
160
+ // The wordmark fades in during the last of the spin (inner ring ~20° from rest with the default 100ms gap).
161
+ settledAt: 0.6,
162
+ },
163
+ fade: {
164
+ label: 'Fade',
165
+ duration: 700,
166
+ stagger: 220,
167
+ ring: () => ({
168
+ keyframes: [
169
+ { opacity: 0, transform: 'scale(0.85)' },
170
+ { opacity: 1, transform: 'scale(1)' },
171
+ ],
172
+ easing: 'ease-out',
173
+ }),
174
+ },
175
+ };
176
+
177
+ const FONT_FAMILY = 'Poiret One';
178
+
179
+ /** Wordmark size and offset from the mark, in viewBox units (the mark is 256 square). */
180
+ const WORDMARK_SIZE = 150;
181
+ const WORDMARK_X = 216;
182
+
183
+ type WordmarkMetrics = {
184
+ /** Baseline that puts the centre of the "c" on the mark's centre. */
185
+ baseline: number;
186
+ width: number;
187
+ };
188
+
189
+ /** Loads Poiret One, then measures the wordmark so layout is final before anything animates. */
190
+ const useWordmarkMetrics = (): WordmarkMetrics | undefined => {
191
+ const [metrics, setMetrics] = useState<WordmarkMetrics>();
192
+ useEffect(() => {
193
+ if (!document.querySelector(`link[href="${FONT_URL}"]`)) {
194
+ const link = document.createElement('link');
195
+ link.rel = 'stylesheet';
196
+ link.href = FONT_URL;
197
+ document.head.appendChild(link);
198
+ }
199
+
200
+ let cancelled = false;
201
+ const font = `${WORDMARK_SIZE}px "${FONT_FAMILY}"`;
202
+ const measure = () => {
203
+ const context = document.createElement('canvas').getContext('2d');
204
+ if (cancelled || !context) {
205
+ return;
206
+ }
207
+ // Lists the same fallback as the <text> element, so a failed font load still measures what renders.
208
+ context.font = `${font}, sans-serif`;
209
+ const glyph = context.measureText('c');
210
+ const glyphCentre = (glyph.actualBoundingBoxAscent - glyph.actualBoundingBoxDescent) / 2;
211
+ setMetrics({ baseline: 128 + glyphCentre, width: context.measureText('composer').width });
212
+ };
213
+ // A failed load (offline, blocked) must not stall the ident.
214
+ void document.fonts.load(font).then(measure, measure);
215
+
216
+ return () => {
217
+ cancelled = true;
218
+ };
219
+ }, []);
220
+
221
+ return metrics;
222
+ };
223
+
224
+ type ComposerIdentProps = {
225
+ variant: VariantName;
226
+ size?: number;
227
+ /** Multiplier on every duration and delay; >1 is slower. */
228
+ speed?: number;
229
+ /** Pause between the last ring landing and the wordmark (ms). */
230
+ wordmarkDelay?: number;
231
+ wordmark?: boolean;
232
+ };
233
+
234
+ const ComposerIdent = ({
235
+ variant,
236
+ size = 160,
237
+ speed = 1,
238
+ wordmarkDelay = 100,
239
+ wordmark = true,
240
+ }: ComposerIdentProps) => {
241
+ const metrics = useWordmarkMetrics();
242
+ const ringRefs = useRef<(SVGGElement | null)[]>([]);
243
+ const pathRefs = useRef<(SVGPathElement | null)[]>([]);
244
+ const wordmarkRef = useRef<SVGTextElement>(null);
245
+
246
+ useEffect(() => {
247
+ if (!metrics) {
248
+ return;
249
+ }
250
+
251
+ const { ring, outerFirst, duration, stagger, grow, settledAt = 1 } = variants[variant];
252
+ const count = composerRingPaths.length;
253
+ const animations = ringRefs.current.flatMap((element, index) => {
254
+ if (!element) {
255
+ return [];
256
+ }
257
+ const order = outerFirst ? count - 1 - index : index;
258
+ const { keyframes, easing } = ring(index);
259
+ return [
260
+ element.animate(keyframes, {
261
+ duration: duration * speed,
262
+ delay: order * stagger * speed,
263
+ easing,
264
+ fill: 'both',
265
+ }),
266
+ ];
267
+ });
268
+
269
+ let ringsEnd = (count - 1) * stagger + duration * settledAt;
270
+ if (grow) {
271
+ // Paths carry different x scales, so convert each arm's travel into viewBox units before timing it.
272
+ const growDurations = pathRefs.current.map((element, index) => {
273
+ const scaleX = element?.transform.baseVal.consolidate()?.matrix.a ?? 1;
274
+ return (growDistances[index] * scaleX) / grow.rate;
275
+ });
276
+ const longest = Math.max(...growDurations);
277
+ // Shorter arms start later at the same rate so every arm closes together.
278
+ const growEnd =
279
+ grow.finishAt === undefined
280
+ ? ringsEnd + grow.delay + longest
281
+ : (count - 1) * stagger + duration * grow.finishAt;
282
+ pathRefs.current.forEach((element, index) => {
283
+ if (element) {
284
+ const growDuration = growDurations[index];
285
+ animations.push(
286
+ element.animate(
287
+ [{ d: `path("${symmetricPaths[index]}")` }, { d: `path("${composerRingPaths[index].d}")` }],
288
+ {
289
+ duration: growDuration * speed,
290
+ delay: (growEnd - growDuration) * speed,
291
+ easing: 'linear',
292
+ fill: 'both',
293
+ },
294
+ ),
295
+ );
296
+ }
297
+ });
298
+ ringsEnd = Math.max(ringsEnd, growEnd);
299
+ }
300
+
301
+ if (wordmarkRef.current) {
302
+ animations.push(
303
+ wordmarkRef.current.animate([{ opacity: 0 }, { opacity: 1 }], {
304
+ duration: 1200 * speed,
305
+ delay: (ringsEnd + wordmarkDelay) * speed,
306
+ easing: 'ease-in-out',
307
+ fill: 'both',
308
+ }),
309
+ );
310
+ }
311
+
312
+ return () => animations.forEach((animation) => animation.cancel());
313
+ }, [metrics, variant, speed, wordmarkDelay, wordmark]);
314
+
315
+ // The lockup is sized to its final extent up front so nothing reflows while it animates.
316
+ const width = wordmark && metrics ? WORDMARK_X + metrics.width : 256;
317
+ return (
318
+ <svg width={(size * width) / 256} height={size} viewBox={`0 0 ${width} 256`} className='overflow-visible shrink-0'>
319
+ {composerRingPaths.map(({ transform, fill, d }, index) => (
320
+ <g
321
+ key={index}
322
+ ref={(element) => {
323
+ ringRefs.current[index] = element;
324
+ }}
325
+ style={{ opacity: 0, transformBox: 'view-box', transformOrigin: '128px 128px' }}
326
+ >
327
+ <path
328
+ ref={(element) => {
329
+ pathRefs.current[index] = element;
330
+ }}
331
+ transform={transform}
332
+ d={d}
333
+ style={{ fill }}
334
+ />
335
+ </g>
336
+ ))}
337
+ {wordmark && metrics && (
338
+ <text
339
+ ref={wordmarkRef}
340
+ x={WORDMARK_X}
341
+ y={metrics.baseline}
342
+ fill='white'
343
+ fontFamily={`"${FONT_FAMILY}", sans-serif`}
344
+ fontSize={WORDMARK_SIZE}
345
+ style={{ opacity: 0 }}
346
+ >
347
+ composer
348
+ </text>
349
+ )}
350
+ </svg>
351
+ );
352
+ };
353
+
354
+ type StoryArgs = ComposerIdentProps;
355
+
356
+ const DefaultStory = (props: StoryArgs) => {
357
+ const [run, setRun] = useState(0);
358
+ return (
359
+ <div className='relative flex justify-center items-center w-full h-dvh overflow-hidden bg-black'>
360
+ <ComposerIdent key={run} {...props} />
361
+ <div className='absolute bottom-4 right-4'>
362
+ <Button.Root onClick={() => setRun((value) => value + 1)}>Replay</Button.Root>
363
+ </div>
364
+ </div>
365
+ );
366
+ };
367
+
368
+ const GridStory = ({ size = 72, ...props }: Omit<StoryArgs, 'variant'>) => {
369
+ const [run, setRun] = useState(0);
370
+ return (
371
+ <div className='flex flex-col gap-12 p-12 w-full h-dvh overflow-hidden bg-black'>
372
+ <div>
373
+ <Button.Root onClick={() => setRun((value) => value + 1)}>Replay</Button.Root>
374
+ </div>
375
+ {VARIANT_NAMES.map((variant) => (
376
+ <div key={variant} className='grid grid-cols-[8rem_1fr] items-center'>
377
+ <span className={mx('text-sm text-fg-muted')}>{variants[variant].label}</span>
378
+ <ComposerIdent key={run} {...props} variant={variant} size={size} />
379
+ </div>
380
+ ))}
381
+ </div>
382
+ );
383
+ };
384
+
385
+ const meta = {
386
+ title: 'ui/brand/components/ComposerIdent',
387
+ render: DefaultStory,
388
+ decorators: [withTheme()],
389
+ parameters: {
390
+ layout: 'fullscreen',
391
+ },
392
+ argTypes: {
393
+ variant: { control: 'select', options: VARIANT_NAMES },
394
+ size: { control: { type: 'range', min: 32, max: 320, step: 8 } },
395
+ speed: { control: { type: 'range', min: 0.25, max: 4, step: 0.25 } },
396
+ wordmarkDelay: { control: { type: 'range', min: 0, max: 2000, step: 50 } },
397
+ },
398
+ args: {
399
+ variant: 'grow',
400
+ size: 160,
401
+ speed: 1,
402
+ wordmarkDelay: 100,
403
+ wordmark: true,
404
+ },
405
+ } satisfies Meta<typeof DefaultStory>;
406
+
407
+ export default meta;
408
+
409
+ type Story = StoryObj<typeof meta>;
410
+
411
+ export const Grow: Story = {};
412
+
413
+ export const Ripple: Story = { args: { variant: 'ripple' } };
414
+
415
+ export const Focus: Story = { args: { variant: 'focus' } };
416
+
417
+ export const Spin: Story = { args: { variant: 'spin' } };
418
+
419
+ export const Turn: Story = { args: { variant: 'turn' } };
420
+
421
+ export const Sweep: Story = { args: { variant: 'sweep' } };
422
+
423
+ export const Fade: Story = { args: { variant: 'fade' } };
424
+
425
+ export const All: Story = {
426
+ render: (args: StoryArgs) => <GridStory {...args} size={72} />,
427
+ };