panelui-native 0.81.0 → 0.83.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 (73) hide show
  1. package/lib/module/components/bottom-sheet/index.js +189 -21
  2. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  3. package/lib/module/components/button/index.js +13 -0
  4. package/lib/module/components/button/index.js.map +1 -1
  5. package/lib/module/components/button-group/index.js +3 -0
  6. package/lib/module/components/button-group/index.js.map +1 -1
  7. package/lib/module/components/image-generation/dot-field.js +267 -0
  8. package/lib/module/components/image-generation/dot-field.js.map +1 -0
  9. package/lib/module/components/image-generation/index.js +459 -0
  10. package/lib/module/components/image-generation/index.js.map +1 -0
  11. package/lib/module/components/kpi/index.js +2 -4
  12. package/lib/module/components/kpi/index.js.map +1 -1
  13. package/lib/module/components/menu/index.js +8 -16
  14. package/lib/module/components/menu/index.js.map +1 -1
  15. package/lib/module/components/panelside/index.js +340 -33
  16. package/lib/module/components/panelside/index.js.map +1 -1
  17. package/lib/module/components/select/index.js +127 -23
  18. package/lib/module/components/select/index.js.map +1 -1
  19. package/lib/module/components/slide-button/index.js +610 -0
  20. package/lib/module/components/slide-button/index.js.map +1 -0
  21. package/lib/module/components/slide-button/slide-button-track.js +98 -0
  22. package/lib/module/components/slide-button/slide-button-track.js.map +1 -0
  23. package/lib/module/icons/index.js +553 -1934
  24. package/lib/module/icons/index.js.map +1 -1
  25. package/lib/module/index.js +3 -1
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/native/index.js +24 -0
  28. package/lib/module/native/index.js.map +1 -1
  29. package/lib/module/primitives/portal.js +15 -1
  30. package/lib/module/primitives/portal.js.map +1 -1
  31. package/lib/typescript/src/components/bottom-sheet/index.d.ts +17 -1
  32. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  33. package/lib/typescript/src/components/button/index.d.ts +12 -0
  34. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  35. package/lib/typescript/src/components/button-group/index.d.ts +9 -0
  36. package/lib/typescript/src/components/button-group/index.d.ts.map +1 -1
  37. package/lib/typescript/src/components/image-generation/dot-field.d.ts +96 -0
  38. package/lib/typescript/src/components/image-generation/dot-field.d.ts.map +1 -0
  39. package/lib/typescript/src/components/image-generation/index.d.ts +134 -0
  40. package/lib/typescript/src/components/image-generation/index.d.ts.map +1 -0
  41. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
  42. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  43. package/lib/typescript/src/components/panelside/index.d.ts +194 -10
  44. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  45. package/lib/typescript/src/components/select/index.d.ts +75 -2
  46. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  47. package/lib/typescript/src/components/slide-button/index.d.ts +317 -0
  48. package/lib/typescript/src/components/slide-button/index.d.ts.map +1 -0
  49. package/lib/typescript/src/components/slide-button/slide-button-track.d.ts +70 -0
  50. package/lib/typescript/src/components/slide-button/slide-button-track.d.ts.map +1 -0
  51. package/lib/typescript/src/icons/index.d.ts +136 -189
  52. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  53. package/lib/typescript/src/index.d.ts +4 -2
  54. package/lib/typescript/src/index.d.ts.map +1 -1
  55. package/lib/typescript/src/native/index.d.ts +26 -0
  56. package/lib/typescript/src/native/index.d.ts.map +1 -1
  57. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  58. package/package.json +3 -2
  59. package/src/components/bottom-sheet/index.tsx +223 -20
  60. package/src/components/button/index.tsx +10 -2
  61. package/src/components/button-group/index.tsx +1 -0
  62. package/src/components/image-generation/dot-field.ts +282 -0
  63. package/src/components/image-generation/index.tsx +537 -0
  64. package/src/components/kpi/index.tsx +2 -4
  65. package/src/components/menu/index.tsx +10 -15
  66. package/src/components/panelside/index.tsx +498 -33
  67. package/src/components/select/index.tsx +158 -31
  68. package/src/components/slide-button/index.tsx +681 -0
  69. package/src/components/slide-button/slide-button-track.ts +100 -0
  70. package/src/icons/index.tsx +338 -1327
  71. package/src/index.ts +28 -0
  72. package/src/native/index.ts +45 -0
  73. package/src/primitives/portal.tsx +15 -1
