@odyn-app/api 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +24 -2
  2. package/dist/index.d.ts +492 -0
  3. package/dist/index.js +487 -0
  4. package/dist/ui/button.d.ts +10 -0
  5. package/dist/ui/button.js +31 -0
  6. package/dist/ui/cn.d.ts +3 -0
  7. package/dist/ui/cn.js +6 -0
  8. package/dist/ui/index.d.ts +10 -0
  9. package/dist/ui/index.js +10 -0
  10. package/dist/ui/orb/orb-base.module.css +21 -0
  11. package/dist/ui/orb/orb-geometry-cache.d.ts +1 -0
  12. package/dist/ui/orb/orb-geometry-cache.js +11 -0
  13. package/dist/ui/orb/orb-helix-geometry.d.ts +7 -0
  14. package/dist/ui/orb/orb-helix-geometry.js +130 -0
  15. package/dist/ui/orb/orb-helix.module.css +155 -0
  16. package/dist/ui/orb/orb-lattice-geometry.d.ts +35 -0
  17. package/dist/ui/orb/orb-lattice-geometry.js +148 -0
  18. package/dist/ui/orb/orb-lattice.module.css +165 -0
  19. package/dist/ui/orb/orb-lens-primary.module.css +195 -0
  20. package/dist/ui/orb/orb-lens-secondary.module.css +143 -0
  21. package/dist/ui/orb/orb-lens.module.css +44 -0
  22. package/dist/ui/orb/orb-morph-geometry.d.ts +7 -0
  23. package/dist/ui/orb/orb-morph-geometry.js +88 -0
  24. package/dist/ui/orb/orb-morph.module.css +62 -0
  25. package/dist/ui/orb/orb-ring-geometry.d.ts +8 -0
  26. package/dist/ui/orb/orb-ring-geometry.js +42 -0
  27. package/dist/ui/orb/orb-ring.module.css +113 -0
  28. package/dist/ui/orb/orb-types.d.ts +23 -0
  29. package/dist/ui/orb/orb-types.js +48 -0
  30. package/dist/ui/orb/orb.d.ts +17 -0
  31. package/dist/ui/orb/orb.js +40 -0
  32. package/dist/ui/setting-row.d.ts +9 -0
  33. package/dist/ui/setting-row.js +9 -0
  34. package/dist/ui/skeleton.d.ts +3 -0
  35. package/dist/ui/skeleton.js +6 -0
  36. package/dist/ui/theme.css +232 -0
  37. package/package.json +56 -4
