apptify 0.2.0 → 0.5.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.
Files changed (72) hide show
  1. package/README.md +95 -22
  2. package/dist/apptify.css +1 -1
  3. package/dist/apptify.js +4153 -2177
  4. package/dist/components/button.d.ts +1 -1
  5. package/dist/components/content.d.ts +73 -362
  6. package/dist/components/date-picker.d.ts +10 -0
  7. package/dist/components/forms.d.ts +77 -18
  8. package/dist/components/foundation.d.ts +2061 -111
  9. package/dist/components/gallery-scrollbar.d.ts +46 -0
  10. package/dist/components/mobile-image-viewer.d.ts +166 -0
  11. package/dist/components/navibar.d.ts +80 -0
  12. package/dist/components/overlays.d.ts +127 -158
  13. package/dist/components/tabs.d.ts +117 -0
  14. package/dist/core/context.d.ts +573 -9
  15. package/dist/core/desktop-image-enter.d.ts +6 -0
  16. package/dist/core/desktop-image-return.d.ts +1 -0
  17. package/dist/core/device.d.ts +5 -0
  18. package/dist/core/image-gallery-motion.d.ts +2 -0
  19. package/dist/core/image-geometry.d.ts +28 -0
  20. package/dist/core/image-motion.d.ts +18 -0
  21. package/dist/core/layers.d.ts +2 -0
  22. package/dist/core/motion.d.ts +2 -0
  23. package/dist/core/popup-motion.d.ts +12 -0
  24. package/dist/core/popup-placement.d.ts +31 -0
  25. package/dist/core/preview-navigation.d.ts +5 -0
  26. package/dist/index.d.ts +1214 -339
  27. package/docs/COMPONENTS.md +17 -15
  28. package/docs/PLAYGROUND-TEMPLATE.md +74 -0
  29. package/docs/VALIDATION.md +24 -0
  30. package/package.json +26 -7
  31. package/templates/playground/README.md +17 -0
  32. package/templates/playground/gitignore +3 -0
  33. package/templates/playground/index.html +26 -0
  34. package/templates/playground/package.json +26 -0
  35. package/templates/playground/playground/App.vue +73 -0
  36. package/templates/playground/playground/ComponentDemo.vue +234 -0
  37. package/templates/playground/playground/catalog.ts +80 -0
  38. package/templates/playground/playground/components/ComponentIndex.vue +62 -0
  39. package/templates/playground/playground/components/PageFooter.vue +3 -0
  40. package/templates/playground/playground/components/UserMenu.vue +132 -0
  41. package/templates/playground/playground/components/not-found-paper-scene.vue +210 -0
  42. package/templates/playground/playground/components/not-found-paper-story.ts +149 -0
  43. package/templates/playground/playground/env.d.ts +1 -0
  44. package/templates/playground/playground/main.ts +8 -0
  45. package/templates/playground/playground/router/index.ts +24 -0
  46. package/templates/playground/playground/style.css +228 -0
  47. package/templates/playground/playground/views/[...all].vue +68 -0
  48. package/templates/playground/playground/views/components.vue +126 -0
  49. package/templates/playground/playground/views/index.vue +75 -0
  50. package/templates/playground/playground/views/settings.vue +98 -0
  51. package/templates/playground/public/images/SOURCES.md +21 -0
  52. package/templates/playground/public/images/airpods-max-orange.jpg +0 -0
  53. package/templates/playground/public/images/architecture.jpg +0 -0
  54. package/templates/playground/public/images/city.jpg +0 -0
  55. package/templates/playground/public/images/coast.jpg +0 -0
  56. package/templates/playground/public/images/desert.jpg +0 -0
  57. package/templates/playground/public/images/food.jpg +0 -0
  58. package/templates/playground/public/images/forest.jpg +0 -0
  59. package/templates/playground/public/images/lake.jpg +0 -0
  60. package/templates/playground/public/images/mountains.jpg +0 -0
  61. package/templates/playground/public/images/snow.jpg +0 -0
  62. package/templates/playground/public/images/sunset.jpg +0 -0
  63. package/templates/playground/public/images/waterfall.jpg +0 -0
  64. package/templates/playground/source-snapshot.json +111 -0
  65. package/templates/playground/tsconfig.json +24 -0
  66. package/templates/playground/vite.config.ts +9 -0
  67. package/tooling/create-playground.mjs +35 -0
  68. package/tooling/prepare-package.mjs +35 -0
  69. package/tooling/prepare-playground.mjs +90 -0
  70. package/tooling/test-playground-package.mjs +188 -0
  71. package/tooling/vite.d.ts +5 -0
  72. package/tooling/vite.js +10 -0
