apptify 0.7.2 → 0.8.1

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.
@@ -0,0 +1,197 @@
1
+ interface Point { x: number; y: number }
2
+ export interface GatewaySnapshot {
3
+ seconds: number
4
+ phase: number
5
+ intro: boolean
6
+ zoom: number
7
+ bug: Point & { angle: number; scale: number; visible: boolean; captured: boolean }
8
+ mouth: Point
9
+ tip: Point
10
+ tongueVisible: boolean
11
+ contact: number
12
+ shoot: number
13
+ retractEnd: number
14
+ period: number
15
+ introEnd: number
16
+ }
17
+ export interface GatewayStory {
18
+ seek(seconds: number): GatewaySnapshot
19
+ play(): void
20
+ pause(): void
21
+ getState(): GatewaySnapshot
22
+ setReducedMotion(value: boolean): void
23
+ dispose(): void
24
+ timing: Readonly<{ contact: number; shoot: number; retractEnd: number; period: number; introEnd: number; initialPhase: number; zoomStart: number }>
25
+ }
26
+ type GatewayArt = SVGSVGElement & { gatewayStory?: GatewayStory }
27
+
28
+ export function createGatewayStory(svg: GatewayArt, reducedMotion = false): GatewayStory {
29
+ const get = (name: string) => {
30
+ const el = svg.querySelector<SVGElement>(`[data-part="${name}"]`);
31
+ if (!el) throw new Error(`Missing gateway part: ${name}`);
32
+ return el;
33
+ };
34
+ const parts = Object.fromEntries([
35
+ 'camera','bug','bug-pitch','bug-bag','bug-legs','wing-front','wing-back','bug-shadow',
36
+ 'frog-torso','frog-head','frog-throat','eye-left','eye-right','pupil-left','pupil-right',
37
+ 'mouth-closed','mouth-open','mouth-cavity','mouth-inner','lip-head','lip-front',
38
+ 'tongue','tongue-edge','tongue-fill','tongue-shine','tongue-tip','flag-keep','flag-town'
39
+ ].map(name => [name, get(name)]));
40
+ const clamp = (x: number, a=0, b=1) => Math.max(a, Math.min(b, x));
41
+ const mix = (a: number,b: number,t: number) => a+(b-a)*t;
42
+ const smooth = (x: number) => { x=clamp(x); return x*x*(3-2*x); };
43
+ const segment = (t: number,a: number,b: number) => smooth((t-a)/(b-a));
44
+ const pulse = (t: number,a: number,b: number) => t>a && t<b ? Math.sin(Math.PI*(t-a)/(b-a)) ** 2 : 0;
45
+ const attr = (name: string,key: string,value: string | number) => parts[name].setAttribute(key,String(value));
46
+ const show = (name: string, yes: boolean) => attr(name,'display',yes?'inline':'none');
47
+ const n = (x: number) => Number(x.toFixed(4));
48
+ const root = {x:636, y:539};
49
+ const speed=174, startX=-130, catchX=500;
50
+ const contact=(catchX-startX)/speed;
51
+ const shoot=contact-.155;
52
+ const retractEnd=contact+.37;
53
+ const period=4.85;
54
+ const initialPhase=1.25;
55
+ const zoomStart=retractEnd-initialPhase+.18;
56
+ const introEnd=zoomStart+1.55;
57
+ const flight = (t: number) => {
58
+ const x=startX+speed*t;
59
+ const y=408+11*Math.sin(x*.010+.6)+4*Math.sin(x*.032+.4)-.018*(x-400);
60
+ const dy=11*.010*Math.cos(x*.010+.6)+4*.032*Math.cos(x*.032+.4)-.018;
61
+ return {x,y,angle:Math.atan2(dy,1)*180/Math.PI*.48};
62
+ };
63
+ const hit=flight(contact);
64
+ const bezier = (a: number,b: number,c: number,d: number,t: number) => (1-t)**3*a+3*(1-t)**2*t*b+3*(1-t)*t*t*c+t**3*d;
65
+ let disposed=false, raf=0, clock=0, lastNow=0, userPaused=false;
66
+ let inView=true;
67
+ const reduce=window.matchMedia('(prefers-reduced-motion: reduce)');
68
+ let observer: IntersectionObserver | undefined;
69
+ let snapshot: GatewaySnapshot | undefined;
70
+
71
+ function render(seconds: number): GatewaySnapshot {
72
+ const intro=seconds<introEnd;
73
+ const t=intro ? Math.min(initialPhase+seconds,period-.01) : (seconds-introEnd)%period;
74
+ const anticipating=segment(t,shoot-.25,shoot)*(1-segment(t,shoot,shoot+.12));
75
+ const action=pulse(t,shoot,retractEnd+.10);
76
+ const gulp=pulse(t,retractEnd+.08,retractEnd+.42);
77
+
78
+ const hx=-3.5*action, hy=1.5*anticipating-1.5*action+.7*gulp;
79
+ const headAngle=-1.0*action;
80
+ const headTransform=`translate(${n(hx)} ${n(hy)}) rotate(${n(headAngle)} 0 -100)`;
81
+ attr('frog-head','transform',headTransform);
82
+ attr('lip-head','transform',headTransform);
83
+ attr('frog-torso','transform',`scale(${n(1+.008*anticipating)} ${n(1-.016*anticipating+.004*Math.sin(seconds*1.8))})`);
84
+ attr('frog-throat','transform',`translate(0 -57) scale(${n(1+.035*gulp)} ${n(1+.055*gulp)}) translate(0 57)`);
85
+
86
+ const rad=headAngle*Math.PI/180;
87
+ const localMouth={x:-2, y:-90};
88
+ const mouth={x:root.x+hx+localMouth.x*Math.cos(rad)-(localMouth.y+100)*Math.sin(rad),
89
+ y:root.y+hy-100+localMouth.x*Math.sin(rad)+(localMouth.y+100)*Math.cos(rad)};
90
+ let bug=flight(Math.min(t,contact)), scale=1, pitch=bug.angle, captured=false;
91
+ let tip={...mouth}, tongueVisible=false, retract=0;
92
+ if(t>=shoot && t<contact) {
93
+ const u=clamp((t-shoot)/(contact-shoot));
94
+ const extension=1-(1-u)**2;
95
+ tip={x:mix(mouth.x,hit.x,extension), y:mix(mouth.y,hit.y,extension)};
96
+ tongueVisible=true;
97
+ } else if(t>=contact && t<retractEnd) {
98
+ retract=clamp((t-contact)/(retractEnd-contact));
99
+ const u=retract;
100
+
101
+
102
+ bug={x:bezier(hit.x,hit.x+23,mouth.x-22,mouth.x,u),
103
+ y:bezier(hit.y,hit.y+2,mouth.y-12,mouth.y,u), angle:0};
104
+ scale=1-smooth((u-.79)/.21);
105
+ pitch=mix(hit.angle,20,smooth(u));
106
+ tip={x:bug.x,y:bug.y};
107
+ tongueVisible=true;
108
+ captured=true;
109
+ }
110
+ const visible=t<retractEnd && scale>.0001;
111
+ show('bug',visible);
112
+
113
+ if(visible) {
114
+ attr('bug','transform',`translate(${n(bug.x)} ${n(bug.y)}) scale(${n(scale)})`);
115
+ attr('bug-pitch','transform',`rotate(${n(pitch)})`);
116
+ }
117
+ const wing=2*Math.PI*11.3*seconds;
118
+ const flutter=captured?1.2:1;
119
+ attr('wing-front','transform',`translate(-5 -9) rotate(${n(-18+35*Math.sin(wing)*flutter)}) scale(1 ${n(.45+.55*Math.abs(Math.cos(wing)))}) translate(5 9)`);
120
+ attr('wing-back','transform',`translate(-5 -8) rotate(${n(18-33*Math.sin(wing+.9)*flutter)}) scale(1 ${n(.55+.45*Math.abs(Math.cos(wing+.9)))}) translate(5 8)`);
121
+ attr('bug-bag','transform',`rotate(${n(captured?-16*smooth(retract):5.5*Math.sin(seconds*5.1-.9)-3)} -22 -2)`);
122
+ attr('bug-legs','d',captured?'M-19 8-26 10m16 0-5 8m23-7 7 5':`M-19 8-28 ${n(18+2*Math.sin(seconds*8))}m18-8-2 13m20-12 8 10`);
123
+ show('bug-shadow',visible && !captured);
124
+ attr('bug-shadow','cx',n(bug.x));
125
+ attr('bug-shadow','rx',n(18+(bug.y-400)*.06));
126
+
127
+ const look=t<contact?clamp((bug.x-root.x)/120,-1,1):(1-segment(t,retractEnd,retractEnd+.55))*-.45;
128
+ const lookY=t<contact?clamp((bug.y-(root.y-140))/80,-1,1):.15;
129
+ for(const name of ['pupil-left','pupil-right']) attr(name,'transform',`translate(${n(look*3.8)} ${n(lookY*2)})`);
130
+ const blink=1-.94*Math.max(pulse(t,retractEnd+.12,retractEnd+.36),pulse(t,.53,.70));
131
+ attr('eye-left','transform',`translate(-44 -140) scale(1 ${n(blink)})`);
132
+ attr('eye-right','transform',`translate(44 -140) scale(1 ${n(blink)})`);
133
+ const opening=segment(t,shoot-.09,shoot+.035)*(1-segment(t,retractEnd-.015,retractEnd+.13));
134
+ const w=10+10*opening, bottom=-94+18*opening;
135
+ show('mouth-closed',opening<.03);
136
+ show('mouth-open',opening>=.03);
137
+ attr('mouth-cavity','d',`M${-w} -96Q0 ${n(-98-2*opening)} ${w} -96Q${n(w+1)} ${n(bottom)} 0 ${n(bottom)}Q${n(-w-1)} ${n(bottom)} ${-w} -96Z`);
138
+ attr('mouth-inner','d',`M${n(-12*opening)} ${n(bottom-2*opening)}Q0 ${n(bottom-7*opening)} ${n(12*opening)} ${n(bottom-2*opening)}Q0 ${n(bottom)} ${n(-12*opening)} ${n(bottom-2*opening)}Z`);
139
+ attr('lip-front','d',opening<.03?'M-24-92Q0-82 25-92':`M${n(-w-1)} ${n(bottom-5)}Q0 ${n(bottom+6)} ${n(w+1)} ${n(bottom-5)}`);
140
+ attr('lip-front','stroke-width',opening<.03?1.5:4.5);
141
+ show('tongue',tongueVisible);
142
+ if(tongueVisible) {
143
+
144
+ const length=Math.hypot(tip.x-mouth.x,tip.y-mouth.y);
145
+ const bend=Math.min(7,length*.035)*(captured?Math.sin(retract*Math.PI):.45);
146
+ const path=(offset: number)=>`M${n(mouth.x)} ${n(mouth.y+offset)} Q${n((mouth.x+tip.x)/2)} ${n((mouth.y+tip.y)/2+bend+offset)} ${n(tip.x)} ${n(tip.y+offset)}`;
147
+ const thickness=clamp(length/14);
148
+ attr('tongue-edge','stroke-width',n(12*thickness)); attr('tongue-fill','stroke-width',n(8.2*thickness)); attr('tongue-shine','stroke-width',n(1.3*thickness));
149
+ attr('tongue-edge','d',path(thickness)); attr('tongue-fill','d',path(0)); attr('tongue-shine','d',path(-2.5*thickness));
150
+ attr('tongue-tip','transform',`translate(${n(tip.x)} ${n(tip.y)}) rotate(${n(Math.atan2(tip.y-mouth.y,tip.x-mouth.x)*180/Math.PI)}) scale(${n(Math.min(1,length/10))})`);
151
+ }
152
+ attr('flag-keep','transform',`translate(1014 127) skewY(${n(Math.sin(seconds*2)*2)}) translate(-1014 -127)`);
153
+ attr('flag-town','transform',`translate(955 225) skewY(${n(Math.sin(seconds*2+.8)*2)}) translate(-955 -225)`);
154
+ let zoom=1, focus={x:600,y:360};
155
+ if(intro) {
156
+ const out=segment(seconds,zoomStart,introEnd);
157
+ const focusBug=t<retractEnd?bug:mouth;
158
+ zoom=mix(3.05,1,out);
159
+ focus={x:mix(focusBug.x-8,600,out),y:mix(focusBug.y-13,360,out)};
160
+ }
161
+ attr('camera','transform',`translate(600 360) scale(${n(zoom)}) translate(${n(-focus.x)} ${n(-focus.y)})`);
162
+ svg.dataset.story=reduce.matches || reducedMotion ? "static" : intro ? "intro" : "loop";
163
+ snapshot={seconds,phase:t,intro,zoom,bug:{...bug,scale,visible,captured},mouth,tip,tongueVisible,contact,shoot,retractEnd,period,introEnd};
164
+ return snapshot;
165
+ }
166
+ const allowed=()=>!disposed&&!userPaused&&!document.hidden&&inView&&!reduce.matches&&!reducedMotion;
167
+ function stopRAF() { if(raf) cancelAnimationFrame(raf); raf=0; lastNow=0; }
168
+ function tick(now: number) {
169
+ raf=0; if(!allowed()) {lastNow=0;return;}
170
+ if(lastNow) clock+=Math.min((now-lastNow)/1000,.08);
171
+ lastNow=now; render(clock); raf=requestAnimationFrame(tick);
172
+ }
173
+ function sync() { svg.dataset.playing=String(allowed()); if(allowed()) {if(!raf) {lastNow=0;raf=requestAnimationFrame(tick);}} else stopRAF(); }
174
+ function motionChanged() {
175
+ if(reduce.matches || reducedMotion) {stopRAF();render(introEnd+2.7);sync();}
176
+ else {clock=Math.max(clock,introEnd);render(clock);sync();}
177
+ }
178
+ document.addEventListener('visibilitychange',sync);
179
+ reduce.addEventListener('change',motionChanged);
180
+ if('IntersectionObserver' in window) {
181
+ observer=new IntersectionObserver(entries=>{inView=entries[0]?.isIntersecting??true;sync();},{threshold:0});
182
+ observer.observe(svg);
183
+ }
184
+ if(reduce.matches || reducedMotion) render(introEnd+2.7); else render(0);
185
+ const controller={
186
+ seek(seconds: number){if(!Number.isFinite(seconds)||seconds<0) throw new RangeError('seconds must be finite and non-negative');userPaused=true;sync();clock=seconds;return render(clock);},
187
+ play(){userPaused=false;sync();},
188
+ pause(){userPaused=true;sync();},
189
+ getState(){return snapshot!;},
190
+ setReducedMotion(value: boolean){reducedMotion=value;motionChanged();},
191
+ dispose(){if(disposed)return;disposed=true;stopRAF();svg.dataset.playing="false";observer?.disconnect();document.removeEventListener('visibilitychange',sync);reduce.removeEventListener('change',motionChanged);delete svg.gatewayStory;},
192
+ timing:Object.freeze({contact,shoot,retractEnd,period,introEnd,initialPhase,zoomStart})
193
+ };
194
+ svg.gatewayStory=controller;
195
+ sync();
196
+ return controller;
197
+ }
@@ -28,6 +28,12 @@ export default defineHandbook({
28
28
  "default": "false",
29
29
  "description": "使用 button 单选并更新 modelValue;false 时有 href 的项渲染为链接,其余作为内容。"
30
30
  },
31
+ {
32
+ "name": "border",
33
+ "type": "boolean",
34
+ "default": "false",
35
+ "description": "显示列表项之间的分隔线;默认不显示。"
36
+ },
31
37
  {
32
38
  "name": "disabled",
33
39
  "type": "boolean",
@@ -11,5 +11,5 @@ export default defineHandbook({
11
11
  events: [{ name: 'interaction', type: '() => void', description: '拖动、点击轨道、键盘或滚轮操作时触发。' }],
12
12
  slots: [],
13
13
  methods: [{ name: 'refresh()', type: '() => void', description: '重新测量目标及滚动位置。尺寸、内容和滚动变化默认自动同步。' }],
14
- notes: ['AppleProvider 默认启用全局接管,无需逐个包裹内容。滚动条使用 fixed,不占布局空间;z-index 取 max(80, 目标及祖先层级),弹窗内留在所属弹层上下文。', '原本主动隐藏滚动条的轮播、标签栏与日期滚轮保持原有交互;data-apple-scrollbar-ignore 可排除指定容器。'],
14
+ notes: ['AppleProvider 默认启用全局接管,无需逐个包裹内容。滚动条使用 fixed,不占布局空间;z-index 跟随目标及祖先层级,默认为 0,弹窗内留在所属弹层上下文。', '原本主动隐藏滚动条的轮播、标签栏与日期滚轮保持原有交互;data-apple-scrollbar-ignore 可排除指定容器。'],
15
15
  })
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <main class="main-content paper-502-page">
3
+ <apple-card class="paper-502-card" shadow="none" zoom="none">
4
+ <template #title>
5
+ <div class="paper-502-heading">
6
+ <p class="paper-502-number" aria-hidden="true">502</p>
7
+ <h1>网关错误</h1>
8
+ </div>
9
+ </template>
10
+ <p class="paper-502-copy">包裹在城门口被拦住了。<br />服务暂时无法响应,请稍后再试。</p>
11
+ <div class="paper-502-actions">
12
+ <router-link v-slot="{ href, navigate }" :to="homePath" custom>
13
+ <apple-button :href="href" @click="navigate">返回首页</apple-button>
14
+ </router-link>
15
+ <apple-link as="button" @click="goBack"><ArrowLeft :size="16" aria-hidden="true" /> 返回上一页</apple-link>
16
+ </div>
17
+ </apple-card>
18
+ <GatewayPaperScene class="paper-502-illustration" />
19
+ </main>
20
+ </template>
21
+
22
+ <route lang="json">
23
+ { "alias": "/error/502", "meta": { "title": "网关错误" } }
24
+ </route>
25
+
26
+ <script setup lang="ts">
27
+ import { ArrowLeft } from 'lucide-vue-next'
28
+ import { useRouter } from 'vue-router'
29
+ import GatewayPaperScene from '../components/gateway-paper-scene.vue'
30
+
31
+ const router = useRouter()
32
+ const homePath = router.getRoutes().some(page => page.path === '/') ? '/' : '/settings'
33
+ function goBack() {
34
+ if (typeof router.options.history.state.back === 'string') router.back()
35
+ else void router.push(homePath)
36
+ }
37
+ </script>
38
+
39
+ <style scoped>
40
+ .paper-502-page { max-width: 1360px; display: grid; grid-template-columns: minmax(260px, .78fr) minmax(0, 1.65fr); align-content: center; align-items: center; column-gap: 16px; padding-top: 38px; padding-bottom: calc(38px + 64px); }
41
+ .paper-502-card { background: transparent; border: 0; overflow: visible; }
42
+ .paper-502-card :deep(.apple-card__body) { padding: 0 0 0 8px; }
43
+ .paper-502-card :deep(.apple-card__heading) { margin: 0; }
44
+ .paper-502-heading { width: 100%; }
45
+ .paper-502-number { color: var(--apple-text); font-family: Georgia, 'Times New Roman', serif; font-size: clamp(78px, 8.2vw, 118px); line-height: .9; letter-spacing: -.075em; margin: 0 0 28px; font-weight: 400; }
46
+ .paper-502-heading h1 { font-size: clamp(26px, 2.7vw, 35px); line-height: 1.35; letter-spacing: -.035em; font-weight: 600; margin: 0 0 17px; }
47
+ .paper-502-copy { font-size: 13px; line-height: 1.95; color: var(--apple-secondary); margin: 0; text-wrap: pretty; }
48
+ .paper-502-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 27px; }
49
+ .paper-502-actions .apple-link { font-size: 12px; padding-inline: 8px; color: var(--apple-secondary); }
50
+ .paper-502-illustration { min-width: 0; }
51
+ @media (max-width: 880px) {
52
+ .paper-502-page { grid-template-columns: minmax(0, 1fr); max-width: 680px; padding-top: 18px; }
53
+ .paper-502-illustration { grid-row: 1; }
54
+ .paper-502-card { margin-top: 26px; }
55
+ .paper-502-card :deep(.apple-card__body) { padding: 0; text-align: center; }
56
+ .paper-502-number { font-size: 70px; margin-bottom: 18px; }
57
+ .paper-502-heading h1 { font-size: 26px; margin-bottom: 12px; }
58
+ .paper-502-actions { justify-content: center; margin-top: 18px; }
59
+ }
60
+ @media (max-width: 420px) {
61
+ .paper-502-page { padding-inline: 18px; padding-top: 12px; }
62
+ .paper-502-copy { font-size: 12px; }
63
+ .paper-502-card { margin-top: 22px; }
64
+ }
65
+ </style>
@@ -50,7 +50,7 @@ async function adapt(directory) {
50
50
  source = source.replace('to="/" aria-label="Apptify 首页"', ':to="homePath" aria-label="Apptify 首页"')
51
51
  source = source.replace(navigationEnd, "].filter(item => router.getRoutes().some(page => page.path === item.href))\nconst homePath = navigation[0]?.href ?? '/'\n\nfunction navigate")
52
52
  }
53
- if (['[...all].vue', '403.vue'].some(view => path === join(output, 'src/views', view))) {
53
+ if (['[...all].vue', '403.vue', '502.vue'].some(view => path === join(output, 'src/views', view))) {
54
54
  source = source.replace('to="/" custom', ':to="homePath" custom')
55
55
  source = source.replace('const router = useRouter()', "const router = useRouter()\nconst homePath = router.getRoutes().some(page => page.path === '/') ? '/' : '/settings'")
56
56
  source = source.replace("else void router.push('/')", 'else void router.push(homePath)')
@@ -92,7 +92,7 @@ await writeFile(join(output, 'gitignore'), 'node_modules/\ndist/\n.env.local\n')
92
92
  await writeFile(join(output, 'README.md'), [
93
93
  "# Apptify 应用模板",
94
94
  "",
95
- "可编辑的 Vue 应用,包含导航、账号表单、外观设置、403、404 与组件示例。建议 Node.js 22 或 24 LTS。",
95
+ "可编辑的 Vue 应用,包含导航、账号表单、外观设置、403、404、502 与组件示例。建议 Node.js 22 或 24 LTS。",
96
96
  "",
97
97
  "```sh",
98
98
  "npm install",