@lark-apaas/coding-registry-buildless 0.1.21 → 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/design-canvas.jsx +5 -5
- 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 +9 -1
- package/scripts/design-canvas.js +2189 -0
- package/scripts/ios-frame.js +338 -0
- package/scripts/macos-window.js +260 -0
|
@@ -684,7 +684,7 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
684
684
|
// ticks leave scale unchanged — skip the cross-frame post for those.
|
|
685
685
|
if (lastPostedScale.current !== scale) {
|
|
686
686
|
lastPostedScale.current = scale;
|
|
687
|
-
window.parent.postMessage({ type: 'miaoda:canvas:zoom', scale }, '*');
|
|
687
|
+
try { window.parent.postMessage({ type: 'miaoda:canvas:zoom', scale }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
688
688
|
}
|
|
689
689
|
// A replayed viewport is the presenter's framing, not this viewer's — it
|
|
690
690
|
// must not overwrite the viewport this end will restore when opening the
|
|
@@ -1057,11 +1057,11 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
1057
1057
|
// images/fonts is after our mount-time announce, so re-announce.
|
|
1058
1058
|
// Clear the pan-tick guard so apply() re-posts the current scale
|
|
1059
1059
|
// even if it's unchanged — the host just reset dcScale to 1.
|
|
1060
|
-
window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*');
|
|
1060
|
+
try { window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
1061
1061
|
// Re-announce the upgrade capability too — the host's readyGen reset
|
|
1062
1062
|
// (fires on iframe 'load', which for image/font-heavy canvases lands
|
|
1063
1063
|
// after the mount-time announce) drops prior state, so re-declare here.
|
|
1064
|
-
window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*');
|
|
1064
|
+
try { window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
1065
1065
|
lastPostedScale.current = undefined;
|
|
1066
1066
|
// The re-announce apply() must not change who owns the view: a
|
|
1067
1067
|
// viewport seed can land BEFORE the iframe's native 'load' fires this
|
|
@@ -1106,13 +1106,13 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
1106
1106
|
// lastPostedScale reset mirrors the miaoda:canvas:probe handler: the layout
|
|
1107
1107
|
// effect's restore-path apply() may already have posted the restored
|
|
1108
1108
|
// scale (before miaoda:canvas:present), so clear the guard to re-post it in order.
|
|
1109
|
-
window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*');
|
|
1109
|
+
try { window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
1110
1110
|
// Generic "upgrade" capability signal — announce that this artifact offers
|
|
1111
1111
|
// an upgrade entry so the host can show/hide its upgrade button. Decoupled
|
|
1112
1112
|
// from the canvas zoom-proxy protocol above; any product may post the same
|
|
1113
1113
|
// message. Listener (onHostMsg) is already registered above → announce-after-
|
|
1114
1114
|
// register order holds. No client-side behavior here.
|
|
1115
|
-
window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*');
|
|
1115
|
+
try { window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
1116
1116
|
lastPostedScale.current = undefined;
|
|
1117
1117
|
apply();
|
|
1118
1118
|
|
|
@@ -190,7 +190,7 @@ function useTweaks(defaults) {
|
|
|
190
190
|
const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
|
|
191
191
|
? keyOrEdits : { [keyOrEdits]: val };
|
|
192
192
|
setValues((prev) => ({ ...prev, ...edits }));
|
|
193
|
-
window.parent.postMessage({ type: 'miaoda:tweaks:set-keys', edits }, '*');
|
|
193
|
+
try { window.parent.postMessage({ type: 'miaoda:tweaks:set-keys', edits }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
194
194
|
// Same-window signal so in-page listeners (deck-stage rail thumbnails)
|
|
195
195
|
// can react — the parent message only reaches the host, not peers.
|
|
196
196
|
window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
|
|
@@ -248,13 +248,13 @@ function TweaksPanel({ title = 'Tweaks', children }) {
|
|
|
248
248
|
else if (t === 'miaoda:tweaks:deactivate') setOpen(false);
|
|
249
249
|
};
|
|
250
250
|
window.addEventListener('message', onMsg);
|
|
251
|
-
window.parent.postMessage({ type: 'miaoda:tweaks:available' }, '*');
|
|
251
|
+
try { window.parent.postMessage({ type: 'miaoda:tweaks:available' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
252
252
|
return () => window.removeEventListener('message', onMsg);
|
|
253
253
|
}, []);
|
|
254
254
|
|
|
255
255
|
const dismiss = () => {
|
|
256
256
|
setOpen(false);
|
|
257
|
-
window.parent.postMessage({ type: 'miaoda:tweaks:dismissed' }, '*');
|
|
257
|
+
try { window.parent.postMessage({ type: 'miaoda:tweaks:dismissed' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
258
258
|
};
|
|
259
259
|
|
|
260
260
|
const onDragStart = (e) => {
|
|
@@ -586,8 +586,11 @@ Object.assign(window, {
|
|
|
586
586
|
document.head.appendChild(s);
|
|
587
587
|
})();
|
|
588
588
|
|
|
589
|
-
const __twkSendChat = (text) =>
|
|
590
|
-
|
|
589
|
+
const __twkSendChat = (text) => {
|
|
590
|
+
// Posting to the host must never take the component down: the parent frame can be
|
|
591
|
+
// cross-origin or already gone, and postMessage throws in both cases.
|
|
592
|
+
try { window.parent.postMessage({ type: 'miaoda:tweaks:chat', text }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
593
|
+
};
|
|
591
594
|
|
|
592
595
|
const __TWK_SPARK_PATH = 'M18.3658 62.2435L36.7823 51.9165L37.0858 51.012L36.7823 50.5083H35.8716L32.7853 50.3206L22.2616 50.0389L13.1546 49.6634L4.30054 49.194L2.07438 48.7246L0 45.9551L0.202378 44.5938L2.07438 43.3264L4.75589 43.5611L10.6755 43.9836L19.5801 44.5938L26.0056 44.9693L35.568 45.9551H37.0858L37.2882 45.3448L36.7823 44.9693L36.3775 44.5938L27.1693 38.3507L17.2022 31.7789L11.9909 27.9767L9.20822 26.0522L7.79157 24.2684L7.18443 20.3254L9.71416 17.5089L13.1546 17.7436L14.0147 17.9783L17.5057 20.654L24.9431 26.4277L34.6573 33.5627L36.0739 34.7362L36.6444 34.3512L36.7317 34.079L36.0739 32.9994L30.8121 23.4704L25.1961 13.7537L22.6664 9.71675L22.0086 7.32277C21.7539 6.31812 21.6039 5.48695 21.6039 4.45938L24.4878 0.516349L26.1068 0L30.0026 0.516349L31.6216 1.92457L34.0502 7.46359L37.9459 16.1476L44.0173 27.9767L45.7881 31.4973L46.7494 34.7362L47.1036 35.722H47.7107V35.1587L48.2166 28.4931L49.1274 20.3254L50.0381 9.81063L50.3416 6.85336L51.8089 3.28586L54.7434 1.36128L57.0201 2.44092L58.8921 5.11655L58.6391 6.85336L57.5261 14.0822L55.3505 25.395L53.9338 32.9994H54.7434L55.7047 32.0136L59.5498 26.944L65.9753 18.8702L68.8086 15.6782L72.1479 12.1577L74.2729 10.4678H78.3204L81.2549 14.8802L79.9395 19.4335L75.7907 24.6909L72.3503 29.1503L67.4173 35.7593L64.3563 41.0732L64.6308 41.5116L65.3682 41.4487L76.499 39.0548L82.5198 37.9751L89.7042 36.7547L92.9423 38.2568L93.2964 39.8058L92.0316 42.9509L84.3412 44.8285L75.3354 46.6592L61.9245 49.8162L61.776 49.9356L61.9513 50.1956L67.9991 50.743L70.5795 50.8839H76.9038L88.6923 51.7757L91.7786 53.7942L93.6 56.282L93.2964 58.2066L88.5405 60.6006L82.1656 59.0985L67.2402 55.531L62.1302 54.2636H61.4218V54.6861L65.6718 58.8638L73.514 65.9049L83.2787 75.0114L83.7846 77.2646L82.5198 79.0483L81.2043 78.8606L72.6032 72.3827L69.264 69.4724L61.776 63.1354H61.2701V63.7926L62.9903 66.3274L72.1479 80.081L72.6032 84.3057L71.9455 85.667L69.5676 86.5119L66.9872 86.0425L61.5736 78.4851L56.0588 70.0357L51.6065 62.4313L51.0687 62.7708L48.419 91.0652L47.2048 92.5204L44.3715 93.6L41.9935 91.8162L40.7286 88.9059L41.9935 83.1322L43.5114 75.6217L44.7256 69.6602L45.8387 62.2435L46.5185 59.7659L46.4584 59.6001L45.9153 59.6914L40.3239 67.3601L31.824 78.8606L25.0949 86.0425L23.4759 86.6997L20.6932 85.2445L20.9462 82.6628L22.5146 80.3627L31.824 68.5336L37.44 61.1639L41.0595 56.9335L41.0243 56.3216L40.8245 56.3046L16.0891 72.4297L11.6874 72.993L9.76476 71.2092L10.0177 68.2989L10.9284 67.3601L18.3658 62.2435Z';
|
|
593
596
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-registry-buildless",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Miaoda registry (buildless runtime) — shadcn-style copy-in SDKs (globals + CDN-pinned, browser Babel) for buildless static HTML stacks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/* BEGIN USAGE */
|
|
2
|
+
// android-frame.js — Simplified Android (Material 3) device frame, as custom elements.
|
|
3
|
+
// Based on Figma M3 spec. No dependencies, no image assets, no React.
|
|
4
|
+
// Defines: <android-device>, <android-status-bar>, <android-list-item>, <android-nav-bar>, <android-keyboard>
|
|
5
|
+
//
|
|
6
|
+
// Usage — wrap your screen content in <android-device> to get the bezel, status bar
|
|
7
|
+
// and gesture nav. Load with a plain <script src="android-frame.js"></script>:
|
|
8
|
+
//
|
|
9
|
+
// <android-device>
|
|
10
|
+
// ...your screen content...
|
|
11
|
+
// </android-device>
|
|
12
|
+
// <android-device dark keyboard>…</android-device>
|
|
13
|
+
// <android-device width="360" height="800">…</android-device> <!-- smaller device size -->
|
|
14
|
+
//
|
|
15
|
+
// Attributes:
|
|
16
|
+
// <android-device> width (412) · height (892) · dark · keyboard
|
|
17
|
+
// <android-status-bar> dark
|
|
18
|
+
// <android-list-item> headline · supporting · leading (one or two chars for the avatar)
|
|
19
|
+
// <android-nav-bar> dark
|
|
20
|
+
// Booleans are by attribute presence: `dark` on, absent off.
|
|
21
|
+
/* END USAGE */
|
|
22
|
+
|
|
23
|
+
(() => {
|
|
24
|
+
const MD_C = {
|
|
25
|
+
surface: '#f4fbf8',
|
|
26
|
+
surfaceVariant: '#dae5e1',
|
|
27
|
+
inverseOnSurface: '#ecf2ef',
|
|
28
|
+
secondaryContainer: '#cde8e1',
|
|
29
|
+
primaryFixedDim: '#83d5c6',
|
|
30
|
+
onSurface: '#171d1b',
|
|
31
|
+
onSurfaceVar: '#49454f',
|
|
32
|
+
onPrimaryContainer: '#00201c',
|
|
33
|
+
primary: '#006a60',
|
|
34
|
+
frameBorder: 'rgba(116,119,117,0.5)',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* React version renders text and attributes through JSX, which escapes them. These
|
|
38
|
+
* elements build their shadow with innerHTML template strings, so that protection is
|
|
39
|
+
* gone — every value that comes from an author attribute, from persisted state, or
|
|
40
|
+
* from a Magic Share replay has to go through here. The artboard label matters most:
|
|
41
|
+
* it is renameable, it round-trips through the sidecar, AND a peer can push one in
|
|
42
|
+
* over Magic Share, so an unescaped one is script execution in the follower's page.
|
|
43
|
+
*/
|
|
44
|
+
const esc = (v) =>
|
|
45
|
+
String(v ?? '').replace(
|
|
46
|
+
/[&<>"']/g,
|
|
47
|
+
(ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Non-numeric input falls through into a style="..." attribute, so it must be
|
|
51
|
+
// escaped too — a quote in it would break out of the attribute.
|
|
52
|
+
const px = (v) => (/^-?\d+(\.\d+)?$/.test(String(v)) ? `${v}px` : esc(v));
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* display:contents — the host generates no box of its own, so the shadow's root
|
|
56
|
+
* div becomes the parent's layout child directly. That is what keeps this port
|
|
57
|
+
* pixel-identical to the React version: the JSX component renders that same div
|
|
58
|
+
* with nothing wrapping it. `block` would add an outer box that swallows the
|
|
59
|
+
* flex properties meant for the inner one, showing up as a few pixels of drift
|
|
60
|
+
* in the visual regression.
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* The React version lives in the light DOM, so its box model comes from the
|
|
64
|
+
* product's own `*` reset (most products set border-box). That reset never
|
|
65
|
+
* reaches into a shadow root, and hardcoding either value gets half the products
|
|
66
|
+
* wrong. `inherit` is wrong too — it passes an inline border-box down to
|
|
67
|
+
* descendants, which a `*` rule would not. Copying the host's computed value is
|
|
68
|
+
* the only thing that matches the React version exactly: the host is a light DOM
|
|
69
|
+
* element, so the product's `*` rule does hit it.
|
|
70
|
+
*/
|
|
71
|
+
const hostCss = (el) =>
|
|
72
|
+
'<style>:host{display:contents}*,*::before,*::after{box-sizing:' +
|
|
73
|
+
getComputedStyle(el).boxSizing +
|
|
74
|
+
'}</style>';
|
|
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
|
+
if (this.shadowRoot) this._render();
|
|
86
|
+
}
|
|
87
|
+
attr(name, fallback) {
|
|
88
|
+
const v = this.getAttribute(name);
|
|
89
|
+
return v === null || v === '' ? fallback : v;
|
|
90
|
+
}
|
|
91
|
+
get dark() {
|
|
92
|
+
return this.hasAttribute('dark');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ───────────────────────────────────────────────────────────
|
|
97
|
+
// Status bar (time left, wifi/cell/battery right)
|
|
98
|
+
// ───────────────────────────────────────────────────────────
|
|
99
|
+
class AndroidStatusBar extends Base {
|
|
100
|
+
static get observedAttributes() {
|
|
101
|
+
return ['dark'];
|
|
102
|
+
}
|
|
103
|
+
_render() {
|
|
104
|
+
const c = this.dark ? '#fff' : MD_C.onSurface;
|
|
105
|
+
this.shadowRoot.innerHTML =
|
|
106
|
+
hostCss(this) +
|
|
107
|
+
`
|
|
108
|
+
<div part="frame" style="height:40px;display:flex;align-items:center;justify-content:space-between;
|
|
109
|
+
padding:0 16px;position:relative;font-family:Roboto,system-ui,sans-serif">
|
|
110
|
+
<div style="width:128px;display:flex;align-items:center;gap:8px">
|
|
111
|
+
<span style="font-size:14px;font-weight:400;letter-spacing:0.25px;line-height:20px;color:${c}">9:30</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div style="position:absolute;left:50%;top:8px;transform:translateX(-50%);
|
|
114
|
+
width:24px;height:24px;border-radius:100px;background:#2e2e2e"></div>
|
|
115
|
+
<div style="display:flex;align-items:center">
|
|
116
|
+
<div style="display:flex;padding-right:2px">
|
|
117
|
+
<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right:-2px">
|
|
118
|
+
<path d="M8 13.3L.67 5.97a10.37 10.37 0 0114.66 0L8 13.3z" fill="${c}"/>
|
|
119
|
+
</svg>
|
|
120
|
+
<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right:-2px">
|
|
121
|
+
<path d="M14.67 14.67V1.33L1.33 14.67h13.34z" fill="${c}"/>
|
|
122
|
+
</svg>
|
|
123
|
+
</div>
|
|
124
|
+
<svg width="16" height="16" viewBox="0 0 16 16">
|
|
125
|
+
<rect x="3.75" y="2" width="8.5" height="13" rx="1.5" fill="${c}"/>
|
|
126
|
+
<rect x="5.5" y="0.9" width="5" height="2" rx="0.5" fill="${c}"/>
|
|
127
|
+
</svg>
|
|
128
|
+
</div>
|
|
129
|
+
</div>`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ───────────────────────────────────────────────────────────
|
|
134
|
+
// List item (Material 3)
|
|
135
|
+
// ───────────────────────────────────────────────────────────
|
|
136
|
+
class AndroidListItem extends Base {
|
|
137
|
+
static get observedAttributes() {
|
|
138
|
+
return ['headline', 'supporting', 'leading'];
|
|
139
|
+
}
|
|
140
|
+
_render() {
|
|
141
|
+
const leading = this.attr('leading', '');
|
|
142
|
+
const supporting = this.attr('supporting', '');
|
|
143
|
+
this.shadowRoot.innerHTML =
|
|
144
|
+
hostCss(this) +
|
|
145
|
+
`
|
|
146
|
+
<div part="frame" style="display:flex;align-items:center;gap:16px;padding:12px 16px;
|
|
147
|
+
min-height:56px;box-sizing:border-box;font-family:Roboto,system-ui,sans-serif">
|
|
148
|
+
${
|
|
149
|
+
leading
|
|
150
|
+
? `<div style="width:40px;height:40px;border-radius:50%;background:${MD_C.primary};color:#fff;
|
|
151
|
+
display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:500;flex-shrink:0">${leading}</div>`
|
|
152
|
+
: ''
|
|
153
|
+
}
|
|
154
|
+
<div style="flex:1;min-width:0">
|
|
155
|
+
<div style="font-size:16px;color:${MD_C.onSurface};line-height:24px">${esc(this.attr('headline', ''))}</div>
|
|
156
|
+
${supporting ? `<div style="font-size:14px;color:${MD_C.onSurfaceVar};line-height:20px">${esc(supporting)}</div>` : ''}
|
|
157
|
+
</div>
|
|
158
|
+
</div>`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ───────────────────────────────────────────────────────────
|
|
163
|
+
// Gesture nav bar (pill)
|
|
164
|
+
// ───────────────────────────────────────────────────────────
|
|
165
|
+
class AndroidNavBar extends Base {
|
|
166
|
+
static get observedAttributes() {
|
|
167
|
+
return ['dark'];
|
|
168
|
+
}
|
|
169
|
+
_render() {
|
|
170
|
+
this.shadowRoot.innerHTML =
|
|
171
|
+
hostCss(this) +
|
|
172
|
+
`
|
|
173
|
+
<div part="frame" style="height:24px;display:flex;align-items:center;justify-content:center">
|
|
174
|
+
<div style="width:108px;height:4px;border-radius:2px;
|
|
175
|
+
background:${this.dark ? '#fff' : MD_C.onSurface};opacity:0.4"></div>
|
|
176
|
+
</div>`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ───────────────────────────────────────────────────────────
|
|
181
|
+
// Keyboard — Gboard (Material 3)
|
|
182
|
+
// ───────────────────────────────────────────────────────────
|
|
183
|
+
class AndroidKeyboard extends Base {
|
|
184
|
+
_render() {
|
|
185
|
+
const key = (l, { flex = 1, bg = MD_C.surface, r = 6, minW, fs = 21 } = {}) =>
|
|
186
|
+
`<div style="height:46px;border-radius:${r}px;flex:${flex};${minW ? `min-width:${minW}px;` : ''}
|
|
187
|
+
background:${bg};display:flex;align-items:center;justify-content:center;
|
|
188
|
+
font-family:Roboto,system-ui;font-size:${fs}px;color:${MD_C.onPrimaryContainer}">${l}</div>`;
|
|
189
|
+
const row = (keys, style = '') =>
|
|
190
|
+
`<div style="display:flex;gap:6px;justify-content:center;${style}">${keys.map((l) => key(l)).join('')}</div>`;
|
|
191
|
+
|
|
192
|
+
this.shadowRoot.innerHTML =
|
|
193
|
+
hostCss(this) +
|
|
194
|
+
`
|
|
195
|
+
<div part="frame" style="background:${MD_C.inverseOnSurface};padding:0 8px 8px;
|
|
196
|
+
display:flex;flex-direction:column;gap:4px">
|
|
197
|
+
<div style="height:44px"></div>
|
|
198
|
+
<div style="display:flex;flex-direction:column;gap:12px">
|
|
199
|
+
${row(['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'])}
|
|
200
|
+
${row(['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], 'padding:0 20px;')}
|
|
201
|
+
<div style="display:flex;gap:6px">
|
|
202
|
+
${key('', { bg: MD_C.surfaceVariant })}
|
|
203
|
+
<div style="display:flex;gap:6px;flex:7;min-width:274px">
|
|
204
|
+
${['z', 'x', 'c', 'v', 'b', 'n', 'm'].map((l) => key(l)).join('')}
|
|
205
|
+
</div>
|
|
206
|
+
${key('', { bg: MD_C.surfaceVariant })}
|
|
207
|
+
</div>
|
|
208
|
+
<div style="display:flex;gap:6px">
|
|
209
|
+
${key('?123', { bg: MD_C.secondaryContainer, r: 100, minW: 58, fs: 14 })}
|
|
210
|
+
${key(',', { bg: MD_C.surfaceVariant })}
|
|
211
|
+
${key('', { flex: 3, minW: 154 })}
|
|
212
|
+
${key('.', { bg: MD_C.surfaceVariant })}
|
|
213
|
+
${key('', { bg: MD_C.primaryFixedDim, r: 100, minW: 58 })}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ───────────────────────────────────────────────────────────
|
|
221
|
+
// Device frame — wraps everything
|
|
222
|
+
// ───────────────────────────────────────────────────────────
|
|
223
|
+
class AndroidDevice extends Base {
|
|
224
|
+
static get observedAttributes() {
|
|
225
|
+
return ['width', 'height', 'dark', 'keyboard'];
|
|
226
|
+
}
|
|
227
|
+
_render() {
|
|
228
|
+
const dark = this.dark;
|
|
229
|
+
const d = dark ? ' dark' : '';
|
|
230
|
+
this.shadowRoot.innerHTML =
|
|
231
|
+
hostCss(this) +
|
|
232
|
+
`
|
|
233
|
+
<div part="frame" style="width:${px(this.attr('width', 412))};height:${px(this.attr('height', 892))};
|
|
234
|
+
border-radius:18px;overflow:hidden;background:${dark ? '#1d1b20' : MD_C.surface};
|
|
235
|
+
border:8px solid ${MD_C.frameBorder};display:flex;flex-direction:column;box-sizing:border-box">
|
|
236
|
+
<android-status-bar${d}></android-status-bar>
|
|
237
|
+
<div style="flex:1;overflow:auto"><slot></slot></div>
|
|
238
|
+
${this.hasAttribute('keyboard') ? '<android-keyboard></android-keyboard>' : ''}
|
|
239
|
+
<android-nav-bar${d}></android-nav-bar>
|
|
240
|
+
</div>`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const [tag, ctor] of [
|
|
245
|
+
['android-status-bar', AndroidStatusBar],
|
|
246
|
+
['android-list-item', AndroidListItem],
|
|
247
|
+
['android-nav-bar', AndroidNavBar],
|
|
248
|
+
['android-keyboard', AndroidKeyboard],
|
|
249
|
+
['android-device', AndroidDevice],
|
|
250
|
+
]) {
|
|
251
|
+
if (!customElements.get(tag)) customElements.define(tag, ctor);
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/* BEGIN USAGE */
|
|
2
|
+
// browser-window.js — Simplified Chrome browser window (dark theme, macOS), as custom elements.
|
|
3
|
+
// No dependencies, no image assets, no React. All inline styles + inline SVG.
|
|
4
|
+
// Defines: <browser-window>, <browser-tab-bar>, <browser-toolbar>, <browser-tab>, <browser-traffic-lights>
|
|
5
|
+
//
|
|
6
|
+
// Usage — wrap your page content in <browser-window> to get the tab bar + URL bar.
|
|
7
|
+
// Tabs are light-DOM children; the window marks the active one from `active-index`:
|
|
8
|
+
//
|
|
9
|
+
// <browser-window width="1100" height="680" url="acme.design/pricing" active-index="0">
|
|
10
|
+
// <browser-tab title="Pricing"></browser-tab>
|
|
11
|
+
// <browser-tab title="Docs"></browser-tab>
|
|
12
|
+
// ...your page content...
|
|
13
|
+
// </browser-window>
|
|
14
|
+
//
|
|
15
|
+
// Attributes:
|
|
16
|
+
// <browser-window> width (900) · height (600) · url ("example.com") · active-index (0)
|
|
17
|
+
// <browser-tab> title ("New Tab") · active
|
|
18
|
+
// <browser-toolbar> url
|
|
19
|
+
// With no <browser-tab> children the window renders a single "New Tab".
|
|
20
|
+
// `active` on a tab is for authoring; the parent rewrites data-active on every render,
|
|
21
|
+
// so when both are present the parent wins.
|
|
22
|
+
/* END USAGE */
|
|
23
|
+
|
|
24
|
+
(() => {
|
|
25
|
+
const C = {
|
|
26
|
+
barBg: '#202124',
|
|
27
|
+
tabBg: '#35363a',
|
|
28
|
+
text: '#e8eaed',
|
|
29
|
+
dim: '#9aa0a6',
|
|
30
|
+
urlBg: '#282a2d',
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* React version renders text and attributes through JSX, which escapes them. These
|
|
34
|
+
* elements build their shadow with innerHTML template strings, so that protection is
|
|
35
|
+
* gone — every value that comes from an author attribute, from persisted state, or
|
|
36
|
+
* from a Magic Share replay has to go through here. The artboard label matters most:
|
|
37
|
+
* it is renameable, it round-trips through the sidecar, AND a peer can push one in
|
|
38
|
+
* over Magic Share, so an unescaped one is script execution in the follower's page.
|
|
39
|
+
*/
|
|
40
|
+
const esc = (v) =>
|
|
41
|
+
String(v ?? '').replace(
|
|
42
|
+
/[&<>"']/g,
|
|
43
|
+
(ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Non-numeric input falls through into a style="..." attribute, so it must be
|
|
47
|
+
// escaped too — a quote in it would break out of the attribute.
|
|
48
|
+
const px = (v) => (/^-?\d+(\.\d+)?$/.test(String(v)) ? `${v}px` : esc(v));
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* display:contents — the host generates no box of its own, so the shadow's root
|
|
52
|
+
* div becomes the parent's layout child directly. That is what keeps this port
|
|
53
|
+
* pixel-identical to the React version: the JSX component renders that same div
|
|
54
|
+
* with nothing wrapping it. `block` would add an outer box that swallows the
|
|
55
|
+
* flex properties meant for the inner one, showing up as a few pixels of drift
|
|
56
|
+
* in the visual regression.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* The React version lives in the light DOM, so its box model comes from the
|
|
60
|
+
* product's own `*` reset (most products set border-box). That reset never
|
|
61
|
+
* reaches into a shadow root, and hardcoding either value gets half the products
|
|
62
|
+
* wrong. `inherit` is wrong too — it passes an inline border-box down to
|
|
63
|
+
* descendants, which a `*` rule would not. Copying the host's computed value is
|
|
64
|
+
* the only thing that matches the React version exactly: the host is a light DOM
|
|
65
|
+
* element, so the product's `*` rule does hit it.
|
|
66
|
+
*/
|
|
67
|
+
const hostCss = (el) =>
|
|
68
|
+
'<style>:host{display:contents}*,*::before,*::after{box-sizing:' +
|
|
69
|
+
getComputedStyle(el).boxSizing +
|
|
70
|
+
'}</style>';
|
|
71
|
+
|
|
72
|
+
class Base extends HTMLElement {
|
|
73
|
+
constructor() {
|
|
74
|
+
super();
|
|
75
|
+
this.attachShadow({ mode: 'open' });
|
|
76
|
+
}
|
|
77
|
+
connectedCallback() {
|
|
78
|
+
this._render();
|
|
79
|
+
}
|
|
80
|
+
attributeChangedCallback() {
|
|
81
|
+
if (this.shadowRoot) this._render();
|
|
82
|
+
}
|
|
83
|
+
attr(name, fallback) {
|
|
84
|
+
const v = this.getAttribute(name);
|
|
85
|
+
return v === null || v === '' ? fallback : v;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class ChromeTrafficLights extends Base {
|
|
90
|
+
_render() {
|
|
91
|
+
const dot = (bg) =>
|
|
92
|
+
`<div style="width:12px;height:12px;border-radius:50%;background:${bg}"></div>`;
|
|
93
|
+
this.shadowRoot.innerHTML =
|
|
94
|
+
hostCss(this) +
|
|
95
|
+
`
|
|
96
|
+
<div part="frame" style="display:flex;gap:8px;padding:0 14px">
|
|
97
|
+
${dot('#ff5f57')}${dot('#febc2e')}${dot('#28c840')}
|
|
98
|
+
</div>`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class ChromeTab extends Base {
|
|
103
|
+
static get observedAttributes() {
|
|
104
|
+
return ['title', 'active', 'data-active'];
|
|
105
|
+
}
|
|
106
|
+
_render() {
|
|
107
|
+
// The parent rewrites data-active on every reflow; an author-written `active`
|
|
108
|
+
// only takes effect when there is no parent to override it.
|
|
109
|
+
const active = this.hasAttribute('data-active') || this.hasAttribute('active');
|
|
110
|
+
const curve = (flip) => `
|
|
111
|
+
<svg width="8" height="10" viewBox="0 0 8 10"
|
|
112
|
+
style="position:absolute;bottom:0;${flip ? 'right' : 'left'}:-8px;${flip ? 'transform:scaleX(-1);' : ''}">
|
|
113
|
+
<path d="M0 10C2 9 6 8 8 0V10H0Z" fill="${C.tabBg}"/>
|
|
114
|
+
</svg>`;
|
|
115
|
+
this.shadowRoot.innerHTML =
|
|
116
|
+
hostCss(this) +
|
|
117
|
+
`
|
|
118
|
+
<div part="frame" style="position:relative;height:34px;align-self:flex-end;padding:0 12px;
|
|
119
|
+
display:flex;align-items:center;gap:8px;
|
|
120
|
+
background:${active ? C.tabBg : 'transparent'};border-radius:8px 8px 0 0;
|
|
121
|
+
min-width:120px;max-width:220px;font-family:system-ui,sans-serif;font-size:12px;
|
|
122
|
+
color:${active ? C.text : C.dim}">
|
|
123
|
+
${active ? curve(false) + curve(true) : ''}
|
|
124
|
+
<div style="width:14px;height:14px;border-radius:50%;background:#5f6368;flex-shrink:0"></div>
|
|
125
|
+
<span style="flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(this.attr('title', 'New Tab'))}</span>
|
|
126
|
+
</div>`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Project active-index onto data-active on the children.
|
|
132
|
+
*
|
|
133
|
+
* Same shape as how deck-stage marks its slides: list state is written by the
|
|
134
|
+
* parent onto light DOM children, so authors and tests can read it without
|
|
135
|
+
* piercing a shadow root.
|
|
136
|
+
*/
|
|
137
|
+
function markTabs(host) {
|
|
138
|
+
const tabs = [...host.querySelectorAll(':scope > browser-tab')];
|
|
139
|
+
const idx = Number(host.getAttribute('active-index') ?? 0);
|
|
140
|
+
tabs.forEach((t, i) => t.toggleAttribute('data-active', i === idx));
|
|
141
|
+
return tabs;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Supply one default tab when the author wrote none, matching the React
|
|
145
|
+
* version's default `tabs` value. */
|
|
146
|
+
function ensureDefaultTab(host, slotName) {
|
|
147
|
+
if (host.querySelector(':scope > browser-tab')) return;
|
|
148
|
+
const t = document.createElement('browser-tab');
|
|
149
|
+
t.setAttribute('title', 'New Tab');
|
|
150
|
+
if (slotName) t.setAttribute('slot', slotName);
|
|
151
|
+
t.toggleAttribute('data-active', true);
|
|
152
|
+
host.appendChild(t);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
class ChromeTabBar extends Base {
|
|
156
|
+
static get observedAttributes() {
|
|
157
|
+
return ['active-index'];
|
|
158
|
+
}
|
|
159
|
+
_render() {
|
|
160
|
+
this.shadowRoot.innerHTML =
|
|
161
|
+
hostCss(this) +
|
|
162
|
+
`
|
|
163
|
+
<div part="frame" style="display:flex;align-items:center;height:44px;background:${C.barBg};padding-right:8px">
|
|
164
|
+
<browser-traffic-lights></browser-traffic-lights>
|
|
165
|
+
<div style="display:flex;align-items:flex-end;height:100%;padding-left:4px;flex:1"><slot></slot></div>
|
|
166
|
+
</div>`;
|
|
167
|
+
ensureDefaultTab(this);
|
|
168
|
+
markTabs(this);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
class ChromeToolbar extends Base {
|
|
173
|
+
static get observedAttributes() {
|
|
174
|
+
return ['url'];
|
|
175
|
+
}
|
|
176
|
+
_render() {
|
|
177
|
+
const dot = `<div style="width:28px;height:28px;display:flex;align-items:center;justify-content:center">
|
|
178
|
+
<div style="width:16px;height:16px;border-radius:50%;background:${C.dim};opacity:0.4"></div>
|
|
179
|
+
</div>`;
|
|
180
|
+
this.shadowRoot.innerHTML =
|
|
181
|
+
hostCss(this) +
|
|
182
|
+
`
|
|
183
|
+
<div part="frame" style="height:40px;background:${C.tabBg};display:flex;align-items:center;gap:4px;padding:0 8px">
|
|
184
|
+
${dot}
|
|
185
|
+
<div style="flex:1;height:30px;border-radius:15px;background:${C.urlBg};display:flex;
|
|
186
|
+
align-items:center;gap:8px;padding:0 14px;margin:0 6px">
|
|
187
|
+
<div style="width:12px;height:12px;border-radius:50%;background:${C.dim};opacity:0.4"></div>
|
|
188
|
+
<span style="flex:1;color:${C.text};font-size:13px;font-family:system-ui,sans-serif">${esc(this.attr('url', 'example.com'))}</span>
|
|
189
|
+
</div>
|
|
190
|
+
${dot}
|
|
191
|
+
</div>`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
class ChromeWindow extends Base {
|
|
196
|
+
static get observedAttributes() {
|
|
197
|
+
return ['width', 'height', 'url', 'active-index'];
|
|
198
|
+
}
|
|
199
|
+
connectedCallback() {
|
|
200
|
+
this._render();
|
|
201
|
+
if (!this._mo) {
|
|
202
|
+
// Re-mark after the author adds or removes a tab — same reason deck-stage
|
|
203
|
+
// watches for slide changes.
|
|
204
|
+
this._mo = new MutationObserver(() => this._markOwnTabs());
|
|
205
|
+
this._mo.observe(this, { childList: true });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
disconnectedCallback() {
|
|
209
|
+
this._mo?.disconnect();
|
|
210
|
+
this._mo = null;
|
|
211
|
+
}
|
|
212
|
+
_markOwnTabs() {
|
|
213
|
+
for (const t of this.querySelectorAll(':scope > browser-tab')) t.setAttribute('slot', 'tab');
|
|
214
|
+
ensureDefaultTab(this, 'tab');
|
|
215
|
+
markTabs(this);
|
|
216
|
+
}
|
|
217
|
+
_render() {
|
|
218
|
+
this.shadowRoot.innerHTML =
|
|
219
|
+
hostCss(this) +
|
|
220
|
+
`
|
|
221
|
+
<div part="frame" style="width:${px(this.attr('width', 900))};height:${px(this.attr('height', 600))};
|
|
222
|
+
border-radius:10px;overflow:hidden;border:1px solid #DEE0E3;
|
|
223
|
+
display:flex;flex-direction:column;background:${C.tabBg}">
|
|
224
|
+
<div style="display:flex;align-items:center;height:44px;background:${C.barBg};padding-right:8px">
|
|
225
|
+
<browser-traffic-lights></browser-traffic-lights>
|
|
226
|
+
<div style="display:flex;align-items:flex-end;height:100%;padding-left:4px;flex:1">
|
|
227
|
+
<slot name="tab"></slot>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<browser-toolbar url="${esc(this.attr('url', 'example.com'))}"></browser-toolbar>
|
|
231
|
+
<div style="flex:1;background:#fff;overflow:auto"><slot></slot></div>
|
|
232
|
+
</div>`;
|
|
233
|
+
this._markOwnTabs();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const [tag, ctor] of [
|
|
238
|
+
['browser-traffic-lights', ChromeTrafficLights],
|
|
239
|
+
['browser-tab', ChromeTab],
|
|
240
|
+
['browser-tab-bar', ChromeTabBar],
|
|
241
|
+
['browser-toolbar', ChromeToolbar],
|
|
242
|
+
['browser-window', ChromeWindow],
|
|
243
|
+
]) {
|
|
244
|
+
if (!customElements.get(tag)) customElements.define(tag, ctor);
|
|
245
|
+
}
|
|
246
|
+
})();
|
package/scripts/deck-stage.js
CHANGED
|
@@ -224,7 +224,15 @@
|
|
|
224
224
|
* reuses. It costs one thing — render() also resets that img's `sizes`,
|
|
225
225
|
* discarding the rail-width hint _materialize sets; the URL still matches the
|
|
226
226
|
* slide's, so the thumb reuses the slide's decoded bitmap. */
|
|
227
|
-
|
|
227
|
+
/* The device frames (ios-/android-/macos-/browser-) get in on the *other* half
|
|
228
|
+
* of the rule. Their content lives in a shadow root, so cloneNode captures none
|
|
229
|
+
* of it — for them re-mounting is not "adds nothing", it is the only way to get
|
|
230
|
+
* the content at all. They do satisfy clause 2: purely presentational, read
|
|
231
|
+
* attributes and paint the shadow, no storage and no messages.
|
|
232
|
+
* design-canvas is deliberately NOT on this list — it reads localStorage,
|
|
233
|
+
* fetches its sidecar and posts to the host on mount, so running it once per
|
|
234
|
+
* thumbnail would corrupt the real state. */
|
|
235
|
+
const KEEP_LIVE_PREFIXES = ['cd-', 'ios-', 'android-', 'macos-', 'browser-'];
|
|
228
236
|
const keepsCloneLive = (el) => KEEP_LIVE_PREFIXES.some((p) => el.localName.startsWith(p));
|
|
229
237
|
|
|
230
238
|
/* Why a clone needs a substrate rule of its own: _collectAuthorCss samples
|