@social.io/catalog 1.3.0 → 2.0.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 (124) hide show
  1. package/.smartconfig.json +51 -0
  2. package/dist_bundle/bundle.js +5450 -3267
  3. package/dist_ts_web/00_commitinfo_data.js +3 -3
  4. package/dist_ts_web/elements/index.d.ts +20 -3
  5. package/dist_ts_web/elements/index.js +27 -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 +37 -0
  15. package/dist_ts_web/elements/sio-channel-rail.js +275 -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-message-input.d.ts +25 -4
  45. package/dist_ts_web/elements/sio-message-input.js +231 -226
  46. package/dist_ts_web/elements/sio-paper-thumb.d.ts +24 -0
  47. package/dist_ts_web/elements/sio-paper-thumb.js +293 -0
  48. package/dist_ts_web/elements/sio-pdf-viewer.d.ts +2 -0
  49. package/dist_ts_web/elements/sio-pdf-viewer.js +344 -244
  50. package/dist_ts_web/elements/sio-segmented.d.ts +32 -0
  51. package/dist_ts_web/elements/sio-segmented.js +217 -0
  52. package/dist_ts_web/elements/sio-step-timeline.d.ts +28 -0
  53. package/dist_ts_web/elements/sio-step-timeline.js +222 -0
  54. package/dist_ts_web/elements/sio-tab-bar.d.ts +36 -0
  55. package/dist_ts_web/elements/sio-tab-bar.js +232 -0
  56. package/dist_ts_web/elements/sio-unread-badge.d.ts +25 -0
  57. package/dist_ts_web/elements/sio-unread-badge.js +203 -0
  58. package/dist_ts_web/elements/sio-waveform.d.ts +34 -0
  59. package/dist_ts_web/elements/sio-waveform.js +304 -0
  60. package/dist_ts_web/elements/tokens.d.ts +70 -0
  61. package/dist_ts_web/elements/tokens.js +277 -0
  62. package/dist_ts_web/pages/firstpage.d.ts +5 -0
  63. package/dist_ts_web/pages/firstpage.js +21 -2
  64. package/dist_ts_web/pages/mainpage.d.ts +6 -0
  65. package/dist_ts_web/pages/mainpage.js +111 -39
  66. package/{license → license.md} +3 -1
  67. package/package.json +23 -29
  68. package/readme.hints.md +81 -0
  69. package/readme.md +85 -151
  70. package/ts_web/00_commitinfo_data.ts +2 -2
  71. package/ts_web/elements/index.ts +30 -7
  72. package/ts_web/elements/sio-ai-card.ts +175 -0
  73. package/ts_web/elements/sio-avatar.ts +295 -0
  74. package/ts_web/elements/sio-bucket-header.ts +157 -0
  75. package/ts_web/elements/sio-button.ts +149 -127
  76. package/ts_web/elements/sio-channel-rail.ts +243 -0
  77. package/ts_web/elements/sio-chip.ts +134 -0
  78. package/ts_web/elements/sio-cmdk.ts +461 -0
  79. package/ts_web/elements/sio-combox.ts +327 -204
  80. package/ts_web/elements/sio-composer.ts +306 -0
  81. package/ts_web/elements/sio-conversation-selector.ts +350 -228
  82. package/ts_web/elements/sio-conversation-view.ts +573 -457
  83. package/ts_web/elements/sio-dropdown-menu.ts +112 -130
  84. package/ts_web/elements/sio-fab.ts +192 -227
  85. package/ts_web/elements/sio-glass-toolbar.ts +140 -0
  86. package/ts_web/elements/sio-icon.ts +147 -25
  87. package/ts_web/elements/sio-image-lightbox.ts +62 -64
  88. package/ts_web/elements/sio-inset-list.ts +140 -0
  89. package/ts_web/elements/sio-kbd.ts +80 -0
  90. package/ts_web/elements/sio-list-row.ts +393 -0
  91. package/ts_web/elements/sio-message-input.ts +142 -221
  92. package/ts_web/elements/sio-paper-thumb.ts +244 -0
  93. package/ts_web/elements/sio-pdf-viewer.ts +58 -60
  94. package/ts_web/elements/sio-segmented.ts +183 -0
  95. package/ts_web/elements/sio-step-timeline.ts +179 -0
  96. package/ts_web/elements/sio-tab-bar.ts +195 -0
  97. package/ts_web/elements/sio-unread-badge.ts +147 -0
  98. package/ts_web/elements/sio-waveform.ts +239 -0
  99. package/ts_web/elements/tokens.ts +297 -0
  100. package/ts_web/pages/firstpage.ts +20 -1
  101. package/ts_web/pages/mainpage.ts +111 -39
  102. package/dist_bundle/bundle.js.map +0 -7
  103. package/dist_ts_web/elements/00colors.d.ts +0 -108
  104. package/dist_ts_web/elements/00colors.js +0 -137
  105. package/dist_ts_web/elements/00fonts.d.ts +0 -60
  106. package/dist_ts_web/elements/00fonts.js +0 -129
  107. package/dist_ts_web/elements/00tokens.d.ts +0 -123
  108. package/dist_ts_web/elements/00tokens.js +0 -176
  109. package/dist_ts_web/elements/sio-conversation-list.d.ts +0 -20
  110. package/dist_ts_web/elements/sio-conversation-list.js +0 -495
  111. package/dist_ts_web/elements/sio-recorder.d.ts +0 -60
  112. package/dist_ts_web/elements/sio-recorder.js +0 -173
  113. package/dist_ts_web/elements/sio-subwidget-conversations.d.ts +0 -16
  114. package/dist_ts_web/elements/sio-subwidget-conversations.js +0 -350
  115. package/dist_ts_web/elements/sio-subwidget-onboardme.d.ts +0 -7
  116. package/dist_ts_web/elements/sio-subwidget-onboardme.js +0 -243
  117. package/dist_watch/bundle.js +0 -81694
  118. package/dist_watch/bundle.js.map +0 -7
  119. package/dist_watch/index.html +0 -25
  120. package/npmextra.json +0 -19
  121. package/ts_web/elements/00colors.ts +0 -164
  122. package/ts_web/elements/00fonts.ts +0 -145
  123. package/ts_web/elements/00tokens.ts +0 -197
  124. package/ts_web/elements/sio-recorder.ts +0 -213