@@ -0,0 +1,195 @@
1
+ /* focus — attention travels the cast: each circle pulls into focus in turn.
2
+ Four circles on the corners of a square, one size for all of them, so the
3
+ only thing separating them is which one is sharp.
4
+
5
+ A second longer than the three-circle version it grew out of: the square
6
+ has four stations to visit and each one keeps the same unhurried second.
7
+
8
+ The delays count down rather than up because a more negative delay seeds a
9
+ circle further into its cycle: -3s of a 4s cycle runs three quarters ahead,
10
+ which is what sends focus round the square clockwise. */
11
+ .variants[data-variant="B1"] :global([data-orb-shape]) {
12
+ --orb-d: 6px;
13
+ animation: orb-focus 4s var(--orb-ease-smooth) infinite both;
14
+ }
15
+
16
+ .variants[data-variant="B1"] :global([data-orb-shape="a"]) {
17
+ --orb-ox: -4.5px;
18
+ --orb-oy: -4.5px;
19
+ animation-delay: 0s;
20
+ }
21
+
22
+ .variants[data-variant="B1"] :global([data-orb-shape="b"]) {
23
+ --orb-ox: 4.5px;
24
+ --orb-oy: -4.5px;
25
+ animation-delay: -3s;
26
+ }
27
+
28
+ .variants[data-variant="B1"] :global([data-orb-shape="c"]) {
29
+ --orb-ox: 4.5px;
30
+ --orb-oy: 4.5px;
31
+ animation-delay: -2s;
32
+ }
33
+
34
+ .variants[data-variant="B1"] :global([data-orb-shape="d"]) {
35
+ --orb-ox: -4.5px;
36
+ --orb-oy: 4.5px;
37
+ animation-delay: -1s;
38
+ }
39
+
40
+ /* Opacity gradient around the square: active = 1.0, next neighbour = 0.30,
41
+ diagonal = 0.10, far = 0.05. Two circles are always clearly visible, the
42
+ rest are ghost hints. */
43
+ @keyframes orb-focus {
44
+ 0%,
45
+ 100% {
46
+ opacity: 0.05;
47
+ filter: blur(2px);
48
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.12);
49
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
50
+ }
51
+ 12% {
52
+ opacity: 1;
53
+ filter: blur(0);
54
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1);
55
+ animation-timing-function: linear;
56
+ }
57
+ 22% {
58
+ opacity: 1;
59
+ filter: blur(0);
60
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1);
61
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
62
+ }
63
+ /* Next neighbour — one quarter away: clearly visible */
64
+ 38% {
65
+ opacity: 0.3;
66
+ filter: blur(1.2px);
67
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.06);
68
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
69
+ }
70
+ /* Diagonal — half a cycle away: ghost */
71
+ 58% {
72
+ opacity: 0.1;
73
+ filter: blur(1.8px);
74
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.1);
75
+ animation-timing-function: linear;
76
+ }
77
+ /* Far neighbour — three quarters away: barely there */
78
+ 82% {
79
+ opacity: 0.05;
80
+ filter: blur(2px);
81
+ transform: translate(var(--orb-ox), var(--orb-oy)) scale(1.12);
82
+ }
83
+ }
84
+
85
+ /* drift — the cast circles the stage on one track, sharp at the front and
86
+ blurred away at the back, so the orb reads as looking around. Uniform
87
+ size: the depth cue is doing the work, a size ladder would fight it. */
88
+ .variants[data-variant="B2"] :global([data-orb-shape]) {
89
+ animation: orb-revolve 3.3s linear infinite both;
90
+ }
91
+
92
+ /* Evenly spaced around the track, so one is always at the front. */
93
+ .variants[data-variant="B2"] :global([data-orb-shape="a"]) {
94
+ animation-delay: 0s;
95
+ }
96
+
97
+ .variants[data-variant="B2"] :global([data-orb-shape="b"]) {
98
+ animation-delay: -1.1s;
99
+ }
100
+
101
+ .variants[data-variant="B2"] :global([data-orb-shape="c"]) {
102
+ animation-delay: -2.2s;
103
+ }
104
+
105
+ /* rotate() then translateY() walks a circle. Linear all the way: an eased
106
+ rotation on a circular path reads as a wobble, not as travel. */
107
+ @keyframes orb-revolve {
108
+ 0% {
109
+ opacity: 1;
110
+ filter: blur(0);
111
+ transform: rotate(0deg) translateY(6.5px) scale(1);
112
+ }
113
+ 25% {
114
+ opacity: 0.55;
115
+ filter: blur(1.3px);
116
+ transform: rotate(90deg) translateY(6.5px) scale(0.82);
117
+ }
118
+ 50% {
119
+ opacity: 0.28;
120
+ filter: blur(2.4px);
121
+ transform: rotate(180deg) translateY(6.5px) scale(0.66);
122
+ }
123
+ 75% {
124
+ opacity: 0.55;
125
+ filter: blur(1.3px);
126
+ transform: rotate(270deg) translateY(6.5px) scale(0.82);
127
+ }
128
+ 100% {
129
+ opacity: 1;
130
+ filter: blur(0);
131
+ transform: rotate(360deg) translateY(6.5px) scale(1);
132
+ }
133
+ }
134
+
135
+ /* bloom — shapes emanate from the centre, blurring out as they grow.
136
+ Linear keeps the total ink even; on a front-loaded curve the shapes
137
+ jump to their large, blurred end state and the orb alternates between
138
+ a heavy blot and an empty haze. */
139
+ .variants[data-variant="B3"] :global([data-orb-shape]) {
140
+ animation: orb-bloom 4.2s linear infinite both;
141
+ }
142
+
143
+ .variants[data-variant="B3"] :global([data-orb-shape="a"]) {
144
+ animation-delay: 0s;
145
+ }
146
+
147
+ .variants[data-variant="B3"] :global([data-orb-shape="b"]) {
148
+ animation-delay: -1.4s;
149
+ }
150
+
151
+ .variants[data-variant="B3"] :global([data-orb-shape="c"]) {
152
+ animation-delay: -2.8s;
153
+ }
154
+
155
+ /* Each ripple dies at 62% and waits out the rest, so the three overlapping
156
+ blooms leave gaps. Without the gap the aggregate is a constant haze and
157
+ the outward motion stops reading at all. Sharp circle appears, holds
158
+ briefly, then dissolves outward — blur only kicks in once opacity starts
159
+ dropping, so the circle stays crisp while it's visible and the blur reads
160
+ as the ripple dissipating. */
161
+ @keyframes orb-bloom {
162
+ 0% {
163
+ opacity: 0;
164
+ filter: blur(0);
165
+ transform: scale(0.35);
166
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
167
+ }
168
+ 8% {
169
+ opacity: 1;
170
+ filter: blur(0);
171
+ transform: scale(0.55);
172
+ animation-timing-function: linear;
173
+ }
174
+ 24% {
175
+ opacity: 1;
176
+ filter: blur(0);
177
+ transform: scale(0.72);
178
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
179
+ }
180
+ 42% {
181
+ opacity: 0.1;
182
+ filter: blur(1.8px);
183
+ transform: scale(1.5);
184
+ }
185
+ 62% {
186
+ opacity: 0;
187
+ filter: blur(2.8px);
188
+ transform: scale(2.4);
189
+ }
190
+ 100% {
191
+ opacity: 0;
192
+ filter: blur(2.8px);
193
+ transform: scale(2.4);
194
+ }
195
+ }
@@ -0,0 +1,143 @@
1
+ /* converge — a single circle traces an equilateral triangle (top → bottom-right
2
+ → bottom-left → top) with handoff-style easing: full size and sharp at each
3
+ vertex, smaller and slightly blurred in transit. orbB breathes at the
4
+ centroid as a subtle depth cue; orbC is hidden. */
5
+ .variants[data-variant="B4"] :global([data-orb-shape="a"]) {
6
+ animation: orb-converge 3.6s linear infinite both;
7
+ }
8
+ .variants[data-variant="B4"] :global([data-orb-shape="b"]) {
9
+ animation: orb-breathe 3.6s ease-in-out infinite both;
10
+ }
11
+ .variants[data-variant="B4"] :global([data-orb-shape="c"]) {
12
+ display: none;
13
+ }
14
+
15
+ @keyframes orb-converge {
16
+ 0% {
17
+ transform: translate(0px, -5px) scale(1);
18
+ filter: blur(0);
19
+ animation-timing-function: linear;
20
+ }
21
+ 10% {
22
+ transform: translate(0px, -5px) scale(1);
23
+ filter: blur(0);
24
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
25
+ }
26
+ 22% {
27
+ transform: translate(2.15px, -1.25px) scale(0.72);
28
+ filter: blur(0.8px);
29
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
30
+ }
31
+ 33% {
32
+ transform: translate(4.3px, 2.5px) scale(1);
33
+ filter: blur(0);
34
+ animation-timing-function: linear;
35
+ }
36
+ 43% {
37
+ transform: translate(4.3px, 2.5px) scale(1);
38
+ filter: blur(0);
39
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
40
+ }
41
+ 55% {
42
+ transform: translate(0px, 2.5px) scale(0.72);
43
+ filter: blur(0.8px);
44
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
45
+ }
46
+ 66% {
47
+ transform: translate(-4.3px, 2.5px) scale(1);
48
+ filter: blur(0);
49
+ animation-timing-function: linear;
50
+ }
51
+ 77% {
52
+ transform: translate(-4.3px, 2.5px) scale(1);
53
+ filter: blur(0);
54
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
55
+ }
56
+ 88% {
57
+ transform: translate(-2.15px, -1.25px) scale(0.72);
58
+ filter: blur(0.8px);
59
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
60
+ }
61
+ 100% {
62
+ transform: translate(0px, -5px) scale(1);
63
+ filter: blur(0);
64
+ }
65
+ }
66
+
67
+ /* handoff — the cast crosses the focal plane one after another, always left
68
+ to right, like work being passed on. The shorthand curve is only a
69
+ fallback; every segment below sets its own. */
70
+ .variants[data-variant="B5"] :global([data-orb-shape]) {
71
+ animation: orb-handoff 2.8s linear infinite both;
72
+ }
73
+
74
+ /* Half a cycle apart, so one is always at the focal plane while the other is
75
+ invisible at an end and the loop point cannot be seen. */
76
+ .variants[data-variant="B5"] :global([data-orb-shape="a"]) {
77
+ animation-delay: 0s;
78
+ }
79
+
80
+ .variants[data-variant="B5"] :global([data-orb-shape="c"]) {
81
+ animation-delay: -1.4s;
82
+ }
83
+
84
+ /* The third holds the centre and breathes — a soft depth cue behind the
85
+ traffic rather than another traveller. */
86
+ .variants[data-variant="B5"] :global([data-orb-shape="b"]) {
87
+ animation-name: orb-breathe;
88
+ animation-duration: 3.6s;
89
+ }
90
+
91
+ /* Enters small from the left, reaches standard size at the focal plane, then
92
+ shrinks and fades out to the right. At the dwell (centre) the circle is
93
+ exactly 1× — no pulsing, no bounce, just a clean handoff. */
94
+ @keyframes orb-handoff {
95
+ 0% {
96
+ opacity: 0;
97
+ filter: blur(2.4px);
98
+ transform: translateX(-11px) scale(0.55);
99
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
100
+ }
101
+ 22% {
102
+ opacity: 1;
103
+ filter: blur(0);
104
+ transform: translateX(-1px) scale(1);
105
+ animation-timing-function: linear;
106
+ }
107
+ 37% {
108
+ opacity: 1;
109
+ filter: blur(0);
110
+ transform: translateX(0) scale(1);
111
+ animation-timing-function: linear;
112
+ }
113
+ 52% {
114
+ opacity: 1;
115
+ filter: blur(0);
116
+ transform: translateX(1px) scale(1);
117
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
118
+ }
119
+ 70% {
120
+ opacity: 0;
121
+ filter: blur(2.4px);
122
+ transform: translateX(11px) scale(0.55);
123
+ }
124
+ 100% {
125
+ opacity: 0;
126
+ filter: blur(2.4px);
127
+ transform: translateX(11px) scale(0.55);
128
+ }
129
+ }
130
+
131
+ @keyframes orb-breathe {
132
+ 0%,
133
+ 100% {
134
+ opacity: 0.16;
135
+ filter: blur(2.4px);
136
+ transform: scale(1.2);
137
+ }
138
+ 50% {
139
+ opacity: 0.32;
140
+ filter: blur(1.6px);
141
+ transform: scale(0.98);
142
+ }
143
+ }
@@ -0,0 +1,44 @@
1
+ /* Shared stage for the Lens family. Variant choreography is split by group. */
2
+ .lens {
3
+ position: absolute;
4
+ left: 0;
5
+ top: 0;
6
+ width: 28px;
7
+ height: 28px;
8
+ transform-origin: 0 0;
9
+ transform: scale(var(--orb-k, 1));
10
+ }
11
+
12
+ .shape {
13
+ position: absolute;
14
+ left: 50%;
15
+ top: 50%;
16
+ width: var(--orb-d, 7px);
17
+ height: var(--orb-d, 7px);
18
+ margin: calc(var(--orb-d, 7px) / -2) 0 0 calc(var(--orb-d, 7px) / -2);
19
+ border-radius: 50%;
20
+ background: currentColor;
21
+ }
22
+
23
+ :global([data-orb-paused]) .lens,
24
+ :global([data-orb-paused]) .shape {
25
+ animation-play-state: paused;
26
+ }
27
+
28
+ @media (prefers-reduced-motion: reduce) {
29
+ .lens,
30
+ .shape {
31
+ animation: none !important;
32
+ }
33
+
34
+ .shape {
35
+ opacity: 0.3 !important;
36
+ filter: blur(1.4px) !important;
37
+ transform: none !important;
38
+ }
39
+
40
+ .shape:first-child {
41
+ opacity: 1 !important;
42
+ filter: blur(0) !important;
43
+ }
44
+ }
@@ -0,0 +1,7 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { MorphVariant } from './orb-types';
3
+ export interface MorphDot {
4
+ readonly key: number;
5
+ readonly style: CSSProperties;
6
+ }
7
+ export declare const morphDots: (variant: MorphVariant) => readonly MorphDot[];
@@ -0,0 +1,88 @@
1
+ import { memoiseByVariant } from './orb-geometry-cache';
2
+ const DOT_COUNT = 8;
3
+ const RADIUS = 7;
4
+ const circle = (index) => {
5
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2;
6
+ return [Math.cos(angle) * RADIUS, Math.sin(angle) * RADIUS];
7
+ };
8
+ const square = (index) => {
9
+ const halfSize = RADIUS * 0.85;
10
+ const corners = [
11
+ [-halfSize, -halfSize],
12
+ [halfSize, -halfSize],
13
+ [halfSize, halfSize],
14
+ [-halfSize, halfSize],
15
+ ];
16
+ const progress = ((index / DOT_COUNT) * 4 + 0.5) % 4;
17
+ const side = Math.floor(progress) % 4;
18
+ const fraction = progress - Math.floor(progress);
19
+ const from = corners[side];
20
+ const to = corners[(side + 1) % 4];
21
+ return [
22
+ from[0] + (to[0] - from[0]) * fraction,
23
+ from[1] + (to[1] - from[1]) * fraction,
24
+ ];
25
+ };
26
+ const circleAt = (turn) => (index) => {
27
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2 + turn;
28
+ return [Math.cos(angle) * RADIUS, Math.sin(angle) * RADIUS];
29
+ };
30
+ const scatter = (index) => {
31
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2;
32
+ return [-Math.cos(angle) * RADIUS, Math.sin(angle) * RADIUS];
33
+ };
34
+ const diamond = (index) => {
35
+ const corners = [
36
+ [0, -RADIUS],
37
+ [RADIUS, 0],
38
+ [0, RADIUS],
39
+ [-RADIUS, 0],
40
+ ];
41
+ const progress = (index / DOT_COUNT) * 4;
42
+ const side = Math.floor(progress) % 4;
43
+ const fraction = progress - Math.floor(progress);
44
+ const from = corners[side];
45
+ const to = corners[(side + 1) % 4];
46
+ return [
47
+ from[0] + (to[0] - from[0]) * fraction,
48
+ from[1] + (to[1] - from[1]) * fraction,
49
+ ];
50
+ };
51
+ const centre = (index) => {
52
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2;
53
+ return [Math.cos(angle) * 1.5, Math.sin(angle) * 1.5];
54
+ };
55
+ function morphShapes(variant) {
56
+ switch (variant) {
57
+ case 'M1':
58
+ return [circle, square, diamond, square];
59
+ case 'M2':
60
+ return [centre, circle, centre, circle];
61
+ case 'M3':
62
+ return [circleAt(0), circleAt(Math.PI / 2), circleAt(Math.PI), circleAt(Math.PI * 1.5)];
63
+ case 'M4':
64
+ return [circle, diamond, circle, diamond];
65
+ case 'M5':
66
+ return [circle, scatter, circle, scatter];
67
+ }
68
+ }
69
+ function buildMorphDots(variant) {
70
+ const shapes = morphShapes(variant);
71
+ const pair = (point) => `${point[0].toFixed(1)}px, ${point[1].toFixed(1)}px`;
72
+ return Array.from({ length: DOT_COUNT }, (_, index) => ({
73
+ key: index,
74
+ style: {
75
+ '--m-1': pair(shapes[0](index)),
76
+ '--m-2': pair(shapes[1](index)),
77
+ '--m-3': pair(shapes[2](index)),
78
+ '--m-4': pair(shapes[3](index)),
79
+ ...(variant === 'M5'
80
+ ? {
81
+ '--m-depth': Math.abs(Math.cos((index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2)).toFixed(2),
82
+ animationDelay: `${-index * 10}ms`,
83
+ }
84
+ : {}),
85
+ },
86
+ }));
87
+ }
88
+ export const morphDots = memoiseByVariant(buildMorphDots);
@@ -0,0 +1,62 @@
1
+ /* ---- Morph ---- */
2
+ .morph {
3
+ position: absolute;
4
+ inset: 0;
5
+ transform: scale(var(--orb-k, 1));
6
+ }
7
+
8
+ .morphDot {
9
+ position: absolute;
10
+ left: 50%;
11
+ top: 50%;
12
+ width: 3px;
13
+ height: 3px;
14
+ margin: -1.5px 0 0 -1.5px;
15
+ border-radius: 50%;
16
+ background: currentColor;
17
+ animation: orb-morph 4.8s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
18
+ }
19
+
20
+ @keyframes orb-morph {
21
+ 0%, 5% { transform: translate(var(--m-1)); }
22
+ 25%, 30% { transform: translate(var(--m-2)); }
23
+ 50%, 55% { transform: translate(var(--m-3)); }
24
+ 75%, 80% { transform: translate(var(--m-4)); }
25
+ 100% { transform: translate(var(--m-1)); }
26
+ }
27
+
28
+ .morph[data-variant="M2"] {
29
+ animation: orb-morph-twist 9.6s linear infinite;
30
+ }
31
+
32
+ .morph[data-variant="M4"] {
33
+ animation: orb-morph-twist 9.6s linear infinite;
34
+ }
35
+
36
+ .morph[data-variant="M5"] .morphDot {
37
+ animation: orb-morph-scatter 2.8s cubic-bezier(0.4, 0, 0.2, 1) infinite both;
38
+ }
39
+
40
+ @keyframes orb-morph-scatter {
41
+ 0%, 12% { transform: translate(var(--m-1)); opacity: 1; }
42
+ 38%, 62% { transform: translate(var(--m-2)); opacity: calc(1 - 0.6 * var(--m-depth, 0)); }
43
+ 88%, 100% { transform: translate(var(--m-1)); opacity: 1; }
44
+ }
45
+
46
+ @keyframes orb-morph-twist {
47
+ from { transform: scale(var(--orb-k, 1)) rotate(0deg); }
48
+ to { transform: scale(var(--orb-k, 1)) rotate(360deg); }
49
+ }
50
+
51
+
52
+ :global([data-orb-paused]) .morph,
53
+ :global([data-orb-paused]) .morphDot {
54
+ animation-play-state: paused;
55
+ }
56
+
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .morph,
59
+ .morphDot {
60
+ animation: none !important;
61
+ }
62
+ }
@@ -0,0 +1,8 @@
1
+ import type { RingVariant } from './orb-types';
2
+ export interface RingDot {
3
+ readonly key: number;
4
+ readonly rx: number;
5
+ readonly ry: number;
6
+ readonly delay: number;
7
+ }
8
+ export declare const ringDots: (variant: RingVariant) => readonly RingDot[];
@@ -0,0 +1,42 @@
1
+ import { memoiseByVariant } from './orb-geometry-cache';
2
+ const DOT_COUNT = 8;
3
+ const RADIUS = 8;
4
+ function ringDuration(variant) {
5
+ switch (variant) {
6
+ case 'C1':
7
+ return 1600;
8
+ case 'C2':
9
+ return 2000;
10
+ case 'C3':
11
+ return 1800;
12
+ case 'C4':
13
+ return 1600;
14
+ case 'C5':
15
+ return 2200;
16
+ }
17
+ }
18
+ function ringDelay(variant, index) {
19
+ const duration = ringDuration(variant);
20
+ switch (variant) {
21
+ case 'C1':
22
+ case 'C2':
23
+ case 'C3':
24
+ return -((DOT_COUNT - 1 - index) / DOT_COUNT) * duration;
25
+ case 'C4':
26
+ return index % 2 === 0 ? 0 : -(duration / 2);
27
+ case 'C5':
28
+ return -(((index * 3) % DOT_COUNT) / DOT_COUNT) * duration;
29
+ }
30
+ }
31
+ function buildRingDots(variant) {
32
+ return Array.from({ length: DOT_COUNT }, (_, index) => {
33
+ const angle = (index / DOT_COUNT) * Math.PI * 2 - Math.PI / 2;
34
+ return {
35
+ key: index,
36
+ rx: Math.cos(angle) * RADIUS,
37
+ ry: Math.sin(angle) * RADIUS,
38
+ delay: ringDelay(variant, index),
39
+ };
40
+ });
41
+ }
42
+ export const ringDots = memoiseByVariant(buildRingDots);
@@ -0,0 +1,113 @@
1
+ /* --- Ring: eight circles on a fixed ring ----------------------------- */
2
+ .ring {
3
+ position: absolute;
4
+ inset: 0;
5
+ transform: scale(var(--orb-k, 1));
6
+ --orb-ring-rest: 0.22;
7
+ }
8
+
9
+ :global(.dark) .ring {
10
+ --orb-ring-rest: 0.3;
11
+ }
12
+
13
+
14
+ .ringDot {
15
+ position: absolute;
16
+ left: 50%;
17
+ top: 50%;
18
+ width: 3px;
19
+ height: 3px;
20
+ margin: -1.5px 0 0 -1.5px;
21
+ border-radius: 50%;
22
+ background: currentColor;
23
+ transform: translate(var(--orb-rx), var(--orb-ry));
24
+ }
25
+
26
+ .ring[data-variant="C1"] .ringDot {
27
+ opacity: var(--orb-ring-rest);
28
+ animation: orb-ring-chase 1.6s linear infinite both;
29
+ }
30
+
31
+ @keyframes orb-ring-chase {
32
+ 0%, 11% {
33
+ opacity: 1;
34
+ }
35
+ 12.5%, 100% {
36
+ opacity: var(--orb-ring-rest);
37
+ }
38
+ }
39
+
40
+ .ring[data-variant="C2"] .ringDot {
41
+ animation: orb-ring-pulse 2s ease-in-out infinite both;
42
+ }
43
+
44
+ @keyframes orb-ring-pulse {
45
+ 0%, 100% {
46
+ opacity: 0.18;
47
+ transform: translate(var(--orb-rx), var(--orb-ry)) scale(0.7);
48
+ }
49
+ 50% {
50
+ opacity: 1;
51
+ transform: translate(var(--orb-rx), var(--orb-ry)) scale(1.15);
52
+ }
53
+ }
54
+
55
+ .ring[data-variant="C3"] .ringDot {
56
+ animation: orb-ring-comet 1.8s ease-in-out infinite both;
57
+ }
58
+
59
+ @keyframes orb-ring-comet {
60
+ 0%, 100% {
61
+ opacity: 0.08;
62
+ transform: translate(var(--orb-rx), var(--orb-ry));
63
+ }
64
+ 12% {
65
+ opacity: 1;
66
+ transform: translate(var(--orb-rx), var(--orb-ry));
67
+ animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
68
+ }
69
+ 35% {
70
+ opacity: 0.5;
71
+ transform: translate(var(--orb-rx), var(--orb-ry));
72
+ }
73
+ 60% {
74
+ opacity: 0.12;
75
+ transform: translate(var(--orb-rx), var(--orb-ry));
76
+ }
77
+ }
78
+
79
+ .ring[data-variant="C4"] .ringDot {
80
+ animation: orb-ring-stagger 1.6s ease-in-out infinite both;
81
+ }
82
+
83
+ @keyframes orb-ring-stagger {
84
+ 0%, 100% {
85
+ opacity: 1;
86
+ transform: translate(var(--orb-rx), var(--orb-ry));
87
+ }
88
+ 50% {
89
+ opacity: 0.15;
90
+ transform: translate(var(--orb-rx), var(--orb-ry));
91
+ }
92
+ }
93
+
94
+ .ring[data-variant="C5"] .ringDot {
95
+ animation: orb-ring-comet 1.8s ease-in-out infinite both;
96
+ }
97
+
98
+
99
+ :global([data-orb-paused]) .ring,
100
+ :global([data-orb-paused]) .ringDot {
101
+ animation-play-state: paused;
102
+ }
103
+
104
+ @media (prefers-reduced-motion: reduce) {
105
+ .ring,
106
+ .ringDot {
107
+ animation: none !important;
108
+ }
109
+
110
+ .ringDot {
111
+ opacity: 0.7 !important;
112
+ }
113
+ }