@@ -0,0 +1,210 @@
1
+ <template>
2
+ <figure ref="scene" class="paper-scene" :class="{ 'paper-scene--dark': dark }" :data-story="story" :data-playing="canAnimate && !suspended" :data-motion="motion">
3
+ <div class="paper-scene__window">
4
+ <svg ref="art" class="paper-scene__art" viewBox="0 0 1000 660" role="img" :aria-labelledby="`${uid}-title ${uid}-description`" xmlns="http://www.w3.org/2000/svg">
5
+ <defs>
6
+ <linearGradient :id="`${uid}-sky`" x2=".3" y2="1"><stop stop-color="var(--paper-sky-top)" /><stop offset="1" stop-color="var(--paper-sky)" /></linearGradient>
7
+ <linearGradient :id="`${uid}-water`" gradientUnits="userSpaceOnUse" x1="520" y1="400" x2="930" y2="660"><stop stop-color="var(--paper-water-light)" /><stop offset="1" stop-color="var(--paper-water)" /></linearGradient>
8
+ <linearGradient :id="`${uid}-bark`"><stop stop-color="#996442" /><stop offset=".48" stop-color="#b58052" /><stop offset="1" stop-color="#81543b" /></linearGradient>
9
+ <linearGradient :id="`${uid}-hole`" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#302e29" /><stop offset="1" stop-color="#5b412d" /></linearGradient>
10
+ <filter :id="`${uid}-paper-shadow`" x="-30%" y="-25%" width="170%" height="175%"><feDropShadow dx="1" dy="6" stdDeviation="3" flood-color="#39291e" flood-opacity=".13" /></filter>
11
+ <pattern :id="`${uid}-grain`" width="41" height="37" patternUnits="userSpaceOnUse"><path d="M4 6h1m15 11h1m13-9h.7M9 29h1m26 3h.8" stroke="#5e472e" stroke-opacity=".14" stroke-width="1" /><path d="M8 11l3-1m13 16 3-1m-4-23 2-1" stroke="#fff8e5" stroke-opacity=".2" stroke-width=".7" /></pattern>
12
+ <clipPath :id="`${uid}-frame`"><path d="M36 74Q37 38 75 35L909 20Q957 19 963 68L986 565Q987 608 942 614L84 635Q29 635 27 585Z" /></clipPath>
13
+ <clipPath :id="`${uid}-river-clip`"><path d="M550 435C655 419 655 451 744 459S900 472 1020 449V663H261C297 557 379 522 465 506S516 459 550 435Z" /></clipPath>
14
+ <clipPath :id="`${uid}-drift-clip`"><path d="M578 388C592 395 607 413 621 435S638 460 655 476L635 490C620 467 609 449 602 434S578 408 567 401Z" /><path d="M550 435C655 419 655 451 744 459S900 472 1020 449V663H261C297 557 379 522 465 506S516 459 550 435Z" /></clipPath>
15
+ <mask :id="`${uid}-root-outlet`" maskUnits="userSpaceOnUse" x="300" y="0" width="450" height="480"><rect x="300" width="450" height="480" fill="white" /><path d="M586 410 599 407 628 437 648 459 620 460 602 435Z" fill="black" /></mask>
16
+ <g :id="`${uid}-acorn`"><path d="M-12-3 11-5 12 6 5 16-1 19-10 12-14 4Z" fill="#bf8247" /><path d="M-12-3-1-2 0 18-10 12-14 4Z" fill="#dca661" /><path d="M-14-5-7-12 6-13 14-6 13-1-13 2Z" fill="#675137" /><path d="M-13-4 0-8 13-5M-7-10-4-5 1-9 6-5 9-8" fill="none" stroke="#a28b58" stroke-width="1.5" /><path d="M0-12 2-19 5-20 4-12Z" fill="#675137" /><path d="m3 3 5-2-2 8-3 3Z" fill="#ebbc78" opacity=".7" /></g>
17
+ <g :id="`${uid}-leaf`"><path d="M0 0C-31-3-39-22-27-39-7-36 5-19 0 0Z" fill="currentColor" /><path d="M0 0-24-32" stroke="#f5e2ae" stroke-width="1" opacity=".6" /></g>
18
+ <g :id="`${uid}-fir`"><path d="M0-132-50-48-30-50-67 2-39-4-72 46H64L36-5 59 1 26-51 43-46Z" fill="currentColor" /><path d="M0-132-2 46H64L36-5 59 1 26-51 43-46Z" fill="#fff" opacity=".07" /><path d="M-2 26v37" stroke="currentColor" stroke-width="8" /></g>
19
+ </defs>
20
+ <!-- The paper frame stays still while the whole diorama pulls back. -->
21
+ <path d="M44 90Q43 52 83 49L915 33Q970 35 974 85L994 574Q996 620 945 628L86 647Q35 647 36 598Z" fill="var(--paper-frame-shadow)" />
22
+ <g :clip-path="`url(#${uid}-frame)`">
23
+ <path d="M0 0H1000V660H0Z" :fill="`url(#${uid}-sky)`" />
24
+ <g data-part="camera" class="paper-camera">
25
+ <circle cx="765" cy="167" r="54" fill="var(--paper-sun)" />
26
+ <path d="M-220 313 63 177 156 221 314 141 432 244 593 215 793 316 1110 216V740H-220Z" fill="var(--paper-mountain-far)" />
27
+ <path d="m63 177 93 44 158-80-55 103-65 23-118-31-169 118Z" fill="var(--paper-mountain-fold)" />
28
+ <path d="M-160 359 67 281 248 313 382 263 487 307 646 283 823 344 1110 271V740H-160Z" fill="var(--paper-mountain-near)" />
29
+ <g opacity=".5" color="var(--paper-fir-far)"><use :href="`#${uid}-fir`" transform="translate(145 360) scale(.74)" /><use :href="`#${uid}-fir`" transform="translate(241 330) scale(.48)" /><use :href="`#${uid}-fir`" transform="translate(803 360) scale(.7)" /><use :href="`#${uid}-fir`" transform="translate(873 340) scale(.48)" /><use :href="`#${uid}-fir`" transform="translate(919 374) scale(.86)" /></g>
30
+ <path d="M-200 408Q77 331 316 378T729 366Q865 335 1170 403V740H-200Z" fill="var(--paper-ground-back)" />
31
+ <path d="M-220 495Q151 351 432 431T1020 441V751H-220Z" fill="var(--paper-ground)" />
32
+ <!-- The river is below the close-up crop until the reveal. -->
33
+ <path d="M550 435C655 419 655 451 744 459S900 472 1020 449V663H261C297 557 379 522 465 506S516 459 550 435Z" :fill="`url(#${uid}-water)`" />
34
+ <path d="M550 435C655 419 655 451 744 459S900 472 1020 449" fill="none" stroke="var(--paper-water-edge)" stroke-width="7" />
35
+ <g :clip-path="`url(#${uid}-river-clip)`">
36
+ <g data-part="current" fill="none" stroke="var(--paper-ripple)" stroke-linecap="round"><path d="M351 535q50-15 103-9m69-35q50-13 109 1m37-24q41 5 76 2m23 31q45 10 91 4m-455 63q50-11 104-4m53-16q47-7 93 2m33 32q45 12 91 5m54-41q50 12 100 1m-574 58q64-12 128-4m111 17q66 0 134 14m32-47q50 10 105 0m44-76q42 5 80-3" stroke-width="2.2" opacity=".62" /><path d="M592 466h29m72 70h57m-252 47h43m324-63h28m-481 33h28m425 61h57m-400 28h65m326-71h27" stroke-width="1" opacity=".68" /></g>
37
+ </g>
38
+ <!-- Bank layers hide the drifting resets. -->
39
+ <path d="M-210 479Q74 375 303 411L485 417 586 432 562 460 511 469 497 493 386 526 242 565-203 594Z" fill="var(--paper-bank-shadow)" />
40
+ <path d="M-210 456Q38 371 267 399L485 409 587 432 553 444 500 447 472 476 373 502 238 537-200 574Z" fill="var(--paper-bank)" />
41
+ <path d="m224 472 105-16 59 9-118 27Z" fill="var(--paper-bank-fold)" />
42
+ <path d="M40 457q61-12 126-11m88-12 66-8m-164 57 42-7m167-33 25-6" fill="none" stroke="var(--paper-grass)" stroke-width="2" opacity=".5" />
43
+ <path d="M578 388C592 395 607 413 621 435S638 460 655 476L635 490C620 467 609 449 602 434S578 408 567 401Z" :fill="`url(#${uid}-water)`" />
44
+ <!-- Drifters are above water but behind the trunk; the tree masks their entry. -->
45
+ <g data-part="drifters" :clip-path="`url(#${uid}-drift-clip)`">
46
+ <g v-for="(nut, index) in driftwood" :key="index" :data-drifter="index" :transform="`translate(${nut.x} ${nut.y})`"><ellipse cx="2" cy="13" rx="22" ry="3" fill="var(--paper-ripple)" opacity=".3" /><g :transform="`rotate(${nut.angle}) scale(${nut.scale})`"><use :href="`#${uid}-acorn`" /></g></g>
47
+ </g>
48
+ <!-- The cut in the root exposes the same water and acorns underneath. -->
49
+ <g data-part="tree" :filter="`url(#${uid}-paper-shadow)`" :mask="`url(#${uid}-root-outlet)`">
50
+ <path d="M456 44 570 33 582 161 649 109 682 114 591 224 591 327 612 400 651 434 572 432 548 417 493 435 426 439 465 402 477 327 468 229 427 186 375 179 351 145 440 156 467 177Z" :fill="`url(#${uid}-bark)`" />
51
+ <path d="m456 44 26 133 13 154-23 79-46 29 67-4 28-47-6-202-19-148Z" fill="#cf9b67" />
52
+ <path d="m545 37 14 147-7 103 23 91-3 54 79 2-39-34-21-73V224l91-110-33-5-67 52-12-128Z" fill="#865939" />
53
+ <path d="m516 47 14 145-7 51m-35-39 14 77-5 51m83-131 25-20 31-41m-168 46-25-16-27-7m114 91-2 29" fill="none" stroke="#5e422f" stroke-width="3" opacity=".42" />
54
+ <path d="m481 60 12 108m14 31 4 37m-27 126-10 42m114-11 19 25" stroke="#e2b77d" stroke-width="2" opacity=".65" />
55
+ <path d="M522 269Q560 253 576 289L582 352 568 387 510 386 497 362 502 297Z" fill="#70452f" />
56
+ <path d="M528 281Q555 265 567 297L574 350 562 374 514 374 507 353 511 303Z" :fill="`url(#${uid}-hole)`" />
57
+ <path d="m516 303 12-22 15-6-17 25-8 51 7 24-11-1-7-21Z" fill="#432f24" />
58
+ <path d="m569 377 15 6 4 15 15 9-14 8-20-19Z" fill="#352f27" /><path d="m577 381 5 11 16 17-8 2-12-14-9-20Z" fill="#c39666" />
59
+ </g>
60
+ <ellipse data-part="actor-shadow" class="paper-actor-shadow" cx="-35" cy="0" rx="70" ry="8" fill="#503c28" opacity=".13" />
61
+ <!-- One articulated squirrel, from the first hop to the last sigh. -->
62
+ <g data-part="squirrel" class="paper-squirrel">
63
+ <!-- Planted feet live outside the torso and its breathing motion. -->
64
+ <g data-part="back-foot" class="paper-back-foot"><path d="m-56-28 23 3-5 20-11 7-13-8Z" fill="#a25a39" /><path d="m-57-13-5 10 9 6 30-1 3-8-19-11Z" fill="#874b32" /></g>
65
+ <g data-part="front-foot" class="paper-front-foot"><path d="m-24-27 17 1 7 22-13 3-12-13Z" fill="#ba6c42" /><path d="m-9-15 4 8 23 5 3 7-36-1-9-9Z" fill="#aa5d3b" /></g>
66
+ <!-- One torso transform carries the neck, head, arms and held acorn. -->
67
+ <g data-part="body" class="paper-body"><g data-part="breath" class="paper-breath">
68
+ <g data-part="tail" class="paper-tail">
69
+ <path d="M-53-25-84-15-116-30-137-61-141-99-128-137-99-161-65-159-46-139-49-115-70-100-91-102-101-90-96-73-77-58-55-50Z" fill="#ac5739" />
70
+ <path d="m-137-61-4-38 13-38 29-24 34 2-28 18-20 31-3 29 14 34 25 17-7 15-32-15Z" fill="#da8954" />
71
+ <path d="m-99-161 34 2 19 20-3 24-21 15-21-2 25-19-4-20Z" fill="#efb979" />
72
+ <path d="m-93-141-20 31-3 29 14 34 25 17-19-43-5-17 10-12 21 2-13-13Z" fill="#c66c42" />
73
+ <path d="m-128-132 15 22m-3 29-21 20m44-80 23 0m-32 94-14 17" stroke="#ffe0a2" stroke-width="1.2" fill="none" opacity=".5" />
74
+ </g>
75
+ <g data-part="torso"><path d="M-49-91-19-87 0-62 4-34-12-11-41-9-65-28-68-56Z" fill="#d8864f" /><path d="m-49-91-19 35 3 28 24 19-3-32 7-35 18-11Z" fill="#bb653e" /><path d="m-20-86 18 23 6 29-16 23-14-4-9-26 4-27Z" fill="#f5d39a" /><path d="m-37-76-7 35 3 32 15-6-9-26 4-27Z" fill="#eaa668" /><path d="m-66-54 22 13-4 20m46-42-22 19" fill="none" stroke="#f7bf7d" stroke-width="1.1" opacity=".8" /></g>
76
+ <path data-part="neck" d="M-35-91-20-98 3-84 4-70-13-65-36-77Z" fill="#d8864f" />
77
+ <g data-part="head" class="paper-head">
78
+ <path d="m-32-111-3-36 17 15 8 27Z" fill="#c87748" /><path d="m-25-121-6-20 10 11 7 20Z" fill="#f0b780" /><path d="m-9-115 8-32 12 13-3 27Z" fill="#e8a267" /><path d="m-3-122 4-19 6 10-4 16Z" fill="#8d5139" />
79
+ <path d="M-32-112-10-126 11-118 22-99 41-91 30-78 5-76-17-85-33-96Z" fill="#e39a5d" /><path d="m-32-112 22-14-7 41-16-11Z" fill="#bd6940" /><path d="m11-118 11 19 19 8-11 13-25 2 2-14 10-7Z" fill="#f7d7a1" /><path d="m-10-126 21 8 6 21-17-9Z" fill="#efb577" /><path d="m35-95 9 3-3 7-7-3Z" fill="#45372e" />
80
+ <ellipse cx="11" cy="-105" rx="4.1" ry="5" fill="#342e27" /><circle cx="12" cy="-106.5" r="1.3" fill="#fff8df" />
81
+ <path data-part="eyelid" class="paper-eyelid" d="m5-112 12 2-2 6-10-4Z" fill="#d48954" /><path data-part="brow" class="paper-brow" d="m7-114 8 2" stroke="#754c34" stroke-width="1.7" stroke-linecap="round" /><path d="m22-83 7-1m-32-9-11-2" fill="none" stroke="#9d6945" stroke-width="1.2" stroke-linecap="round" />
82
+ </g>
83
+ <g data-part="cradle" class="paper-cradle">
84
+ <path d="M-5-69 6-63 13-46 27-42 25-35 7-38-9-55Z" fill="#ac5c3b" />
85
+ <g data-part="held-acorn"><use :href="`#${uid}-acorn`" transform="translate(20 -54) rotate(14) scale(.94)" /></g>
86
+ <g data-part="arm"><path d="m-10-75 12 7 8 12 17-2 4 7-22 8-17-11-10-11Z" fill="#bf6c43" /><path d="m-10-75 12 7 8 12-9 3-15-15Z" fill="#edab6e" /><path d="m18-56 3 6m-7-5 3 6" stroke="#7f4a32" stroke-width="1" /></g>
87
+ </g>
88
+ </g></g></g>
89
+ <!-- The hollow stays empty throughout the story. -->
90
+ <path d="m569 282 8 10 5 60-14 35-11-1 10-22 3-28-4-34Z" fill="#b58052" /><path d="m513 374 21 6 23-3 11 10-22 9-39-9-7-13Z" fill="#c89662" /><path d="m507 387 39 9 22-9-10 13-36-4Z" fill="#805538" />
91
+ <!-- Overlapping canopy pieces with visible folds and paper shadows. -->
92
+ <g :filter="`url(#${uid}-paper-shadow)`">
93
+ <path d="m315 4 79-52 101 20 57 67-29 104-126 27-108-62Z" fill="var(--paper-leaf-dark)" /><path d="m552-31 97 12 70 76-21 89-100 18-83-54-18-68Z" fill="var(--paper-leaf-dark)" />
94
+ <path d="m315 4 79-52 101 20-63 66-39 95-104-21Z" fill="var(--paper-leaf-mid)" /><path d="m432 38 63-66 57 67-29 104-130-14Z" fill="var(--paper-leaf-light)" /><path d="m552-31 97 12 70 76-109 32-95 21-18-68Z" fill="var(--paper-leaf-mid)" /><path d="m719 57-21 89-100 18 12-75Z" fill="var(--paper-leaf-light)" />
95
+ <path d="m259 91 57-69 104 11 45 60-31 87-110 19-70-44Z" fill="var(--paper-leaf-gold)" /><path d="m259 91 165 14 10 75-110 19-70-44Z" fill="var(--paper-leaf-mid)" /><path d="m316 22 108 83-165-14Z" fill="var(--paper-leaf-pale)" />
96
+ <path d="m434 102 71-55 88 29 21 68-64 70-93-27-37-45Z" fill="var(--paper-leaf-gold)" /><path d="m505 47 9 100 36 67 64-70-21-68Z" fill="var(--paper-leaf-pale)" /><path d="m434 102 80 45 36 67-93-27-37-45Z" fill="var(--paper-leaf-mid)" />
97
+ <path d="m617 108 44-51 80 23 39 61-19 58-95 23-71-53Z" fill="var(--paper-leaf-mid)" /><path d="m661 57 43 91 57 51-95 23-71-53 22-61Z" fill="var(--paper-leaf-gold)" /><path d="m704 148 76-7-19 58Z" fill="var(--paper-leaf-pale)" />
98
+ <path d="m316 22 108 83 10 75m71-133 9 100 36 67m147-72-31 80" fill="none" stroke="#ffebbc" stroke-width="1.5" opacity=".38" />
99
+ </g>
100
+ <g color="var(--paper-leaf-gold)"><use :href="`#${uid}-leaf`" transform="translate(346 438) rotate(120) scale(.62)" /></g>
101
+ <g fill="var(--paper-grass)"><path d="m228 435-6-31 15 26 2-40 8 42 19-15-10 26Z" /><path d="m663 436 3-27 7 25 12-14-7 22Z" /><path d="m362 456-3-19 9 15 5-24 4 24Z" /></g>
102
+ <path d="M-200 634Q47 543 223 590L349 650 1113 680V880H-200Z" fill="var(--paper-foreground)" />
103
+ <g color="var(--paper-foreground)"><use :href="`#${uid}-fir`" transform="translate(46 562) scale(1.48)" /></g>
104
+ <g color="var(--paper-leaf-mid)"><use :href="`#${uid}-leaf`" transform="translate(197 603) rotate(38) scale(1.3)" /><use :href="`#${uid}-leaf`" transform="translate(212 612) rotate(100) scale(1.1)" /></g>
105
+ <path d="M-100-100H1200V800H-100Z" :fill="`url(#${uid}-grain)`" pointer-events="none" />
106
+ </g>
107
+ <path d="M40 78 43 581Q44 614 80 613" fill="none" stroke="#fff8de" stroke-width="2" opacity=".35" />
108
+ </g>
109
+ <g class="paper-scene__edition" fill="var(--apple-secondary)" font-size="10" letter-spacing="2"><text x="44" y="20">ACORN AWAY</text><text x="942" y="650" text-anchor="end">PAPER TALES — 01</text></g>
110
+ </svg>
111
+ </div>
112
+ </figure>
113
+ </template>
114
+
115
+ <script setup lang="ts">
116
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue'
117
+ import { Pause, Play, RotateCcw } from 'lucide-vue-next'
118
+ import { useApple } from 'apptify'
119
+ import { resolveMotion } from 'apptify'
120
+ import { createPaperStory, type PaperStory } from './not-found-paper-story'
121
+
122
+ const uid = `paper-${useId().replace(/:/g, '')}`
123
+ const apple = useApple()
124
+ const scene = ref<HTMLElement>()
125
+ const art = ref<SVGSVGElement>()
126
+ const media = typeof window === 'undefined' ? undefined : window.matchMedia('(prefers-reduced-motion: reduce)')
127
+ const systemReduced = ref(media?.matches ?? false)
128
+ const motion = computed(() => resolveMotion('inherit', apple.motion.value.mode, apple.motion.value.reduced || systemReduced.value))
129
+ const canAnimate = computed(() => motion.value === 'full')
130
+ const dark = computed(() => apple.theme.value.current.scheme === 'dark')
131
+ const userPaused = ref(false)
132
+ const hidden = ref(typeof document !== 'undefined' && document.hidden)
133
+ const offscreen = ref(false)
134
+ const suspended = computed(() => userPaused.value || hidden.value || offscreen.value)
135
+ const story = ref<'intro' | 'settled' | 'static'>('static')
136
+ const driftwood = [{ x: 630, y: 482, angle: 18, scale: .68 }, { x: 737, y: 508, angle: -26, scale: .82 }, { x: 838, y: 543, angle: 51, scale: .72 }, { x: 933, y: 568, angle: -13, scale: .9 }, { x: 529, y: 550, angle: -39, scale: .65 }]
137
+ let player: PaperStory | undefined
138
+ let observer: IntersectionObserver | undefined
139
+ let mounted = false
140
+ function syncPlayback() { player?.setPaused(suspended.value) }
141
+ function stop() { player?.dispose(); player = undefined }
142
+ function play(intro: boolean) {
143
+ stop()
144
+ if (!mounted || !art.value || !scene.value || !canAnimate.value) { story.value = 'static'; return }
145
+ story.value = intro ? 'intro' : 'settled'
146
+ player = createPaperStory(art.value, scene.value, intro, () => { story.value = 'settled' })
147
+ syncPlayback()
148
+ }
149
+ function replay() { userPaused.value = false; play(true) }
150
+ function togglePause() { userPaused.value = !userPaused.value }
151
+ function visibilityChanged() { hidden.value = document.hidden }
152
+ function mediaChanged() { systemReduced.value = media?.matches ?? false }
153
+ watch(suspended, syncPlayback, { flush: 'sync' })
154
+ watch(canAnimate, enabled => {
155
+ if (!mounted) return
156
+ // Changing a preference never forces the entrance story to repeat.
157
+ if (enabled) play(false)
158
+ else { stop(); story.value = 'static' }
159
+ }, { flush: 'post' })
160
+ onMounted(async () => {
161
+ mounted = true
162
+ document.addEventListener('visibilitychange', visibilityChanged)
163
+ media?.addEventListener('change', mediaChanged)
164
+ if (typeof IntersectionObserver !== 'undefined' && scene.value) {
165
+ observer = new IntersectionObserver(([entry]) => { offscreen.value = !entry.isIntersecting }, { threshold: 0 })
166
+ observer.observe(scene.value)
167
+ }
168
+ // The provider restores persisted motion preferences in its mounted hook.
169
+ await nextTick()
170
+ if (mounted) play(true)
171
+ })
172
+ onBeforeUnmount(() => {
173
+ mounted = false
174
+ stop()
175
+ observer?.disconnect()
176
+ media?.removeEventListener('change', mediaChanged)
177
+ document.removeEventListener('visibilitychange', visibilityChanged)
178
+ })
179
+ </script>
180
+
181
+ <style scoped>
182
+ .paper-scene { --paper-sky-top: #f4ead8; --paper-sky: #e8e3c9; --paper-sun: #fdf6d5; --paper-frame-shadow: #d4cbbb; --paper-mountain-far: #dadcc1; --paper-mountain-fold: #e5e2ca; --paper-mountain-near: #bcc7ac; --paper-fir-far: #869981; --paper-ground-back: #aebc91; --paper-ground: #c5c69d; --paper-bank: #d3c99e; --paper-bank-fold: #c2bb8c; --paper-bank-shadow: #9e9b79; --paper-water-light: #adcaca; --paper-water: #78a7ac; --paper-water-edge: #d6dfc7; --paper-ripple: #e2efdf; --paper-grass: #89966c; --paper-leaf-dark: #8e814a; --paper-leaf-mid: #b5a460; --paper-leaf-light: #c5b574; --paper-leaf-gold: #d2b16b; --paper-leaf-pale: #e2c384; --paper-foreground: #76866a; margin: 0; position: relative; }
183
+ .paper-scene--dark { --paper-sky-top: #253536; --paper-sky: #354a47; --paper-sun: #ddcd9d; --paper-frame-shadow: #121d1f; --paper-mountain-far: #3a5149; --paper-mountain-fold: #3f5850; --paper-mountain-near: #466357; --paper-fir-far: #78917b; --paper-ground-back: #60795c; --paper-ground: #788462; --paper-bank: #9c9d72; --paper-bank-fold: #868b61; --paper-bank-shadow: #606e54; --paper-water-light: #638e8f; --paper-water: #355e6a; --paper-water-edge: #a1b59b; --paper-ripple: #b1d1c7; --paper-grass: #526c4b; --paper-leaf-dark: #696b40; --paper-leaf-mid: #8b8c50; --paper-leaf-light: #a3a066; --paper-leaf-gold: #ae985c; --paper-leaf-pale: #c0ac71; --paper-foreground: #334e42; }
184
+ .paper-scene__window { overflow: hidden; padding: 4px; }
185
+ .paper-scene__art { display: block; width: 100%; height: auto; overflow: visible; }
186
+ .paper-camera { transform-origin: 0 0; }
187
+ .paper-squirrel { transform: translate(462px, 428px) scale(.8); }
188
+ .paper-actor-shadow { transform: translate(462px, 432px) scale(.8); transform-origin: 0 0; }
189
+ .paper-tail { transform: rotate(-51deg); transform-origin: -64px -21px; }
190
+ .paper-body { transform: translate(-7px, 2px) rotate(9deg) scale(1.04, .9); transform-origin: -38px 0; }
191
+ .paper-breath { transform-origin: -38px 0; }
192
+ .paper-head { transform: rotate(18deg); transform-origin: -19px -88px; }
193
+ .paper-cradle { transform: rotate(-6deg); transform-origin: -10px -66px; }
194
+ .paper-back-foot { transform-origin: -24px 3px; }
195
+ .paper-front-foot { transform: translate(9px, 0px); transform-origin: 18px 5px; }
196
+ .paper-eyelid { opacity: 1; }
197
+ .paper-brow { transform: rotate(12deg); transform-origin: 10px -113px; }
198
+ .paper-scene__caption { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 46px; padding-inline: 4.5%; margin-top: 10px; }
199
+ .paper-scene__story-label { position: relative; flex: 1; min-width: 0; height: 20px; font-size: 11px; color: var(--apple-secondary); letter-spacing: .015em; }
200
+ .paper-caption { position: absolute; inset: 0 auto auto 0; white-space: nowrap; opacity: 0; }
201
+ .paper-caption--last { opacity: 1; }
202
+ .paper-scene__controls { display: flex; align-items: center; gap: 0; flex-shrink: 0; }
203
+ .paper-scene__controls .apple-button { color: var(--apple-secondary); font-size: 10px; min-height: 44px; }
204
+ .paper-scene__controls .apple-button--icon { width: 36px; }
205
+ .paper-scene__replay { padding-inline: 7px; }
206
+ .paper-scene__controls :deep(svg) { width: 13px; height: 13px; }
207
+ .paper-scene__still { font-size: 10px; color: var(--apple-secondary); }
208
+ @media (max-width: 880px) { .paper-scene__window { margin-inline: -12px; } .paper-scene__caption { margin-top: 0; padding-inline: 2%; } }
209
+ @media (max-width: 420px) { .paper-scene__story-label { font-size: 9px; letter-spacing: 0; } .paper-scene__caption { padding-inline: 0; gap: 2px; } .paper-scene__edition { font-size: 12px; } .paper-scene__controls .apple-button { font-size: 9px; } }
210
+ </style>
@@ -0,0 +1,149 @@
1
+ /** Articulated paper animation: two bounds, one sigh, then a fixed ambient cast.
2
+ * All keyframes are prepared once; playback needs no RAF, timers or DOM insertion.
3
+ */
4
+ export interface PaperStory { setPaused(paused: boolean): void; dispose(): void }
5
+ const DURATION = 14_000
6
+ type Cue = Keyframe & { at: number }
7
+ const pose = (at: number, x = 0, y = 0, angle = 0, sx = 1, sy = sx, easing = 'ease-in-out'): Cue => ({
8
+ at, transform: `translate(${x}px, ${y}px) rotate(${angle}deg) scale(${sx}, ${sy})`, easing,
9
+ })
10
+
11
+ export function createPaperStory(art: SVGSVGElement, scene: HTMLElement, intro: boolean, settled: () => void): PaperStory {
12
+ let disposed = false
13
+ let paused = false
14
+ let narratives: Animation[] = []
15
+ const ambient: Animation[] = []
16
+ const target = (part: string) => art.querySelector<SVGElement>(`[data-part="${part}"]`)!
17
+ const cue = (element: Element | null, name: string, frames: Cue[]) => {
18
+ if (!element) return
19
+ const animation = element.animate(frames.map(({ at, ...frame }) => ({ ...frame, offset: at / DURATION })), { duration: DURATION, fill: 'both', easing: 'linear' })
20
+ animation.id = `paper-story-${name}`
21
+ narratives.push(animation)
22
+ return animation
23
+ }
24
+ const joint = (part: string, frames: Array<[number, number]>) => cue(target(part), part, frames.map(([at, angle]) => ({ at, transform: `rotate(${angle}deg)`, easing: 'ease-in-out' })))
25
+ const loop = (element: Element, name: string, frames: Keyframe[], duration: number, phase = 0, easing = 'ease-in-out') => {
26
+ const animation = element.animate(frames, { duration, iterations: Infinity, easing })
27
+ animation.id = `paper-loop-${name}`
28
+ animation.currentTime = phase
29
+ if (paused) animation.pause()
30
+ ambient.push(animation)
31
+ }
32
+ loop(target('current'), 'river', [{ transform: 'translate(-18px, -2px)', opacity: 0 }, { transform: 'translate(0px, 0px)', opacity: 1 }, { transform: 'translate(18px, 2px)', opacity: 0 }], 6800, 0, 'linear')
33
+ art.querySelectorAll<SVGElement>('[data-drifter]').forEach((element, index) => {
34
+ const lane = index % 2
35
+ // The first position is behind solid bark; the root opening exposes each nut.
36
+ // Neither an individual acorn nor its wake ever fades in or out.
37
+ loop(element, `acorn-${index}`, [
38
+ { transform: 'translate(579px, 391px)', offset: 0 },
39
+ { transform: 'translate(605px, 421px)', offset: .12 },
40
+ { transform: 'translate(627px, 458px)', offset: .24 },
41
+ { transform: `translate(665px, ${488 + lane * 8}px)`, offset: .35 },
42
+ { transform: `translate(820px, ${514 + lane * 14}px)`, offset: .65 },
43
+ { transform: `translate(1065px, ${549 + lane * 19}px)`, offset: 1 },
44
+ ], 20_000, index * 4000, 'linear')
45
+ })
46
+ // Feet are outside this shared torso/neck/head/arms transform and stay planted.
47
+ const breathe = () => loop(target('breath'), 'breath', [{ transform: 'scale(1, 1)' }, { transform: 'scale(1, .987)' }, { transform: 'scale(1, 1)' }], 4600)
48
+
49
+ if (intro) {
50
+ cue(target('camera'), 'camera', [
51
+ { at: 0, transform: 'translate(-290px, -122px) scale(1.72)' },
52
+ { at: 4700, transform: 'translate(-290px, -122px) scale(1.72)', easing: 'cubic-bezier(.45, 0, .2, 1)' },
53
+ { at: 8700, transform: 'translate(0px, 0px) scale(1)' },
54
+ { at: DURATION, transform: 'translate(0px, 0px) scale(1)' },
55
+ ])
56
+ const actor: Cue[] = [pose(0, 150, 428, 0, .8, .8, 'linear')]
57
+ const shadow: Cue[] = [{ at: 0, transform: 'translate(150px, 432px) scale(.8)', opacity: .13 }]
58
+ // The world anchor is only airborne during a bound. Squash, counterbalance,
59
+ // toe-off and foot placement are separate poses, with feet fixed on contact.
60
+ const bound = (start: number, duration: number, x0: number, x1: number, height: number) => {
61
+ for (let i = 0; i <= 32; i++) {
62
+ const t = i / 32, lift = 4 * t * (1 - t), x = x0 + (x1 - x0) * t
63
+ actor.push(pose(start + duration * t, x, 428 - height * lift, 0, .8, .8, 'linear'))
64
+ shadow.push({ at: start + duration * t, transform: `translate(${x}px, 432px) scale(${.8 - lift * .12}, ${.8 - lift * .26})`, opacity: .13 - lift * .065 })
65
+ }
66
+ }
67
+ bound(550, 720, 150, 326, 68)
68
+ bound(1830, 600, 326, 462, 45)
69
+ actor.push(pose(DURATION, 462, 428, 0, .8))
70
+ shadow.push({ at: DURATION, transform: 'translate(462px, 432px) scale(.8)', opacity: .13 })
71
+ cue(target('squirrel'), 'squirrel', actor)
72
+ cue(target('actor-shadow'), 'shadow', shadow)
73
+
74
+ // Anticipation → toe-off extension → compact flight → landing compression.
75
+ // The second bound is shorter and flows out of the first landing's recovery.
76
+ cue(target('body'), 'body', [
77
+ pose(0), pose(190), pose(430, 3, 2, 12, 1.07, .78, 'cubic-bezier(.5,0,.8,.4)'),
78
+ pose(550, 6, -1, 16, .96, 1.08, 'ease-out'), pose(760, 6, 0, 10, 1, .96),
79
+ pose(980, 1, 0, 2, 1.02, .96), pose(1190, -2, 0, -4, .98, 1.04),
80
+ pose(1270, 1, 0, 1, 1, 1, 'cubic-bezier(.15,.6,.3,1)'),
81
+ pose(1380, 10, 2, 15, 1.1, .76), pose(1590, 2, 0, 3, 1, .97),
82
+ pose(1730, 4, 2, 11, 1.07, .8, 'cubic-bezier(.5,0,.8,.4)'),
83
+ pose(1830, 7, -1, 15, .97, 1.07, 'ease-out'), pose(2000, 5, 0, 9, 1, .95),
84
+ pose(2220, 0, 0, -1, 1, 1), pose(2370, -2, 0, -4, .98, 1.03),
85
+ pose(2430, 1, 0, 1, 1, 1, 'cubic-bezier(.15,.6,.3,1)'),
86
+ pose(2550, 9, 2, 14, 1.09, .79), pose(2820, 0, 0, 2, 1, .99),
87
+ pose(3180, 0, 0, 1, 1, 1.015), pose(3850, -2, 1, 4, 1.01, .97),
88
+ pose(5650, -7, 2, 9, 1.04, .9), pose(DURATION, -7, 2, 9, 1.04, .9),
89
+ ])
90
+ cue(target('back-foot'), 'back-foot', [
91
+ pose(0), pose(430, 0, 0, 0, 1, 1, 'ease-in'), pose(550, 0, 0, 27),
92
+ pose(760, -7, -13, 12), pose(1020, -6, -15, 3), pose(1200, -2, -3, 8),
93
+ pose(1310), pose(1730, 0, 0, 0, 1, 1, 'ease-in'), pose(1830, 0, 0, 25),
94
+ pose(2020, -6, -12, 11), pose(2240, -4, -10, 2), pose(2380, -1, -2, 6),
95
+ pose(2470), pose(DURATION),
96
+ ])
97
+ cue(target('front-foot'), 'front-foot', [
98
+ pose(0), pose(420), pose(550, 0, 0, 22), pose(710, -10, -16, 20),
99
+ pose(980, -4, -11, 5), pose(1150, 4, -2, -9), pose(1270),
100
+ pose(1730), pose(1830, 0, 0, 21), pose(2000, -9, -14, 18),
101
+ pose(2200, -2, -8, 2), pose(2350, 4, -1, -8), pose(2430),
102
+ pose(3200), pose(5600, 9), pose(DURATION, 9),
103
+ ])
104
+ joint('tail', [[0, 0], [430, 16], [550, -8], [720, -24], [970, -6], [1240, 17], [1410, 9], [1630, -5], [1740, 12], [1830, -8], [1990, -21], [2200, -5], [2440, 16], [2660, -6], [3050, 0], [3550, -8], [4650, -29], [6100, -51], [DURATION, -51]])
105
+ // Head rotates about an overlapping neck joint; no independent translation.
106
+ joint('head', [[0, -3], [430, -8], [585, -12], [810, -4], [1040, 2], [1240, -3], [1410, -12], [1630, -3], [1750, -7], [1870, -11], [2070, -3], [2290, 2], [2460, -4], [2640, -10], [2880, -2], [3250, 0], [4050, 10], [5500, 18], [DURATION, 18]])
107
+ // Both arms and the last acorn form one cradle, including during the sigh.
108
+ joint('cradle', [[0, 0], [430, -4], [620, 5], [950, -7], [1280, 3], [1530, -3], [1750, -4], [1900, 4], [2220, -6], [2460, 3], [2830, 0], [4300, -4], [5600, -6], [DURATION, -6]])
109
+ cue(target('eyelid'), 'eyelid', [{ at: 0, opacity: 0 }, { at: 3330, opacity: 0 }, { at: 5200, opacity: 1 }, { at: DURATION, opacity: 1 }])
110
+ joint('brow', [[0, -13], [3300, -13], [5050, 12], [DURATION, 12]])
111
+ // Establish the flowing pantry below the initial crop before pulling back.
112
+ cue(target('drifters'), 'release', [{ at: 0, visibility: 'hidden' }, { at: 4400, visibility: 'hidden' }, { at: 4401, visibility: 'visible' }, { at: DURATION, visibility: 'visible' }])
113
+ const captionTimes = [0, 3300, 6600, 9500, DURATION]
114
+ for (let index = 0; index < 4; index++) {
115
+ const start = captionTimes[index], end = captionTimes[index + 1]
116
+ const frames: Cue[] = [{ at: 0, opacity: index === 0 ? 1 : 0 }]
117
+ if (index > 0) frames.push({ at: start - 220, opacity: 0 }, { at: start + 220, opacity: 1 })
118
+ if (index < 3) frames.push({ at: end - 240, opacity: 1 }, { at: end + 150, opacity: 0 })
119
+ frames.push({ at: DURATION, opacity: index === 3 ? 1 : 0 })
120
+ cue(scene.querySelector(`[data-caption="${index}"]`), `caption-${index}`, frames)
121
+ }
122
+ const clock = cue(target('breath'), 'clock', [{ at: 0, transform: 'scale(1, 1)' }, { at: 3200, transform: 'scale(1, 1)' }, { at: 3700, transform: 'scale(1, 1.015)', easing: 'ease-out' }, { at: 5500, transform: 'scale(1, 1)' }, { at: DURATION, transform: 'scale(1, 1)' }])!
123
+ clock.onfinish = () => {
124
+ if (disposed) return
125
+ // CSS is exactly the last pose, so releasing finite effects cannot snap.
126
+ for (const animation of narratives) { animation.onfinish = null; animation.cancel() }
127
+ narratives = []
128
+ breathe()
129
+ settled()
130
+ }
131
+ } else breathe()
132
+ return {
133
+ setPaused(value) {
134
+ if (disposed) return
135
+ paused = value
136
+ for (const animation of [...narratives, ...ambient]) {
137
+ if (animation.playState === 'finished' || animation.playState === 'idle') continue
138
+ if (paused) animation.pause()
139
+ else animation.play()
140
+ }
141
+ },
142
+ dispose() {
143
+ disposed = true
144
+ for (const animation of [...narratives, ...ambient]) { animation.onfinish = null; animation.cancel() }
145
+ narratives = []
146
+ ambient.length = 0
147
+ },
148
+ }
149
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite-plugin-pages/client" />
@@ -0,0 +1,8 @@
1
+ import 'apptify/style.css'
2
+ import { createApp } from 'vue'
3
+ import { createAppleUI } from 'apptify'
4
+ import App from './App.vue'
5
+ import router from './router'
6
+ import './style.css'
7
+
8
+ createApp(App).use(createAppleUI({ theme: 'light', motion: 'auto', persist: true })).use(router).mount('#app')
@@ -0,0 +1,24 @@
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+ import routes from 'virtual:generated-pages'
3
+ import { nextTick } from 'vue'
4
+
5
+ const router = createRouter({
6
+ history: createWebHistory(import.meta.env.BASE_URL),
7
+ routes,
8
+ async scrollBehavior(to, from, savedPosition) {
9
+ await nextTick()
10
+ if (savedPosition) return { ...savedPosition, behavior: 'instant' }
11
+ if (to.hash && document.getElementById(to.hash.slice(1))) {
12
+ const motion = document.querySelector('#app > .apple-provider')?.getAttribute('data-apple-motion')
13
+ return { el: to.hash, top: 96, behavior: to.path === from.path && motion === 'full' ? 'smooth' : 'instant' }
14
+ }
15
+ return { top: 0, behavior: 'instant' }
16
+ },
17
+ })
18
+
19
+ router.afterEach(to => {
20
+ const titles: Record<string, string> = { '/': '首页', '/components': '组件', '/settings': '设置' }
21
+ document.title = `${titles[to.path] ?? '页面不存在'} · Apptify`
22
+ })
23
+
24
+ export default router