@lark-apaas/coding-registry-buildless 0.1.20 → 0.1.22

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.
@@ -0,0 +1,338 @@
1
+ /* BEGIN USAGE */
2
+ // ios-frame.js — Simplified iOS 26 (Liquid Glass) device frame, as custom elements.
3
+ // Based on the iOS 26 UI Kit + Figma status bar spec. No assets, no deps, no React.
4
+ // Defines: <ios-device>, <ios-status-bar>, <ios-list>, <ios-list-row>, <ios-keyboard>
5
+ //
6
+ // Usage — wrap your screen content in <ios-device> to get the bezel, status bar
7
+ // and home indicator. Load with a plain <script src="ios-frame.js"></script>:
8
+ //
9
+ // <ios-device>
10
+ // ...your screen content...
11
+ // </ios-device>
12
+ // <ios-device dark keyboard>…</ios-device>
13
+ // <ios-device width="390" height="844">…</ios-device> <!-- smaller device size -->
14
+ //
15
+ // Attributes:
16
+ // <ios-device> width (402) · height (874) · dark · keyboard
17
+ // <ios-status-bar> dark · time ("9:41")
18
+ // <ios-list> header · dark — rows go in as light-DOM children
19
+ // <ios-list-row> title · detail · icon (a CSS color) · dark · is-last · no-chevron
20
+ // <ios-keyboard> dark
21
+ // Booleans are by attribute presence: `dark` on, absent off. `no-chevron` turns the
22
+ // row's chevron OFF (it is on by default).
23
+ //
24
+ // Safe areas — REQUIRED on every screen. The status bar (top) and home indicator
25
+ // (bottom) float OVER your content; inset it or it overlaps them.
26
+ // --ios-safe-top top inset (Dynamic Island + status bar)
27
+ // --ios-safe-bottom bottom inset (home indicator)
28
+ /* END USAGE */
29
+
30
+ (() => {
31
+ /**
32
+ * React version renders text and attributes through JSX, which escapes them. These
33
+ * elements build their shadow with innerHTML template strings, so that protection is
34
+ * gone — every value that comes from an author attribute, from persisted state, or
35
+ * from a Magic Share replay has to go through here. The artboard label matters most:
36
+ * it is renameable, it round-trips through the sidecar, AND a peer can push one in
37
+ * over Magic Share, so an unescaped one is script execution in the follower's page.
38
+ */
39
+ const esc = (v) =>
40
+ String(v ?? '').replace(
41
+ /[&<>"']/g,
42
+ (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch],
43
+ );
44
+
45
+ // Non-numeric input falls through into a style="..." attribute, so it must be
46
+ // escaped too — a quote in it would break out of the attribute.
47
+ const px = (v) => (/^-?\d+(\.\d+)?$/.test(String(v)) ? `${v}px` : esc(v));
48
+
49
+ /**
50
+ * display:contents — the host generates no box of its own, so the shadow's root
51
+ * div becomes the parent's layout child directly. That is what keeps this port
52
+ * pixel-identical to the React version: the JSX component renders that same div
53
+ * with nothing wrapping it. `block` would add an outer box that swallows the
54
+ * flex properties meant for the inner one, showing up as a few pixels of drift
55
+ * in the visual regression.
56
+ */
57
+ /**
58
+ * The React version lives in the light DOM, so its box model comes from the
59
+ * product's own `*` reset (most products set border-box). That reset never
60
+ * reaches into a shadow root, and hardcoding either value gets half the products
61
+ * wrong. `inherit` is wrong too — it passes an inline border-box down to
62
+ * descendants, which a `*` rule would not. Copying the host's computed value is
63
+ * the only thing that matches the React version exactly: the host is a light DOM
64
+ * element, so the product's `*` rule does hit it.
65
+ */
66
+ const hostCss = (el) =>
67
+ '<style>:host{display:contents}*,*::before,*::after{box-sizing:' +
68
+ getComputedStyle(el).boxSizing +
69
+ '}</style>';
70
+
71
+ /**
72
+ * Every element is read-attributes-then-repaint-the-shadow. No incremental
73
+ * update: these are purely presentational frames, and one repaint costs far
74
+ * less than the diffing logic it would take to avoid it.
75
+ */
76
+ class Base extends HTMLElement {
77
+ constructor() {
78
+ super();
79
+ this.attachShadow({ mode: 'open' });
80
+ }
81
+ connectedCallback() {
82
+ this._render();
83
+ }
84
+ attributeChangedCallback() {
85
+ // Elements that already carry attributes at upgrade time land here too,
86
+ // before the shadow root exists.
87
+ if (this.shadowRoot) this._render();
88
+ }
89
+ /** Attribute read with a fallback. */
90
+ attr(name, fallback) {
91
+ const v = this.getAttribute(name);
92
+ return v === null || v === '' ? fallback : v;
93
+ }
94
+ get dark() {
95
+ return this.hasAttribute('dark');
96
+ }
97
+ }
98
+
99
+ // ───────────────────────────────────────────────────────────
100
+ // Status bar
101
+ // ───────────────────────────────────────────────────────────
102
+ class IOSStatusBar extends Base {
103
+ static get observedAttributes() {
104
+ return ['dark', 'time'];
105
+ }
106
+ _render() {
107
+ const c = this.dark ? '#fff' : '#000';
108
+ this.shadowRoot.innerHTML =
109
+ hostCss(this) +
110
+ `
111
+ <div part="frame" style="display:flex;gap:154px;align-items:center;justify-content:center;
112
+ padding:21px 24px 19px;box-sizing:border-box;position:relative;z-index:20;width:100%">
113
+ <div style="flex:1;height:22px;display:flex;align-items:center;justify-content:center;padding-top:1.5px">
114
+ <span style="font-family:-apple-system,'SF Pro',system-ui;font-weight:590;
115
+ font-size:17px;line-height:22px;color:${c}">${esc(this.attr('time', '9:41'))}</span>
116
+ </div>
117
+ <div style="flex:1;height:22px;display:flex;align-items:center;justify-content:center;gap:7px;padding-top:1px;padding-right:1px">
118
+ <svg width="19" height="12" viewBox="0 0 19 12">
119
+ <rect x="0" y="7.5" width="3.2" height="4.5" rx="0.7" fill="${c}"/>
120
+ <rect x="4.8" y="5" width="3.2" height="7" rx="0.7" fill="${c}"/>
121
+ <rect x="9.6" y="2.5" width="3.2" height="9.5" rx="0.7" fill="${c}"/>
122
+ <rect x="14.4" y="0" width="3.2" height="12" rx="0.7" fill="${c}"/>
123
+ </svg>
124
+ <svg width="17" height="12" viewBox="0 0 17 12">
125
+ <path d="M8.5 3.2C10.8 3.2 12.9 4.1 14.4 5.6L15.5 4.5C13.7 2.7 11.2 1.5 8.5 1.5C5.8 1.5 3.3 2.7 1.5 4.5L2.6 5.6C4.1 4.1 6.2 3.2 8.5 3.2Z" fill="${c}"/>
126
+ <path d="M8.5 6.8C9.9 6.8 11.1 7.3 12 8.2L13.1 7.1C11.8 5.9 10.2 5.1 8.5 5.1C6.8 5.1 5.2 5.9 3.9 7.1L5 8.2C5.9 7.3 7.1 6.8 8.5 6.8Z" fill="${c}"/>
127
+ <circle cx="8.5" cy="10.5" r="1.5" fill="${c}"/>
128
+ </svg>
129
+ <svg width="27" height="13" viewBox="0 0 27 13">
130
+ <rect x="0.5" y="0.5" width="23" height="12" rx="3.5" stroke="${c}" stroke-opacity="0.35" fill="none"/>
131
+ <rect x="2" y="2" width="20" height="9" rx="2" fill="${c}"/>
132
+ <path d="M25 4.5V8.5C25.8 8.2 26.5 7.2 26.5 6.5C26.5 5.8 25.8 4.8 25 4.5Z" fill="${c}" fill-opacity="0.4"/>
133
+ </svg>
134
+ </div>
135
+ </div>`;
136
+ }
137
+ }
138
+
139
+ // ───────────────────────────────────────────────────────────
140
+ // Grouped list (inset card, r:26) + row (52px)
141
+ // ───────────────────────────────────────────────────────────
142
+ class IOSListRow extends Base {
143
+ static get observedAttributes() {
144
+ return ['title', 'detail', 'icon', 'dark', 'is-last', 'no-chevron'];
145
+ }
146
+ _render() {
147
+ const dark = this.dark;
148
+ const text = dark ? '#fff' : '#000';
149
+ const sec = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';
150
+ const ter = dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)';
151
+ const sep = dark ? 'rgba(84,84,88,0.65)' : 'rgba(60,60,67,0.12)';
152
+ const icon = this.attr('icon', '');
153
+ const detail = this.attr('detail', '');
154
+ // chevron defaults on: the attribute's presence turns it OFF (same shape as
155
+ // deck-stage's no-rail).
156
+ const chevron = !this.hasAttribute('no-chevron');
157
+ this.shadowRoot.innerHTML =
158
+ hostCss(this) +
159
+ `
160
+ <div part="frame" style="display:flex;align-items:center;min-height:52px;padding:0 16px;
161
+ position:relative;font-family:-apple-system,system-ui;font-size:17px;letter-spacing:-0.43px">
162
+ ${icon ? `<div style="width:30px;height:30px;border-radius:7px;background:${icon};margin-right:12px;flex-shrink:0"></div>` : ''}
163
+ <div style="flex:1;color:${text}">${esc(this.attr('title', ''))}</div>
164
+ ${detail ? `<span style="color:${sec};margin-right:6px">${esc(detail)}</span>` : ''}
165
+ ${
166
+ chevron
167
+ ? `<svg width="8" height="14" viewBox="0 0 8 14" style="flex-shrink:0">
168
+ <path d="M1 1l6 6-6 6" stroke="${ter}" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
169
+ </svg>`
170
+ : ''
171
+ }
172
+ ${
173
+ this.hasAttribute('is-last')
174
+ ? ''
175
+ : `<div style="position:absolute;bottom:0;right:0;left:${icon ? '58px' : '16px'};height:0.5px;background:${sep}"></div>`
176
+ }
177
+ </div>`;
178
+ }
179
+ }
180
+
181
+ class IOSList extends Base {
182
+ static get observedAttributes() {
183
+ return ['header', 'dark'];
184
+ }
185
+ _render() {
186
+ const dark = this.dark;
187
+ const header = this.attr('header', '');
188
+ this.shadowRoot.innerHTML =
189
+ hostCss(this) +
190
+ `
191
+ <div part="frame">
192
+ ${
193
+ header
194
+ ? `<div part="header" style="font-family:-apple-system,system-ui;font-size:13px;
195
+ color:${dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'};text-transform:uppercase;
196
+ padding:8px 36px 6px;letter-spacing:-0.08px">${header}</div>`
197
+ : ''
198
+ }
199
+ <div part="card" style="background:${dark ? '#1C1C1E' : '#fff'};border-radius:26px;
200
+ margin:0 16px;overflow:hidden"><slot></slot></div>
201
+ </div>`;
202
+ }
203
+ }
204
+
205
+ // ───────────────────────────────────────────────────────────
206
+ // Keyboard — iOS 26 liquid glass
207
+ // ───────────────────────────────────────────────────────────
208
+ class IOSKeyboard extends Base {
209
+ static get observedAttributes() {
210
+ return ['dark'];
211
+ }
212
+ _render() {
213
+ const dark = this.dark;
214
+ const glyph = dark ? 'rgba(255,255,255,0.7)' : '#595959';
215
+ const sugg = dark ? 'rgba(255,255,255,0.6)' : '#333';
216
+ const keyBg = dark ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.85)';
217
+
218
+ const icons = {
219
+ shift: `<svg width="19" height="17" viewBox="0 0 19 17"><path d="M9.5 1L1 9.5h4.5V16h8V9.5H18L9.5 1z" fill="${glyph}"/></svg>`,
220
+ del: `<svg width="23" height="17" viewBox="0 0 23 17"><path d="M7 1h13a2 2 0 012 2v11a2 2 0 01-2 2H7l-6-7.5L7 1z" fill="none" stroke="${glyph}" stroke-width="1.6" stroke-linejoin="round"/><path d="M10 5l7 7M17 5l-7 7" stroke="${glyph}" stroke-width="1.6" stroke-linecap="round"/></svg>`,
221
+ ret: `<svg width="20" height="14" viewBox="0 0 20 14"><path d="M18 1v6H4m0 0l4-4M4 7l4 4" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
222
+ };
223
+
224
+ const key = (content, { w, flex, ret, fs = 25 } = {}) => `
225
+ <div style="height:42px;border-radius:8.5px;${flex ? 'flex:1;' : ''}${w ? `width:${w}px;` : ''}min-width:0;
226
+ background:${ret ? '#08f' : keyBg};box-shadow:0 1px 0 rgba(0,0,0,0.075);
227
+ display:flex;align-items:center;justify-content:center;
228
+ font-family:-apple-system,'SF Compact',system-ui;font-size:${fs}px;font-weight:458;
229
+ color:${ret ? '#fff' : glyph}">${content}</div>`;
230
+
231
+ const row = (keys, pad = 0) =>
232
+ `<div style="display:flex;gap:6.5px;justify-content:center;padding:0 ${pad}px">
233
+ ${keys.map((l) => key(l, { flex: true })).join('')}
234
+ </div>`;
235
+
236
+ this.shadowRoot.innerHTML =
237
+ hostCss(this) +
238
+ `
239
+ <div part="frame" style="position:relative;z-index:15;border-radius:27px;overflow:hidden;
240
+ padding:11px 0 2px;display:flex;flex-direction:column;align-items:center;
241
+ box-shadow:${dark ? '0 -2px 20px rgba(0,0,0,0.09)' : '0 -1px 6px rgba(0,0,0,0.018), 0 -3px 20px rgba(0,0,0,0.012)'}">
242
+ <div style="position:absolute;inset:0;border-radius:27px;
243
+ backdrop-filter:blur(12px) saturate(180%);-webkit-backdrop-filter:blur(12px) saturate(180%);
244
+ background:${dark ? 'rgba(120,120,128,0.14)' : 'rgba(255,255,255,0.25)'}"></div>
245
+ <div style="position:absolute;inset:0;border-radius:27px;pointer-events:none;
246
+ box-shadow:${
247
+ dark
248
+ ? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15)'
249
+ : 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)'
250
+ };
251
+ border:${dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)'}"></div>
252
+
253
+ <div style="display:flex;gap:20px;align-items:center;padding:8px 22px 13px;width:100%;
254
+ box-sizing:border-box;position:relative">
255
+ ${['"The"', 'the', 'to']
256
+ .map(
257
+ (w, i) =>
258
+ `${i > 0 ? '<div style="width:1px;height:25px;background:#ccc;opacity:0.3"></div>' : ''}
259
+ <div style="flex:1;text-align:center;font-family:-apple-system,system-ui;font-size:17px;
260
+ color:${sugg};letter-spacing:-0.43px;line-height:22px">${w}</div>`,
261
+ )
262
+ .join('')}
263
+ </div>
264
+
265
+ <div style="display:flex;flex-direction:column;gap:13px;padding:0 6.5px;width:100%;
266
+ box-sizing:border-box;position:relative">
267
+ ${row(['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'])}
268
+ ${row(['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], 20)}
269
+ <div style="display:flex;gap:14.25px;align-items:center">
270
+ ${key(icons.shift, { w: 45 })}
271
+ <div style="display:flex;gap:6.5px;flex:1">
272
+ ${['z', 'x', 'c', 'v', 'b', 'n', 'm'].map((l) => key(l, { flex: true })).join('')}
273
+ </div>
274
+ ${key(icons.del, { w: 45 })}
275
+ </div>
276
+ <div style="display:flex;gap:6px;align-items:center">
277
+ ${key('ABC', { w: 92.25, fs: 18 })}
278
+ ${key('', { flex: true })}
279
+ ${key(icons.ret, { w: 92.25, ret: true })}
280
+ </div>
281
+ </div>
282
+
283
+ <div style="height:56px;width:100%;position:relative"></div>
284
+ </div>`;
285
+ }
286
+ }
287
+
288
+ // ───────────────────────────────────────────────────────────
289
+ // Device frame
290
+ // ───────────────────────────────────────────────────────────
291
+ class IOSDevice extends Base {
292
+ static get observedAttributes() {
293
+ return ['width', 'height', 'dark', 'keyboard'];
294
+ }
295
+ _render() {
296
+ const dark = this.dark;
297
+ const d = dark ? ' dark' : '';
298
+ this.shadowRoot.innerHTML =
299
+ hostCss(this) +
300
+ `
301
+ <div part="frame" style="width:${px(this.attr('width', 402))};height:${px(this.attr('height', 874))};
302
+ border-radius:48px;overflow:hidden;position:relative;
303
+ background:${dark ? '#000' : '#F2F2F7'};border:1px solid #DEE0E3;
304
+ font-family:-apple-system,system-ui,sans-serif;-webkit-font-smoothing:antialiased;
305
+ --ios-safe-top:62px;--ios-safe-bottom:34px">
306
+ <div part="island" style="position:absolute;top:11px;left:50%;transform:translateX(-50%);
307
+ width:126px;height:37px;border-radius:24px;background:#000;z-index:50"></div>
308
+ <div style="position:absolute;top:0;left:0;right:0;z-index:10">
309
+ <ios-status-bar${d}></ios-status-bar>
310
+ </div>
311
+ <!-- isolation:isolate gives the screen content its own stacking context, so
312
+ a z-index the author writes inside it cannot paint over the status bar
313
+ (10), the dynamic island (50) or the home indicator (60). -->
314
+ <div style="height:100%;display:flex;flex-direction:column;isolation:isolate">
315
+ <div style="flex:1;overflow:auto"><slot></slot></div>
316
+ ${this.hasAttribute('keyboard') ? `<ios-keyboard${d}></ios-keyboard>` : ''}
317
+ </div>
318
+ <div part="home-indicator" style="position:absolute;bottom:0;left:0;right:0;z-index:60;height:34px;
319
+ display:flex;justify-content:center;align-items:flex-end;padding-bottom:8px;pointer-events:none">
320
+ <div style="width:139px;height:5px;border-radius:100px;
321
+ background:${dark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.25)'}"></div>
322
+ </div>
323
+ </div>`;
324
+ }
325
+ }
326
+
327
+ // A duplicate define throws — copies of this file can coexist on one page, so
328
+ // the guard is mandatory.
329
+ for (const [tag, ctor] of [
330
+ ['ios-status-bar', IOSStatusBar],
331
+ ['ios-list-row', IOSListRow],
332
+ ['ios-list', IOSList],
333
+ ['ios-keyboard', IOSKeyboard],
334
+ ['ios-device', IOSDevice],
335
+ ]) {
336
+ if (!customElements.get(tag)) customElements.define(tag, ctor);
337
+ }
338
+ })();
@@ -0,0 +1,260 @@
1
+ /* BEGIN USAGE */
2
+ // macos-window.js — Simplified macOS Tahoe (Liquid Glass) window, as custom elements.
3
+ // Based on the macOS Tahoe UI Kit. No image assets, no dependencies, no React.
4
+ // Defines: <macos-window>, <macos-sidebar>, <macos-sidebar-item>, <macos-sidebar-header>,
5
+ // <macos-toolbar>, <macos-glass>, <macos-traffic-lights>
6
+ //
7
+ // Usage — wrap your app content in <macos-window> to get the window chrome
8
+ // (traffic lights + titlebar). Sidebar rows go into the `sidebar` slot:
9
+ //
10
+ // <macos-window width="980" height="620" title="Documents">
11
+ // <macos-sidebar-header slot="sidebar" title="Favorites"></macos-sidebar-header>
12
+ // <macos-sidebar-item slot="sidebar" label="Projects"></macos-sidebar-item>
13
+ // <macos-sidebar-item slot="sidebar" label="Drafts" selected></macos-sidebar-item>
14
+ // ...your app content...
15
+ // </macos-window>
16
+ //
17
+ // Attributes:
18
+ // <macos-window> width (900) · height (600) · title ("Folder")
19
+ // <macos-sidebar-item> label · selected
20
+ // <macos-sidebar-header> title
21
+ // <macos-toolbar> title
22
+ // <macos-glass> radius (296) · dark
23
+ // Booleans are by attribute presence. <macos-window> renders its own <macos-sidebar>;
24
+ // you only supply the rows.
25
+ /* END USAGE */
26
+
27
+ (() => {
28
+ // Single quotes around font names, deliberately: this string goes into a
29
+ // style="..." attribute, and a double quote inside the value closes the
30
+ // attribute early — every declaration after it is dropped, which shows up as
31
+ // font sizes falling back to the default.
32
+ const FONT = "-apple-system, BlinkMacSystemFont, 'SF Pro', 'Helvetica Neue', sans-serif";
33
+ /**
34
+ * React version renders text and attributes through JSX, which escapes them. These
35
+ * elements build their shadow with innerHTML template strings, so that protection is
36
+ * gone — every value that comes from an author attribute, from persisted state, or
37
+ * from a Magic Share replay has to go through here. The artboard label matters most:
38
+ * it is renameable, it round-trips through the sidecar, AND a peer can push one in
39
+ * over Magic Share, so an unescaped one is script execution in the follower's page.
40
+ */
41
+ const esc = (v) =>
42
+ String(v ?? '').replace(
43
+ /[&<>"']/g,
44
+ (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch],
45
+ );
46
+
47
+ // Non-numeric input falls through into a style="..." attribute, so it must be
48
+ // escaped too — a quote in it would break out of the attribute.
49
+ const px = (v) => (/^-?\d+(\.\d+)?$/.test(String(v)) ? `${v}px` : esc(v));
50
+
51
+ /**
52
+ * display:contents — the host generates no box of its own, so the shadow's root
53
+ * div becomes the parent's layout child directly. That is what keeps this port
54
+ * pixel-identical to the React version: the JSX component renders that same div
55
+ * with nothing wrapping it. `block` would add an outer box that swallows the
56
+ * flex properties meant for the inner one, showing up as a few pixels of drift
57
+ * in the visual regression.
58
+ */
59
+ /**
60
+ * The React version lives in the light DOM, so its box model comes from the
61
+ * product's own `*` reset (most products set border-box). That reset never
62
+ * reaches into a shadow root, and hardcoding either value gets half the products
63
+ * wrong. `inherit` is wrong too — it passes an inline border-box down to
64
+ * descendants, which a `*` rule would not. Copying the host's computed value is
65
+ * the only thing that matches the React version exactly: the host is a light DOM
66
+ * element, so the product's `*` rule does hit it.
67
+ */
68
+ const hostCss = (el) =>
69
+ '<style>:host{display:contents}*,*::before,*::after{box-sizing:' +
70
+ getComputedStyle(el).boxSizing +
71
+ '}</style>';
72
+
73
+ class Base extends HTMLElement {
74
+ constructor() {
75
+ super();
76
+ this.attachShadow({ mode: 'open' });
77
+ }
78
+ connectedCallback() {
79
+ this._render();
80
+ }
81
+ attributeChangedCallback() {
82
+ if (this.shadowRoot) this._render();
83
+ }
84
+ attr(name, fallback) {
85
+ const v = this.getAttribute(name);
86
+ return v === null || v === '' ? fallback : v;
87
+ }
88
+ }
89
+
90
+ // ───────────────────────────────────────────────────────────
91
+ // Liquid glass primitive — blur + white tint + inset highlight
92
+ // ───────────────────────────────────────────────────────────
93
+ class MacGlass extends Base {
94
+ static get observedAttributes() {
95
+ return ['radius', 'dark'];
96
+ }
97
+ _render() {
98
+ const dark = this.hasAttribute('dark');
99
+ const r = px(this.attr('radius', 296));
100
+ this.shadowRoot.innerHTML =
101
+ hostCss(this) +
102
+ `
103
+ <div part="frame" style="position:relative;border-radius:${r}">
104
+ <div part="glass" style="position:absolute;inset:0;border-radius:${r};
105
+ background:${dark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.35)'};
106
+ backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);
107
+ border:${dark ? '0.5px solid rgba(255,255,255,0.12)' : '0.5px solid rgba(255,255,255,0.6)'};
108
+ box-shadow:${
109
+ dark
110
+ ? '0 8px 40px rgba(0,0,0,0.2)'
111
+ : '0 8px 40px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.4)'
112
+ }"></div>
113
+ <div style="position:relative;z-index:1"><slot></slot></div>
114
+ </div>`;
115
+ }
116
+ }
117
+
118
+ class MacTrafficLights extends Base {
119
+ _render() {
120
+ const dot = (bg) =>
121
+ `<div style="width:14px;height:14px;border-radius:50%;background:${bg};border:0.5px solid rgba(0,0,0,0.1)"></div>`;
122
+ this.shadowRoot.innerHTML =
123
+ hostCss(this) +
124
+ `
125
+ <div part="frame" style="display:flex;gap:9px;align-items:center;padding:1px">
126
+ ${dot('#ff736a')}${dot('#febc2e')}${dot('#19c332')}
127
+ </div>`;
128
+ }
129
+ }
130
+
131
+ // ───────────────────────────────────────────────────────────
132
+ // Toolbar — title + glass pill icons
133
+ // ───────────────────────────────────────────────────────────
134
+ class MacToolbar extends Base {
135
+ static get observedAttributes() {
136
+ return ['title'];
137
+ }
138
+ _render() {
139
+ this.shadowRoot.innerHTML =
140
+ hostCss(this) +
141
+ `
142
+ <div part="frame" style="display:flex;gap:8px;align-items:center;padding:8px;flex-shrink:0">
143
+ <div style="font-family:${FONT};font-size:15px;font-weight:700;color:rgba(0,0,0,0.85);
144
+ white-space:nowrap;padding-left:8px">${esc(this.attr('title', 'Folder'))}</div>
145
+ <div style="flex:1"></div>
146
+ <macos-glass>
147
+ <div style="width:36px;height:36px;display:flex;align-items:center;justify-content:center">
148
+ <div style="width:14px;height:14px;border-radius:50%;background:#4c4c4c;opacity:0.4"></div>
149
+ </div>
150
+ </macos-glass>
151
+ <macos-glass>
152
+ <div style="width:140px;height:36px;display:flex;align-items:center;gap:6px;padding:0 12px">
153
+ <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
154
+ <circle cx="5.5" cy="5.5" r="4" stroke="#727272" stroke-width="1.5"/>
155
+ <path d="M8.5 8.5l3 3" stroke="#727272" stroke-width="1.5" stroke-linecap="round"/>
156
+ </svg>
157
+ <span style="font-family:${FONT};font-size:13px;font-weight:500;color:#727272">Search</span>
158
+ </div>
159
+ </macos-glass>
160
+ </div>`;
161
+ }
162
+ }
163
+
164
+ // ───────────────────────────────────────────────────────────
165
+ // Sidebar — frosted glass panel floating inside the window
166
+ // ───────────────────────────────────────────────────────────
167
+ class MacSidebarItem extends Base {
168
+ static get observedAttributes() {
169
+ return ['label', 'selected'];
170
+ }
171
+ _render() {
172
+ const selected = this.hasAttribute('selected');
173
+ this.shadowRoot.innerHTML =
174
+ hostCss(this) +
175
+ `
176
+ <div part="frame" style="display:flex;align-items:center;gap:6px;height:24px;
177
+ padding:4px 10px 4px 6px;margin:0 10px;border-radius:8px;position:relative;
178
+ font-family:${FONT};font-size:11px;font-weight:500">
179
+ ${selected ? '<div style="position:absolute;inset:0;border-radius:8px;background:rgba(0,0,0,0.11);mix-blend-mode:multiply"></div>' : ''}
180
+ <div style="width:14px;height:14px;border-radius:50%;
181
+ background:${selected ? '#007aff' : 'rgba(0,0,0,0.4)'};opacity:${selected ? 1 : 0.5};
182
+ flex-shrink:0;position:relative"></div>
183
+ <span style="color:rgba(0,0,0,0.85);position:relative">${esc(this.attr('label', ''))}</span>
184
+ </div>`;
185
+ }
186
+ }
187
+
188
+ class MacSidebarHeader extends Base {
189
+ static get observedAttributes() {
190
+ return ['title'];
191
+ }
192
+ _render() {
193
+ this.shadowRoot.innerHTML =
194
+ hostCss(this) +
195
+ `
196
+ <div part="frame" style="padding:14px 18px 5px;font-family:${FONT};font-size:11px;
197
+ font-weight:700;color:rgba(0,0,0,0.5)">${esc(this.attr('title', ''))}</div>`;
198
+ }
199
+ }
200
+
201
+ class MacSidebar extends Base {
202
+ _render() {
203
+ this.shadowRoot.innerHTML =
204
+ hostCss(this) +
205
+ `
206
+ <div part="frame" style="width:220px;height:100%;padding:8px;flex-shrink:0;
207
+ position:relative;display:flex;flex-direction:column">
208
+ <div style="position:absolute;inset:8px;border-radius:18px;
209
+ background:rgba(210,225,245,0.45);
210
+ backdrop-filter:blur(50px) saturate(200%);-webkit-backdrop-filter:blur(50px) saturate(200%);
211
+ border:0.5px solid rgba(255,255,255,0.5);
212
+ box-shadow:0 8px 40px rgba(0,0,0,0.10), inset 0 1px 0 rgba(255,255,255,0.35)"></div>
213
+ <div style="position:relative;z-index:1;padding:10px 0;display:flex;flex-direction:column;gap:2px">
214
+ <div style="height:32px;display:flex;align-items:center;justify-content:space-between;
215
+ padding:0 10px;margin-bottom:4px">
216
+ <macos-traffic-lights></macos-traffic-lights>
217
+ </div>
218
+ <slot></slot>
219
+ </div>
220
+ </div>`;
221
+ }
222
+ }
223
+
224
+ // ───────────────────────────────────────────────────────────
225
+ // Window — r:26, big shadow, sidebar + toolbar + content
226
+ // ───────────────────────────────────────────────────────────
227
+ class MacWindow extends Base {
228
+ static get observedAttributes() {
229
+ return ['width', 'height', 'title'];
230
+ }
231
+ _render() {
232
+ this.shadowRoot.innerHTML =
233
+ hostCss(this) +
234
+ `
235
+ <div part="frame" style="width:${px(this.attr('width', 900))};height:${px(this.attr('height', 600))};
236
+ border-radius:26px;overflow:hidden;background:#fff;border:1px solid #DEE0E3;
237
+ display:flex;position:relative;font-family:${FONT}">
238
+ <!-- This element renders the sidebar panel; the author supplies only the rows.
239
+ Same shape as the React version's <MacSidebar>{sidebar}</MacSidebar>. -->
240
+ <macos-sidebar><slot name="sidebar"></slot></macos-sidebar>
241
+ <div style="flex:1;display:flex;flex-direction:column">
242
+ <macos-toolbar title="${esc(this.attr('title', 'Folder'))}"></macos-toolbar>
243
+ <div style="flex:1;overflow:auto;padding:4px 8px"><slot></slot></div>
244
+ </div>
245
+ </div>`;
246
+ }
247
+ }
248
+
249
+ for (const [tag, ctor] of [
250
+ ['macos-glass', MacGlass],
251
+ ['macos-traffic-lights', MacTrafficLights],
252
+ ['macos-toolbar', MacToolbar],
253
+ ['macos-sidebar-item', MacSidebarItem],
254
+ ['macos-sidebar-header', MacSidebarHeader],
255
+ ['macos-sidebar', MacSidebar],
256
+ ['macos-window', MacWindow],
257
+ ]) {
258
+ if (!customElements.get(tag)) customElements.define(tag, ctor);
259
+ }
260
+ })();