@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
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
|
+
})();
|