@social.io/catalog 1.3.0 → 2.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 (130) hide show
  1. package/.smartconfig.json +51 -0
  2. package/dist_bundle/bundle.js +5455 -3248
  3. package/dist_ts_web/00_commitinfo_data.js +3 -3
  4. package/dist_ts_web/elements/index.d.ts +21 -3
  5. package/dist_ts_web/elements/index.js +28 -7
  6. package/dist_ts_web/elements/sio-ai-card.d.ts +39 -0
  7. package/dist_ts_web/elements/sio-ai-card.js +229 -0
  8. package/dist_ts_web/elements/sio-avatar.d.ts +46 -0
  9. package/dist_ts_web/elements/sio-avatar.js +379 -0
  10. package/dist_ts_web/elements/sio-bucket-header.d.ts +36 -0
  11. package/dist_ts_web/elements/sio-bucket-header.js +197 -0
  12. package/dist_ts_web/elements/sio-button.d.ts +7 -3
  13. package/dist_ts_web/elements/sio-button.js +253 -152
  14. package/dist_ts_web/elements/sio-channel-rail.d.ts +38 -0
  15. package/dist_ts_web/elements/sio-channel-rail.js +288 -0
  16. package/dist_ts_web/elements/sio-chip.d.ts +25 -0
  17. package/dist_ts_web/elements/sio-chip.js +188 -0
  18. package/dist_ts_web/elements/sio-cmdk.d.ts +59 -0
  19. package/dist_ts_web/elements/sio-cmdk.js +471 -0
  20. package/dist_ts_web/elements/sio-combox.d.ts +55 -1
  21. package/dist_ts_web/elements/sio-combox.js +458 -261
  22. package/dist_ts_web/elements/sio-composer.d.ts +48 -0
  23. package/dist_ts_web/elements/sio-composer.js +357 -0
  24. package/dist_ts_web/elements/sio-conversation-selector.d.ts +41 -1
  25. package/dist_ts_web/elements/sio-conversation-selector.js +426 -238
  26. package/dist_ts_web/elements/sio-conversation-view.d.ts +34 -6
  27. package/dist_ts_web/elements/sio-conversation-view.js +738 -564
  28. package/dist_ts_web/elements/sio-dropdown-menu.d.ts +6 -6
  29. package/dist_ts_web/elements/sio-dropdown-menu.js +223 -169
  30. package/dist_ts_web/elements/sio-fab.d.ts +23 -5
  31. package/dist_ts_web/elements/sio-fab.js +259 -235
  32. package/dist_ts_web/elements/sio-glass-toolbar.d.ts +22 -0
  33. package/dist_ts_web/elements/sio-glass-toolbar.js +178 -0
  34. package/dist_ts_web/elements/sio-icon.d.ts +1 -0
  35. package/dist_ts_web/elements/sio-icon.js +254 -94
  36. package/dist_ts_web/elements/sio-image-lightbox.d.ts +3 -4
  37. package/dist_ts_web/elements/sio-image-lightbox.js +282 -191
  38. package/dist_ts_web/elements/sio-inset-list.d.ts +28 -0
  39. package/dist_ts_web/elements/sio-inset-list.js +191 -0
  40. package/dist_ts_web/elements/sio-kbd.d.ts +16 -0
  41. package/dist_ts_web/elements/sio-kbd.js +117 -0
  42. package/dist_ts_web/elements/sio-list-row.d.ts +55 -0
  43. package/dist_ts_web/elements/sio-list-row.js +462 -0
  44. package/dist_ts_web/elements/sio-logo-render.d.ts +42 -0
  45. package/dist_ts_web/elements/sio-logo-render.js +306 -0
  46. package/dist_ts_web/elements/sio-logo.d.ts +64 -0
  47. package/dist_ts_web/elements/sio-logo.js +393 -0
  48. package/dist_ts_web/elements/sio-message-input.d.ts +25 -4
  49. package/dist_ts_web/elements/sio-message-input.js +231 -226
  50. package/dist_ts_web/elements/sio-paper-thumb.d.ts +24 -0
  51. package/dist_ts_web/elements/sio-paper-thumb.js +293 -0
  52. package/dist_ts_web/elements/sio-pdf-viewer.d.ts +2 -0
  53. package/dist_ts_web/elements/sio-pdf-viewer.js +344 -244
  54. package/dist_ts_web/elements/sio-segmented.d.ts +32 -0
  55. package/dist_ts_web/elements/sio-segmented.js +217 -0
  56. package/dist_ts_web/elements/sio-step-timeline.d.ts +28 -0
  57. package/dist_ts_web/elements/sio-step-timeline.js +222 -0
  58. package/dist_ts_web/elements/sio-tab-bar.d.ts +36 -0
  59. package/dist_ts_web/elements/sio-tab-bar.js +232 -0
  60. package/dist_ts_web/elements/sio-unread-badge.d.ts +25 -0
  61. package/dist_ts_web/elements/sio-unread-badge.js +203 -0
  62. package/dist_ts_web/elements/sio-waveform.d.ts +34 -0
  63. package/dist_ts_web/elements/sio-waveform.js +304 -0
  64. package/dist_ts_web/elements/tokens.d.ts +70 -0
  65. package/dist_ts_web/elements/tokens.js +277 -0
  66. package/dist_ts_web/pages/firstpage.d.ts +5 -0
  67. package/dist_ts_web/pages/firstpage.js +21 -2
  68. package/dist_ts_web/pages/mainpage.d.ts +6 -0
  69. package/dist_ts_web/pages/mainpage.js +111 -39
  70. package/{license → license.md} +3 -1
  71. package/package.json +23 -29
  72. package/readme.hints.md +82 -0
  73. package/readme.md +113 -146
  74. package/ts_web/00_commitinfo_data.ts +2 -2
  75. package/ts_web/elements/index.ts +31 -7
  76. package/ts_web/elements/sio-ai-card.ts +175 -0
  77. package/ts_web/elements/sio-avatar.ts +295 -0
  78. package/ts_web/elements/sio-bucket-header.ts +157 -0
  79. package/ts_web/elements/sio-button.ts +149 -127
  80. package/ts_web/elements/sio-channel-rail.ts +256 -0
  81. package/ts_web/elements/sio-chip.ts +134 -0
  82. package/ts_web/elements/sio-cmdk.ts +461 -0
  83. package/ts_web/elements/sio-combox.ts +327 -204
  84. package/ts_web/elements/sio-composer.ts +306 -0
  85. package/ts_web/elements/sio-conversation-selector.ts +350 -228
  86. package/ts_web/elements/sio-conversation-view.ts +573 -457
  87. package/ts_web/elements/sio-dropdown-menu.ts +112 -130
  88. package/ts_web/elements/sio-fab.ts +192 -227
  89. package/ts_web/elements/sio-glass-toolbar.ts +140 -0
  90. package/ts_web/elements/sio-icon.ts +147 -25
  91. package/ts_web/elements/sio-image-lightbox.ts +62 -64
  92. package/ts_web/elements/sio-inset-list.ts +140 -0
  93. package/ts_web/elements/sio-kbd.ts +80 -0
  94. package/ts_web/elements/sio-list-row.ts +393 -0
  95. package/ts_web/elements/sio-logo-render.ts +367 -0
  96. package/ts_web/elements/sio-logo.ts +311 -0
  97. package/ts_web/elements/sio-message-input.ts +142 -221
  98. package/ts_web/elements/sio-paper-thumb.ts +244 -0
  99. package/ts_web/elements/sio-pdf-viewer.ts +58 -60
  100. package/ts_web/elements/sio-segmented.ts +183 -0
  101. package/ts_web/elements/sio-step-timeline.ts +179 -0
  102. package/ts_web/elements/sio-tab-bar.ts +195 -0
  103. package/ts_web/elements/sio-unread-badge.ts +147 -0
  104. package/ts_web/elements/sio-waveform.ts +239 -0
  105. package/ts_web/elements/tokens.ts +297 -0
  106. package/ts_web/pages/firstpage.ts +20 -1
  107. package/ts_web/pages/mainpage.ts +111 -39
  108. package/dist_bundle/bundle.js.map +0 -7
  109. package/dist_ts_web/elements/00colors.d.ts +0 -108
  110. package/dist_ts_web/elements/00colors.js +0 -137
  111. package/dist_ts_web/elements/00fonts.d.ts +0 -60
  112. package/dist_ts_web/elements/00fonts.js +0 -129
  113. package/dist_ts_web/elements/00tokens.d.ts +0 -123
  114. package/dist_ts_web/elements/00tokens.js +0 -176
  115. package/dist_ts_web/elements/sio-conversation-list.d.ts +0 -20
  116. package/dist_ts_web/elements/sio-conversation-list.js +0 -495
  117. package/dist_ts_web/elements/sio-recorder.d.ts +0 -60
  118. package/dist_ts_web/elements/sio-recorder.js +0 -173
  119. package/dist_ts_web/elements/sio-subwidget-conversations.d.ts +0 -16
  120. package/dist_ts_web/elements/sio-subwidget-conversations.js +0 -350
  121. package/dist_ts_web/elements/sio-subwidget-onboardme.d.ts +0 -7
  122. package/dist_ts_web/elements/sio-subwidget-onboardme.js +0 -243
  123. package/dist_watch/bundle.js +0 -81694
  124. package/dist_watch/bundle.js.map +0 -7
  125. package/dist_watch/index.html +0 -25
  126. package/npmextra.json +0 -19
  127. package/ts_web/elements/00colors.ts +0 -164
  128. package/ts_web/elements/00fonts.ts +0 -145
  129. package/ts_web/elements/00tokens.ts +0 -197
  130. package/ts_web/elements/sio-recorder.ts +0 -213
