@majapisoftwares/ui 6.0.0 → 6.1.2

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 (73) hide show
  1. package/AGENTS.md +27 -27
  2. package/README.md +79 -79
  3. package/THIRD_PARTY_NOTICES.md +39 -39
  4. package/components.json +25 -25
  5. package/docs/MIGRATION-0.2.md +20 -20
  6. package/docs/MIGRATION-6.0.md +33 -33
  7. package/docs/NEXT-STARTER-VALIDATION.md +14 -14
  8. package/docs/STANDARDS.md +41 -41
  9. package/docs/VALIDATION.md +11 -11
  10. package/docs/VERSIONING.md +28 -28
  11. package/docs/validation/preference-titles.md +22 -0
  12. package/package.json +88 -84
  13. package/scripts/check-standards.mjs +41 -41
  14. package/src/components/ui/accordion.tsx +82 -82
  15. package/src/components/ui/alert-dialog.tsx +187 -187
  16. package/src/components/ui/alert.tsx +77 -77
  17. package/src/components/ui/avatar.tsx +108 -108
  18. package/src/components/ui/badge.tsx +55 -55
  19. package/src/components/ui/breadcrumb.tsx +125 -125
  20. package/src/components/ui/button-group.tsx +89 -89
  21. package/src/components/ui/button.tsx +59 -59
  22. package/src/components/ui/card.tsx +101 -101
  23. package/src/components/ui/checkbox.tsx +27 -27
  24. package/src/components/ui/collapsible.tsx +21 -21
  25. package/src/components/ui/context-menu.tsx +271 -271
  26. package/src/components/ui/dialog.tsx +161 -161
  27. package/src/components/ui/dropdown-menu.tsx +271 -271
  28. package/src/components/ui/empty.tsx +105 -105
  29. package/src/components/ui/field.tsx +238 -238
  30. package/src/components/ui/hover-card.tsx +50 -50
  31. package/src/components/ui/input-group.tsx +155 -155
  32. package/src/components/ui/input.tsx +21 -21
  33. package/src/components/ui/kbd.tsx +28 -28
  34. package/src/components/ui/label.tsx +19 -19
  35. package/src/components/ui/orb.css +1002 -1002
  36. package/src/components/ui/orb.tsx +752 -752
  37. package/src/components/ui/popover.tsx +89 -89
  38. package/src/components/ui/progress.tsx +82 -82
  39. package/src/components/ui/radio-group.tsx +37 -37
  40. package/src/components/ui/scroll-area.tsx +54 -54
  41. package/src/components/ui/select.tsx +204 -204
  42. package/src/components/ui/separator.tsx +24 -24
  43. package/src/components/ui/sheet.tsx +139 -139
  44. package/src/components/ui/sidebar.tsx +1039 -1035
  45. package/src/components/ui/skeleton.tsx +15 -15
  46. package/src/components/ui/slider.tsx +53 -53
  47. package/src/components/ui/sonner.tsx +44 -44
  48. package/src/components/ui/spinner.tsx +32 -32
  49. package/src/components/ui/switch.tsx +31 -31
  50. package/src/components/ui/table.tsx +121 -121
  51. package/src/components/ui/tabs.tsx +81 -81
  52. package/src/components/ui/textarea.tsx +19 -19
  53. package/src/components/ui/toggle-group.tsx +89 -89
  54. package/src/components/ui/toggle.tsx +44 -44
  55. package/src/components/ui/tooltip.tsx +65 -65
  56. package/src/hooks/use-mobile.ts +20 -20
  57. package/src/lib/i18n.tsx +109 -109
  58. package/src/lib/utils.ts +1 -1
  59. package/src/patterns/app-shell.tsx +177 -175
  60. package/src/patterns/data-table.tsx +2486 -2486
  61. package/src/patterns/details-sheet.tsx +54 -53
  62. package/src/patterns/filter-tabs.tsx +53 -53
  63. package/src/patterns/majapi-logo.tsx +32 -32
  64. package/src/patterns/page-header.tsx +62 -62
  65. package/src/patterns/preferences.tsx +15 -7
  66. package/src/patterns/status-badge.tsx +24 -24
  67. package/src/patterns/user-menu.tsx +218 -0
  68. package/src/patterns/workspace-switcher.tsx +176 -0
  69. package/src/styles.css +165 -165
  70. package/src/templates/admin-dashboard.tsx +44 -44
  71. package/src/templates/auth-layout.tsx +30 -30
  72. package/src/templates/login-form.tsx +129 -129
  73. package/src/templates/signup-form.tsx +177 -177
