@lark-apaas/coding-registry-buildless 0.1.11-alpha.20260729134521 → 0.1.11-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -847,6 +847,10 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
847
847
  // Clear the pan-tick guard so apply() re-posts the current scale
848
848
  // even if it's unchanged — the host just reset dcScale to 1.
849
849
  window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*');
850
+ // Re-announce the upgrade capability too — the host's readyGen reset
851
+ // (fires on iframe 'load', which for image/font-heavy canvases lands
852
+ // after the mount-time announce) drops prior state, so re-declare here.
853
+ window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*');
850
854
  lastPostedScale.current = undefined;
851
855
  apply();
852
856
  }
@@ -860,6 +864,12 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
860
864
  // effect's restore-path apply() may already have posted the restored
861
865
  // scale (before miaoda:canvas:present), so clear the guard to re-post it in order.
862
866
  window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*');
867
+ // Generic "upgrade" capability signal — announce that this artifact offers
868
+ // an upgrade entry so the host can show/hide its upgrade button. Decoupled
869
+ // from the canvas zoom-proxy protocol above; any product may post the same
870
+ // message. Listener (onHostMsg) is already registered above → announce-after-
871
+ // register order holds. No client-side behavior here.
872
+ window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*');
863
873
  lastPostedScale.current = undefined;
864
874
  apply();
865
875
 
@@ -141,8 +141,12 @@ function IOSDevice({
141
141
  <div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 10 }}>
142
142
  <IOSStatusBar dark={dark} />
143
143
  </div>
144
- {/* content */}
145
- <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
144
+ {/* content — isolate:isolate keeps the screen in its own stacking
145
+ context so any z-index authored inside it stays *below* the frame
146
+ chrome (status bar z:10, island z:50, home indicator z:60); a
147
+ sticky/opaque header with a high z-index can no longer paint over
148
+ the status bar. */}
149
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column', isolation: 'isolate' }}>
146
150
  <div style={{ flex: 1, overflow: 'auto' }}>{children}</div>
147
151
  {keyboard && <IOSKeyboard dark={dark} />}
148
152
  </div>
@@ -62,107 +62,113 @@
62
62
  /* END USAGE */
63
63
  // ─────────────────────────────────────────────────────────────────────────────
64
64
 