@@ -0,0 +1,282 @@
1
+ /**
2
+ * The dot field's drawing, kept out of the component so it can be checked
3
+ * without a renderer — the same split `progress-button-hold` uses.
4
+ *
5
+ * ## Nothing is built while it is running
6
+ *
7
+ * The field is hundreds of dots and it is on screen for as long as a model
8
+ * takes to answer, on a page that may be showing several at once. So every
9
+ * frame it will ever draw is built when the box is measured, and playing it is
10
+ * handing react-native-svg a string that already exists.
11
+ *
12
+ * Two earlier versions did this the obvious ways and both stalled a page of
13
+ * them on scroll: one rebuilt every dot as a path string on every frame, the
14
+ * other clipped a moving shape of light to a path with hundreds of subpaths in
15
+ * it, which is re-rasterized every time the view is drawn.
16
+ */
17
+
18
+ /** Points between one dot and the next. */
19
+ export const DOT_GAP = 10;
20
+
21
+ /** The dot's radius where the light is not, and where it is strongest. */
22
+ export const DOT_RADIUS = 1;
23
+ export const DOT_LIT_RADIUS = 1.9;
24
+
25
+ /** How visible the resting grid is, under everything. */
26
+ export const DOT_REST_OPACITY = 0.16;
27
+
28
+ /**
29
+ * How visible the lit dots are, together.
30
+ *
31
+ * "Together" is the operative word: they are drawn as two layers fading into
32
+ * one another, and this is what the pair composites to, not what either one
33
+ * carries. {@link crossfadeAlphas} is what holds that true.
34
+ */
35
+ export const LIT_OPACITY = 0.9;
36
+
37
+ /** How many still frames one loop is drawn as. */
38
+ export const FRAMES = 24;
39
+
40
+ /**
41
+ * The most dots the field will draw, whatever box it is given.
42
+ *
43
+ * The grid opens up rather than the count running away: a full-width field on
44
+ * a tablet gets the same picture drawn coarser, which costs the same and looks
45
+ * the same at arm's length.
46
+ */
47
+ const MAX_DOTS = 700;
48
+
49
+ /** How the light moves through the field. */
50
+ export type DotFieldAnimation = 'drift' | 'pulse' | 'scan';
51
+
52
+ /** The gap this box is drawn at, opened up if the grid would be too dense. */
53
+ export function gapFor(width: number, height: number): number {
54
+ if (width <= 0 || height <= 0) return DOT_GAP;
55
+ const cells = (width / DOT_GAP + 1) * (height / DOT_GAP + 1);
56
+ return cells > MAX_DOTS ? DOT_GAP * Math.sqrt(cells / MAX_DOTS) : DOT_GAP;
57
+ }
58
+
59
+ /** A circle as an SVG subpath, which is two half arcs. */
60
+ function circle(x: number, y: number, r: number): string {
61
+ const d = (r * 2).toFixed(2);
62
+ return `M${(x - r).toFixed(2)},${y.toFixed(2)}a${r},${r} 0 1,0 ${d},0a${r},${r} 0 1,0 -${d},0`;
63
+ }
64
+
65
+ /**
66
+ * Every dot's anchor, in reading order.
67
+ *
68
+ * Laid out inside the box by the widest a dot can be drawn, and counted with
69
+ * `floor` rather than `ceil`, so no part of any dot falls outside it.
70
+ *
71
+ * Counted with `ceil` the grid overhangs by up to half a gap on every side.
72
+ * That is invisible while the light stays near the middle and obvious the
73
+ * moment it reaches an edge — which the scanning band does by design, and a
74
+ * short wide box does simply by being short. The dots then draw outside the
75
+ * card, which looks like the animation has escaped it.
76
+ *
77
+ * The row and column are still centred in whatever is left over, because laid
78
+ * out from the origin the field ends with a strip of nothing down one edge.
79
+ */
80
+ function anchors(width: number, height: number): [number, number][] {
81
+ const gap = gapFor(width, height);
82
+ const margin = DOT_LIT_RADIUS;
83
+ const innerWidth = Math.max(0, width - margin * 2);
84
+ const innerHeight = Math.max(0, height - margin * 2);
85
+
86
+ const columns = Math.floor(innerWidth / gap) + 1;
87
+ const rows = Math.floor(innerHeight / gap) + 1;
88
+ const offsetX = margin + (innerWidth - (columns - 1) * gap) / 2;
89
+ const offsetY = margin + (innerHeight - (rows - 1) * gap) / 2;
90
+
91
+ const out: [number, number][] = [];
92
+ for (let row = 0; row < rows; row += 1) {
93
+ for (let column = 0; column < columns; column += 1) {
94
+ out.push([offsetX + column * gap, offsetY + row * gap]);
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /** The resting grid as one path. Drawn once, under the light. */
101
+ export function gridPath(width: number, height: number): string {
102
+ if (width <= 0 || height <= 0) return '';
103
+ let path = '';
104
+ for (const [x, y] of anchors(width, height)) path += circle(x, y, DOT_RADIUS);
105
+ return path;
106
+ }
107
+
108
+ /** How many dots {@link gridPath} would draw for a box. */
109
+ export function dotCount(width: number, height: number): number {
110
+ if (width <= 0 || height <= 0) return 0;
111
+ return anchors(width, height).length;
112
+ }
113
+
114
+ /**
115
+ * How strongly a dot at `distance` from the light is lit.
116
+ *
117
+ * Smoothstep rather than a linear ramp: linear gives the light a hard rim,
118
+ * because the eye finds the discontinuity in the first derivative. This one
119
+ * arrives and leaves at zero slope, so the light has no edge.
120
+ */
121
+ export function influenceAt(distance: number, radius: number): number {
122
+ if (radius <= 0) return 0;
123
+ const proximity = Math.max(0, 1 - distance / radius);
124
+ return proximity * proximity * (3 - 2 * proximity);
125
+ }
126
+
127
+ /**
128
+ * Where the light is at a phase of the loop, and how wide it reaches.
129
+ *
130
+ * `drift` wanders around the middle on a figure-eight: one horizontal pass to
131
+ * two vertical, so the path arrives back where it started and the loop has no
132
+ * seam in it. The amplitudes are small because a light that reaches the corners
133
+ * stops reading as one source.
134
+ *
135
+ * The two frequencies used not to divide into each other, on the reasoning that
136
+ * a path which never closes is one the eye cannot learn. It closes anyway —
137
+ * every animation here restarts at the end of its period — so all that bought
138
+ * was a jump of four normal steps, once a pass. A figure-eight is not a shape
139
+ * anybody follows over four seconds of soft light on a dot grid.
140
+ *
141
+ * `pulse` is a ring leaving the centre, and has expanded past the last dot
142
+ * before it restarts, so it fades out rather than snapping back. `scan` crosses
143
+ * as a band, which is the same maths with the light infinitely tall.
144
+ */
145
+ function lightAt(
146
+ animation: DotFieldAnimation,
147
+ phase: number,
148
+ width: number,
149
+ height: number
150
+ ): { x: number; y: number; radius: number; ring: number } {
151
+ const short = Math.min(width, height);
152
+ const turn = phase * Math.PI * 2;
153
+
154
+ if (animation === 'pulse') {
155
+ const reach = Math.sqrt(width * width + height * height) / 2;
156
+ return { x: width / 2, y: height / 2, radius: short * 0.3, ring: phase * reach };
157
+ }
158
+
159
+ if (animation === 'scan') {
160
+ // A band: infinitely tall, so only the horizontal distance counts.
161
+ //
162
+ // It starts and ends half a band clear of the edges rather than a whole
163
+ // one — far enough to ramp in and out, near enough that some of it is
164
+ // always on the field. Travelling fully off, the loop spends several
165
+ // frames drawing nothing at all, which reads as the component having
166
+ // stopped rather than as a band that has passed.
167
+ const radius = width * 0.28;
168
+ return {
169
+ x: -radius * 0.5 + phase * (width + radius),
170
+ y: Number.NaN,
171
+ radius,
172
+ ring: 0,
173
+ };
174
+ }
175
+
176
+ return {
177
+ x: width / 2 + Math.sin(turn) * width * 0.26,
178
+ y: height / 2 + Math.cos(turn * 2) * height * 0.22,
179
+ radius: short * 0.42,
180
+ ring: 0,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * The lit dots at one phase of the loop, as a path.
186
+ *
187
+ * Only the dots the light actually reaches are in it — the rest are already
188
+ * drawn by the resting grid underneath, so this is a fraction of the field
189
+ * rather than all of it.
190
+ *
191
+ * `points` is the same grid {@link anchors} would build, passed in by a caller
192
+ * that is about to ask for every frame of a loop. Building it here instead cost
193
+ * a fresh array of several hundred pairs per frame, to arrive at the identical
194
+ * grid twenty-four times over.
195
+ */
196
+ export function litPath(
197
+ width: number,
198
+ height: number,
199
+ phase: number,
200
+ animation: DotFieldAnimation = 'drift',
201
+ points?: [number, number][]
202
+ ): string {
203
+ if (width <= 0 || height <= 0) return '';
204
+
205
+ const light = lightAt(animation, phase, width, height);
206
+ let path = '';
207
+
208
+ for (const [x, y] of points ?? anchors(width, height)) {
209
+ const deltaX = x - light.x;
210
+ const deltaY = Number.isNaN(light.y) ? 0 : y - light.y;
211
+ const distance = Math.abs(
212
+ Math.sqrt(deltaX * deltaX + deltaY * deltaY) - light.ring
213
+ );
214
+ const influence = influenceAt(distance, light.radius);
215
+ if (influence <= 0.02) continue;
216
+ path += circle(x, y, DOT_RADIUS + influence * (DOT_LIT_RADIUS - DOT_RADIUS));
217
+ }
218
+
219
+ return path;
220
+ }
221
+
222
+ /**
223
+ * Every frame of the loop, built once.
224
+ *
225
+ * A still frame the light is somewhere legible in is also what a paused or
226
+ * reduced-motion field shows, so there is no separate code path for holding it.
227
+ */
228
+ export function litFrames(
229
+ width: number,
230
+ height: number,
231
+ animation: DotFieldAnimation = 'drift'
232
+ ): string[] {
233
+ const points = width > 0 && height > 0 ? anchors(width, height) : [];
234
+ return Array.from({ length: FRAMES }, (_unused, index) =>
235
+ litPath(width, height, index / FRAMES, animation, points)
236
+ );
237
+ }
238
+
239
+ /** One pass of the loop, in milliseconds, per animation. */
240
+ export const PERIOD: Record<DotFieldAnimation, number> = {
241
+ drift: 4200,
242
+ pulse: 2200,
243
+ scan: 1900,
244
+ };
245
+
246
+ /** Which frame a moment falls on. */
247
+ export function frameAt(time: number, animation: DotFieldAnimation): number {
248
+ 'worklet';
249
+ const phase = (time % PERIOD[animation]) / PERIOD[animation];
250
+ return Math.min(FRAMES - 1, Math.floor(phase * FRAMES));
251
+ }
252
+
253
+ /**
254
+ * Where a moment falls in the loop, as a frame index with its fraction kept.
255
+ *
256
+ * {@link frameAt} rounds this down, and rounding it down is what made the field
257
+ * a flipbook: twenty-four pictures spread over the period, which on the slowest
258
+ * animation is a new one every 175ms however fast the screen refreshes. The
259
+ * fraction is what the two layers cross-fade on, so the light moves at the rate
260
+ * the display can draw rather than the rate the frames were built at.
261
+ */
262
+ export function framePhase(time: number, animation: DotFieldAnimation): number {
263
+ 'worklet';
264
+ return ((time % PERIOD[animation]) / PERIOD[animation]) * FRAMES;
265
+ }
266
+
267
+ /**
268
+ * What the outgoing and incoming layers are worth, a fraction `t` of the way
269
+ * from one frame to the next.
270
+ *
271
+ * Not `[1 - t, t]`. The layers are drawn over one another, so a dot lit in both
272
+ * of them composites to `1 - (1-a)(1-b)`, and two half-strength copies of it
273
+ * come to 0.70 rather than 0.90 — the field dips a fifth in the middle of every
274
+ * step, which six steps a second turns into a flicker. So only the outgoing
275
+ * layer ramps, and the incoming one is solved for: whatever leaves the pair at
276
+ * {@link LIT_OPACITY} the whole way across.
277
+ */
278
+ export function crossfadeAlphas(t: number): [number, number] {
279
+ 'worklet';
280
+ const out = LIT_OPACITY * (1 - t);
281
+ return [out, 1 - (1 - LIT_OPACITY) / (1 - out)];
282
+ }