@@ -1,752 +1,752 @@
1
- "use client";
2
-
3
- // Adapted from AICSS Orbs (MIT, https://github.com/kvnkld/aicss); kept close to
4
- // upstream, so its unused helpers stay.
5
- /* eslint-disable @typescript-eslint/no-unused-vars */
6
-
7
- import type { CSSProperties } from "react";
8
- import { defineMessages, useUiMessages } from "../../lib/i18n";
9
-
10
- /** The stage the geometry is tuned on; --orb-k scales it to `size`. */
11
- const STAGE = 28;
12
-
13
- /** Default rendered size - 20×20 indicator box. */
14
- const SIZE = 20;
15
-
16
- export type LatticeVariant = "S1" | "S2" | "S3" | "S4" | "S5";
17
- export type LensVariant = "B1" | "B2" | "B3" | "B4" | "B5";
18
- export type RingVariant = "C1" | "C2" | "C3" | "C4" | "C5";
19
- export type HelixVariant = "G1" | "G2" | "G3" | "G4" | "G5";
20
- export type MorphVariant = "M1" | "M2" | "M3" | "M4" | "M5";
21
- export type OrbVariant =
22
- LatticeVariant | LensVariant | RingVariant | HelixVariant | MorphVariant;
23
-
24
- export const LATTICE_VARIANTS: LatticeVariant[] = [
25
- "S1",
26
- "S2",
27
- "S3",
28
- "S4",
29
- "S5",
30
- ];
31
-
32
- export const LENS_VARIANTS: LensVariant[] = ["B1", "B2", "B3", "B4", "B5"];
33
-
34
- export const RING_VARIANTS: RingVariant[] = ["C1", "C2", "C3", "C4", "C5"];
35
-
36
- export const HELIX_VARIANTS: HelixVariant[] = ["G1", "G2", "G3", "G4", "G5"];
37
-
38
- export const MORPH_VARIANTS: MorphVariant[] = ["M1", "M2", "M3", "M4", "M5"];
39
-
40
- export const ORB_TASKS: Record<OrbVariant, string> = {
41
- S1: "Thinking",
42
- S2: "Processing",
43
- S3: "Working",
44
- S4: "Searching",
45
- S5: "Finalizing",
46
- B1: "Thinking",
47
- B2: "Searching",
48
- B3: "Generating",
49
- B4: "Solving",
50
- B5: "Routing",
51
- C1: "Loading",
52
- C2: "Listening",
53
- C3: "Streaming",
54
- C4: "Analyzing",
55
- C5: "Compiling",
56
- G1: "Processing",
57
- G2: "Sequencing",
58
- G3: "Uploading",
59
- G4: "Syncing",
60
- G5: "Idling",
61
- M1: "Shaping",
62
- M2: "Expanding",
63
- M3: "Unfolding",
64
- M4: "Transforming",
65
- M5: "Dispersing",
66
- };
67
-
68
- /** Translated task names per variant (`ORB_TASKS` is the English upstream list). */
69
- export const ORB_TASK_LABELS = defineMessages<Record<OrbVariant, string>>({
70
- en: ORB_TASKS,
71
- "pt-BR": {
72
- S1: "Pensando",
73
- S2: "Processando",
74
- S3: "Trabalhando",
75
- S4: "Buscando",
76
- S5: "Finalizando",
77
- B1: "Pensando",
78
- B2: "Buscando",
79
- B3: "Gerando",
80
- B4: "Resolvendo",
81
- B5: "Roteando",
82
- C1: "Carregando",
83
- C2: "Ouvindo",
84
- C3: "Transmitindo",
85
- C4: "Analisando",
86
- C5: "Compilando",
87
- G1: "Processando",
88
- G2: "Sequenciando",
89
- G3: "Enviando",
90
- G4: "Sincronizando",
91
- G5: "Ocioso",
92
- M1: "Moldando",
93
- M2: "Expandindo",
94
- M3: "Desdobrando",
95
- M4: "Transformando",
96
- M5: "Dispersando",
97
- },
98
- });
99
-
100
- function isLattice(v: OrbVariant): v is LatticeVariant {
101
- return (LATTICE_VARIANTS as OrbVariant[]).includes(v);
102
- }
103
-
104
- function isRing(v: OrbVariant): v is RingVariant {
105
- return (RING_VARIANTS as OrbVariant[]).includes(v);
106
- }
107
-
108
- function isHelix(v: OrbVariant): v is HelixVariant {
109
- return (HELIX_VARIANTS as OrbVariant[]).includes(v);
110
- }
111
-
112
- function isMorph(v: OrbVariant): v is MorphVariant {
113
- return (MORPH_VARIANTS as OrbVariant[]).includes(v);
114
- }
115
-
116
- const N = 3; // lattice is N×N
117
- const PITCH = 6; // centre-to-centre spacing in stage px; the dot size is CSS
118
- const MID = (N - 1) / 2;
119
-
120
- /** Clockwise walk of the lattice perimeter - the track `orbit` runs on. */
121
- const RING: [number, number][] = (() => {
122
- const ring: [number, number][] = [];
123
- for (let x = 0; x < N; x++) ring.push([x, 0]);
124
- for (let y = 1; y < N; y++) ring.push([N - 1, y]);
125
- for (let x = N - 2; x >= 0; x--) ring.push([x, N - 1]);
126
- for (let y = N - 2; y >= 1; y--) ring.push([0, y]);
127
- return ring;
128
- })();
129
-
130
- const RING_INDEX = new Map(RING.map(([x, y], i) => [x + "," + y, i]));
131
-
132
- /**
133
- * Per-cell `animation-delay` in ms. Negative values seed a cell partway
134
- * into its cycle, which is what turns 8 identical animations into one
135
- * comet travelling the ring.
136
- */
137
- function cellDelay(v: LatticeVariant, x: number, y: number): number {
138
- const dx = x - MID;
139
- const dy = y - MID;
140
- const ring = Math.max(Math.abs(dx), Math.abs(dy));
141
- switch (v) {
142
- // Radiates from the centre on a round wavefront. Centre leads a beat
143
- // early so the next swell doesn't sit behind the outer fade.
144
- case "S1":
145
- return Math.hypot(dx, dy) * 700 - (dx === 0 && dy === 0 ? 180 : 0);
146
- // A broad band crosses the grid on the diagonal. The spread is close to
147
- // the wave duration, which both widens the band and makes the sweep
148
- // continuous - the far corner restarts as the near one does.
149
- case "S2":
150
- return ((x + y) / (2 * (N - 1))) * 1500;
151
- // One head with a decaying tail, running the perimeter clockwise.
152
- case "S3": {
153
- const i = RING_INDEX.get(x + "," + y);
154
- if (i === undefined) return 0;
155
- return -(((RING.length - i) % RING.length) / RING.length) * 1700;
156
- }
157
- // A soft column travels left to right.
158
- case "S4":
159
- return (x / (N - 1)) * 1100;
160
- // Like S3 but scrambled order - the pulse jumps pseudo-randomly.
161
- case "S5": {
162
- const i = RING_INDEX.get(x + "," + y);
163
- if (i === undefined) return 0;
164
- const scrambled = (i * 3) % RING.length;
165
- return -(scrambled / RING.length) * 1700;
166
- }
167
- }
168
- }
169
-
170
- /**
171
- * `settle` gathers each cell from a position rotated one way around the
172
- * centre and releases it to the mirror rotation, so the cycle keeps swirling
173
- * the same way instead of rewinding to where it came from.
174
- */
175
- const SWIRL = 1.05; // radians of rotation at each end, ~60°
176
- const SPREAD = 1.6; // outward push, on top of the rotation
177
-
178
- /** Offset from a cell's own grid slot to its swirled position, in stage px. */
179
- function swirl(x: number, y: number, angle: number): [number, number] {
180
- const dx = x - MID;
181
- const dy = y - MID;
182
- const cos = Math.cos(angle);
183
- const sin = Math.sin(angle);
184
- return [
185
- ((dx * cos - dy * sin) * SPREAD - dx) * PITCH,
186
- ((dx * sin + dy * cos) * SPREAD - dy) * PITCH,
187
- ];
188
- }
189
-
190
- interface Cell {
191
- key: string;
192
- left: number;
193
- top: number;
194
- delay: number;
195
- /** Where `settle` gathers this cell from, and releases it to. */
196
- ax: number;
197
- ay: number;
198
- bx: number;
199
- by: number;
200
- /** Sits out the choreography (interior cells during `orbit`). */
201
- still: boolean;
202
- /** Centre cell - the static frame under reduced motion. */
203
- mid: boolean;
204
- }
205
-
206
- /** The 9 lattice cells, with position, phase and swirl vectors. */
207
- function latticeCells(v: LatticeVariant): Cell[] {
208
- const cells: Cell[] = [];
209
- for (let y = 0; y < N; y++) {
210
- for (let x = 0; x < N; x++) {
211
- const [ax, ay] = swirl(x, y, -SWIRL);
212
- const [bx, by] = swirl(x, y, SWIRL);
213
- cells.push({
214
- key: x + "," + y,
215
- left: x * PITCH,
216
- top: y * PITCH,
217
- delay: cellDelay(v, x, y),
218
- ax,
219
- ay,
220
- bx,
221
- by,
222
- still: (v === "S3" || v === "S5") && !RING_INDEX.has(x + "," + y),
223
- mid: x === MID && y === MID,
224
- });
225
- }
226
- }
227
- return cells;
228
- }
229
-
230
- const RING_N = 8;
231
- const RING_R = 8;
232
-
233
- interface RingDot {
234
- key: number;
235
- rx: number;
236
- ry: number;
237
- delay: number;
238
- }
239
-
240
- function ringDuration(v: RingVariant): number {
241
- switch (v) {
242
- case "C1":
243
- return 1600;
244
- case "C2":
245
- return 2000;
246
- case "C3":
247
- return 1800;
248
- case "C4":
249
- return 1600;
250
- case "C5":
251
- return 2200;
252
- }
253
- }
254
-
255
- function ringDelay(v: RingVariant, i: number): number {
256
- const dur = ringDuration(v);
257
- switch (v) {
258
- case "C1":
259
- return -((RING_N - 1 - i) / RING_N) * dur;
260
- case "C2":
261
- case "C3":
262
- return -((RING_N - 1 - i) / RING_N) * dur;
263
- case "C4":
264
- return i % 2 === 0 ? 0 : -(dur / 2);
265
- case "C5": {
266
- const scrambled = (i * 3) % RING_N;
267
- return -(scrambled / RING_N) * dur;
268
- }
269
- default:
270
- return -(i / RING_N) * dur;
271
- }
272
- }
273
-
274
- function ringDots(v: RingVariant): RingDot[] {
275
- const dots: RingDot[] = [];
276
- for (let i = 0; i < RING_N; i++) {
277
- const angle = (i / RING_N) * Math.PI * 2 - Math.PI / 2;
278
- dots.push({
279
- key: i,
280
- rx: Math.cos(angle) * RING_R,
281
- ry: Math.sin(angle) * RING_R,
282
- delay: ringDelay(v, i),
283
- });
284
- }
285
- return dots;
286
- }
287
-
288
- const GLOBE_R = 8.5;
289
- const GLOBE_TILT = (14 * Math.PI) / 180;
290
- const GLOBE_STEPS = 8;
291
-
292
- const GLOBE_RINGS: { lat: number; count: number }[] = [
293
- { lat: 52, count: 8 },
294
- { lat: 26, count: 8 },
295
- { lat: 0, count: 8 },
296
- { lat: -26, count: 8 },
297
- { lat: -52, count: 8 },
298
- ];
299
-
300
- interface GlobeDot {
301
- key: number;
302
- style: Record<string, string>;
303
- css: string;
304
- }
305
-
306
- function projectGlobe(x: number, y: number, z: number, spin: number) {
307
- const cs = Math.cos(spin);
308
- const ss = Math.sin(spin);
309
- const x1 = x * cs - z * ss;
310
- const z1 = x * ss + z * cs;
311
- const y1 = y;
312
- const ct = Math.cos(GLOBE_TILT);
313
- const st = Math.sin(GLOBE_TILT);
314
- return {
315
- x: x1,
316
- y: y1 * ct - z1 * st,
317
- z: y1 * st + z1 * ct,
318
- };
319
- }
320
-
321
- function globeOpacity(z: number) {
322
- const t = Math.max(0, Math.min(1, (z / GLOBE_R + 0.15) / 1.15));
323
- return 0.12 + 0.88 * t * t;
324
- }
325
-
326
- type RingMove = { ring: number; angle: number };
327
- const RING_HALF = Math.PI;
328
- const RING_ARC = 3;
329
-
330
- function ringDir(ring: number) {
331
- return ring % 2 === 0 ? -1 : 1;
332
- }
333
-
334
- const G3_MOVES: RingMove[] = (() => {
335
- const moves: RingMove[] = [];
336
- for (let pass = 0; pass < 2; pass++) {
337
- for (let r = 0; r < GLOBE_RINGS.length; r++) {
338
- moves.push({ ring: r, angle: ringDir(r) * RING_HALF });
339
- }
340
- }
341
- return moves;
342
- })();
343
-
344
- const G4_MOVES: RingMove[] = [2, 1, 3, 0, 4, 2, 1, 3, 0, 4].map((ring) => ({
345
- ring,
346
- angle: ringDir(ring) * RING_HALF,
347
- }));
348
-
349
- function ringTurnPoses(
350
- x0: number,
351
- y0: number,
352
- z0: number,
353
- ringIndex: number,
354
- moves: RingMove[],
355
- ): [number, number, number][] {
356
- let x = x0;
357
- let y = y0;
358
- let z = z0;
359
- const poses: [number, number, number][] = [[x, y, z]];
360
- for (let m = 0; m < moves.length; m++) {
361
- const move = moves[m];
362
- const xS = x;
363
- const yS = y;
364
- const zS = z;
365
- for (let s = 1; s <= RING_ARC; s++) {
366
- if (ringIndex === move.ring) {
367
- const a = move.angle * (s / RING_ARC);
368
- const c = Math.cos(a);
369
- const sn = Math.sin(a);
370
- x = xS * c - zS * sn;
371
- y = yS;
372
- z = xS * sn + zS * c;
373
- }
374
- poses.push([x, y, z]);
375
- }
376
- }
377
- return poses;
378
- }
379
-
380
- const G5_SLOW = 0.4;
381
- const G5_BURST = (Math.PI * 2 - G5_SLOW * 4) / 4;
382
- const G5_POSES: { s: number; spin: number }[] = (() => {
383
- const poses: { s: number; spin: number }[] = [{ s: 1.0, spin: 0 }];
384
- let spin = 0;
385
- const steps: { s: number; kind: "slow" | "burst" }[] = [
386
- { s: 1.0, kind: "slow" },
387
- { s: 0.9, kind: "burst" },
388
- { s: 0.9, kind: "slow" },
389
- { s: 0.8, kind: "burst" },
390
- { s: 0.8, kind: "slow" },
391
- { s: 0.9, kind: "burst" },
392
- { s: 0.9, kind: "slow" },
393
- { s: 1.0, kind: "burst" },
394
- ];
395
- for (const step of steps) {
396
- spin += step.kind === "slow" ? G5_SLOW : G5_BURST;
397
- poses.push({ s: step.s, spin });
398
- }
399
- return poses;
400
- })();
401
-
402
- function globeKeyframeStyle(
403
- x0: number,
404
- y0: number,
405
- z0: number,
406
- variant: HelixVariant,
407
- ringIndex: number,
408
- j = 0,
409
- ): Record<string, string> {
410
- const style: Record<string, string> = {};
411
-
412
- if (variant === "G5") {
413
- for (let k = 0; k < G5_POSES.length; k++) {
414
- const sc = G5_POSES[k].s;
415
- const spin = G5_POSES[k].spin;
416
- const p = projectGlobe(x0 * sc, y0 * sc, z0 * sc, spin);
417
- style["--g" + k + "x"] = p.x.toFixed(2) + "px";
418
- style["--g" + k + "y"] = (-p.y).toFixed(2) + "px";
419
- style["--g" + k + "o"] = globeOpacity(p.z).toFixed(3);
420
- }
421
- return style;
422
- }
423
-
424
- if (variant === "G3" || variant === "G4") {
425
- const poses = ringTurnPoses(
426
- x0,
427
- y0,
428
- z0,
429
- ringIndex,
430
- variant === "G3" ? G3_MOVES : G4_MOVES,
431
- );
432
- for (let k = 0; k < poses.length; k++) {
433
- const pos = poses[k];
434
- const p = projectGlobe(pos[0], pos[1], pos[2], 0);
435
- style["--g" + k + "x"] = p.x.toFixed(2) + "px";
436
- style["--g" + k + "y"] = (-p.y).toFixed(2) + "px";
437
- style["--g" + k + "o"] = globeOpacity(p.z).toFixed(3);
438
- }
439
- return style;
440
- }
441
-
442
- const dir = variant === "G2" && ringIndex % 2 === 1 ? -1 : 1;
443
-
444
- for (let k = 0; k < GLOBE_STEPS; k++) {
445
- const phase = k / GLOBE_STEPS;
446
- const spin = dir * phase * Math.PI * 2;
447
- const p = projectGlobe(x0, y0, z0, spin);
448
- style["--g" + k + "x"] = p.x.toFixed(2) + "px";
449
- style["--g" + k + "y"] = (-p.y).toFixed(2) + "px";
450
- style["--g" + k + "o"] = globeOpacity(p.z).toFixed(3);
451
- }
452
- return style;
453
- }
454
-
455
- function globeDots(v: HelixVariant): GlobeDot[] {
456
- const dots: GlobeDot[] = [];
457
- let idx = 0;
458
- for (let ringIndex = 0; ringIndex < GLOBE_RINGS.length; ringIndex++) {
459
- const ring = GLOBE_RINGS[ringIndex];
460
- const latRad = (ring.lat * Math.PI) / 180;
461
- const y0 = Math.sin(latRad) * GLOBE_R;
462
- const ringR = Math.cos(latRad) * GLOBE_R;
463
- for (let j = 0; j < ring.count; j++) {
464
- const lon = (j / ring.count) * Math.PI * 2;
465
- const style = globeKeyframeStyle(
466
- Math.cos(lon) * ringR,
467
- y0,
468
- Math.sin(lon) * ringR,
469
- v,
470
- ringIndex,
471
- j,
472
- );
473
- dots.push({
474
- key: idx,
475
- style,
476
- css: Object.keys(style)
477
- .map((k) => k + ":" + style[k])
478
- .join(";"),
479
- });
480
- idx++;
481
- }
482
- }
483
- return dots;
484
- }
485
-
486
- const MORPH_N = 8;
487
- const MORPH_R = 7;
488
-
489
- type ShapeFn = (i: number) => [number, number];
490
-
491
- const shapeCircle: ShapeFn = (i) => {
492
- const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2;
493
- return [Math.cos(a) * MORPH_R, Math.sin(a) * MORPH_R];
494
- };
495
-
496
- const shapeOctagon: ShapeFn = (i) => {
497
- const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2;
498
- const r = MORPH_R * 0.92;
499
- const sector = Math.round(a / (Math.PI / 4)) * (Math.PI / 4);
500
- return [Math.cos(sector) * r, Math.sin(sector) * r];
501
- };
502
-
503
- const shapeSquare: ShapeFn = (i) => {
504
- const h = MORPH_R * 0.85;
505
- const corners: [number, number][] = [
506
- [-h, -h],
507
- [h, -h],
508
- [h, h],
509
- [-h, h],
510
- ];
511
- const t = ((i / MORPH_N) * 4 + 0.5) % 4;
512
- const side = Math.floor(t) % 4;
513
- const frac = t - Math.floor(t);
514
- const from = corners[side];
515
- const to = corners[(side + 1) % 4];
516
- return [
517
- from[0] + (to[0] - from[0]) * frac,
518
- from[1] + (to[1] - from[1]) * frac,
519
- ];
520
- };
521
-
522
- const shapeCircleAt =
523
- (turn: number): ShapeFn =>
524
- (i) => {
525
- const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2 + turn;
526
- return [Math.cos(a) * MORPH_R, Math.sin(a) * MORPH_R];
527
- };
528
-
529
- const SCATTER_TRAIL = 0.12;
530
-
531
- const shapeScatterA: ShapeFn = (i) => {
532
- const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2;
533
- return [-Math.cos(a) * MORPH_R, Math.sin(a) * MORPH_R];
534
- };
535
-
536
- const shapeScatterB: ShapeFn = shapeCircle;
537
- const shapeScatterC: ShapeFn = shapeScatterA;
538
-
539
- const shapeDiamond: ShapeFn = (i) => {
540
- const corners: [number, number][] = [
541
- [0, -MORPH_R],
542
- [MORPH_R, 0],
543
- [0, MORPH_R],
544
- [-MORPH_R, 0],
545
- ];
546
- const t = (i / MORPH_N) * 4;
547
- const side = Math.floor(t) % 4;
548
- const frac = t - Math.floor(t);
549
- const from = corners[side];
550
- const to = corners[(side + 1) % 4];
551
- return [
552
- from[0] + (to[0] - from[0]) * frac,
553
- from[1] + (to[1] - from[1]) * frac,
554
- ];
555
- };
556
-
557
- const shapeCenter: ShapeFn = (i) => {
558
- const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2;
559
- return [Math.cos(a) * 1.5, Math.sin(a) * 1.5];
560
- };
561
-
562
- function morphShapes(v: MorphVariant): [ShapeFn, ShapeFn, ShapeFn, ShapeFn] {
563
- switch (v) {
564
- case "M1":
565
- return [shapeCircle, shapeSquare, shapeDiamond, shapeSquare];
566
- case "M2":
567
- return [shapeCenter, shapeCircle, shapeCenter, shapeCircle];
568
- case "M3":
569
- return [
570
- shapeCircleAt(0),
571
- shapeCircleAt(Math.PI / 2),
572
- shapeCircleAt(Math.PI),
573
- shapeCircleAt(Math.PI * 1.5),
574
- ];
575
- case "M4":
576
- return [shapeCircle, shapeDiamond, shapeCircle, shapeDiamond];
577
- case "M5":
578
- return [shapeCircle, shapeScatterA, shapeScatterB, shapeScatterC];
579
- }
580
- }
581
-
582
- interface MorphDot {
583
- key: number;
584
- m1: string;
585
- m2: string;
586
- m3: string;
587
- m4: string;
588
- delay?: string;
589
- depth?: string;
590
- }
591
-
592
- function morphDots(v: MorphVariant): MorphDot[] {
593
- const [s1, s2, s3, s4] = morphShapes(v);
594
- const dots: MorphDot[] = [];
595
- for (let i = 0; i < MORPH_N; i++) {
596
- const [x1, y1] = s1(i);
597
- const [x2, y2] = s2(i);
598
- const [x3, y3] = s3(i);
599
- const [x4, y4] = s4(i);
600
- dots.push({
601
- key: i,
602
- m1: x1.toFixed(1) + "px, " + y1.toFixed(1) + "px",
603
- m2: x2.toFixed(1) + "px, " + y2.toFixed(1) + "px",
604
- m3: x3.toFixed(1) + "px, " + y3.toFixed(1) + "px",
605
- m4: x4.toFixed(1) + "px, " + y4.toFixed(1) + "px",
606
- delay: v === "M5" ? -i * 10 + "ms" : undefined,
607
- depth:
608
- v === "M5"
609
- ? Math.abs(
610
- Math.cos((i / MORPH_N) * Math.PI * 2 - Math.PI / 2),
611
- ).toFixed(2)
612
- : undefined,
613
- });
614
- }
615
- return dots;
616
- }
617
-
618
- export interface OrbProps {
619
- variant?: OrbVariant;
620
- /** Rendered edge length in px. The 28px geometry scales to fit. */
621
- size?: number;
622
- /** Accessible label, and the status text when `pill` is set. */
623
- label?: string;
624
- /** Wraps the orb and its label in a status pill. */
625
- pill?: boolean;
626
- className?: string;
627
- style?: CSSProperties;
628
- }
629
-
630
- export function Orb({
631
- variant = "S1",
632
- size = SIZE,
633
- label,
634
- pill,
635
- className,
636
- style,
637
- }: OrbProps) {
638
- const messages = useUiMessages();
639
- const text = label ?? `${messages.loading}…`;
640
- return (
641
- <span
642
- className={"orb-root" + (className ? " " + className : "")}
643
- data-pill={pill ? "" : undefined}
644
- style={style}
645
- >
646
- <span
647
- className={"orb-glyph"}
648
- // In pill form the visible label already carries the meaning, so
649
- // the glyph steps out of the accessibility tree.
650
- role={pill ? undefined : "img"}
651
- aria-label={pill ? undefined : text}
652
- aria-hidden={pill ? true : undefined}
653
- style={
654
- {
655
- width: size,
656
- height: size,
657
- "--orb-k": size / STAGE,
658
- } as CSSProperties
659
- }
660
- >
661
- {isLattice(variant) ? (
662
- <span className={"orb-lattice"} data-variant={variant}>
663
- {latticeCells(variant).map((c) => (
664
- <span
665
- key={c.key}
666
- className={"orb-cell"}
667
- data-still={c.still ? "" : undefined}
668
- data-mid={c.mid ? "" : undefined}
669
- style={
670
- {
671
- left: c.left,
672
- top: c.top,
673
- animationDelay: c.delay + "ms",
674
- "--orb-ax": c.ax + "px",
675
- "--orb-ay": c.ay + "px",
676
- "--orb-bx": c.bx + "px",
677
- "--orb-by": c.by + "px",
678
- } as CSSProperties
679
- }
680
- />
681
- ))}
682
- </span>
683
- ) : isRing(variant) ? (
684
- <span className={"orb-ring"} data-variant={variant}>
685
- {ringDots(variant).map((d) => (
686
- <span
687
- key={d.key}
688
- className={"orb-ring-dot"}
689
- style={
690
- {
691
- "--orb-rx": d.rx + "px",
692
- "--orb-ry": d.ry + "px",
693
- animationDelay: d.delay + "ms",
694
- } as CSSProperties
695
- }
696
- />
697
- ))}
698
- </span>
699
- ) : isHelix(variant) ? (
700
- <span className={"orb-helix"} data-variant={variant}>
701
- {globeDots(variant).map((d) => (
702
- <span
703
- key={d.key}
704
- className={"orb-helix-dot"}
705
- style={d.style as CSSProperties}
706
- />
707
- ))}
708
- </span>
709
- ) : isMorph(variant) ? (
710
- <span className={"orb-morph"} data-variant={variant}>
711
- {morphDots(variant).map((d) => (
712
- <span
713
- key={d.key}
714
- className={"orb-morph-dot"}
715
- style={
716
- {
717
- "--m-1": d.m1,
718
- "--m-2": d.m2,
719
- "--m-3": d.m3,
720
- "--m-4": d.m4,
721
- "--m-depth": d.depth,
722
- animationDelay: d.delay,
723
- } as CSSProperties
724
- }
725
- />
726
- ))}
727
- </span>
728
- ) : (
729
- <span className={"orb-lens"} data-variant={variant}>
730
- <span className={"orb-shape" + " " + "orb-shape-a"} />
731
- <span className={"orb-shape" + " " + "orb-shape-b"} />
732
- <span className={"orb-shape" + " " + "orb-shape-c"} />
733
- {/* focus is the one variant that needs a fourth circle: its cast
734
- sits on the corners of a square, and three corners do not
735
- make a square. */}
736
- {variant === "B1" && (
737
- <span className={"orb-shape" + " " + "orb-shape-d"} />
738
- )}
739
- </span>
740
- )}
741
- </span>
742
- {pill && <span className={"orb-pill-label"}>{text}</span>}
743
- </span>
744
- );
745
- }
746
-
747
- /* Usage:
748
- <Orb variant="S4" />
749
- <Orb variant="B4" size={40} />
750
- <Orb variant="C3" />
751
- <Orb variant="B2" label="Searching the web…" pill />
752
- */
1
+ "use client";
2
+
3
+ // Adapted from AICSS Orbs (MIT, https://github.com/kvnkld/aicss); kept close to
4
+ // upstream, so its unused helpers stay.
5
+ /* eslint-disable @typescript-eslint/no-unused-vars */
6
+
7
+ import type { CSSProperties } from "react";
8
+ import { defineMessages, useUiMessages } from "../../lib/i18n";
9
+
10
+ /** The stage the geometry is tuned on; --orb-k scales it to `size`. */
11
+ const STAGE = 28;
12
+
13
+ /** Default rendered size - 20×20 indicator box. */
14
+ const SIZE = 20;
15
+
16
+ export type LatticeVariant = "S1" | "S2" | "S3" | "S4" | "S5";
17
+ export type LensVariant = "B1" | "B2" | "B3" | "B4" | "B5";
18
+ export type RingVariant = "C1" | "C2" | "C3" | "C4" | "C5";
19
+ export type HelixVariant = "G1" | "G2" | "G3" | "G4" | "G5";
20
+ export type MorphVariant = "M1" | "M2" | "M3" | "M4" | "M5";
21
+ export type OrbVariant =
22
+ LatticeVariant | LensVariant | RingVariant | HelixVariant | MorphVariant;
23
+
24
+ export const LATTICE_VARIANTS: LatticeVariant[] = [
25
+ "S1",
26
+ "S2",
27
+ "S3",
28
+ "S4",
29
+ "S5",
30
+ ];
31
+
32
+ export const LENS_VARIANTS: LensVariant[] = ["B1", "B2", "B3", "B4", "B5"];
33
+
34
+ export const RING_VARIANTS: RingVariant[] = ["C1", "C2", "C3", "C4", "C5"];
35
+
36
+ export const HELIX_VARIANTS: HelixVariant[] = ["G1", "G2", "G3", "G4", "G5"];
37
+
38
+ export const MORPH_VARIANTS: MorphVariant[] = ["M1", "M2", "M3", "M4", "M5"];
39
+
40
+ export const ORB_TASKS: Record<OrbVariant, string> = {
41
+ S1: "Thinking",
42
+ S2: "Processing",
43
+ S3: "Working",
44
+ S4: "Searching",
45
+ S5: "Finalizing",
46
+ B1: "Thinking",
47
+ B2: "Searching",
48
+ B3: "Generating",
49
+ B4: "Solving",
50
+ B5: "Routing",
51
+ C1: "Loading",
52
+ C2: "Listening",
53
+ C3: "Streaming",
54
+ C4: "Analyzing",
55
+ C5: "Compiling",
56
+ G1: "Processing",
57
+ G2: "Sequencing",
58
+ G3: "Uploading",
59
+ G4: "Syncing",
60
+ G5: "Idling",
61
+ M1: "Shaping",
62
+ M2: "Expanding",
63
+ M3: "Unfolding",
64
+ M4: "Transforming",
65
+ M5: "Dispersing",
66
+ };
67
+
68
+ /** Translated task names per variant (`ORB_TASKS` is the English upstream list). */
69
+ export const ORB_TASK_LABELS = defineMessages<Record<OrbVariant, string>>({
70
+ en: ORB_TASKS,
71
+ "pt-BR": {
72
+ S1: "Pensando",
73
+ S2: "Processando",
74
+ S3: "Trabalhando",
75
+ S4: "Buscando",
76
+ S5: "Finalizando",
77
+ B1: "Pensando",
78
+ B2: "Buscando",
79
+ B3: "Gerando",
80
+ B4: "Resolvendo",
81
+ B5: "Roteando",
82
+ C1: "Carregando",
83
+ C2: "Ouvindo",
84
+ C3: "Transmitindo",
85
+ C4: "Analisando",
86
+ C5: "Compilando",
87
+ G1: "Processando",
88
+ G2: "Sequenciando",
89
+ G3: "Enviando",
90
+ G4: "Sincronizando",
91
+ G5: "Ocioso",
92
+ M1: "Moldando",
93
+ M2: "Expandindo",
94
+ M3: "Desdobrando",
95
+ M4: "Transformando",
96
+ M5: "Dispersando",
97
+ },
98
+ });
99
+
100
+ function isLattice(v: OrbVariant): v is LatticeVariant {
101
+ return (LATTICE_VARIANTS as OrbVariant[]).includes(v);
102
+ }
103
+
104
+ function isRing(v: OrbVariant): v is RingVariant {
105
+ return (RING_VARIANTS as OrbVariant[]).includes(v);
106
+ }
107
+
108
+ function isHelix(v: OrbVariant): v is HelixVariant {
109
+ return (HELIX_VARIANTS as OrbVariant[]).includes(v);
110
+ }
111
+
112
+ function isMorph(v: OrbVariant): v is MorphVariant {
113
+ return (MORPH_VARIANTS as OrbVariant[]).includes(v);
114
+ }
115
+
116
+ const N = 3; // lattice is N×N
117
+ const PITCH = 6; // centre-to-centre spacing in stage px; the dot size is CSS
118
+ const MID = (N - 1) / 2;
119
+
120
+ /** Clockwise walk of the lattice perimeter - the track `orbit` runs on. */
121
+ const RING: [number, number][] = (() => {
122
+ const ring: [number, number][] = [];
123
+ for (let x = 0; x < N; x++) ring.push([x, 0]);
124
+ for (let y = 1; y < N; y++) ring.push([N - 1, y]);
125
+ for (let x = N - 2; x >= 0; x--) ring.push([x, N - 1]);
126
+ for (let y = N - 2; y >= 1; y--) ring.push([0, y]);
127
+ return ring;
128
+ })();
129
+
130
+ const RING_INDEX = new Map(RING.map(([x, y], i) => [x + "," + y, i]));
131
+
132
+ /**
133
+ * Per-cell `animation-delay` in ms. Negative values seed a cell partway
134
+ * into its cycle, which is what turns 8 identical animations into one
135
+ * comet travelling the ring.
136
+ */
137
+ function cellDelay(v: LatticeVariant, x: number, y: number): number {
138
+ const dx = x - MID;
139
+ const dy = y - MID;
140
+ const ring = Math.max(Math.abs(dx), Math.abs(dy));
141
+ switch (v) {
142
+ // Radiates from the centre on a round wavefront. Centre leads a beat
143
+ // early so the next swell doesn't sit behind the outer fade.
144
+ case "S1":
145
+ return Math.hypot(dx, dy) * 700 - (dx === 0 && dy === 0 ? 180 : 0);
146
+ // A broad band crosses the grid on the diagonal. The spread is close to
147
+ // the wave duration, which both widens the band and makes the sweep
148
+ // continuous - the far corner restarts as the near one does.
149
+ case "S2":
150
+ return ((x + y) / (2 * (N - 1))) * 1500;
151
+ // One head with a decaying tail, running the perimeter clockwise.
152
+ case "S3": {
153
+ const i = RING_INDEX.get(x + "," + y);
154
+ if (i === undefined) return 0;
155
+ return -(((RING.length - i) % RING.length) / RING.length) * 1700;
156
+ }
157
+ // A soft column travels left to right.
158
+ case "S4":
159
+ return (x / (N - 1)) * 1100;
160
+ // Like S3 but scrambled order - the pulse jumps pseudo-randomly.
161
+ case "S5": {
162
+ const i = RING_INDEX.get(x + "," + y);
163
+ if (i === undefined) return 0;
164
+ const scrambled = (i * 3) % RING.length;
165
+ return -(scrambled / RING.length) * 1700;
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * `settle` gathers each cell from a position rotated one way around the
172
+ * centre and releases it to the mirror rotation, so the cycle keeps swirling
173
+ * the same way instead of rewinding to where it came from.
174
+ */
175
+ const SWIRL = 1.05; // radians of rotation at each end, ~60°
176
+ const SPREAD = 1.6; // outward push, on top of the rotation
177
+
178
+ /** Offset from a cell's own grid slot to its swirled position, in stage px. */
179
+ function swirl(x: number, y: number, angle: number): [number, number] {
180
+ const dx = x - MID;
181
+ const dy = y - MID;
182
+ const cos = Math.cos(angle);
183
+ const sin = Math.sin(angle);
184
+ return [
185
+ ((dx * cos - dy * sin) * SPREAD - dx) * PITCH,
186
+ ((dx * sin + dy * cos) * SPREAD - dy) * PITCH,
187
+ ];
188
+ }
189
+
190
+ interface Cell {
191
+ key: string;
192
+ left: number;
193
+ top: number;
194
+ delay: number;
195
+ /** Where `settle` gathers this cell from, and releases it to. */
196
+ ax: number;
197
+ ay: number;
198
+ bx: number;
199
+ by: number;
200
+ /** Sits out the choreography (interior cells during `orbit`). */
201
+ still: boolean;
202
+ /** Centre cell - the static frame under reduced motion. */
203
+ mid: boolean;
204
+ }
205
+
206
+ /** The 9 lattice cells, with position, phase and swirl vectors. */
207
+ function latticeCells(v: LatticeVariant): Cell[] {
208
+ const cells: Cell[] = [];
209
+ for (let y = 0; y < N; y++) {
210
+ for (let x = 0; x < N; x++) {
211
+ const [ax, ay] = swirl(x, y, -SWIRL);
212
+ const [bx, by] = swirl(x, y, SWIRL);
213
+ cells.push({
214
+ key: x + "," + y,
215
+ left: x * PITCH,
216
+ top: y * PITCH,
217
+ delay: cellDelay(v, x, y),
218
+ ax,
219
+ ay,
220
+ bx,
221
+ by,
222
+ still: (v === "S3" || v === "S5") && !RING_INDEX.has(x + "," + y),
223
+ mid: x === MID && y === MID,
224
+ });
225
+ }
226
+ }
227
+ return cells;
228
+ }
229
+
230
+ const RING_N = 8;
231
+ const RING_R = 8;
232
+
233
+ interface RingDot {
234
+ key: number;
235
+ rx: number;
236
+ ry: number;
237
+ delay: number;
238
+ }
239
+
240
+ function ringDuration(v: RingVariant): number {
241
+ switch (v) {
242
+ case "C1":
243
+ return 1600;
244
+ case "C2":
245
+ return 2000;
246
+ case "C3":
247
+ return 1800;
248
+ case "C4":
249
+ return 1600;
250
+ case "C5":
251
+ return 2200;
252
+ }
253
+ }
254
+
255
+ function ringDelay(v: RingVariant, i: number): number {
256
+ const dur = ringDuration(v);
257
+ switch (v) {
258
+ case "C1":
259
+ return -((RING_N - 1 - i) / RING_N) * dur;
260
+ case "C2":
261
+ case "C3":
262
+ return -((RING_N - 1 - i) / RING_N) * dur;
263
+ case "C4":
264
+ return i % 2 === 0 ? 0 : -(dur / 2);
265
+ case "C5": {
266
+ const scrambled = (i * 3) % RING_N;
267
+ return -(scrambled / RING_N) * dur;
268
+ }
269
+ default:
270
+ return -(i / RING_N) * dur;
271
+ }
272
+ }
273
+
274
+ function ringDots(v: RingVariant): RingDot[] {
275
+ const dots: RingDot[] = [];
276
+ for (let i = 0; i < RING_N; i++) {
277
+ const angle = (i / RING_N) * Math.PI * 2 - Math.PI / 2;
278
+ dots.push({
279
+ key: i,
280
+ rx: Math.cos(angle) * RING_R,
281
+ ry: Math.sin(angle) * RING_R,
282
+ delay: ringDelay(v, i),
283
+ });
284
+ }
285
+ return dots;
286
+ }
287
+
288
+ const GLOBE_R = 8.5;
289
+ const GLOBE_TILT = (14 * Math.PI) / 180;
290
+ const GLOBE_STEPS = 8;
291
+
292
+ const GLOBE_RINGS: { lat: number; count: number }[] = [
293
+ { lat: 52, count: 8 },
294
+ { lat: 26, count: 8 },
295
+ { lat: 0, count: 8 },
296
+ { lat: -26, count: 8 },
297
+ { lat: -52, count: 8 },
298
+ ];
299
+
300
+ interface GlobeDot {
301
+ key: number;
302
+ style: Record<string, string>;
303
+ css: string;
304
+ }
305
+
306
+ function projectGlobe(x: number, y: number, z: number, spin: number) {
307
+ const cs = Math.cos(spin);
308
+ const ss = Math.sin(spin);
309
+ const x1 = x * cs - z * ss;
310
+ const z1 = x * ss + z * cs;
311
+ const y1 = y;
312
+ const ct = Math.cos(GLOBE_TILT);
313
+ const st = Math.sin(GLOBE_TILT);
314
+ return {
315
+ x: x1,
316
+ y: y1 * ct - z1 * st,
317
+ z: y1 * st + z1 * ct,
318
+ };
319
+ }
320
+
321
+ function globeOpacity(z: number) {
322
+ const t = Math.max(0, Math.min(1, (z / GLOBE_R + 0.15) / 1.15));
323
+ return 0.12 + 0.88 * t * t;
324
+ }
325
+
326
+ type RingMove = { ring: number; angle: number };
327
+ const RING_HALF = Math.PI;
328
+ const RING_ARC = 3;
329
+
330
+ function ringDir(ring: number) {
331
+ return ring % 2 === 0 ? -1 : 1;
332
+ }
333
+
334
+ const G3_MOVES: RingMove[] = (() => {
335
+ const moves: RingMove[] = [];
336
+ for (let pass = 0; pass < 2; pass++) {
337
+ for (let r = 0; r < GLOBE_RINGS.length; r++) {
338
+ moves.push({ ring: r, angle: ringDir(r) * RING_HALF });
339
+ }
340
+ }
341
+ return moves;
342
+ })();
343
+
344
+ const G4_MOVES: RingMove[] = [2, 1, 3, 0, 4, 2, 1, 3, 0, 4].map((ring) => ({
345
+ ring,
346
+ angle: ringDir(ring) * RING_HALF,
347
+ }));
348
+
349
+ function ringTurnPoses(
350
+ x0: number,
351
+ y0: number,
352
+ z0: number,
353
+ ringIndex: number,
354
+ moves: RingMove[],
355
+ ): [number, number, number][] {
356
+ let x = x0;
357
+ let y = y0;
358
+ let z = z0;
359
+ const poses: [number, number, number][] = [[x, y, z]];
360
+ for (let m = 0; m < moves.length; m++) {
361
+ const move = moves[m];
362
+ const xS = x;
363
+ const yS = y;
364
+ const zS = z;
365
+ for (let s = 1; s <= RING_ARC; s++) {
366
+ if (ringIndex === move.ring) {
367
+ const a = move.angle * (s / RING_ARC);
368
+ const c = Math.cos(a);
369
+ const sn = Math.sin(a);
370
+ x = xS * c - zS * sn;
371
+ y = yS;
372
+ z = xS * sn + zS * c;
373
+ }
374
+ poses.push([x, y, z]);
375
+ }
376
+ }
377
+ return poses;
378
+ }
379
+
380
+ const G5_SLOW = 0.4;
381
+ const G5_BURST = (Math.PI * 2 - G5_SLOW * 4) / 4;
382
+ const G5_POSES: { s: number; spin: number }[] = (() => {
383
+ const poses: { s: number; spin: number }[] = [{ s: 1.0, spin: 0 }];
384
+ let spin = 0;
385
+ const steps: { s: number; kind: "slow" | "burst" }[] = [
386
+ { s: 1.0, kind: "slow" },
387
+ { s: 0.9, kind: "burst" },
388
+ { s: 0.9, kind: "slow" },
389
+ { s: 0.8, kind: "burst" },
390
+ { s: 0.8, kind: "slow" },
391
+ { s: 0.9, kind: "burst" },
392
+ { s: 0.9, kind: "slow" },
393
+ { s: 1.0, kind: "burst" },
394
+ ];
395
+ for (const step of steps) {
396
+ spin += step.kind === "slow" ? G5_SLOW : G5_BURST;
397
+ poses.push({ s: step.s, spin });
398
+ }
399
+ return poses;
400
+ })();
401
+
402
+ function globeKeyframeStyle(
403
+ x0: number,
404
+ y0: number,
405
+ z0: number,
406
+ variant: HelixVariant,
407
+ ringIndex: number,
408
+ j = 0,
409
+ ): Record<string, string> {
410
+ const style: Record<string, string> = {};
411
+
412
+ if (variant === "G5") {
413
+ for (let k = 0; k < G5_POSES.length; k++) {
414
+ const sc = G5_POSES[k].s;
415
+ const spin = G5_POSES[k].spin;
416
+ const p = projectGlobe(x0 * sc, y0 * sc, z0 * sc, spin);
417
+ style["--g" + k + "x"] = p.x.toFixed(2) + "px";
418
+ style["--g" + k + "y"] = (-p.y).toFixed(2) + "px";
419
+ style["--g" + k + "o"] = globeOpacity(p.z).toFixed(3);
420
+ }
421
+ return style;
422
+ }
423
+
424
+ if (variant === "G3" || variant === "G4") {
425
+ const poses = ringTurnPoses(
426
+ x0,
427
+ y0,
428
+ z0,
429
+ ringIndex,
430
+ variant === "G3" ? G3_MOVES : G4_MOVES,
431
+ );
432
+ for (let k = 0; k < poses.length; k++) {
433
+ const pos = poses[k];
434
+ const p = projectGlobe(pos[0], pos[1], pos[2], 0);
435
+ style["--g" + k + "x"] = p.x.toFixed(2) + "px";
436
+ style["--g" + k + "y"] = (-p.y).toFixed(2) + "px";
437
+ style["--g" + k + "o"] = globeOpacity(p.z).toFixed(3);
438
+ }
439
+ return style;
440
+ }
441
+
442
+ const dir = variant === "G2" && ringIndex % 2 === 1 ? -1 : 1;
443
+
444
+ for (let k = 0; k < GLOBE_STEPS; k++) {
445
+ const phase = k / GLOBE_STEPS;
446
+ const spin = dir * phase * Math.PI * 2;
447
+ const p = projectGlobe(x0, y0, z0, spin);
448
+ style["--g" + k + "x"] = p.x.toFixed(2) + "px";
449
+ style["--g" + k + "y"] = (-p.y).toFixed(2) + "px";
450
+ style["--g" + k + "o"] = globeOpacity(p.z).toFixed(3);
451
+ }
452
+ return style;
453
+ }
454
+
455
+ function globeDots(v: HelixVariant): GlobeDot[] {
456
+ const dots: GlobeDot[] = [];
457
+ let idx = 0;
458
+ for (let ringIndex = 0; ringIndex < GLOBE_RINGS.length; ringIndex++) {
459
+ const ring = GLOBE_RINGS[ringIndex];
460
+ const latRad = (ring.lat * Math.PI) / 180;
461
+ const y0 = Math.sin(latRad) * GLOBE_R;
462
+ const ringR = Math.cos(latRad) * GLOBE_R;
463
+ for (let j = 0; j < ring.count; j++) {
464
+ const lon = (j / ring.count) * Math.PI * 2;
465
+ const style = globeKeyframeStyle(
466
+ Math.cos(lon) * ringR,
467
+ y0,
468
+ Math.sin(lon) * ringR,
469
+ v,
470
+ ringIndex,
471
+ j,
472
+ );
473
+ dots.push({
474
+ key: idx,
475
+ style,
476
+ css: Object.keys(style)
477
+ .map((k) => k + ":" + style[k])
478
+ .join(";"),
479
+ });
480
+ idx++;
481
+ }
482
+ }
483
+ return dots;
484
+ }
485
+
486
+ const MORPH_N = 8;
487
+ const MORPH_R = 7;
488
+
489
+ type ShapeFn = (i: number) => [number, number];
490
+
491
+ const shapeCircle: ShapeFn = (i) => {
492
+ const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2;
493
+ return [Math.cos(a) * MORPH_R, Math.sin(a) * MORPH_R];
494
+ };
495
+
496
+ const shapeOctagon: ShapeFn = (i) => {
497
+ const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2;
498
+ const r = MORPH_R * 0.92;
499
+ const sector = Math.round(a / (Math.PI / 4)) * (Math.PI / 4);
500
+ return [Math.cos(sector) * r, Math.sin(sector) * r];
501
+ };
502
+
503
+ const shapeSquare: ShapeFn = (i) => {
504
+ const h = MORPH_R * 0.85;
505
+ const corners: [number, number][] = [
506
+ [-h, -h],
507
+ [h, -h],
508
+ [h, h],
509
+ [-h, h],
510
+ ];
511
+ const t = ((i / MORPH_N) * 4 + 0.5) % 4;
512
+ const side = Math.floor(t) % 4;
513
+ const frac = t - Math.floor(t);
514
+ const from = corners[side];
515
+ const to = corners[(side + 1) % 4];
516
+ return [
517
+ from[0] + (to[0] - from[0]) * frac,
518
+ from[1] + (to[1] - from[1]) * frac,
519
+ ];
520
+ };
521
+
522
+ const shapeCircleAt =
523
+ (turn: number): ShapeFn =>
524
+ (i) => {
525
+ const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2 + turn;
526
+ return [Math.cos(a) * MORPH_R, Math.sin(a) * MORPH_R];
527
+ };
528
+
529
+ const SCATTER_TRAIL = 0.12;
530
+
531
+ const shapeScatterA: ShapeFn = (i) => {
532
+ const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2;
533
+ return [-Math.cos(a) * MORPH_R, Math.sin(a) * MORPH_R];
534
+ };
535
+
536
+ const shapeScatterB: ShapeFn = shapeCircle;
537
+ const shapeScatterC: ShapeFn = shapeScatterA;
538
+
539
+ const shapeDiamond: ShapeFn = (i) => {
540
+ const corners: [number, number][] = [
541
+ [0, -MORPH_R],
542
+ [MORPH_R, 0],
543
+ [0, MORPH_R],
544
+ [-MORPH_R, 0],
545
+ ];
546
+ const t = (i / MORPH_N) * 4;
547
+ const side = Math.floor(t) % 4;
548
+ const frac = t - Math.floor(t);
549
+ const from = corners[side];
550
+ const to = corners[(side + 1) % 4];
551
+ return [
552
+ from[0] + (to[0] - from[0]) * frac,
553
+ from[1] + (to[1] - from[1]) * frac,
554
+ ];
555
+ };
556
+
557
+ const shapeCenter: ShapeFn = (i) => {
558
+ const a = (i / MORPH_N) * Math.PI * 2 - Math.PI / 2;
559
+ return [Math.cos(a) * 1.5, Math.sin(a) * 1.5];
560
+ };
561
+
562
+ function morphShapes(v: MorphVariant): [ShapeFn, ShapeFn, ShapeFn, ShapeFn] {
563
+ switch (v) {
564
+ case "M1":
565
+ return [shapeCircle, shapeSquare, shapeDiamond, shapeSquare];
566
+ case "M2":
567
+ return [shapeCenter, shapeCircle, shapeCenter, shapeCircle];
568
+ case "M3":
569
+ return [
570
+ shapeCircleAt(0),
571
+ shapeCircleAt(Math.PI / 2),
572
+ shapeCircleAt(Math.PI),
573
+ shapeCircleAt(Math.PI * 1.5),
574
+ ];
575
+ case "M4":
576
+ return [shapeCircle, shapeDiamond, shapeCircle, shapeDiamond];
577
+ case "M5":
578
+ return [shapeCircle, shapeScatterA, shapeScatterB, shapeScatterC];
579
+ }
580
+ }
581
+
582
+ interface MorphDot {
583
+ key: number;
584
+ m1: string;
585
+ m2: string;
586
+ m3: string;
587
+ m4: string;
588
+ delay?: string;
589
+ depth?: string;
590
+ }
591
+
592
+ function morphDots(v: MorphVariant): MorphDot[] {
593
+ const [s1, s2, s3, s4] = morphShapes(v);
594
+ const dots: MorphDot[] = [];
595
+ for (let i = 0; i < MORPH_N; i++) {
596
+ const [x1, y1] = s1(i);
597
+ const [x2, y2] = s2(i);
598
+ const [x3, y3] = s3(i);
599
+ const [x4, y4] = s4(i);
600
+ dots.push({
601
+ key: i,
602
+ m1: x1.toFixed(1) + "px, " + y1.toFixed(1) + "px",
603
+ m2: x2.toFixed(1) + "px, " + y2.toFixed(1) + "px",
604
+ m3: x3.toFixed(1) + "px, " + y3.toFixed(1) + "px",
605
+ m4: x4.toFixed(1) + "px, " + y4.toFixed(1) + "px",
606
+ delay: v === "M5" ? -i * 10 + "ms" : undefined,
607
+ depth:
608
+ v === "M5"
609
+ ? Math.abs(
610
+ Math.cos((i / MORPH_N) * Math.PI * 2 - Math.PI / 2),
611
+ ).toFixed(2)
612
+ : undefined,
613
+ });
614
+ }
615
+ return dots;
616
+ }
617
+
618
+ export interface OrbProps {
619
+ variant?: OrbVariant;
620
+ /** Rendered edge length in px. The 28px geometry scales to fit. */
621
+ size?: number;
622
+ /** Accessible label, and the status text when `pill` is set. */
623
+ label?: string;
624
+ /** Wraps the orb and its label in a status pill. */
625
+ pill?: boolean;
626
+ className?: string;
627
+ style?: CSSProperties;
628
+ }
629
+
630
+ export function Orb({
631
+ variant = "S1",
632
+ size = SIZE,
633
+ label,
634
+ pill,
635
+ className,
636
+ style,
637
+ }: OrbProps) {
638
+ const messages = useUiMessages();
639
+ const text = label ?? `${messages.loading}…`;
640
+ return (
641
+ <span
642
+ className={"orb-root" + (className ? " " + className : "")}
643
+ data-pill={pill ? "" : undefined}
644
+ style={style}
645
+ >
646
+ <span
647
+ className={"orb-glyph"}
648
+ // In pill form the visible label already carries the meaning, so
649
+ // the glyph steps out of the accessibility tree.
650
+ role={pill ? undefined : "img"}
651
+ aria-label={pill ? undefined : text}
652
+ aria-hidden={pill ? true : undefined}
653
+ style={
654
+ {
655
+ width: size,
656
+ height: size,
657
+ "--orb-k": size / STAGE,
658
+ } as CSSProperties
659
+ }
660
+ >
661
+ {isLattice(variant) ? (
662
+ <span className={"orb-lattice"} data-variant={variant}>
663
+ {latticeCells(variant).map((c) => (
664
+ <span
665
+ key={c.key}
666
+ className={"orb-cell"}
667
+ data-still={c.still ? "" : undefined}
668
+ data-mid={c.mid ? "" : undefined}
669
+ style={
670
+ {
671
+ left: c.left,
672
+ top: c.top,
673
+ animationDelay: c.delay + "ms",
674
+ "--orb-ax": c.ax + "px",
675
+ "--orb-ay": c.ay + "px",
676
+ "--orb-bx": c.bx + "px",
677
+ "--orb-by": c.by + "px",
678
+ } as CSSProperties
679
+ }
680
+ />
681
+ ))}
682
+ </span>
683
+ ) : isRing(variant) ? (
684
+ <span className={"orb-ring"} data-variant={variant}>
685
+ {ringDots(variant).map((d) => (
686
+ <span
687
+ key={d.key}
688
+ className={"orb-ring-dot"}
689
+ style={
690
+ {
691
+ "--orb-rx": d.rx + "px",
692
+ "--orb-ry": d.ry + "px",
693
+ animationDelay: d.delay + "ms",
694
+ } as CSSProperties
695
+ }
696
+ />
697
+ ))}
698
+ </span>
699
+ ) : isHelix(variant) ? (
700
+ <span className={"orb-helix"} data-variant={variant}>
701
+ {globeDots(variant).map((d) => (
702
+ <span
703
+ key={d.key}
704
+ className={"orb-helix-dot"}
705
+ style={d.style as CSSProperties}
706
+ />
707
+ ))}
708
+ </span>
709
+ ) : isMorph(variant) ? (
710
+ <span className={"orb-morph"} data-variant={variant}>
711
+ {morphDots(variant).map((d) => (
712
+ <span
713
+ key={d.key}
714
+ className={"orb-morph-dot"}
715
+ style={
716
+ {
717
+ "--m-1": d.m1,
718
+ "--m-2": d.m2,
719
+ "--m-3": d.m3,
720
+ "--m-4": d.m4,
721
+ "--m-depth": d.depth,
722
+ animationDelay: d.delay,
723
+ } as CSSProperties
724
+ }
725
+ />
726
+ ))}
727
+ </span>
728
+ ) : (
729
+ <span className={"orb-lens"} data-variant={variant}>
730
+ <span className={"orb-shape" + " " + "orb-shape-a"} />
731
+ <span className={"orb-shape" + " " + "orb-shape-b"} />
732
+ <span className={"orb-shape" + " " + "orb-shape-c"} />
733
+ {/* focus is the one variant that needs a fourth circle: its cast
734
+ sits on the corners of a square, and three corners do not
735
+ make a square. */}
736
+ {variant === "B1" && (
737
+ <span className={"orb-shape" + " " + "orb-shape-d"} />
738
+ )}
739
+ </span>
740
+ )}
741
+ </span>
742
+ {pill && <span className={"orb-pill-label"}>{text}</span>}
743
+ </span>
744
+ );
745
+ }
746
+
747
+ /* Usage:
748
+ <Orb variant="S4" />
749
+ <Orb variant="B4" size={40} />
750
+ <Orb variant="C3" />
751
+ <Orb variant="B2" label="Searching the web…" pill />
752
+ */