65
+ // Palette follows the Arco/Semi neutral token system the 创意设计模式 "设计风格"
66
+ // panel is designed in (Figma node 2008-22724): a solid white card on neutral
67
+ // grays instead of the previous warm-glass look.
68
+ // text-1 #1F2329 text-2 #646A73 text-3 #86909C
69
+ // border #DEE0E3 fill #EBECEE / #F2F3F5 accent(selected/on) #1F2329
65
70
  const __TWEAKS_STYLE = `
66
- .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
71
+ .twk-panel{position:fixed;right:16px;top:16px;z-index:2147483646;width:280px;
67
72
  max-height:calc(100vh - 32px);display:flex;flex-direction:column;
68
- transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
69
- background:rgba(250,249,247,.78);color:#29261b;
70
- -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
71
- border:.5px solid rgba(255,255,255,.6);border-radius:14px;
72
- box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
73
- font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
73
+ transform:scale(var(--dc-inv-zoom,1));transform-origin:top right;
74
+ background:#fff;color:#1F2329;
75
+ border:.5px solid #DEE0E3;border-radius:12px;
76
+ box-shadow:0 8px 24px 8px rgba(0,0,0,.04),0 6px 12px rgba(0,0,0,.04),0 2px 6px rgba(0,0,0,.04);
77
+ font:12px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
74
78
  .twk-hd{display:flex;align-items:center;justify-content:space-between;
75
- padding:10px 8px 10px 14px;cursor:move;user-select:none}
76
- .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
77
- .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
78
- width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
79
- .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
80
- .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
79
+ padding:12px 12px 12px 16px;cursor:move;user-select:none;
80
+ border-bottom:.5px solid #EBECEE}
81
+ .twk-hd b{font-size:14px;font-weight:600;letter-spacing:.01em;color:#1F2329}
82
+ .twk-x{appearance:none;border:0;background:transparent;color:#86909C;
83
+ width:24px;height:24px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
84
+ .twk-x:hover{background:#F2F3F5;color:#1F2329}
85
+ .twk-body{padding:14px 16px 16px;display:flex;flex-direction:column;gap:14px;
81
86
  overflow-y:auto;overflow-x:hidden;min-height:0;
82
- scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
87
+ scrollbar-width:thin;scrollbar-color:#DEE0E3 transparent}
83
88
  .twk-body::-webkit-scrollbar{width:8px}
84
89
  .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
85
- .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
90
+ .twk-body::-webkit-scrollbar-thumb{background:#DEE0E3;border-radius:4px;
86
91
  border:2px solid transparent;background-clip:content-box}
87
- .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
92
+ .twk-body::-webkit-scrollbar-thumb:hover{background:#C9CDD4;
88
93
  border:2px solid transparent;background-clip:content-box}
89
- .twk-row{display:flex;flex-direction:column;gap:5px}
94
+ .twk-row{display:flex;flex-direction:column;gap:8px}
90
95
  .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
91
96
  .twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
92
- color:rgba(41,38,27,.72)}
97
+ color:#1F2329}
93
98
  .twk-lbl>span:first-child{font-weight:500}
94
- .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}
99
+ .twk-val{color:#646A73;font-variant-numeric:tabular-nums}
95
100
 
96
- .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
97
- color:rgba(41,38,27,.45);padding:10px 0 0}
101
+ .twk-sect{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
102
+ color:#86909C;padding:10px 0 0}
98
103
  .twk-sect:first-child{padding-top:0}
99
104
 
100
- .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
101
- border:.5px solid rgba(0,0,0,.1);border-radius:7px;
102
- background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
103
- .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
104
- select.twk-field{padding-right:22px;
105
- background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.5)' d='M0 0h10L5 6z'/></svg>");
106
- background-repeat:no-repeat;background-position:right 8px center}
105
+ .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:32px;padding:0 10px;
106
+ border:.5px solid #DEE0E3;border-radius:8px;
107
+ background:#fff;color:inherit;font:inherit;outline:none}
108
+ .twk-field:focus{border-color:#1F2329;box-shadow:0 0 0 2px rgba(31,35,41,.1)}
109
+ select.twk-field{padding-right:24px;
110
+ background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%234E5969' d='M0 0h10L5 6z'/></svg>");
111
+ background-repeat:no-repeat;background-position:right 10px center}
107
112
 
108
113
  .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
109
- border-radius:999px;background:rgba(0,0,0,.12);outline:none}
114
+ border-radius:999px;background:#EBECEE;outline:none}
110
115
  .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
111
- width:14px;height:14px;border-radius:50%;background:#fff;
112
- border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
113
- .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
114
- background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
116
+ width:16px;height:16px;border-radius:50%;background:#fff;
117
+ border:.5px solid #DEE0E3;box-shadow:0 1px 3px rgba(0,0,0,.16);cursor:default}
118
+ .twk-slider::-moz-range-thumb{width:16px;height:16px;border-radius:50%;
119
+ background:#fff;border:.5px solid #DEE0E3;box-shadow:0 1px 3px rgba(0,0,0,.16);cursor:default}
115
120
 
116
121
  .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
117
- background:rgba(0,0,0,.06);user-select:none}
122
+ background:#EBECEE;user-select:none}
118
123
  .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
119
- background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
124
+ background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.1);
120
125
  transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
121
126
  .twk-seg.dragging .twk-seg-thumb{transition:none}
122
127
  .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
123
- background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
128
+ background:transparent;color:#646A73;font:inherit;font-weight:500;min-height:28px;
124
129
  border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
125
- overflow-wrap:anywhere}
126
-
127
- .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
128
- background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
129
- .twk-toggle[data-on="1"]{background:#34c759}
130
- .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
131
- background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
132
- .twk-toggle[data-on="1"] i{transform:translateX(14px)}
133
-
134
- .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
135
- border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
136
- .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
130
+ overflow-wrap:anywhere;transition:color .15s}
131
+ .twk-seg button[aria-checked="true"]{color:#1F2329}
132
+
133
+ .twk-toggle{position:relative;width:36px;height:20px;border:0;border-radius:999px;
134
+ background:#E4E6E8;transition:background .15s;cursor:default;padding:0}
135
+ .twk-toggle[data-on="1"]{background:#1F2329}
136
+ .twk-toggle i{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;
137
+ background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:transform .15s}
138
+ .twk-toggle[data-on="1"] i{transform:translateX(16px)}
139
+
140
+ .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:32px;padding:0 0 0 10px;
141
+ border:.5px solid #DEE0E3;border-radius:8px;background:#fff}
142
+ .twk-num-lbl{font-weight:500;color:#646A73;cursor:ew-resize;
137
143
  user-select:none;padding-right:8px}
138
144
  .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
139
- font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
145
+ font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 10px 0 0;
140
146
  outline:none;color:inherit;-moz-appearance:textfield}
141
147
  .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
142
148
  -webkit-appearance:none;margin:0}
143
- .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}
149
+ .twk-num-unit{padding-right:10px;color:#86909C}
144
150
 
145
- .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
146
- background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
147
- .twk-btn:hover{background:rgba(0,0,0,.88)}
148
- .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
149
- .twk-btn.secondary:hover{background:rgba(0,0,0,.1)}
151
+ .twk-btn{appearance:none;height:32px;padding:0 14px;border:0;border-radius:8px;
152
+ background:#1F2329;color:#fff;font:inherit;font-weight:500;cursor:default}
153
+ .twk-btn:hover{background:#333A44}
154
+ .twk-btn.secondary{background:#F2F3F5;color:#1F2329}
155
+ .twk-btn.secondary:hover{background:#E5E6EB}
150
156
 
151
- .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
152
- border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
157
+ .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:24px;
158
+ border:.5px solid #DEE0E3;border-radius:6px;padding:0;cursor:default;
153
159
  background:transparent;flex-shrink:0}
154
160
  .twk-swatch::-webkit-color-swatch-wrapper{padding:0}
155
161
  .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
156
162
  .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}
157
163
 
158
- .twk-chips{display:flex;gap:6px}
159
- .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
164
+ .twk-chips{display:flex;gap:8px}
165
+ .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:40px;
160
166
  padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
161
- box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
167
+ box-shadow:0 0 0 .5px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.05);
162
168
  transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
163
169
  .twk-chip:hover{transform:translateY(-1px);
164
- box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
165
- .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
170
+ box-shadow:0 0 0 .5px rgba(0,0,0,.14),0 4px 10px rgba(0,0,0,.1)}
171
+ .twk-chip[data-on="1"]{box-shadow:0 0 0 2px #fff,0 0 0 3.5px rgba(0,0,0,.85),
166
172
  0 2px 6px rgba(0,0,0,.15)}
167
173
  .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
168
174
  display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
@@ -202,6 +208,10 @@ function useTweaks(defaults) {
202
208
  function TweaksPanel({ title = 'Tweaks', children }) {
203
209
  const [open, setOpen] = React.useState(false);
204
210
  const dragRef = React.useRef(null);
211
+ // x = distance from the viewport's right edge, y = distance from its TOP.
212
+ // The panel anchors top-right (right/top in CSS) so it clears the host's
213
+ // bottom toolbar; both axes still grow inward, so clamp/drag math is
214
+ // symmetric with the old bottom-right anchor.
205
215
  const offsetRef = React.useRef({ x: 16, y: 16 });
206
216
  const PAD = 16;
207
217
 
@@ -210,13 +220,13 @@ function TweaksPanel({ title = 'Tweaks', children }) {
210
220
  if (!panel) return;
211
221
  const w = panel.offsetWidth, h = panel.offsetHeight;
212
222
  const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
213
- const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
223
+ const maxTop = Math.max(PAD, window.innerHeight - h - PAD);
214
224
  offsetRef.current = {
215
225
  x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
216
- y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
226
+ y: Math.min(maxTop, Math.max(PAD, offsetRef.current.y)),
217
227
  };
218
228
  panel.style.right = offsetRef.current.x + 'px';
219
- panel.style.bottom = offsetRef.current.y + 'px';
229
+ panel.style.top = offsetRef.current.y + 'px';
220
230
  }, []);
221
231
 
222
232
  React.useEffect(() => {
@@ -253,11 +263,11 @@ function TweaksPanel({ title = 'Tweaks', children }) {
253
263
  const r = panel.getBoundingClientRect();
254
264
  const sx = e.clientX, sy = e.clientY;
255
265
  const startRight = window.innerWidth - r.right;
256
- const startBottom = window.innerHeight - r.bottom;
266
+ const startTop = r.top;
257
267
  const move = (ev) => {
258
268
  offsetRef.current = {
259
269
  x: startRight - (ev.clientX - sx),
260
- y: startBottom - (ev.clientY - sy),
270
+ y: startTop + (ev.clientY - sy),
261
271
  };
262
272
  clampToViewport();
263
273
  };
@@ -274,7 +284,7 @@ function TweaksPanel({ title = 'Tweaks', children }) {
274
284
  <>
275
285
  <style>{__TWEAKS_STYLE}</style>
276
286
  <div ref={dragRef} className="twk-panel" data-miaoda-chrome=""
277
- style={{ right: offsetRef.current.x, bottom: offsetRef.current.y }}>
287
+ style={{ right: offsetRef.current.x, top: offsetRef.current.y }}>
278
288
  <div className="twk-hd" onMouseDown={onDragStart}>
279
289
  <b>{title}</b>
280
290
  <button className="twk-x" aria-label="Close tweaks"
@@ -343,8 +353,8 @@ function TweakRadio({ label, value, options, onChange }) {
343
353
  valueRef.current = value;
344
354
 
345
355
  // Segments wrap mid-word once per-segment width runs out. The track is
346
- // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
347
- // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
356
+ // ~244px (280 panel − 32 body pad − 4 seg pad), each button loses 12px
357
+ // to its own padding, and 12px system-ui averages ~6.6px/char — so 2
348
358
  // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
349
359
  // back to a dropdown rather than wrap.
350
360
  const labelLen = (o) => String(typeof o === 'object' ? o.label : o).length;
@@ -555,23 +565,23 @@ Object.assign(window, {
555
565
  @keyframes twk-blink{50%{opacity:0}}
556
566
  @keyframes twk-fadein{from{opacity:0;transform:translateX(4px)}to{opacity:1;transform:none}}
557
567
  .twk-sugg{display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:8px;
558
- background:rgba(0,0,0,.04);border:.5px solid rgba(0,0,0,.06);transition:all .15s}
559
- .twk-sugg:focus-within{background:rgba(0,0,0,.06);border-color:rgba(0,0,0,.12)}
568
+ background:#F7F8FA;border:.5px solid #EBECEE;transition:all .15s}
569
+ .twk-sugg:focus-within{background:#F2F3F5;border-color:#DEE0E3}
560
570
  .twk-sugg-field{position:relative;flex:1;min-width:0}
561
571
  .twk-sugg-field input{width:100%;height:20px;border:0;background:transparent;
562
572
  font:inherit;outline:none;color:inherit}
563
573
  .twk-sugg-ghost{position:absolute;inset:0;display:flex;align-items:center;
564
- color:rgba(41,38,27,.42);pointer-events:none;white-space:nowrap;overflow:hidden}
565
- .twk-sugg-ghost.hint{color:rgba(41,38,27,.28)}
574
+ color:#86909C;pointer-events:none;white-space:nowrap;overflow:hidden}
575
+ .twk-sugg-ghost.hint{color:#A9AEB8}
566
576
  .twk-sugg-caret{display:inline-block;width:1px;height:13px;margin-left:1px;
567
577
  border-right:1.5px solid currentColor;opacity:.5;animation:twk-blink 1s step-end infinite}
568
578
  .twk-sugg-ideas{appearance:none;border:0;background:transparent;font:inherit;
569
- font-size:10.5px;font-weight:600;color:rgba(41,38,27,.6);cursor:default;padding:0 2px;
579
+ font-size:10.5px;font-weight:600;color:#646A73;cursor:default;padding:0 2px;
570
580
  white-space:nowrap;animation:twk-fadein .25s ease}
571
- .twk-sugg-ideas:hover{color:rgba(41,38,27,.85)}
581
+ .twk-sugg-ideas:hover{color:#1F2329}
572
582
  .twk-sugg-ideas svg{color:#D97757}
573
583
  .twk-sugg-send{appearance:none;border:0;height:20px;padding:0 8px;border-radius:5px;
574
- background:#29261b;color:#fff;font:inherit;font-size:10px;font-weight:600;cursor:default}
584
+ background:#1F2329;color:#fff;font:inherit;font-size:10px;font-weight:600;cursor:default}
575
585
  `;
