@@ -45,7 +45,6 @@ const licenses = Object.keys(licenseMap)
4545const selected = ref <string | null >(null )
4646const downloading = ref (false )
4747
48- // Only the visible badges — TransitionGroup handles enter/leave/move
4948const visibleBadges = computed (() => {
5049 if (! selected .value ) return allBadges
5150 const keys = new Set (licenseMap [selected .value ])
@@ -81,72 +80,168 @@ async function downloadLicense() {
8180
8281<template >
8382 <div class =" ls-root" >
84- <p class =" ls-hint" >👇 Click a license to explore</p >
85-
86- <div class =" ls-buttons" >
87- <button
88- v-for =" name in licenses"
89- :key =" name"
90- :class =" ['ls-btn', { active: selected === name }]"
91- @click =" selectLicense(name)"
92- >
93- {{ name }}
94- </button >
83+ <!-- Fixed-height slot for hint so nothing shifts -->
84+ <div class =" ls-hint-slot" >
85+ <Transition name="hint">
86+ <p v-if =" !selected" class =" ls-hint" >
87+ Pick Your License <span class =" hint-arrow" >↓</span >
88+ </p >
89+ </Transition >
9590 </div >
9691
97- <div class =" ls-badges-wrap" >
98- <TransitionGroup name="badge" tag="div" class="ls-badges">
99- <div
100- v-for =" b in visibleBadges"
101- :key =" b.key"
102- class =" ls-badge"
103- :style =" { '--badge-color': b.color }"
92+ <!-- Buttons area with optional animated border -->
93+ <div class =" ls-buttons-area" >
94+ <!-- Rotating border overlay -->
95+ <Transition name="box-fade">
96+ <div v-if =" !selected" class =" ls-border-ring" aria-hidden =" true" />
97+ </Transition >
98+
99+ <div class =" ls-buttons" >
100+ <button
101+ v-for =" name in licenses"
102+ :key =" name"
103+ :class =" ['ls-btn', { active: selected === name }]"
104+ @click =" selectLicense(name)"
104105 >
105- <span class =" badge-icon" >{{ b.icon }}</span >
106- <div class =" badge-text" >
107- <span class =" badge-label" >{{ b.label }}</span >
108- <span class =" badge-fullname" >{{ b.fullName }}</span >
109- </div >
110- <div class =" badge-desc" >{{ b.desc }}</div >
111- </div >
112- </TransitionGroup >
106+ {{ name }}
107+ </button >
108+ </div >
113109 </div >
114110
115- <Transition name="slide-up">
116- <button
117- v-if =" selected"
118- class =" ls-download"
119- :disabled =" downloading"
120- @click =" downloadLicense"
121- >
122- 📄 {{ downloading ? 'Downloading...' : `Download ${selected} License` }}
123- </button >
124- </Transition >
111+ <!-- Badges + download -->
112+ <div class =" ls-result" >
113+ <div class =" ls-badges-wrap" >
114+ <TransitionGroup name="badge" tag="div" class="ls-badges">
115+ <div
116+ v-for =" b in visibleBadges"
117+ :key =" b.key"
118+ class =" ls-badge"
119+ :style =" { '--badge-color': b.color }"
120+ >
121+ <span class =" badge-icon" >{{ b.icon }}</span >
122+ <div class =" badge-text" >
123+ <span class =" badge-label" >{{ b.label }}</span >
124+ <span class =" badge-fullname" >{{ b.fullName }}</span >
125+ </div >
126+ <div class =" badge-desc" >{{ b.desc }}</div >
127+ </div >
128+ </TransitionGroup >
129+ </div >
130+
131+ <Transition name="slide-up">
132+ <button
133+ v-if =" selected"
134+ class =" ls-download"
135+ :disabled =" downloading"
136+ @click =" downloadLicense"
137+ >
138+ 📄 {{ downloading ? 'Downloading...' : `Download ${selected} License` }}
139+ </button >
140+ </Transition >
141+ </div >
125142 </div >
126143</template >
127144
128145<style scoped>
146+ /* Animated gradient angle */
147+ @property --border-angle {
148+ syntax: '<angle> ';
149+ initial-value : 0deg;
150+ inherits: false;
151+ }
152+
129153.ls-root {
130154 display : flex ;
131155 flex-direction : column ;
132156 align-items : center ;
133157 gap : 12px ;
134- padding : 8px 0 ;
158+ padding : 4px 0 ;
159+ margin-top : -20px ;
160+ width : 100% ;
135161}
136162
163+ /* Fixed-height slot so buttons don't shift when hint disappears */
164+ .ls-hint-slot {
165+ height : 24px ;
166+ display : flex ;
167+ align-items : center ;
168+ justify-content : center ;
169+ }
170+
171+ /* Hint */
137172.ls-hint {
138173 font-size : 14px ;
139- color : var (--vp-c-text-2 );
174+ font-weight : 700 ;
175+ color : var (--vp-c-brand-1 );
140176 margin : 0 ;
141- font-weight : 500 ;
177+ display : flex ;
178+ align-items : center ;
179+ gap : 6px ;
180+ white-space : nowrap ;
181+ }
182+
183+ .hint-arrow {
184+ display : inline-block ;
185+ animation : nudge-down 1.5s ease-in-out infinite ;
186+ }
187+
188+ @keyframes nudge-down {
189+ 0% , 100% { transform : translateY (0 ); }
190+ 50% { transform : translateY (3px ); }
191+ }
192+
193+ .hint-enter-active { transition : all 0.4s cubic-bezier (0.34 , 1.56 , 0.64 , 1 ); }
194+ .hint-leave-active { transition : all 0.2s ease ; }
195+ .hint-enter-from { opacity : 0 ; transform : translateY (-6px ); }
196+ .hint-leave-to { opacity : 0 ; transform : translateY (-6px ); }
197+
198+ /* Buttons area — relative container for the border overlay */
199+ .ls-buttons-area {
200+ position : relative ;
201+ padding : 14px 18px ;
142202}
143203
204+ /* Rotating border ring — sits behind buttons as overlay */
205+ .ls-border-ring {
206+ position : absolute ;
207+ inset : 0 ;
208+ border-radius : 18px ;
209+ padding : 2px ;
210+ background : conic-gradient (
211+ from var (--border-angle ),
212+ var (--vp-c-brand-1 ),
213+ #2563eb ,
214+ #16a34a ,
215+ #ca8a04 ,
216+ #ea580c ,
217+ var (--vp-c-brand-1 )
218+ );
219+ animation : border-spin 3s linear infinite reverse ;
220+ -webkit-mask :
221+ linear-gradient (#fff 0 0 ) content-box ,
222+ linear-gradient (#fff 0 0 );
223+ -webkit-mask-composite : xor;
224+ mask-composite : exclude ;
225+ pointer-events : none ;
226+ }
227+
228+ @keyframes border-spin {
229+ to { --border-angle : 360deg ; }
230+ }
231+
232+ /* Box fade transition */
233+ .box-fade-enter-active { transition : all 0.4s cubic-bezier (0.34 , 1.56 , 0.64 , 1 ); }
234+ .box-fade-leave-active { transition : all 0.3s ease ; }
235+ .box-fade-enter-from { opacity : 0 ; }
236+ .box-fade-leave-to { opacity : 0 ; }
237+
144238/* License buttons */
145239.ls-buttons {
146240 display : flex ;
147241 flex-wrap : wrap ;
148242 gap : 8px ;
149243 justify-content : center ;
244+ align-items : center ;
150245}
151246
152247.ls-btn {
@@ -178,26 +273,36 @@ async function downloadLicense() {
178273 box-shadow : 0 3px 14px rgba (124 , 58 , 237 , 0.25 );
179274}
180275
181- /* Badges container — fixed height prevents hero shake */
276+ /* Result */
277+ .ls-result {
278+ display : flex ;
279+ flex-direction : column ;
280+ align-items : center ;
281+ gap : 12px ;
282+ width : 100% ;
283+ }
284+
182285.ls-badges-wrap {
183- min- height : 100 px ;
286+ height : 46 px ;
184287 width : 100% ;
288+ overflow : visible ;
185289}
186290
187291.ls-badges {
188292 display : flex ;
189293 flex-wrap : wrap ;
190294 gap : 8px ;
191295 justify-content : flex-start ;
296+ align-items : center ;
192297}
193298
194- /* Individual badge */
299+ /* Badge */
195300.ls-badge {
196301 position : relative ;
197302 display : flex ;
198303 align-items : center ;
199304 gap : 6px ;
200- padding : 8 px 14 px ;
305+ padding : 6 px 12 px ;
201306 background : var (--vp-c-bg-soft );
202307 border : 1.5px solid var (--vp-c-divider );
203308 border-radius : 10px ;
@@ -211,7 +316,7 @@ async function downloadLicense() {
211316}
212317
213318.badge-icon {
214- font-size : 18 px ;
319+ font-size : 16 px ;
215320 flex-shrink : 0 ;
216321}
217322
@@ -222,18 +327,18 @@ async function downloadLicense() {
222327}
223328
224329.badge-label {
225- font-size : 13 px ;
330+ font-size : 12 px ;
226331 font-weight : 800 ;
227332 color : var (--vp-c-text-1 );
228333}
229334
230335.badge-fullname {
231- font-size : 11 px ;
336+ font-size : 10 px ;
232337 color : var (--badge-color );
233338 font-weight : 500 ;
234339}
235340
236- /* Tooltip — absolute, doesn't affect layout */
341+ /* Tooltip */
237342.badge-desc {
238343 position : absolute ;
239344 top : calc (100% + 6px );
@@ -260,14 +365,11 @@ async function downloadLicense() {
260365 pointer-events : auto ;
261366}
262367
263- /* === TransitionGroup animations === */
264-
265- /* MOVE: remaining badges slide to new positions with spring physics */
368+ /* TransitionGroup */
266369.badge-move {
267370 transition : transform 0.5s cubic-bezier (0.34 , 1.56 , 0.64 , 1 );
268371}
269372
270- /* ENTER: badges spring up from below */
271373.badge-enter-active {
272374 transition : transform 0.5s cubic-bezier (0.34 , 1.56 , 0.64 , 1 ),
273375 opacity 0.35s ease ;
@@ -278,11 +380,9 @@ async function downloadLicense() {
278380 transform : translateY (40px ) scale (0.6 );
279381}
280382
281- /* LEAVE: badges fall with gravity + rotation */
282383.badge-leave-active {
283384 transition : transform 0.45s cubic-bezier (0.55 , 0 , 1 , 0.45 ),
284385 opacity 0.35s ease ;
285- /* Take out of flow so remaining items can move */
286386 position : absolute ;
287387}
288388
@@ -291,7 +391,7 @@ async function downloadLicense() {
291391 transform : translateY (80px ) rotate (15deg ) scale (0.5 );
292392}
293393
294- /* Download button */
394+ /* Download */
295395.ls-download {
296396 display : inline-flex ;
297397 align-items : center ;
@@ -318,19 +418,8 @@ async function downloadLicense() {
318418 cursor : wait ;
319419}
320420
321- /* Slide-up transition for download button */
322- .slide-up-enter-active {
323- transition : all 0.4s cubic-bezier (0.34 , 1.56 , 0.64 , 1 );
324- }
325- .slide-up-leave-active {
326- transition : all 0.25s ease ;
327- }
328- .slide-up-enter-from {
329- opacity : 0 ;
330- transform : translateY (12px ) scale (0.9 );
331- }
332- .slide-up-leave-to {
333- opacity : 0 ;
334- transform : translateY (8px ) scale (0.95 );
335- }
421+ .slide-up-enter-active { transition : all 0.4s cubic-bezier (0.34 , 1.56 , 0.64 , 1 ); }
422+ .slide-up-leave-active { transition : all 0.25s ease ; }
423+ .slide-up-enter-from { opacity : 0 ; transform : translateY (12px ) scale (0.9 ); }
424+ .slide-up-leave-to { opacity : 0 ; transform : translateY (8px ) scale (0.95 ); }
336425</style >
0 commit comments