@@ -0,0 +1,195 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ property,
5
+ customElement,
6
+ css,
7
+ type TemplateResult,
8
+ } from '@design.estate/dees-element';
9
+ import { sioElementStyles } from './tokens.js';
10
+ import './sio-icon.js';
11
+
12
+ declare global {
13
+ interface HTMLElementTagNameMap {
14
+ 'sio-tab-bar': SioTabBar;
15
+ }
16
+ }
17
+
18
+ export interface ISioTabBarChannel {
19
+ id: string;
20
+ label: string;
21
+ color: string;
22
+ icon: string;
23
+ }
24
+
25
+ export interface ISioTabChangeDetail {
26
+ id: string;
27
+ channel: ISioTabBarChannel;
28
+ }
29
+
30
+ /**
31
+ * The v7 channel set — All / Mail / Chats / Calls / Letters
32
+ * (prototype: SIO_CHANNELS in SocialIOData.jsx).
33
+ */
34
+ const defaultChannels: ISioTabBarChannel[] = [
35
+ { id: 'all', label: 'All', color: 'var(--sio-blue)', icon: 'grid-2x2' },
36
+ { id: 'mail', label: 'Mail', color: 'var(--sio-ch-mail)', icon: 'mail' },
37
+ { id: 'chats', label: 'Chats', color: 'var(--sio-ch-chats)', icon: 'message-circle' },
38
+ { id: 'calls', label: 'Calls', color: 'var(--sio-ch-calls)', icon: 'phone' },
39
+ { id: 'letters', label: 'Letters', color: 'var(--sio-ch-letters)', icon: 'file-text' },
40
+ ];
41
+
42
+ const calendarChannel: ISioTabBarChannel = {
43
+ id: 'calendar',
44
+ label: 'Calendar',
45
+ color: 'var(--sio-blue)',
46
+ icon: 'calendar',
47
+ };
48
+
49
+ /**
50
+ * sio-tab-bar — the mobile bottom channel switcher (v7 prototype:
51
+ * SIOTabBar in SocialIOChannels.jsx).
52
+ *
53
+ * A liquid-glass bar (blur 28px saturate 180%, 0.5px top hairline) with one
54
+ * flex-equal tab per channel: 22px icon over a 10.5px label. The active tab
55
+ * lights up in its channel color; 30px bottom padding clears the iOS home
56
+ * indicator. Selecting a tab dispatches `tab-change` with `{ id, channel }`
57
+ * (bubbles, composed).
58
+ */
59
+ @customElement('sio-tab-bar')
60
+ export class SioTabBar extends DeesElement {
61
+ public static demo = () => html`
62
+ <div style="display: flex; flex-direction: column; gap: 24px; max-width: 390px; font-family: var(--sio-font-sf);">
63
+ <!-- Mail active -->
64
+ <div
65
+ style="border-radius: 20px; overflow: hidden; border: var(--sio-hairline) solid var(--sio-separator); background: var(--sio-bg);"
66
+ >
67
+ <div style="padding: 18px 16px 26px;">
68
+ <div style="font-size: 28px; font-weight: 700; letter-spacing: 0.36px; color: var(--sio-label);">Mail</div>
69
+ <div style="margin-top: 3px; font-size: 13px; color: var(--sio-label-3);">
70
+ 4 unread · Feed 12 · Paper 4 · People 3
71
+ </div>
72
+ </div>
73
+ <sio-tab-bar active="mail"></sio-tab-bar>
74
+ </div>
75
+
76
+ <!-- Chats active, with the optional Calendar tab -->
77
+ <div
78
+ style="border-radius: 20px; overflow: hidden; border: var(--sio-hairline) solid var(--sio-separator); background: var(--sio-bg);"
79
+ >
80
+ <div style="padding: 18px 16px 26px;">
81
+ <div style="font-size: 28px; font-weight: 700; letter-spacing: 0.36px; color: var(--sio-label);">Chats</div>
82
+ <div style="margin-top: 3px; font-size: 13px; color: var(--sio-label-3);">
83
+ 3 new · Lena is typing…
84
+ </div>
85
+ </div>
86
+ <sio-tab-bar active="chats" .withCalendar=${true}></sio-tab-bar>
87
+ </div>
88
+ </div>
89
+ `;
90
+
91
+ @property({ type: Array })
92
+ public accessor channels: ISioTabBarChannel[] = defaultChannels;
93
+
94
+ @property({ type: String })
95
+ public accessor active = 'mail';
96
+
97
+ @property({ type: Boolean })
98
+ public accessor withCalendar = false;
99
+
100
+ public static styles = [
101
+ ...sioElementStyles,
102
+ css`
103
+ :host {
104
+ display: block;
105
+ }
106
+
107
+ /* toolbar glass, hairline on the top edge (the bar sits at the bottom) */
108
+ .bar {
109
+ display: flex;
110
+ padding: 8px 0 30px; /* 30px clears the iOS home indicator */
111
+ background: var(--sio-toolbar-glass-bg);
112
+ backdrop-filter: blur(28px) saturate(180%);
113
+ -webkit-backdrop-filter: blur(28px) saturate(180%);
114
+ border-top: var(--sio-hairline) solid var(--sio-separator);
115
+ }
116
+
117
+ .tab {
118
+ appearance: none;
119
+ border: none;
120
+ margin: 0;
121
+ padding: 0;
122
+ background: transparent;
123
+ flex: 1;
124
+ min-width: 0;
125
+ display: flex;
126
+ flex-direction: column;
127
+ align-items: center;
128
+ gap: 2px;
129
+ font-family: var(--sio-font-sf);
130
+ color: var(--sio-label-3);
131
+ cursor: pointer;
132
+ -webkit-tap-highlight-color: transparent;
133
+ transition: color 120ms ease;
134
+ }
135
+
136
+ .tab.active {
137
+ color: var(--_channel-color, var(--sio-tint));
138
+ }
139
+
140
+ .tab:focus-visible {
141
+ outline: 2px solid color-mix(in oklab, var(--sio-tint) 40%, transparent);
142
+ outline-offset: -2px;
143
+ border-radius: var(--sio-radius-sm);
144
+ }
145
+
146
+ .label {
147
+ font-size: 10.5px;
148
+ font-weight: 500;
149
+ letter-spacing: 0.02px;
150
+ line-height: 1.25;
151
+ white-space: nowrap;
152
+ }
153
+
154
+ .tab.active .label {
155
+ font-weight: 600;
156
+ }
157
+ `,
158
+ ];
159
+
160
+ public render(): TemplateResult {
161
+ const tabs = this.withCalendar ? [...this.channels, calendarChannel] : this.channels;
162
+ return html`
163
+ <nav class="bar" part="bar">
164
+ ${tabs.map(
165
+ (channel) => html`
166
+ <button
167
+ type="button"
168
+ class="tab ${channel.id === this.active ? 'active' : ''}"
169
+ style="--_channel-color: ${channel.color}"
170
+ aria-current=${channel.id === this.active ? 'page' : 'false'}
171
+ @click=${() => this.handleSelect(channel)}
172
+ >
173
+ <sio-icon icon=${channel.icon} size="22" .strokeWidth=${1.7}></sio-icon>
174
+ <span class="label">${channel.label}</span>
175
+ </button>
176
+ `,
177
+ )}
178
+ </nav>
179
+ `;
180
+ }
181
+
182
+ private handleSelect(channel: ISioTabBarChannel): void {
183
+ if (channel.id === this.active) {
184
+ return;
185
+ }
186
+ this.active = channel.id;
187
+ this.dispatchEvent(
188
+ new CustomEvent<ISioTabChangeDetail>('tab-change', {
189
+ detail: { id: channel.id, channel },
190
+ bubbles: true,
191
+ composed: true,
192
+ }),
193
+ );
194
+ }
195
+ }
@@ -0,0 +1,147 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ property,
5
+ customElement,
6
+ css,
7
+ type TemplateResult,
8
+ } from '@design.estate/dees-element';
9
+
10
+ import { sioElementStyles } from './tokens.js';
11
+ import './sio-icon.js'; // used by the static demo
12
+
13
+ export type TSioUnreadBadgeChannel = 'tint' | 'mail' | 'chats' | 'calls' | 'letters' | 'alert';
14
+ export type TSioUnreadBadgeSize = 'md' | 'sm';
15
+
16
+ declare global {
17
+ interface HTMLElementTagNameMap {
18
+ 'sio-unread-badge': SioUnreadBadge;
19
+ }
20
+ }
21
+
22
+ const badgeColors: Record<TSioUnreadBadgeChannel, string> = {
23
+ tint: 'var(--sio-tint)',
24
+ mail: 'var(--sio-ch-mail)',
25
+ chats: 'var(--sio-ch-chats)',
26
+ calls: 'var(--sio-ch-calls)',
27
+ letters: 'var(--sio-ch-letters)',
28
+ alert: 'var(--sio-red)',
29
+ };
30
+
31
+ /**
32
+ * sio-unread-badge — count pill on solid channel color (v7 chat list /
33
+ * channel rail / widget launcher). md: min-width 20, height 20, radius 10,
34
+ * 11.5/700 tabular white. sm (rails): 16px with 10px numerals.
35
+ * `ring` adds the 2px surface ring used when the pill sits on an icon.
36
+ */
37
+ @customElement('sio-unread-badge')
38
+ export class SioUnreadBadge extends DeesElement {
39
+ public static demo = () => html`
40
+ <div style="display: flex; flex-direction: column; gap: 16px;">
41
+ <div style="display: flex; gap: 10px; align-items: center;">
42
+ <sio-unread-badge count="3" channel="chats"></sio-unread-badge>
43
+ <sio-unread-badge count="12" channel="mail"></sio-unread-badge>
44
+ <sio-unread-badge count="1" channel="calls"></sio-unread-badge>
45
+ <sio-unread-badge count="2" channel="letters"></sio-unread-badge>
46
+ <sio-unread-badge count="4"></sio-unread-badge>
47
+ <sio-unread-badge count="1" channel="alert"></sio-unread-badge>
48
+ </div>
49
+ <div style="display: flex; gap: 10px; align-items: center;">
50
+ <sio-unread-badge count="3" channel="chats" size="sm"></sio-unread-badge>
51
+ <sio-unread-badge count="12" channel="mail" size="sm"></sio-unread-badge>
52
+ <sio-unread-badge count="5" channel="calls" size="sm" ring></sio-unread-badge>
53
+ </div>
54
+ <div style="display: flex; gap: 22px; align-items: center;">
55
+ <span style="position: relative; display: inline-flex;">
56
+ <sio-icon icon="envelope" size="20" style="color: var(--sio-ch-mail);"></sio-icon>
57
+ <sio-unread-badge
58
+ count="3"
59
+ channel="mail"
60
+ size="sm"
61
+ ring
62
+ style="position: absolute; top: -5px; right: -9px;"
63
+ ></sio-unread-badge>
64
+ </span>
65
+ <span style="position: relative; display: inline-flex;">
66
+ <sio-icon icon="message-circle" size="20" style="color: var(--sio-ch-chats);"></sio-icon>
67
+ <sio-unread-badge
68
+ count="7"
69
+ channel="chats"
70
+ size="sm"
71
+ ring
72
+ style="position: absolute; top: -5px; right: -9px;"
73
+ ></sio-unread-badge>
74
+ </span>
75
+ </div>
76
+ </div>
77
+ `;
78
+
79
+ @property({ type: Number })
80
+ public accessor count = 0;
81
+
82
+ @property({ type: String })
83
+ public accessor channel: TSioUnreadBadgeChannel = 'tint';
84
+
85
+ @property({ type: String })
86
+ public accessor size: TSioUnreadBadgeSize = 'md';
87
+
88
+ /** 2px surface ring — for pills sitting on rail icons */
89
+ @property({ type: Boolean })
90
+ public accessor ring = false;
91
+
92
+ public static styles = [
93
+ ...sioElementStyles,
94
+ css`
95
+ :host {
96
+ display: inline-flex;
97
+ flex-shrink: 0;
98
+ }
99
+
100
+ .badge {
101
+ box-sizing: border-box;
102
+ display: inline-flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ min-width: 20px;
106
+ height: 20px;
107
+ padding: 0 6px;
108
+ border-radius: 10px;
109
+ background: var(--badge-color);
110
+ color: #fff; /* white numerals on solid channel color per v7 */
111
+ font-family: var(--sio-font-sf);
112
+ font-size: 11.5px;
113
+ font-weight: 700;
114
+ font-variant-numeric: tabular-nums;
115
+ line-height: 1;
116
+ white-space: nowrap;
117
+ user-select: none;
118
+ }
119
+
120
+ .badge.sm {
121
+ min-width: 16px;
122
+ height: 16px;
123
+ padding: 0 5px;
124
+ border-radius: 8px;
125
+ font-size: 10px;
126
+ }
127
+
128
+ .badge.ring {
129
+ box-shadow: 0 0 0 2px var(--sio-surface);
130
+ }
131
+ `,
132
+ ];
133
+
134
+ public render(): TemplateResult {
135
+ if (this.count <= 0) {
136
+ return html``;
137
+ }
138
+ return html`
139
+ <span
140
+ class="badge ${this.size === 'sm' ? 'sm' : ''} ${this.ring ? 'ring' : ''}"
141
+ style="--badge-color: ${badgeColors[this.channel] || badgeColors.tint};"
142
+ >
143
+ ${this.count}
144
+ </span>
145
+ `;
146
+ }
147
+ }
@@ -0,0 +1,239 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ property,
5
+ customElement,
6
+ css,
7
+ type TemplateResult,
8
+ } from '@design.estate/dees-element';
9
+ import { sioElementStyles } from './tokens.js';
10
+
11
+ declare global {
12
+ interface HTMLElementTagNameMap {
13
+ 'sio-waveform': SioWaveform;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * sio-waveform — v7 voicemail / voice-message player.
19
+ *
20
+ * A surface card with a round play/pause control, a deterministic bar-chart
21
+ * waveform (played bars in the channel color, the rest in systemGray @ 45%
22
+ * per the v7 spec) and tabular elapsed / -remaining counters.
23
+ *
24
+ * Clicking the waveform dispatches `seek` ({ progress }); the play control
25
+ * toggles and dispatches `play` / `pause` ({ progress }).
26
+ */
27
+ @customElement('sio-waveform')
28
+ export class SioWaveform extends DeesElement {
29
+ public static demo = () => html`
30
+ <div
31
+ style="display: flex; flex-direction: column; gap: 20px; max-width: 420px; padding: 20px; background: var(--sio-bg); border-radius: 18px; font-family: var(--sio-font-sf);"
32
+ >
33
+ <div>
34
+ <div style="font-size: 22px; font-weight: 700; letter-spacing: -0.4px; color: var(--sio-label);">
35
+ Maren Vogel
36
+ </div>
37
+ <div style="font-size: 13px; color: var(--sio-label-3); margin-top: 2px;">
38
+ Today 09:41 · 1:47 · Voicemail
39
+ </div>
40
+ </div>
41
+ <sio-waveform progress="0.28" elapsed="0:28" remaining="-1:19"></sio-waveform>
42
+ <sio-waveform
43
+ progress="0.62"
44
+ playing
45
+ elapsed="1:06"
46
+ remaining="-0:41"
47
+ color="var(--sio-ch-chats)"
48
+ bars="64"
49
+ ></sio-waveform>
50
+ <sio-waveform progress="0" elapsed="0:00" remaining="-1:47" color="var(--sio-tint)"></sio-waveform>
51
+ </div>
52
+ `;
53
+
54
+ /** Number of waveform bars (the v7 voicemail player draws 50). */
55
+ @property({ type: Number })
56
+ public accessor bars: number = 50;
57
+
58
+ /** Playback position, 0–1. Played bars take the channel color. */
59
+ @property({ type: Number })
60
+ public accessor progress: number = 0;
61
+
62
+ /** Accent for played bars + play control. Any CSS color, defaults to the calls channel. */
63
+ @property({ type: String })
64
+ public accessor color: string = 'var(--sio-ch-calls)';
65
+
66
+ /** Elapsed counter, e.g. "0:28". */
67
+ @property({ type: String })
68
+ public accessor elapsed: string = '0:00';
69
+
70
+ /** Remaining counter, e.g. "-1:19". */
71
+ @property({ type: String })
72
+ public accessor remaining: string = '-0:00';
73
+
74
+ @property({ type: Boolean, reflect: true })
75
+ public accessor playing = false;
76
+
77
+ public static styles = [
78
+ ...sioElementStyles,
79
+ css`
80
+ :host {
81
+ display: block;
82
+
83
+ /* systemGray (#8E8E93) @ 45% — unplayed bars per the v7 spec */
84
+ --sio-waveform-rest: color-mix(in oklab, var(--sio-bucket-later) 45%, transparent);
85
+ }
86
+
87
+ .player {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 12px;
91
+ padding: 14px 16px;
92
+ background: var(--sio-surface);
93
+ border: var(--sio-hairline) solid var(--sio-separator);
94
+ border-radius: 16px;
95
+ }
96
+
97
+ .playButton {
98
+ width: 44px;
99
+ height: 44px;
100
+ flex-shrink: 0;
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ border: none;
105
+ border-radius: 22px;
106
+ padding: 0;
107
+ cursor: pointer;
108
+ font: inherit;
109
+ transition: transform 80ms ease, filter 120ms ease;
110
+ }
111
+ .playButton:hover {
112
+ filter: brightness(1.06);
113
+ }
114
+ .playButton:active {
115
+ transform: scale(0.94);
116
+ }
117
+ .playButton:focus-visible {
118
+ outline: 2px solid color-mix(in oklab, var(--sio-tint) 60%, transparent);
119
+ outline-offset: 2px;
120
+ }
121
+ .playButton svg {
122
+ display: block;
123
+ }
124
+
125
+ .track {
126
+ flex: 1;
127
+ min-width: 0;
128
+ }
129
+
130
+ .wave {
131
+ display: block;
132
+ width: 100%;
133
+ height: 28px;
134
+ cursor: pointer;
135
+ }
136
+
137
+ .counters {
138
+ display: flex;
139
+ justify-content: space-between;
140
+ margin-top: 2px;
141
+ font-size: 11px;
142
+ line-height: 13px;
143
+ color: var(--sio-label-3);
144
+ font-variant-numeric: tabular-nums;
145
+ }
146
+ `,
147
+ ];
148
+
149
+ public render(): TemplateResult {
150
+ const barCount = Math.max(1, Math.floor(this.bars));
151
+ const clamped = Math.min(1, Math.max(0, this.progress));
152
+ const playedCount = Math.round(barCount * clamped);
153
+ const barTemplates: TemplateResult[] = [];
154
+ for (let i = 0; i < barCount; i++) {
155
+ // deterministic pseudo-random heights — transcribed from the v7 prototype
156
+ const h = 4 + Math.abs(Math.sin(i * 0.55) * 11) + Math.abs(Math.cos(i * 1.3) * 6);
157
+ barTemplates.push(html`
158
+ <rect
159
+ x=${i * 4}
160
+ y=${14 - h / 2}
161
+ width="2.2"
162
+ height=${h}
163
+ rx="1.1"
164
+ fill=${i < playedCount ? this.color : 'var(--sio-waveform-rest)'}
165
+ ></rect>
166
+ `);
167
+ }
168
+ return html`
169
+ <div class="player" part="player">
170
+ <button
171
+ class="playButton"
172
+ style="background: ${this.color};"
173
+ aria-label=${this.playing ? 'Pause' : 'Play'}
174
+ @click=${this.handlePlayToggle}
175
+ >
176
+ ${this.playing
177
+ ? html`
178
+ <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
179
+ <rect x="6" y="4.5" width="4.4" height="15" rx="1.6" fill="var(--sio-surface)"></rect>
180
+ <rect x="13.6" y="4.5" width="4.4" height="15" rx="1.6" fill="var(--sio-surface)"></rect>
181
+ </svg>
182
+ `
183
+ : html`
184
+ <svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
185
+ <path d="M7 5l12 7-12 7V5z" fill="var(--sio-surface)"></path>
186
+ </svg>
187
+ `}
188
+ </button>
189
+ <div class="track">
190
+ <svg
191
+ class="wave"
192
+ viewBox="0 0 ${barCount * 4} 28"
193
+ preserveAspectRatio="none"
194
+ role="slider"
195
+ aria-label="Playback position"
196
+ aria-valuemin="0"
197
+ aria-valuemax="1"
198
+ aria-valuenow=${clamped.toFixed(2)}
199
+ @click=${this.handleSeek}
200
+ >
201
+ ${barTemplates}
202
+ </svg>
203
+ <div class="counters">
204
+ <span>${this.elapsed}</span>
205
+ <span>${this.remaining}</span>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ `;
210
+ }
211
+
212
+ private handlePlayToggle() {
213
+ this.playing = !this.playing;
214
+ this.dispatchEvent(
215
+ new CustomEvent(this.playing ? 'play' : 'pause', {
216
+ detail: { progress: Math.min(1, Math.max(0, this.progress)) },
217
+ bubbles: true,
218
+ composed: true,
219
+ }),
220
+ );
221
+ }
222
+
223
+ private handleSeek(event: MouseEvent) {
224
+ const wave = event.currentTarget as SVGSVGElement;
225
+ const rect = wave.getBoundingClientRect();
226
+ if (!rect.width) {
227
+ return;
228
+ }
229
+ const progress = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
230
+ this.progress = progress;
231
+ this.dispatchEvent(
232
+ new CustomEvent('seek', {
233
+ detail: { progress },
234
+ bubbles: true,
235
+ composed: true,
236
+ }),
237
+ );
238
+ }
239
+ }