576
586
  document.head.appendChild(s);
577
587
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-registry-buildless",
3
- "version": "0.1.11-alpha.20260729134521",
3
+ "version": "0.1.11-beta.0",
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": [
@@ -38,10 +38,11 @@
38
38
  * a scrollable vertical card list (browse) and a rotated fullscreen
39
39
  * presenting mode. This needs NO authoring changes: never hand-roll
40
40
  * mobile/responsive slide layouts. Desktop behaviour is unchanged.
41
- * Browse mode renders authored CSS animations/transitions at their
42
- * end state (presenting keeps them) — design animations so the end
43
- * state is the complete slide; never gate content visibility on an
44
- * animation having played.
41
+ * Browse mode renders each slide's static base state and never fires
42
+ * entrance animations — exactly like the thumbnail rail and print
43
+ * (presenting keeps animations). One more surface that requires the
44
+ * visible end-state to be the base style (see Entrance animations
45
+ * below).
45
46
  * (h) preflight mode — add `?preflight=1` to the URL to enter
46
47
  * mobile-list layout on any device (including desktop). Identical to
47
48
  * mobile browse except the outer frame background is transparent,
@@ -144,6 +145,7 @@
144
145
  const MOB_RADIUS = 8; // card corner radius
145
146
  const MOB_BADGE_IDLE_MS = 2000; // scroll idle before the page badge fades
