@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.
- package/components/animations.jsx +173 -6
- package/components/design-canvas.jsx +260 -14
- package/components/tweaks-panel.jsx +8 -5
- package/package.json +1 -1
- package/scripts/android-frame.js +253 -0
- package/scripts/browser-window.js +246 -0
- package/scripts/deck-stage.js +176 -6
- package/scripts/design-canvas.js +2189 -0
- package/scripts/ios-frame.js +338 -0
- package/scripts/macos-window.js +260 -0
|
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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
|
+
})();
|