@lark-apaas/coding-registry-buildless 0.1.2-alpha.0 → 0.1.2-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.
@@ -123,7 +123,6 @@ function AndroidDevice({
123
123
  width, height, borderRadius: 18, overflow: 'hidden',
124
124
  background: dark ? '#1d1b20' : MD_C.surface,
125
125
  border: `8px solid ${MD_C.frameBorder}`,
126
- boxShadow: '0 30px 80px rgba(0,0,0,0.25)',
127
126
  display: 'flex', flexDirection: 'column', boxSizing: 'border-box',
128
127
  }}>
129
128
  <AndroidStatusBar dark={dark} />
@@ -12,6 +12,8 @@
12
12
  //
13
13
  // <Stage> auto-scales to the viewport and provides the scrubber, play/pause,
14
14
  // ←/→ seek, space, and 0-to-reset controls, and persists the playhead.
15
+ // Set the optional `poster` prop (seconds) to the moment your opening scene is
16
+ // fully composed — the product thumbnail freezes on that frame (default ~1s).
15
17
  // Inside <Stage>, any child can call useTime() to read the current
16
18
  // playhead (seconds). Or wrap content in <Sprite start={1} end={4}>...</Sprite>
17
19
  // to only render during that window -- children receive a `localTime` and
@@ -335,17 +337,24 @@ function Stage({
335
337
  fps = 60,
336
338
  loop = true,
337
339
  autoplay = true,
340
+ poster = null,
338
341
  persistKey = 'animstage',
339
342
  children,
340
343
  }) {
344
+ // Thumbnail capture mode: the host appends ?thumbnail=1 before screenshotting.
345
+ // Freeze on a representative still — the author-declared `poster` second, or
346
+ // ~1s as a fallback — paused, with the playback bar hidden, so the product
347
+ // thumbnail is a deterministic frame of the first composed scene.
348
+ const captureMode = typeof location !== 'undefined' && /[?&]thumbnail=/.test(location.search || '');
349
+
341
350
  const [time, setTime] = React.useState(() => {
351
+ if (captureMode) return clamp(poster == null ? 1 : poster, 0, duration);
342
352
  try {
343
353
  const v = parseFloat(localStorage.getItem(persistKey + ':t') || '0');
344
354
  return isFinite(v) ? clamp(v, 0, duration) : 0;
345
355
  } catch { return 0; }
346
356
  });
347
- const [playing, setPlaying] = React.useState(autoplay);
348
- const [hoverTime, setHoverTime] = React.useState(null);
357
+ const [playing, setPlaying] = React.useState(captureMode ? false : autoplay);
349
358
  const [scale, setScale] = React.useState(1);
350
359
 
351
360
  const stageRef = React.useRef(null);
@@ -363,7 +372,7 @@ function Stage({
363
372
  if (!stageRef.current) return;
364
373
  const el = stageRef.current;
365
374
  const measure = () => {
366
- const barH = 44; // playback bar height
375
+ const barH = captureMode ? 0 : 44; // playback bar height (hidden in capture mode)
367
376
  const s = Math.min(
368
377
  el.clientWidth / width,
369
378
  (el.clientHeight - barH) / height
@@ -426,11 +435,9 @@ function Stage({
426
435
  return () => window.removeEventListener('keydown', onKey);
427
436
  }, [duration]);
428
437
 
429
- const displayTime = hoverTime != null ? hoverTime : time;
430
-
431
438
  const ctxValue = React.useMemo(
432
- () => ({ time: displayTime, duration, playing, setTime, setPlaying }),
433
- [displayTime, duration, playing]
439
+ () => ({ time, duration, playing, setTime, setPlaying }),
440
+ [time, duration, playing]
434
441
  );
435
442
 
436
443
  return (
@@ -471,17 +478,18 @@ function Stage({
471
478
  </div>
472
479
  </div>
473
480
 
474
- {/* Playback bar — stacked below canvas, never overlapping */}
475
- <PlaybackBar
476
- time={displayTime}
477
- actualTime={time}
478
- duration={duration}
479
- playing={playing}
480
- onPlayPause={() => setPlaying(p => !p)}
481
- onReset={() => { setTime(0); }}
482
- onSeek={(t) => setTime(t)}
483
- onHover={(t) => setHoverTime(t)}
484
- />
481
+ {/* Playback bar — stacked below canvas, never overlapping. Hidden in
482
+ capture mode so the thumbnail is just the frame, no chrome. */}
483
+ {!captureMode && (
484
+ <PlaybackBar
485
+ time={time}
486
+ duration={duration}
487
+ playing={playing}
488
+ onPlayPause={() => setPlaying(p => !p)}
489
+ onReset={() => { setTime(0); }}
490
+ onSeek={(t) => setTime(t)}
491
+ />
492
+ )}
485
493
  </div>
486
494
  );
487
495
  }
@@ -490,43 +498,56 @@ function Stage({
490
498
  // Play/pause, return-to-begin, scrub track, time display.
491
499
  // Uses fixed-width time fields so layout doesn't thrash.
492
500
 
493
- function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek, onHover }) {
501
+ function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek }) {
494
502
  const trackRef = React.useRef(null);
495
503
  const [dragging, setDragging] = React.useState(false);
504
+ const [trackHover, setTrackHover] = React.useState(null); // { x, t } — px within track + hovered time
496
505
 
497
- const timeFromEvent = React.useCallback((e) => {
506
+ const posFromEvent = React.useCallback((e) => {
498
507
  const rect = trackRef.current.getBoundingClientRect();
499
- const x = clamp((e.clientX - rect.left) / rect.width, 0, 1);
500
- return x * duration;
508
+ const x = clamp(e.clientX - rect.left, 0, rect.width);
509
+ const t = rect.width > 0 ? (x / rect.width) * duration : 0;
510
+ return { x, t };
501
511
  }, [duration]);
502
512
 
503
513
  const onTrackMove = (e) => {
504
514
  if (!trackRef.current) return;
505
- const t = timeFromEvent(e);
506
- if (dragging) {
507
- onSeek(t);
508
- } else {
509
- onHover(t);
510
- }
515
+ const { x, t } = posFromEvent(e);
516
+ setTrackHover({ x, t });
517
+ if (dragging) onSeek(t);
511
518
  };
512
519
 
513
520
  const onTrackLeave = () => {
514
- if (!dragging) onHover(null);
521
+ if (!dragging) setTrackHover(null);
515
522
  };
516
523
 
517
524
  const onTrackDown = (e) => {
525
+ const { x, t } = posFromEvent(e);
518
526
  setDragging(true);
519
- const t = timeFromEvent(e);
527
+ setTrackHover({ x, t });
520
528
  onSeek(t);
521
- onHover(null);
529
+ };
530
+
531
+ // Grab the knob in place: begin dragging without seeking (no jump).
532
+ // stopPropagation keeps the track's click-to-seek from also firing.
533
+ const onBallDown = (e) => {
534
+ e.stopPropagation();
535
+ setDragging(true);
536
+ setTrackHover(posFromEvent(e));
522
537
  };
523
538
 
524
539
  React.useEffect(() => {
525
540
  if (!dragging) return;
526
- const onUp = () => setDragging(false);
541
+ const prevCursor = document.body.style.cursor;
542
+ document.body.style.cursor = 'grabbing'; // stays grabbing even if the pointer leaves the knob mid-drag
543
+ const onUp = () => {
544
+ setDragging(false);
545
+ setTrackHover(null);
546
+ };
527
547
  const onMove = (e) => {
528
548
  if (!trackRef.current) return;
529
- const t = timeFromEvent(e);
549
+ const { x, t } = posFromEvent(e);
550
+ setTrackHover({ x, t });
530
551
  onSeek(t);
531
552
  };
532
553
  window.addEventListener('mouseup', onUp);
@@ -534,66 +555,60 @@ function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek, on
534
555
  return () => {
535
556
  window.removeEventListener('mouseup', onUp);
536
557
  window.removeEventListener('mousemove', onMove);
558
+ document.body.style.cursor = prevCursor;
537
559
  };
538
- }, [dragging, timeFromEvent, onSeek]);
560
+ }, [dragging, posFromEvent, onSeek]);
539
561
 
540
562
  const pct = duration > 0 ? (time / duration) * 100 : 0;
541
563
  const fmt = (t) => {
542
564
  const total = Math.max(0, t);
543
565
  const m = Math.floor(total / 60);
544
566
  const s = Math.floor(total % 60);
545
- const cs = Math.floor((total * 100) % 100);
546
- return `${String(m).padStart(1, '0')}:${String(s).padStart(2, '0')}.${String(cs).padStart(2, '0')}`;
567
+ return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
547
568
  };
548
569
 
549
- const mono = 'JetBrains Mono, ui-monospace, SFMono-Regular, monospace';
570
+ const numFont = '"PingFang SC", -apple-system, BlinkMacSystemFont, system-ui, sans-serif';
550
571
 
551
572
  return (
552
573
  <div style={{
553
574
  display: 'flex', alignItems: 'center', gap: 12,
554
- padding: '8px 16px',
555
- background: 'rgba(20,20,20,0.92)',
556
- borderTop: '1px solid rgba(255,255,255,0.08)',
575
+ padding: '12px',
576
+ background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%)',
557
577
  width: '100%',
558
- maxWidth: 680,
559
- alignSelf: 'center',
560
-
561
- borderRadius: 8,
562
- color: '#f6f4ef',
563
- fontFamily: 'Inter, system-ui, sans-serif',
578
+ color: '#fff',
579
+ fontFamily: numFont,
564
580
  userSelect: 'none',
565
581
  flexShrink: 0,
582
+ boxSizing: 'border-box',
566
583
  }}>
567
- <IconButton onClick={onReset} title="Return to start (0)">
568
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
569
- <path d="M3 2v10M12 2L5 7l7 5V2z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"/>
570
- </svg>
571
- </IconButton>
572
- <IconButton onClick={onPlayPause} title="Play/pause (space)">
584
+ {/* Play / pause — bare white triangle, no button chrome */}
585
+ <IconButton onClick={onPlayPause} tooltip={playing ? '暂停' : '播放'}>
573
586
  {playing ? (
574
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
575
- <rect x="3" y="2" width="3" height="10" fill="currentColor"/>
576
- <rect x="8" y="2" width="3" height="10" fill="currentColor"/>
587
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
588
+ <path d="M3.33333 1.33398C2.59695 1.33398 2 1.93094 2 2.66732V13.334C2 14.0704 2.59695 14.6673 3.33333 14.6673H4.66667C5.40305 14.6673 6 14.0704 6 13.334V2.66732C6 1.93094 5.40305 1.33398 4.66667 1.33398H3.33333Z" fill="currentColor"/>
589
+ <path d="M11.3333 1.33398C10.597 1.33398 10 1.93094 10 2.66732V13.334C10 14.0704 10.597 14.6673 11.3333 14.6673H12.6667C13.403 14.6673 14 14.0704 14 13.334V2.66732C14 1.93094 13.403 1.33398 12.6667 1.33398H11.3333Z" fill="currentColor"/>
577
590
  </svg>
578
591
  ) : (
579
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
580
- <path d="M3 2l9 5-9 5V2z" fill="currentColor"/>
592
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
593
+ <path d="M14.0489 9.13127C14.873 8.60116 14.873 7.39754 14.0489 6.86743L4.74461 0.882617C3.84764 0.305661 2.66699 0.948902 2.66699 2.01454V13.9842C2.66699 15.0498 3.84764 15.693 4.74461 15.1161L14.0489 9.13127Z" fill="currentColor"/>
581
594
  </svg>
582
595
  )}
583
596
  </IconButton>
584
597
 
585
- {/* Current time: fixed width so it doesn't thrash */}
598
+ {/* Current time */}
586
599
  <div style={{
587
- fontFamily: mono,
588
- fontSize: 12,
600
+ fontFamily: numFont,
601
+ fontSize: 14,
602
+ fontWeight: 400,
589
603
  fontVariantNumeric: 'tabular-nums',
590
- width: 64, textAlign: 'right',
591
- color: '#f6f4ef',
604
+ color: '#fff',
605
+ minWidth: 40,
606
+ textAlign: 'center'
592
607
  }}>
593
608
  {fmt(time)}
594
609
  </div>
595
610
 
596
- {/* Scrub track */}
611
+ {/* Scrub track — white fill on translucent-white rail + draggable knob */}
597
612
  <div
598
613
  ref={trackRef}
599
614
  onMouseMove={onTrackMove}
@@ -601,7 +616,7 @@ function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek, on
601
616
  onMouseDown={onTrackDown}
602
617
  style={{
603
618
  flex: 1,
604
- height: 22,
619
+ height: 20,
605
620
  position: 'relative',
606
621
  cursor: 'pointer',
607
622
  display: 'flex', alignItems: 'center',
@@ -610,33 +625,62 @@ function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek, on
610
625
  <div style={{
611
626
  position: 'absolute',
612
627
  left: 0, right: 0, height: 4,
613
- background: 'rgba(255,255,255,0.12)',
628
+ background: 'rgba(255,255,255,0.6)',
614
629
  borderRadius: 2,
615
630
  }}/>
616
631
  <div style={{
617
632
  position: 'absolute',
618
633
  left: 0, width: `${pct}%`, height: 4,
619
- background: 'oklch(72% 0.12 250)',
634
+ background: 'rgba(255,255,255,0.9)',
620
635
  borderRadius: 2,
621
636
  }}/>
622
- <div style={{
623
- position: 'absolute',
624
- left: `${pct}%`, top: '50%',
625
- width: 12, height: 12,
626
- marginLeft: -6, marginTop: -6,
627
- background: '#fff',
628
- borderRadius: 6,
629
- boxShadow: '0 2px 4px rgba(0,0,0,0.4)',
630
- }}/>
637
+ {/* Progress knob — outer div is an enlarged transparent hit area for easier grabbing */}
638
+ <div
639
+ onMouseDown={onBallDown}
640
+ style={{
641
+ position: 'absolute',
642
+ left: `${pct}%`, top: '50%',
643
+ transform: 'translate(-50%, -50%)',
644
+ width: 20, height: 20,
645
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
646
+ cursor: dragging ? 'grabbing' : 'grab',
647
+ zIndex: 5,
648
+ }}
649
+ >
650
+ <div style={{
651
+ width: dragging ? 14 : 12,
652
+ height: dragging ? 14 : 12,
653
+ background: '#fff',
654
+ borderRadius: '50%',
655
+ border: '0.5px solid #D2D5D8',
656
+ boxShadow: '0 2px 6px rgba(0,0,0,0.35)',
657
+ transition: 'width 100ms, height 100ms',
658
+ }}/>
659
+ </div>
660
+ {trackHover && (
661
+ <div style={{
662
+ position: 'absolute',
663
+ left: trackHover.x,
664
+ bottom: '100%',
665
+ transform: 'translateX(-50%)',
666
+ marginBottom: 2,
667
+ pointerEvents: 'none',
668
+ zIndex: 10,
669
+ }}>
670
+ <TooltipBubble text={fmt(trackHover.t)} />
671
+ </div>
672
+ )}
631
673
  </div>
632
674
 
633
- {/* Duration: fixed width */}
675
+ {/* Duration — dimmed */}
634
676
  <div style={{
635
- fontFamily: mono,
636
- fontSize: 12,
677
+ fontFamily: numFont,
678
+ fontSize: 14,
679
+ fontWeight: 400,
637
680
  fontVariantNumeric: 'tabular-nums',
638
- width: 64, textAlign: 'left',
639
- color: 'rgba(246,244,239,0.55)',
681
+ color: 'rgba(255,255,255,0.6)',
682
+ minWidth: 40,
683
+ textAlign: 'center',
640
684
  }}>
641
685
  {fmt(duration)}
642
686
  </div>
@@ -644,31 +688,80 @@ function PlaybackBar({ time, duration, playing, onPlayPause, onReset, onSeek, on
644
688
  );
645
689
  }
646
690
 
647
- function IconButton({ children, onClick, title }) {
691
+
692
+ function IconButton({ children, onClick, tooltip }) {
648
693
  const [hover, setHover] = React.useState(false);
649
694
  return (
650
695
  <button
651
696
  onClick={onClick}
652
- title={title}
697
+ aria-label={tooltip}
653
698
  onMouseEnter={() => setHover(true)}
654
699
  onMouseLeave={() => setHover(false)}
655
700
  style={{
656
- width: 28, height: 28,
701
+ position: 'relative',
702
+ width: 24, height: 24,
657
703
  display: 'flex', alignItems: 'center', justifyContent: 'center',
658
- background: hover ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.04)',
659
- border: '1px solid rgba(255,255,255,0.1)',
704
+ background: hover ? 'rgba(255,255,255,0.1)' : 'transparent',
705
+ border: 'none',
660
706
  borderRadius: 6,
661
- color: '#f6f4ef',
707
+ color: '#fff',
662
708
  cursor: 'pointer',
663
709
  padding: 0,
664
710
  transition: 'background 120ms',
665
711
  }}
666
712
  >
667
713
  {children}
714
+ {tooltip && (
715
+ <div style={{
716
+ position: 'absolute',
717
+ bottom: '100%',
718
+ left: '50%',
719
+ transform: 'translateX(-50%)',
720
+ marginBottom: 8,
721
+ pointerEvents: 'none',
722
+ opacity: hover ? 1 : 0,
723
+ transition: 'opacity 120ms',
724
+ zIndex: 10,
725
+ }}>
726
+ <TooltipBubble text={tooltip} />
727
+ </div>
728
+ )}
668
729
  </button>
669
730
  );
670
731
  }
671
732
 
733
+ // ── Tooltip bubble ────────────────────────────────────────────────────────────
734
+ // Dark rounded bubble with a downward tail. Positioning is up to the caller.
735
+ function TooltipBubble({ text }) {
736
+ return (
737
+ <div style={{ position: 'relative', display: 'inline-block' }}>
738
+ <div style={{
739
+ background: '#1F2329',
740
+ color: '#fff',
741
+ fontSize: 12,
742
+ lineHeight: '16px',
743
+ padding: '6px 12px',
744
+ borderRadius: 6,
745
+ whiteSpace: 'nowrap',
746
+ fontFamily: '"PingFang SC", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',
747
+ fontVariantNumeric: 'tabular-nums',
748
+ boxShadow: '0 4px 8px -8px rgba(0, 0, 0, 0.06), 0 6px 12px 0 rgba(0, 0, 0, 0.04), 0 8px 24px 8px rgba(0, 0, 0, 0.04)',
749
+ }}>
750
+ {text}
751
+ </div>
752
+ <div style={{
753
+ position: 'absolute',
754
+ top: '100%',
755
+ left: '50%',
756
+ transform: 'translate(-50%, -50%) rotate(45deg)',
757
+ width: 9, height: 9,
758
+ borderRadius: '0 0 3px 0',
759
+ background: '#1F2329',
760
+ }}/>
761
+ </div>
762
+ );
763
+ }
764
+
672
765
 
673
766
  Object.assign(window, {
674
767
  Easing, interpolate, animate, clamp,
@@ -105,7 +105,7 @@ function ChromeWindow({
105
105
  return (
106
106
  <div style={{
107
107
  width, height, borderRadius: 10, overflow: 'hidden',
108
- boxShadow: '0 24px 80px rgba(0,0,0,0.35), 0 0 0 1px rgba(0,0,0,0.1)',
108
+ border: '1px solid #DEE0E3',
109
109
  display: 'flex', flexDirection: 'column', background: CHROME_C.tabBg,
110
110
  }}>
111
111
  <ChromeTabBar tabs={tabs} activeIndex={activeIndex} />