146
147
  const MOB_UI_HIDE_MS = 3000; // presenting controls auto-hide
148
+ const CANVAS_RECAP_MS = 1200; // re-snapshot canvas thumbs once chart entrance animations settle
147
149
 
148
150
  const pad2 = (n) => String(n).padStart(2, '0');
149
151
 
@@ -1056,6 +1058,7 @@
1056
1058
  if (this._scaleRaf) cancelAnimationFrame(this._scaleRaf);
1057
1059
  if (this._liveObserver) this._liveObserver.disconnect();
1058
1060
  if (this._railObserver) this._railObserver.disconnect();
1061
+ (this._thumbs || []).forEach((t) => clearTimeout(t._canvasRecapTimer));
1059
1062
  if (this._onTweakChange) window.removeEventListener('tweakchange', this._onTweakChange);
1060
1063
  }
1061
1064
 
@@ -1383,26 +1386,38 @@
1383
1386
  'animation-play-state: running !important; transition-duration: 0s !important; } }';
1384
1387
  }
1385
1388
 
1386
- /** Mobile browse (data-mobile-list) force-shows every card at once, so
1387
- * authored CSS animations all fire together on load — and the ones gated
1388
- * on [data-deck-active] re-fire as scrolling moves that flag between
1389
- * cards. Freeze them at their end state while browsing. Same jump-to-end
1390
- * recipe as the print rule above (NOT animation:none, so a deck that
1391
- * keeps a hidden base state still renders its content), keyed on the
1392
- * host attribute so presenting/desktop keep their animations and no JS
1393
- * toggling is needed. Document-level like the print rule: author
1394
- * animations style light-DOM slide content that shadow CSS can't reach,
1395
- * and <deck-stage> itself is a light-DOM element document CSS can key on. */
1389
+ /** Mobile browse (data-mobile-list) renders each card's static BASE
1390
+ * state — the same semantic as the thumbnail rail, print and
1391
+ * reduced-motion. The USAGE authoring contract makes the base style the
1392
+ * visible end-state (hidden lives only inside keyframes' `from`, gated
1393
+ * on [data-deck-active]), so animation-name:none shows exactly the
1394
+ * intended slide. Freezing at the END state instead would key visible
1395
+ * styling to a flag that MOVES during scroll: keyframe properties
1396
+ * (transform especially — it replaces the base value outright) snap on
1397
+ * and off as [data-deck-active] arrives at and leaves each card,
1398
+ * jolting layout. The timing longhands stay as a backstop: an authored
1399
+ * `animation … !important` that out-cascades our name:none still
1400
+ * collapses to an instant run. The attribute selector is repeated to
1401
+ * out-rank authored !important rules — important-vs-important resolves
1402
+ * by specificity, and the common `[data-deck-active] .x` gate is only
1403
+ * (0,2,0). Document-level like the print rule: author animations style
1404
+ * light-DOM slide content that shadow CSS can't reach, and <deck-stage>
1405
+ * is a light-DOM element document CSS can key on. Presenting/desktop
1406
+ * keep their animations — the rule is keyed on the host attribute, no
1407
+ * JS toggling needed. */
1396
1408
  _syncMobFreezeRule() {
1397
1409
  const id = 'deck-stage-mob-freeze';
1398
1410
  if (document.getElementById(id)) return;
1411
+ const sel = 'deck-stage[data-mobile-list][data-mobile-list][data-mobile-list]';
1399
1412
  const tag = document.createElement('style');
1400
1413
  tag.id = id;
1401
1414
  tag.textContent =
1402
- 'deck-stage[data-mobile-list] *, deck-stage[data-mobile-list] *::before, deck-stage[data-mobile-list] *::after {' +
1415
+ sel + ' *, ' + sel + ' *::before, ' + sel + ' *::after {' +
1416
+ ' animation-name: none !important;' +
1403
1417
  ' animation-delay: -99s !important; animation-duration: .001s !important;' +
1404
1418
  ' animation-iteration-count: 1 !important; animation-fill-mode: both !important;' +
1405
- ' animation-play-state: running !important; transition-duration: 0s !important; }';
1419
+ ' animation-play-state: running !important;' +
1420
+ ' transition-duration: 0s !important; transition-delay: 0s !important; }';
1406
1421
  document.head.appendChild(tag);
1407
1422
  }