@@ -0,0 +1,367 @@
1
+ export type TSioLogoAnimationPhase = 'in' | 'out' | 'wave' | 'loading';
2
+
3
+ export interface ISioLogoRenderOptions {
4
+ size: number;
5
+ dpr: number;
6
+ gap: number;
7
+ radius: number;
8
+ square: boolean;
9
+ background: string;
10
+ mono: string;
11
+ light: boolean;
12
+ }
13
+
14
+ interface ISioLogoPoint {
15
+ x: number;
16
+ y: number;
17
+ }
18
+
19
+ interface ISioLogoFacet {
20
+ fill: string;
21
+ points: [[number, number], [number, number], [number, number]];
22
+ }
23
+
24
+ const facets: ISioLogoFacet[] = [
25
+ { fill: '#4AA0F5', points: [[250, 75], [150, 133.333], [250, 191.667]] },
26
+ { fill: '#0A5AC0', points: [[250, 75], [350, 133.333], [250, 191.667]] },
27
+ { fill: '#DBEBFF', points: [[150, 133.333], [250, 191.667], [150, 250]] },
28
+ { fill: '#A9CDF7', points: [[250, 191.667], [150, 250], [250, 308.333]] },
29
+ { fill: '#1E80E6', points: [[250, 191.667], [350, 250], [250, 308.333]] },
30
+ { fill: '#1466C6', points: [[350, 250], [250, 308.333], [350, 366.667]] },
31
+ { fill: '#74B4F7', points: [[250, 308.333], [350, 366.667], [250, 425]] },
32
+ { fill: '#1E80E6', points: [[250, 308.333], [150, 366.667], [250, 425]] },
33
+ ];
34
+
35
+ const lightFills = [
36
+ '#4AA0F5', '#0A5AC0', '#8FBEF5', '#77ACEE',
37
+ '#1E80E6', '#1466C6', '#5FA6F2', '#1E80E6',
38
+ ];
39
+ const monoOpacities = [0.78, 0.64, 0.96, 0.86, 0.72, 0.66, 0.82, 0.72];
40
+ const tightBounds = { x: 146, y: 72, width: 208, height: 356 };
41
+ const centerX = 250;
42
+ const centerY = 250;
43
+
44
+ export const sioLogoAspect = tightBounds.width / tightBounds.height;
45
+
46
+ const centroids = facets.map((facet) => [
47
+ (facet.points[0][0] + facet.points[1][0] + facet.points[2][0]) / 3,
48
+ (facet.points[0][1] + facet.points[1][1] + facet.points[2][1]) / 3,
49
+ ]);
50
+
51
+ const clamp = (value: number, minimum: number, maximum: number): number =>
52
+ Math.max(minimum, Math.min(maximum, value));
53
+
54
+ const finiteAtLeast = (value: number, minimum: number, fallback: number): number =>
55
+ Number.isFinite(value) ? Math.max(minimum, value) : fallback;
56
+
57
+ const normalizeOptions = (options: ISioLogoRenderOptions): ISioLogoRenderOptions => ({
58
+ ...options,
59
+ size: finiteAtLeast(options.size, 1, 40),
60
+ dpr: clamp(finiteAtLeast(options.dpr, 0.5, 1), 0.5, 4),
61
+ gap: finiteAtLeast(options.gap, 0, 2),
62
+ radius: finiteAtLeast(options.radius, 0, 16),
63
+ });
64
+
65
+ const inset = (
66
+ points: ISioLogoFacet['points'],
67
+ gap: number,
68
+ ): ISioLogoPoint[] => {
69
+ if (gap <= 0) return points.map(([x, y]) => ({ x, y }));
70
+ const centroidX = (points[0][0] + points[1][0] + points[2][0]) / 3;
71
+ const centroidY = (points[0][1] + points[1][1] + points[2][1]) / 3;
72
+ const lines: Array<ISioLogoPoint & { dx: number; dy: number }> = [];
73
+
74
+ for (let index = 0; index < points.length; index += 1) {
75
+ const start = points[index];
76
+ const end = points[(index + 1) % points.length];
77
+ let deltaX = end[0] - start[0];
78
+ let deltaY = end[1] - start[1];
79
+ const length = Math.hypot(deltaX, deltaY);
80
+ deltaX /= length;
81
+ deltaY /= length;
82
+ let normalX = -deltaY;
83
+ let normalY = deltaX;
84
+ const middleX = (start[0] + end[0]) / 2;
85
+ const middleY = (start[1] + end[1]) / 2;
86
+ if ((centroidX - middleX) * normalX + (centroidY - middleY) * normalY < 0) {
87
+ normalX = -normalX;
88
+ normalY = -normalY;
89
+ }
90
+ lines.push({
91
+ x: start[0] + normalX * gap,
92
+ y: start[1] + normalY * gap,
93
+ dx: deltaX,
94
+ dy: deltaY,
95
+ });
96
+ }
97
+
98
+ return lines.map((line, index) => {
99
+ const previous = lines[(index - 1 + lines.length) % lines.length];
100
+ const denominator = previous.dx * line.dy - previous.dy * line.dx;
101
+ const distance =
102
+ ((line.x - previous.x) * line.dy - (line.y - previous.y) * line.dx) /
103
+ denominator;
104
+ return {
105
+ x: previous.x + previous.dx * distance,
106
+ y: previous.y + previous.dy * distance,
107
+ };
108
+ });
109
+ };
110
+
111
+ const createFacetPath = (
112
+ points: ISioLogoFacet['points'],
113
+ gap: number,
114
+ radius: number,
115
+ ): Path2D => {
116
+ const insetPoints = inset(points, gap);
117
+ const path = new Path2D();
118
+ if (radius <= 0) {
119
+ path.moveTo(insetPoints[0].x, insetPoints[0].y);
120
+ for (let index = 1; index < insetPoints.length; index += 1) {
121
+ path.lineTo(insetPoints[index].x, insetPoints[index].y);
122
+ }
123
+ path.closePath();
124
+ return path;
125
+ }
126
+
127
+ for (let index = 0; index < insetPoints.length; index += 1) {
128
+ const vertex = insetPoints[index];
129
+ const previous = insetPoints[(index - 1 + insetPoints.length) % insetPoints.length];
130
+ const next = insetPoints[(index + 1) % insetPoints.length];
131
+ const previousDistance = Math.hypot(previous.x - vertex.x, previous.y - vertex.y);
132
+ const nextDistance = Math.hypot(next.x - vertex.x, next.y - vertex.y);
133
+ const previousRadius = Math.min(radius, previousDistance * 0.5);
134
+ const nextRadius = Math.min(radius, nextDistance * 0.5);
135
+ const entry = {
136
+ x: vertex.x + ((previous.x - vertex.x) / previousDistance) * previousRadius,
137
+ y: vertex.y + ((previous.y - vertex.y) / previousDistance) * previousRadius,
138
+ };
139
+ const exit = {
140
+ x: vertex.x + ((next.x - vertex.x) / nextDistance) * nextRadius,
141
+ y: vertex.y + ((next.y - vertex.y) / nextDistance) * nextRadius,
142
+ };
143
+ if (index === 0) path.moveTo(entry.x, entry.y);
144
+ else path.lineTo(entry.x, entry.y);
145
+ path.quadraticCurveTo(vertex.x, vertex.y, exit.x, exit.y);
146
+ }
147
+ path.closePath();
148
+ return path;
149
+ };
150
+
151
+ const easeOutCubic = (progress: number): number => 1 - Math.pow(1 - progress, 3);
152
+ const easeOutBack = (progress: number): number => {
153
+ const coefficient = 1.70158;
154
+ return 1 + (coefficient + 1) * Math.pow(progress - 1, 3) + coefficient * Math.pow(progress - 1, 2);
155
+ };
156
+ const easeInCubic = (progress: number): number => progress * progress * progress;
157
+ const easeInBack = (progress: number): number => {
158
+ const coefficient = 1.70158;
159
+ return (coefficient + 1) * progress * progress * progress - coefficient * progress * progress;
160
+ };
161
+
162
+ export class SioLogoRenderer {
163
+ private options: ISioLogoRenderOptions;
164
+ private frameHandle: number | null = null;
165
+ private phase: TSioLogoAnimationPhase | null = null;
166
+ private startedAt = 0;
167
+ private stagger = 70;
168
+ private duration = 560;
169
+ private totalDuration = 0;
170
+ private backingWidth = 0;
171
+ private backingHeight = 0;
172
+ private scale = 1;
173
+ private offsetX = 0;
174
+ private offsetY = 0;
175
+ private paths: Path2D[] = [];
176
+ private loadingPaths: Path2D[] = [];
177
+ private fills: string[] = [];
178
+ private opacities: number[] = [];
179
+
180
+ constructor(
181
+ private readonly context: CanvasRenderingContext2D,
182
+ options: ISioLogoRenderOptions,
183
+ ) {
184
+ this.options = normalizeOptions(options);
185
+ this.rebuild();
186
+ }
187
+
188
+ public setOptions(options: ISioLogoRenderOptions): void {
189
+ this.options = normalizeOptions(options);
190
+ this.rebuild();
191
+ if (!this.phase) this.drawStatic();
192
+ }
193
+
194
+ public drawStatic(): void {
195
+ this.clearCanvas();
196
+ for (let index = 0; index < facets.length; index += 1) {
197
+ this.paint(index, 0, 0, 0, 1, this.opacities[index], this.paths[index]);
198
+ }
199
+ this.context.globalAlpha = 1;
200
+ }
201
+
202
+ public clear(): void {
203
+ this.stop();
204
+ this.clearCanvas();
205
+ }
206
+
207
+ public start(phase: TSioLogoAnimationPhase): void {
208
+ this.stop();
209
+ this.phase = phase;
210
+ this.startedAt = performance.now();
211
+ this.stagger = phase === 'wave' ? 55 : 70;
212
+ this.duration = phase === 'wave' ? 420 : 560;
213
+ this.totalDuration = this.stagger * (facets.length - 1) + this.duration;
214
+
215
+ const drawFrame = (time: number): void => {
216
+ if (!this.phase) return;
217
+ const elapsed = time - this.startedAt;
218
+ this.drawAnimationFrame(elapsed);
219
+ if (this.phase !== 'loading' && elapsed >= this.totalDuration) {
220
+ this.settle();
221
+ return;
222
+ }
223
+ this.frameHandle = requestAnimationFrame(drawFrame);
224
+ };
225
+ this.frameHandle = requestAnimationFrame(drawFrame);
226
+ }
227
+
228
+ public stop(): void {
229
+ if (this.frameHandle !== null) {
230
+ cancelAnimationFrame(this.frameHandle);
231
+ this.frameHandle = null;
232
+ }
233
+ this.phase = null;
234
+ }
235
+
236
+ private rebuild(): void {
237
+ const { size, dpr, square, light, mono, gap, radius } = this.options;
238
+ this.backingWidth = Math.round((square ? size : size * sioLogoAspect) * dpr);
239
+ this.backingHeight = Math.round(size * dpr);
240
+ this.context.canvas.width = this.backingWidth;
241
+ this.context.canvas.height = this.backingHeight;
242
+ if (square) {
243
+ this.scale = this.backingWidth / 500;
244
+ this.offsetX = 0;
245
+ this.offsetY = 0;
246
+ } else {
247
+ this.scale = this.backingHeight / tightBounds.height;
248
+ this.offsetX =
249
+ -tightBounds.x * this.scale +
250
+ (this.backingWidth - tightBounds.width * this.scale) / 2;
251
+ this.offsetY = -tightBounds.y * this.scale;
252
+ }
253
+ this.paths = facets.map((facet) => createFacetPath(facet.points, gap, radius));
254
+ this.loadingPaths = facets.map((facet) => createFacetPath(facet.points, gap + 3, 90));
255
+ this.fills = facets.map((facet, index) => mono || (light ? lightFills[index] : facet.fill));
256
+ this.opacities = facets.map((_, index) => (mono ? monoOpacities[index] : 1));
257
+ }
258
+
259
+ private clearCanvas(): void {
260
+ this.context.setTransform(1, 0, 0, 1, 0, 0);
261
+ this.context.clearRect(0, 0, this.backingWidth, this.backingHeight);
262
+ const { background } = this.options;
263
+ if (background && background !== 'transparent' && background !== 'none') {
264
+ this.context.fillStyle = background;
265
+ this.context.fillRect(0, 0, this.backingWidth, this.backingHeight);
266
+ }
267
+ }
268
+
269
+ private paint(
270
+ index: number,
271
+ translateX: number,
272
+ translateY: number,
273
+ rotation: number,
274
+ scale: number,
275
+ opacity: number,
276
+ path: Path2D,
277
+ ): void {
278
+ const [centroidX, centroidY] = centroids[index];
279
+ this.context.setTransform(this.scale, 0, 0, this.scale, this.offsetX, this.offsetY);
280
+ this.context.translate(translateX, translateY);
281
+ this.context.translate(centroidX, centroidY);
282
+ this.context.rotate((rotation * Math.PI) / 180);
283
+ this.context.scale(scale, scale);
284
+ this.context.translate(-centroidX, -centroidY);
285
+ this.context.globalAlpha = opacity;
286
+ this.context.fillStyle = this.fills[index];
287
+ this.context.fill(path);
288
+ }
289
+
290
+ private drawAnimationFrame(elapsed: number): void {
291
+ this.clearCanvas();
292
+ const activePhase = this.phase;
293
+ for (let index = 0; index < facets.length; index += 1) {
294
+ const [centroidX, centroidY] = centroids[index];
295
+ let translateX = 0;
296
+ let translateY = 0;
297
+ let rotation = 0;
298
+ let scale = 1;
299
+ let opacity = this.opacities[index];
300
+ let path = this.paths[index];
301
+
302
+ if (activePhase === 'in') {
303
+ const progress = clamp((elapsed - index * this.stagger) / this.duration, 0, 1);
304
+ const eased = easeOutCubic(progress);
305
+ translateX = (centerX - centroidX) * (1 - eased);
306
+ translateY = (centerY - centroidY) * (1 - eased);
307
+ rotation = (1 - eased) * -16;
308
+ scale = easeOutBack(progress);
309
+ opacity = clamp(progress * 1.6, 0, 1) * this.opacities[index];
310
+ } else if (activePhase === 'out') {
311
+ const reverseIndex = facets.length - 1 - index;
312
+ const progress = clamp((elapsed - reverseIndex * this.stagger) / this.duration, 0, 1);
313
+ const eased = easeInCubic(progress);
314
+ translateX = (centerX - centroidX) * eased;
315
+ translateY = (centerY - centroidY) * eased;
316
+ rotation = progress * 16;
317
+ scale = Math.max(0, 1 - easeInBack(progress));
318
+ opacity = clamp(1 - progress * 1.3, 0, 1) * this.opacities[index];
319
+ } else if (activePhase === 'wave') {
320
+ const progress = clamp((elapsed - index * this.stagger) / this.duration, 0, 1);
321
+ const pulse = Math.sin(progress * Math.PI);
322
+ const radialX = centroidX - centerX;
323
+ const radialY = centroidY - centerY;
324
+ const radialLength = Math.hypot(radialX, radialY) || 1;
325
+ translateX = (radialX / radialLength) * 16 * pulse;
326
+ translateY = (radialY / radialLength) * 16 * pulse;
327
+ scale = 1 - 0.08 * pulse;
328
+ } else if (activePhase === 'loading') {
329
+ const growth = clamp(elapsed / 650, 0, 1);
330
+ const easedGrowth = growth < 0.5
331
+ ? 2 * growth * growth
332
+ : 1 - Math.pow(-2 * growth + 2, 2) / 2;
333
+ const looping = elapsed >= 650;
334
+ const spin = (elapsed / 1500) * 2 * Math.PI;
335
+ const angle = -Math.PI / 2 + index * Math.PI / 4 + spin;
336
+ const destinationX = centerX + 120 * Math.cos(angle);
337
+ const destinationY = centerY + 120 * Math.sin(angle);
338
+ const brightness = 1 - index / facets.length;
339
+ translateX = (destinationX - centroidX) * easedGrowth;
340
+ translateY = (destinationY - centroidY) * easedGrowth;
341
+ scale =
342
+ (1 + (0.62 - 1) * easedGrowth) *
343
+ (looping ? 0.72 + 0.28 * brightness : 1);
344
+ opacity = (looping ? 0.28 + 0.72 * brightness : 1) * this.opacities[index];
345
+ path = looping
346
+ ? this.loadingPaths[index]
347
+ : createFacetPath(
348
+ facets[index].points,
349
+ this.options.gap + 3 * easedGrowth,
350
+ this.options.radius + (90 - this.options.radius) * easedGrowth,
351
+ );
352
+ }
353
+
354
+ this.paint(index, translateX, translateY, rotation, scale, opacity, path);
355
+ }
356
+ this.context.globalAlpha = 1;
357
+ }
358
+
359
+ private settle(): void {
360
+ const finishedPhase = this.phase;
361
+ this.frameHandle = null;
362
+ this.phase = null;
363
+ if (finishedPhase === 'out') this.clearCanvas();
364
+ else this.drawStatic();
365
+ this.context.globalAlpha = 1;
366
+ }
367
+ }
@@ -0,0 +1,311 @@
1
+ import {
2
+ DeesElement,
3
+ css,
4
+ customElement,
5
+ html,
6
+ property,
7
+ type TemplateResult,
8
+ } from '@design.estate/dees-element';
9
+ import { SioLogoRenderer, sioLogoAspect } from './sio-logo-render.js';
10
+ import { sioElementStyles } from './tokens.js';
11
+
12
+ export type TSioLogoTheme = 'auto' | 'light' | 'dark';
13
+
14
+ declare global {
15
+ interface HTMLElementTagNameMap {
16
+ 'sio-logo': SioLogo;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * The social.io faceted S brand mark from the v8 design prototype.
22
+ *
23
+ * The canvas renderer preserves the exact eight-facet geometry and supports
24
+ * entrance, hover-wave, dismiss, and loading animations. Motion is disabled
25
+ * when the visitor requests reduced motion.
26
+ */
27
+ @customElement('sio-logo')
28
+ export class SioLogo extends DeesElement {
29
+ public static demo = () => html`
30
+ <div style="display:flex;align-items:center;gap:32px;padding:24px;background:#101012;">
31
+ <sio-logo size="120" theme="dark"></sio-logo>
32
+ <sio-logo size="120" square background="#556270" radius="22" theme="dark"></sio-logo>
33
+ <sio-logo size="120" loading theme="dark"></sio-logo>
34
+ <sio-logo size="120" mono="#ffffff" no-entrance theme="dark"></sio-logo>
35
+ </div>
36
+ `;
37
+
38
+ /** rendered height in CSS pixels; tight-crop width follows the brand aspect */
39
+ @property({ type: Number })
40
+ public accessor size = 40;
41
+
42
+ @property({ type: String })
43
+ public accessor background = 'transparent';
44
+
45
+ /** inward inset per facet, in 500px design-grid units */
46
+ @property({ type: Number })
47
+ public accessor gap = 2;
48
+
49
+ /** facet corner radius, in 500px design-grid units */
50
+ @property({ type: Number })
51
+ public accessor radius = 16;
52
+
53
+ /** render on a square tile instead of the tight S crop */
54
+ @property({ type: Boolean })
55
+ public accessor square = false;
56
+
57
+ /** collapse the palette to one color while retaining facet depth */
58
+ @property({ type: String })
59
+ public accessor mono = '';
60
+
61
+ /** backdrop palette; auto follows the nearest opaque surface */
62
+ @property({ type: String })
63
+ public accessor theme: TSioLogoTheme = 'auto';
64
+
65
+ @property({ type: Boolean, reflect: true })
66
+ public accessor loading = false;
67
+
68
+ @property({ type: Boolean, attribute: 'no-entrance' })
69
+ public accessor noEntrance = false;
70
+
71
+ @property({ type: Boolean, attribute: 'no-hover' })
72
+ public accessor noHover = false;
73
+
74
+ public static styles = [
75
+ ...sioElementStyles,
76
+ css`
77
+ :host {
78
+ display: inline-flex;
79
+ flex: none;
80
+ line-height: 0;
81
+ }
82
+ canvas {
83
+ display: block;
84
+ }
85
+ `,
86
+ ];
87
+
88
+ private renderer: SioLogoRenderer | null = null;
89
+ private themeObserver: MutationObserver | null = null;
90
+ private colorSchemeMedia: MediaQueryList | null = null;
91
+ private reducedMotionMedia: MediaQueryList | null = null;
92
+ private lastDevicePixelRatio = 0;
93
+ private lifecycleGeneration = 0;
94
+
95
+ private readonly handlePointerEnter = (): void => {
96
+ if (!this.noHover && !this.loading && !this.prefersReducedMotion()) this.wave();
97
+ };
98
+
99
+ private readonly handleAmbientChange = (): void => {
100
+ this.syncRenderer(this.loading ? 'loading' : null);
101
+ };
102
+
103
+ private readonly handleReducedMotionChange = (): void => {
104
+ this.syncRenderer(this.loading ? 'loading' : null);
105
+ };
106
+
107
+ private readonly handleResize = (): void => {
108
+ const currentDevicePixelRatio = window.devicePixelRatio || 1;
109
+ if (currentDevicePixelRatio === this.lastDevicePixelRatio) return;
110
+ this.syncRenderer(this.loading ? 'loading' : null);
111
+ };
112
+
113
+ public async connectedCallback(): Promise<void> {
114
+ const generation = ++this.lifecycleGeneration;
115
+ const connection = super.connectedCallback();
116
+ this.ensureAccessibleHost();
117
+ this.addEventListener('mouseenter', this.handlePointerEnter);
118
+ window.addEventListener('resize', this.handleResize);
119
+ this.observeReducedMotion();
120
+ await connection;
121
+ if (!this.isConnected || generation !== this.lifecycleGeneration) return;
122
+ if (this.hasUpdated) {
123
+ this.observeAmbientTheme();
124
+ this.syncRenderer(this.loading ? 'loading' : null);
125
+ }
126
+ }
127
+
128
+ public async disconnectedCallback(): Promise<void> {
129
+ const generation = ++this.lifecycleGeneration;
130
+ this.cleanupOwnedResources();
131
+ await super.disconnectedCallback();
132
+ if (!this.isConnected && generation === this.lifecycleGeneration) {
133
+ this.cleanupOwnedResources();
134
+ }
135
+ }
136
+
137
+ private cleanupOwnedResources(): void {
138
+ this.removeEventListener('mouseenter', this.handlePointerEnter);
139
+ window.removeEventListener('resize', this.handleResize);
140
+ this.stopAmbientThemeObservation();
141
+ this.stopReducedMotionObservation();
142
+ this.renderer?.stop();
143
+ }
144
+
145
+ public render(): TemplateResult {
146
+ const height = this.normalizedSize;
147
+ const width = this.square ? height : Math.round(height * sioLogoAspect);
148
+ return html`<canvas aria-hidden="true" style="width:${width}px;height:${height}px"></canvas>`;
149
+ }
150
+
151
+ public firstUpdated(changedProperties: Map<PropertyKey, unknown>): void {
152
+ super.firstUpdated(changedProperties);
153
+ const canvas = this.shadowRoot?.querySelector<HTMLCanvasElement>('canvas');
154
+ const context = canvas?.getContext('2d');
155
+ if (!context) return;
156
+ this.renderer = new SioLogoRenderer(context, this.getRendererOptions());
157
+ if (!this.isConnected) {
158
+ this.renderer.drawStatic();
159
+ return;
160
+ }
161
+ this.observeAmbientTheme();
162
+ if (this.loading && !this.prefersReducedMotion()) this.renderer.start('loading');
163
+ else if (!this.noEntrance && !this.prefersReducedMotion()) this.renderer.start('in');
164
+ else this.renderer.drawStatic();
165
+ }
166
+
167
+ public updated(changedProperties: Map<PropertyKey, unknown>): void {
168
+ if (!this.isConnected) {
169
+ this.stopAmbientThemeObservation();
170
+ this.renderer?.stop();
171
+ return;
172
+ }
173
+ const redrawProperties: PropertyKey[] = [
174
+ 'size',
175
+ 'gap',
176
+ 'radius',
177
+ 'square',
178
+ 'background',
179
+ 'mono',
180
+ 'theme',
181
+ ];
182
+ if (redrawProperties.some((propertyName) => changedProperties.has(propertyName))) {
183
+ this.syncRenderer(this.loading ? 'loading' : null);
184
+ }
185
+ if (changedProperties.has('theme')) {
186
+ this.stopAmbientThemeObservation();
187
+ this.observeAmbientTheme();
188
+ }
189
+ if (changedProperties.has('loading')) {
190
+ this.syncRenderer(this.loading ? 'loading' : null);
191
+ }
192
+ }
193
+
194
+ protected themeChanged(): void {
195
+ this.syncRenderer(this.loading ? 'loading' : null);
196
+ }
197
+
198
+ public play(): void {
199
+ if (!this.renderer || !this.isConnected) return;
200
+ if (this.prefersReducedMotion()) this.renderer.drawStatic();
201
+ else this.renderer.start('in');
202
+ }
203
+
204
+ public wave(): void {
205
+ if (!this.renderer || !this.isConnected) return;
206
+ if (this.prefersReducedMotion()) this.renderer.drawStatic();
207
+ else this.renderer.start('wave');
208
+ }
209
+
210
+ public dismiss(): void {
211
+ if (!this.renderer || !this.isConnected) return;
212
+ if (this.prefersReducedMotion()) this.renderer.clear();
213
+ else this.renderer.start('out');
214
+ }
215
+
216
+ private get normalizedSize(): number {
217
+ return Number.isFinite(this.size) ? Math.max(1, this.size) : 40;
218
+ }
219
+
220
+ private getRendererOptions() {
221
+ this.lastDevicePixelRatio = window.devicePixelRatio || 1;
222
+ return {
223
+ size: this.normalizedSize,
224
+ dpr: this.lastDevicePixelRatio,
225
+ gap: Number.isFinite(this.gap) ? Math.max(0, this.gap) : 2,
226
+ radius: Number.isFinite(this.radius) ? Math.max(0, this.radius) : 16,
227
+ square: this.square,
228
+ background: this.background,
229
+ mono: this.mono,
230
+ light: this.isLightBackdrop(),
231
+ };
232
+ }
233
+
234
+ private syncRenderer(animation: 'loading' | null = null): void {
235
+ if (!this.renderer) return;
236
+ this.renderer.stop();
237
+ this.renderer.setOptions(this.getRendererOptions());
238
+ if (animation && this.isConnected && !this.prefersReducedMotion()) {
239
+ this.renderer.start(animation);
240
+ }
241
+ else this.renderer.drawStatic();
242
+ }
243
+
244
+ private prefersReducedMotion(): boolean {
245
+ return this.reducedMotionMedia?.matches ??
246
+ window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ??
247
+ false;
248
+ }
249
+
250
+ private isLightBackdrop(): boolean {
251
+ if (this.theme === 'light') return true;
252
+ if (this.theme === 'dark') return false;
253
+
254
+ let element: Element | null = this;
255
+ while (element) {
256
+ const color = getComputedStyle(element).backgroundColor;
257
+ const match = color.match(
258
+ /rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+))?\s*\)/,
259
+ );
260
+ if (match) {
261
+ const alpha = match[4] === undefined ? 1 : Number.parseFloat(match[4]);
262
+ if (alpha > 0.2) {
263
+ const luminance =
264
+ (0.2126 * Number(match[1]) +
265
+ 0.7152 * Number(match[2]) +
266
+ 0.0722 * Number(match[3])) /
267
+ 255;
268
+ return luminance > 0.55;
269
+ }
270
+ }
271
+ const root = element.getRootNode();
272
+ element = element.parentElement || (root instanceof ShadowRoot ? root.host : null);
273
+ }
274
+ return window.matchMedia?.('(prefers-color-scheme: light)').matches ?? false;
275
+ }
276
+
277
+ private observeAmbientTheme(): void {
278
+ if (this.theme !== 'auto' || this.themeObserver) return;
279
+ this.themeObserver = new MutationObserver(this.handleAmbientChange);
280
+ this.themeObserver.observe(document.documentElement, {
281
+ attributes: true,
282
+ attributeFilter: ['data-sio-theme', 'data-theme', 'class', 'style'],
283
+ });
284
+ this.colorSchemeMedia = window.matchMedia?.('(prefers-color-scheme: light)') ?? null;
285
+ this.colorSchemeMedia?.addEventListener('change', this.handleAmbientChange);
286
+ }
287
+
288
+ private stopAmbientThemeObservation(): void {
289
+ this.themeObserver?.disconnect();
290
+ this.themeObserver = null;
291
+ this.colorSchemeMedia?.removeEventListener('change', this.handleAmbientChange);
292
+ this.colorSchemeMedia = null;
293
+ }
294
+
295
+ private observeReducedMotion(): void {
296
+ if (this.reducedMotionMedia) return;
297
+ this.reducedMotionMedia = window.matchMedia?.('(prefers-reduced-motion: reduce)') ?? null;
298
+ this.reducedMotionMedia?.addEventListener('change', this.handleReducedMotionChange);
299
+ }
300
+
301
+ private stopReducedMotionObservation(): void {
302
+ this.reducedMotionMedia?.removeEventListener('change', this.handleReducedMotionChange);
303
+ this.reducedMotionMedia = null;
304
+ }
305
+
306
+ private ensureAccessibleHost(): void {
307
+ if (this.hasAttribute('aria-hidden')) return;
308
+ if (!this.hasAttribute('role')) this.setAttribute('role', 'img');
309
+ if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', 'social.io');
310
+ }
311
+ }