Skip to content

Commit 3114ae3

Browse files
committed
refactor(docs): audit components for reduced complexity and centralized styling
- Add UnoCSS shortcuts for common patterns (icon-text, btn-*, card-*) - Extract reusable components (DocsBadge, DocsCard, DocsSkeleton, DocsProgressBar) - Create design tokens and transitions CSS files - Move brand colors (discord, vue, mastered) to theme palette - Add color-mix utility classes for theme color opacity - Extract calloutConfig.ts and DocsApiHoverSection from larger components - Add usePopoverPosition composable - Fix a11y: add ARIA attributes, keyboard handlers, proper roles - Switch from lightningcss to postcss to preserve color-mix syntax
1 parent 00472a3 commit 3114ae3

40 files changed

+740
-388
lines changed

apps/docs/src/components.d.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ declare module 'vue' {
4444
AppSettingsThemeEditor: typeof import('./components/app/settings/AppSettingsThemeEditor.vue')['default']
4545
AppSettingsToggle: typeof import('./components/app/settings/AppSettingsToggle.vue')['default']
4646
AppSettingsToggleSection: typeof import('./components/app/settings/AppSettingsToggleSection.vue')['default']
47-
AppSettingsTour: typeof import('./components/app/settings/AppSettingsTour.vue')['default']
4847
AppSkillFilter: typeof import('./components/app/AppSkillFilter.vue')['default']
4948
AppThemePreview: typeof import('./components/app/AppThemePreview.vue')['default']
5049
AppThemeSelector: typeof import('./components/app/AppThemeSelector.vue')['default']
@@ -60,23 +59,24 @@ declare module 'vue' {
6059
DiscoverySkip: typeof import('./components/discovery/DiscoverySkip.vue')['default']
6160
DiscoveryTitle: typeof import('./components/discovery/DiscoveryTitle.vue')['default']
6261
DocsActionChip: typeof import('./components/docs/DocsActionChip.vue')['default']
63-
DocsAlert: typeof import('./components/docs/DocsCallout.vue')['default']
6462
DocsApi: typeof import('./components/docs/DocsApi.vue')['default']
6563
DocsApiCard: typeof import('./components/docs/DocsApiCard.vue')['default']
6664
DocsApiHover: typeof import('./components/docs/DocsApiHover.vue')['default']
65+
DocsApiHoverSection: typeof import('./components/docs/DocsApiHoverSection.vue')['default']
6766
DocsApiLinks: typeof import('./components/docs/DocsApiLinks.vue')['default']
6867
DocsApiSection: typeof import('./components/docs/DocsApiSection.vue')['default']
6968
DocsAsk: typeof import('./components/docs/DocsAsk.vue')['default']
7069
DocsAskForm: typeof import('./components/docs/DocsAskForm.vue')['default']
7170
DocsAskInput: typeof import('./components/docs/DocsAskInput.vue')['default']
7271
DocsAskMessage: typeof import('./components/docs/DocsAskMessage.vue')['default']
7372
DocsAskPanel: typeof import('./components/docs/DocsAskPanel.vue')['default']
74-
DocsAskSheet: typeof import('./components/docs/DocsAskSheet.vue')['default']
7573
DocsBackmatter: typeof import('./components/docs/DocsBackmatter.vue')['default']
7674
DocsBackToTop: typeof import('./components/docs/DocsBackToTop.vue')['default']
75+
DocsBadge: typeof import('./components/docs/DocsBadge.vue')['default']
7776
DocsBenchmarks: typeof import('./components/docs/DocsBenchmarks.vue')['default']
7877
DocsBrowserSupport: typeof import('./components/docs/DocsBrowserSupport.vue')['default']
7978
DocsCallout: typeof import('./components/docs/DocsCallout.vue')['default']
79+
DocsCard: typeof import('./components/docs/DocsCard.vue')['default']
8080
DocsCodeActions: typeof import('./components/docs/DocsCodeActions.vue')['default']
8181
DocsCodeGroup: typeof import('./components/docs/DocsCodeGroup.vue')['default']
8282
DocsDiscoveryStep: typeof import('./components/docs/DocsDiscoveryStep.vue')['default']
@@ -102,10 +102,12 @@ declare module 'vue' {
102102
DocsNavigator: typeof import('./components/docs/DocsNavigator.vue')['default']
103103
DocsPageFeatures: typeof import('./components/docs/meta/DocsPageFeatures.vue')['default']
104104
DocsPageLogo: typeof import('./components/docs/meta/DocsPageLogo.vue')['default']
105+
DocsProgressBar: typeof import('./components/docs/DocsProgressBar.vue')['default']
105106
DocsRelated: typeof import('./components/docs/DocsRelated.vue')['default']
106107
DocsReleases: typeof import('./components/docs/DocsReleases.vue')['default']
107108
DocsRoadmap: typeof import('./components/docs/DocsRoadmap.vue')['default']
108109
DocsSearch: typeof import('./components/docs/DocsSearch.vue')['default']
110+
DocsSkeleton: typeof import('./components/docs/DocsSkeleton.vue')['default']
109111
DocsSkillToggle: typeof import('./components/docs/meta/DocsSkillToggle.vue')['default']
110112
DocsToc: typeof import('./components/docs/DocsToc.vue')['default']
111113
HomeCta: typeof import('./components/home/HomeCta.vue')['default']
@@ -129,6 +131,5 @@ declare module 'vue' {
129131
SkillPrerequisites: typeof import('./components/skillz/SkillPrerequisites.vue')['default']
130132
SkillzResume: typeof import('./components/skillz/SkillzResume.vue')['default']
131133
SkillzTour: typeof import('./components/skillz/SkillzTour.vue')['default']
132-
SkillzToursDocsIntro: typeof import('./components/skillz/tours/SkillzToursDocsIntro.vue')['default']
133134
}
134135
}

apps/docs/src/components/app/AppNav.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
</header>
162162

163163
<!-- URL filter banner -->
164-
<div v-if="navConfig.activeFeatures.value" class="-mt-4 px-4 py-3 mb-4 bg-surface-variant/50 border-b border-divider">
164+
<div v-if="navConfig.activeFeatures.value" class="-mt-4 px-4 py-3 mb-4 bg-surface-variant-50 border-b border-divider">
165165
<p class="text-xs text-on-surface-variant mb-2">
166166
Showing docs for your project
167167
</p>
@@ -176,7 +176,7 @@
176176

177177
<ul class="flex gap-2 flex-col">
178178
<template v-if="filteredOutPage">
179-
<li class="px-4 text-xs font-medium text-on-surface-variant uppercase tracking-wide">
179+
<li class="px-4 section-label">
180180
Active page
181181
</li>
182182

apps/docs/src/components/app/AppNavLink.vue

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
v-if="to"
154154
:aria-current="isActive ? 'page' : undefined"
155155
:as
156-
class="font-semibold inline-flex items-center gap-1 flex-1 min-w-0"
156+
class="font-semibold icon-text flex-1 min-w-0"
157157
:class="[
158158
'hover:underline hover:text-primary focus-visible:underline focus-visible:text-primary',
159159
!isTopLevel && !hasChildren && 'opacity-70 hover:opacity-100 focus-visible:opacity-100',
@@ -168,17 +168,21 @@
168168
</Atom>
169169

170170
<!-- Category header (not navigable) -->
171-
<div
171+
<span
172172
v-else
173173
class="font-semibold flex-1 min-w-0 truncate"
174174
:class="[
175-
isCollapsible && 'cursor-pointer hover:text-primary',
175+
isCollapsible && 'cursor-pointer hover:text-primary focus-visible:text-primary',
176176
containsActivePage && 'text-primary underline',
177177
]"
178+
:role="isCollapsible ? 'button' : undefined"
179+
:tabindex="isCollapsible ? 0 : undefined"
178180
@click="onOpen"
181+
@keydown.enter="onOpen"
182+
@keydown.space.prevent="onOpen"
179183
>
180184
{{ name }}
181-
</div>
185+
</span>
182186
</div>
183187

184188
<!-- Children (always visible for nested items, conditional for top-level) -->
@@ -200,22 +204,3 @@
200204
</Transition>
201205
</li>
202206
</template>
203-
204-
<style scoped>
205-
/* Expand: ease-out (content settles in) */
206-
.expand-enter-active {
207-
grid-template-rows: 1fr;
208-
transition: grid-template-rows 200ms cubic-bezier(0.33, 1, 0.68, 1);
209-
}
210-
211-
/* Collapse: ease-in (snaps shut) */
212-
.expand-leave-active {
213-
grid-template-rows: 1fr;
214-
transition: grid-template-rows 150ms cubic-bezier(0.32, 0, 0.67, 0);
215-
}
216-
217-
.expand-enter-from,
218-
.expand-leave-to {
219-
grid-template-rows: 0fr;
220-
}
221-
</style>

apps/docs/src/components/app/AppSettingsSheet.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
<div class="pt-4 pb-2 border-t border-divider flex justify-between">
123123
<button
124124
aria-label="Enter Developer Mode"
125-
class="inline-flex items-center gap-1 text-xs focus-visible:text-error focus-visible:underline focus-visible:outline-none transition-colors"
125+
class="icon-text text-xs focus-visible:text-error focus-visible:underline focus-visible:outline-none transition-colors"
126126
:class="[devmode.isSelected.value ? 'text-error hover:text-error' : 'text-on-surface/40 hover:text-primary' ]"
127127
type="button"
128128
@click="devmode.toggle()"

apps/docs/src/components/docs/DocsApiHover.vue

Lines changed: 20 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
// Framework
1616
import { IN_BROWSER, useDocumentEventListener } from '@vuetify/v0'
1717
18+
// Components
19+
import DocsApiHoverSection from './DocsApiHoverSection.vue'
20+
21+
// Composables
22+
import { usePopoverPosition } from '@/composables/usePopoverPosition'
23+
1824
// Utilities
1925
import { toKebab } from '@/utilities/strings'
2026
import { computed, onScopeDispose, ref, shallowRef, useTemplateRef } from 'vue'
@@ -82,8 +88,10 @@
8288
const HIDE_DELAY = 100 // Grace period when moving to popover
8389
8490
// Position state
85-
const position = ref({ top: 0, left: 0 })
86-
const flipBelow = shallowRef(false)
91+
const { position, flipped: flipBelow, calculate: calculatePosition } = usePopoverPosition({
92+
maxWidth: 450,
93+
maxHeight: 400,
94+
})
8795
8896
async function showPopover (target: HTMLElement) {
8997
if (!IN_BROWSER) return
@@ -141,29 +149,7 @@
141149
activeTarget.value = target
142150
143151
// Calculate position relative to viewport
144-
const rect = target.getBoundingClientRect()
145-
const popoverMaxWidth = 450
146-
const popoverMaxHeight = 400
147-
const viewportWidth = window.innerWidth
148-
const gap = 8
149-
const padding = 12
150-
151-
// Clamp horizontal position to keep popover within viewport
152-
let left = rect.left + window.scrollX
153-
if (rect.left + popoverMaxWidth > viewportWidth - padding) {
154-
left = Math.max(padding, viewportWidth - popoverMaxWidth - padding + window.scrollX)
155-
}
156-
157-
// Flip below if not enough space above
158-
const spaceAbove = rect.top - gap
159-
flipBelow.value = spaceAbove < popoverMaxHeight
160-
161-
position.value = {
162-
top: flipBelow.value
163-
? rect.bottom + window.scrollY + gap
164-
: rect.top + window.scrollY,
165-
left,
166-
}
152+
calculatePosition(target)
167153
168154
isVisible.value = true
169155
}
@@ -365,123 +351,25 @@
365351
<div class="popover-content">
366352
<!-- Component API -->
367353
<template v-if="componentApi">
368-
<div v-if="displayProps.length > 0" class="popover-section">
369-
<span class="popover-section-title">Props</span>
370-
<ul class="popover-list">
371-
<li v-for="prop in displayProps" :key="prop.name">
372-
<div class="popover-item-header">
373-
<span class="popover-item-name">{{ prop.name }}</span>
374-
<code class="popover-type">{{ prop.type }}</code>
375-
<code v-if="prop.default" class="popover-default">{{ prop.default }}</code>
376-
</div>
377-
<p v-if="prop.description" class="popover-item-description">
378-
{{ prop.description }}
379-
</p>
380-
</li>
381-
</ul>
382-
</div>
383-
384-
<div v-if="displayEvents.length > 0" class="popover-section">
385-
<span class="popover-section-title">Events</span>
386-
<ul class="popover-list">
387-
<li v-for="event in displayEvents" :key="event.name">
388-
<div class="popover-item-header">
389-
<span class="popover-item-name">{{ event.name }}</span>
390-
<code class="popover-type">{{ event.type }}</code>
391-
</div>
392-
<p v-if="event.description" class="popover-item-description">
393-
{{ event.description }}
394-
</p>
395-
</li>
396-
</ul>
397-
</div>
398-
399-
<div v-if="displaySlots.length > 0" class="popover-section">
400-
<span class="popover-section-title">Slots</span>
401-
<ul class="popover-list">
402-
<li v-for="slot in displaySlots" :key="slot.name">
403-
<div class="popover-item-header">
404-
<span class="popover-item-name">{{ slot.name }}</span>
405-
<code v-if="slot.type" class="popover-type">{{ slot.type }}</code>
406-
</div>
407-
<p v-if="slot.description" class="popover-item-description">
408-
{{ slot.description }}
409-
</p>
410-
</li>
411-
</ul>
412-
</div>
354+
<DocsApiHoverSection :items="displayProps" title="Props" />
355+
<DocsApiHoverSection :items="displayEvents" title="Events" />
356+
<DocsApiHoverSection :items="displaySlots" title="Slots" />
413357
</template>
414358

415359
<!-- Composable API -->
416360
<template v-if="composableApi">
417-
<div v-if="displayFunctions.length > 0" class="popover-section">
418-
<span class="popover-section-title">Functions</span>
419-
<ul class="popover-list">
420-
<li v-for="fn in displayFunctions" :key="fn.name">
421-
<div class="popover-item-header">
422-
<span class="popover-item-name">{{ fn.name }}</span>
423-
</div>
424-
<code v-if="fn.signature" class="popover-signature">{{ fn.signature }}</code>
425-
<p v-if="fn.description" class="popover-item-description">
426-
{{ fn.description }}
427-
</p>
428-
</li>
429-
</ul>
430-
</div>
431-
432-
<div v-if="displayOptions.length > 0" class="popover-section">
433-
<span class="popover-section-title">Options</span>
434-
<ul class="popover-list">
435-
<li v-for="opt in displayOptions" :key="opt.name">
436-
<div class="popover-item-header">
437-
<span class="popover-item-name">{{ opt.name }}</span>
438-
<code class="popover-type">{{ opt.type }}</code>
439-
<code v-if="opt.default" class="popover-default">{{ opt.default }}</code>
440-
</div>
441-
<p v-if="opt.description" class="popover-item-description">
442-
{{ opt.description }}
443-
</p>
444-
</li>
445-
</ul>
446-
</div>
447-
448-
<div v-if="displayProperties.length > 0" class="popover-section">
449-
<span class="popover-section-title">Properties</span>
450-
<ul class="popover-list">
451-
<li v-for="prop in displayProperties" :key="prop.name">
452-
<div class="popover-item-header">
453-
<span class="popover-item-name">{{ prop.name }}</span>
454-
<code class="popover-type">{{ prop.type }}</code>
455-
</div>
456-
<p v-if="prop.description" class="popover-item-description">
457-
{{ prop.description }}
458-
</p>
459-
</li>
460-
</ul>
461-
</div>
462-
463-
<div v-if="displayMethods.length > 0" class="popover-section">
464-
<span class="popover-section-title">Methods</span>
465-
<ul class="popover-list">
466-
<li v-for="method in displayMethods" :key="method.name">
467-
<div class="popover-item-header">
468-
<span class="popover-item-name">{{ method.name }}</span>
469-
<code class="popover-type">{{ method.type }}</code>
470-
</div>
471-
<p v-if="method.description" class="popover-item-description">
472-
{{ method.description }}
473-
</p>
474-
</li>
475-
</ul>
476-
</div>
361+
<DocsApiHoverSection :items="displayFunctions" show-signature title="Functions" />
362+
<DocsApiHoverSection :items="displayOptions" title="Options" />
363+
<DocsApiHoverSection :items="displayProperties" title="Properties" />
364+
<DocsApiHoverSection :items="displayMethods" title="Methods" />
477365
</template>
478366
</div>
479367
</template>
480368

481369
<!-- Footer link -->
482-
<div class="popover-footer" @click.prevent.stop="navigateToApi">
370+
<button class="popover-footer" type="button" @click.stop="navigateToApi">
483371
{{ isExternalLink ? 'View Vue docs ↗' : 'View API →' }}
484-
</div>
372+
</button>
485373
</div>
486374
</Transition>
487375
</Teleport>
@@ -707,17 +595,6 @@
707595
.popover-footer:hover {
708596
text-decoration: underline;
709597
}
710-
711-
/* Transition */
712-
.fade-enter-active,
713-
.fade-leave-active {
714-
transition: opacity 0.15s ease;
715-
}
716-
717-
.fade-enter-from,
718-
.fade-leave-to {
719-
opacity: 0;
720-
}
721598
</style>
722599

723600
<style>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script setup lang="ts">
2+
/**
3+
* Reusable section component for DocsApiHover popover content.
4+
* Renders a titled list of API items (props, events, slots, functions, etc.)
5+
*/
6+
export interface ApiItem {
7+
name: string
8+
type?: string
9+
default?: string
10+
signature?: string
11+
description?: string
12+
}
13+
14+
defineProps<{
15+
/** Section title (e.g., "Props", "Events", "Functions") */
16+
title: string
17+
/** List of API items to display */
18+
items: ApiItem[]
19+
/** Show signature instead of type (for functions) */
20+
showSignature?: boolean
21+
}>()
22+
</script>
23+
24+
<template>
25+
<div v-if="items.length > 0" class="popover-section">
26+
<span class="popover-section-title">{{ title }}</span>
27+
<ul class="popover-list">
28+
<li v-for="item in items" :key="item.name">
29+
<div class="popover-item-header">
30+
<span class="popover-item-name">{{ item.name }}</span>
31+
<code v-if="item.type && !showSignature" class="popover-type">{{ item.type }}</code>
32+
<code v-if="item.default" class="popover-default">{{ item.default }}</code>
33+
</div>
34+
<code v-if="item.signature && showSignature" class="popover-signature">{{ item.signature }}</code>
35+
<p v-if="item.description" class="popover-item-description">
36+
{{ item.description }}
37+
</p>
38+
</li>
39+
</ul>
40+
</div>
41+
</template>

0 commit comments

Comments
 (0)