1408
1423
 
@@ -1960,7 +1975,7 @@
1960
1975
  // it scales clones by the rail's _thumbScale, so rewrite the
1961
1976
  // transform for this strip's fixed 132px frames.
1962
1977
  const entry = { slide: e2.slide, frame, host: null, clone: null };
1963
- this._materialize(entry);
1978
+ this._materialize(entry, false); // throwaway entry (not in _thumbs) — skip the recap timer
1964
1979
  if (entry.clone) entry.clone.style.transform = 'scale(' + (132 / this.designWidth) + ')';
1965
1980
  return { el: t, i: e2.i };
1966
1981
  });
@@ -2082,6 +2097,7 @@
2082
2097
  // Orphans — slides removed since last render.
2083
2098
  bySlide.forEach((t) => {
2084
2099
  if (this._railObserver) this._railObserver.unobserve(t.frame);
2100
+ clearTimeout(t._canvasRecapTimer);
2085
2101
  t.thumb.remove();
2086
2102
  });
2087
2103
  // Put thumbs into document order to match _slides. insertBefore on
@@ -2263,7 +2279,7 @@
2263
2279
  }
2264
2280
 
2265
2281
  /** Lazily build the clone for a thumb that has scrolled into view. */
2266
- _materialize(entry) {
2282
+ _materialize(entry, scheduleRecap = true) {
2267
2283
  if (entry.host) return;
2268
2284
  const dw = this.designWidth, dh = this.designHeight;
2269
2285
  let clone = entry.slide.cloneNode(true);
@@ -2295,6 +2311,7 @@
2295
2311
  // drawings appear in the thumbnail instead of a blank rectangle.
2296
2312
  const origCanvases = entry.slide.querySelectorAll('canvas');
2297
2313
  const cloneCanvases = clone.querySelectorAll('canvas');
2314
+ const canvasImgs = []; // {orig, img} pairs kept for the delayed re-snapshot below
2298
2315
  cloneCanvases.forEach((el, ci) => {
2299
2316
  const orig = origCanvases[ci];
2300
2317
  if (!orig || !orig.width || !orig.height) return;
@@ -2306,6 +2323,7 @@
2306
2323
  ';width:' + orig.offsetWidth + 'px;height:' + orig.offsetHeight + 'px;';
2307
2324
  img.className = el.className;
2308
2325
  el.replaceWith(img);
2326
+ canvasImgs.push({ orig, img });
2309
2327
  } catch (e) {}
2310
2328
  });
2311
2329
  // Images: defer decode and let the browser pick the smallest
@@ -2370,6 +2388,30 @@
2370
2388
  // Once materialized the IO callback is a no-op early-return —
2371
2389
  // unobserve so scroll doesn't keep firing it.
2372
2390
  if (this._railObserver) this._railObserver.unobserve(entry.frame);
2391
+ // Chart libraries (ECharts/Chart.js) animate their entrance over ~1s and
2392
+ // draw straight to the canvas bitmap — no DOM mutation, so _liveObserver
2393
+ // never sees the finished frame and the toDataURL snapshot above may have
2394
+ // caught a blank or mid-animation canvas. Re-snapshot once after the
2395
+ // animation settles and overwrite each <img>. Guarded so a thumb that was
2396
+ // refreshed (host swapped) or removed (img detached) since is left alone.
2397
+ // scheduleRecap=false for callers whose entry isn't tracked in _thumbs
2398
+ // (the mobile filmstrip builds throwaway entries the cleanup paths can't
2399
+ // reach) — otherwise the timer leaks and, since that path clones the
2400
+ // whole deck eagerly, N recaps would fire together as a toDataURL storm.
2401
+ if (scheduleRecap && canvasImgs.length) {
2402
+ clearTimeout(entry._canvasRecapTimer);
2403
+ entry._canvasRecapTimer = setTimeout(() => {
2404
+ entry._canvasRecapTimer = null;
2405
+ if (entry.host !== host) return;
2406
+ canvasImgs.forEach(({ orig, img }) => {
2407
+ // orig.isConnected: if the app swapped the chart's <canvas> since,
2408
+ // the detached node still reports nonzero width/height — skip it so
2409
+ // we don't paint stale pixels (the swap already re-cloned the thumb).
2410
+ if (!img.isConnected || !orig.isConnected || !orig.width || !orig.height) return;
2411
+ try { img.src = orig.toDataURL(); } catch (e) {}
2412
+ });
2413
+ }, CANVAS_RECAP_MS);
2414
+ }
2373
2415
  }
2374
2416
 
2375
2417
  /** Re-clone a single thumb (live-update path). No-op if the thumb
@@ -2378,6 +2420,7 @@
2378
2420
  _refreshThumb(slide) {
2379
2421
  const entry = (this._thumbs || []).find((t) => t.slide === slide);
2380
2422
  if (!entry || !entry.host) return;
2423
+ clearTimeout(entry._canvasRecapTimer);
2381
2424
  entry.host.remove();
2382
2425
  entry.host = entry.clone = null;
2383
2426
  this._materialize(entry);