Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build accessible, unstyled Vue 3 components using Reka UI (formerly Radix Vue) with WAI-ARIA compliance.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/generate-components.ts
1#!/usr/bin/env npx tsx2/**3* Generates reka-ui component docs from GitHub meta files4* Run: npx tsx skills/reka-ui/scripts/generate-components.ts5*6* Creates:7* - components.md (index)8* - components/<group>.md (per-group details)9*/1011import { mkdirSync, writeFileSync } from 'node:fs'12import { dirname, join } from 'node:path'13import { fileURLToPath } from 'node:url'1415const REPO = 'unovue/reka-ui'16const BRANCH = 'main'17const BASE_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}`1819interface PropMeta { name: string, description: string, type: string, required: boolean, default?: string }20interface EmitMeta { name: string, description: string, type: string }21interface SlotMeta { name: string, description: string, type: string }2223const COMPONENT_GROUPS: Record<string, { category: string, description: string, components: string[] }> = {24checkbox: { category: 'Form', description: 'Selection control with indeterminate state', components: ['CheckboxGroupRoot', 'CheckboxRoot', 'CheckboxIndicator'] },25combobox: { category: 'Form', description: 'Searchable dropdown with filtering', components: ['ComboboxRoot', 'ComboboxInput', 'ComboboxAnchor', 'ComboboxTrigger', 'ComboboxContent', 'ComboboxViewport', 'ComboboxItem', 'ComboboxItemIndicator', 'ComboboxGroup', 'ComboboxLabel', 'ComboboxEmpty', 'ComboboxSeparator', 'ComboboxArrow', 'ComboboxPortal', 'ComboboxCancel', 'ComboboxVirtualizer'] },26editable: { category: 'Form', description: 'Inline text editing with preview/edit modes', components: ['EditableRoot', 'EditableArea', 'EditableInput', 'EditablePreview', 'EditableSubmitTrigger', 'EditableCancelTrigger', 'EditableEditTrigger'] },27label: { category: 'Form', description: 'Accessible form label', components: ['Label'] },28listbox: { category: 'Form', description: 'Accessible list selection', components: ['ListboxRoot', 'ListboxContent', 'ListboxFilter', 'ListboxItem', 'ListboxItemIndicator', 'ListboxGroup', 'ListboxGroupLabel', 'ListboxVirtualizer'] },29numberField: { category: 'Form', description: 'Numeric input with increment/decrement', components: ['NumberFieldRoot', 'NumberFieldInput', 'NumberFieldIncrement', 'NumberFieldDecrement'] },30pinInput: { category: 'Form', description: 'Multi-character code entry (OTP)', components: ['PinInputRoot', 'PinInputInput'] },31radioGroup: { category: 'Form', description: 'Mutually exclusive selection', components: ['RadioGroupRoot', 'RadioGroupItem', 'RadioGroupIndicator'] },32select: { category: 'Form', description: 'Dropdown selection with grouping', components: ['SelectRoot', 'SelectTrigger', 'SelectPortal', 'SelectContent', 'SelectViewport', 'SelectItem', 'SelectItemText', 'SelectItemIndicator', 'SelectGroup', 'SelectLabel', 'SelectSeparator', 'SelectArrow', 'SelectScrollUpButton', 'SelectScrollDownButton', 'SelectValue', 'SelectIcon'] },33slider: { category: 'Form', description: 'Range input control', components: ['SliderRoot', 'SliderTrack', 'SliderRange', 'SliderThumb'] },34switch: { category: 'Form', description: 'Toggle between two states', components: ['SwitchRoot', 'SwitchThumb'] },35tagsInput: { category: 'Form', description: 'Multiple tag entry and management', components: ['TagsInputRoot', 'TagsInputInput', 'TagsInputItem', 'TagsInputItemText', 'TagsInputItemDelete', 'TagsInputClear'] },36toggle: { category: 'Form', description: 'Single state button toggle', components: ['Toggle'] },37toggleGroup: { category: 'Form', description: 'Multiple toggles with group behavior', components: ['ToggleGroupRoot', 'ToggleGroupItem'] },3839calendar: { category: 'Date', description: 'Date selection grid (alpha)', components: ['CalendarRoot', 'CalendarHeader', 'CalendarHeading', 'CalendarGrid', 'CalendarGridHead', 'CalendarGridBody', 'CalendarGridRow', 'CalendarCell', 'CalendarCellTrigger', 'CalendarHeadCell', 'CalendarNext', 'CalendarPrev'] },40dateField: { category: 'Date', description: 'Date input field (alpha)', components: ['DateFieldRoot', 'DateFieldInput'] },41datePicker: { category: 'Date', description: 'Date picker with calendar (alpha)', components: ['DatePickerRoot', 'DatePickerField', 'DatePickerInput', 'DatePickerTrigger', 'DatePickerContent', 'DatePickerCalendar', 'DatePickerHeader', 'DatePickerHeading', 'DatePickerGrid', 'DatePickerCell', 'DatePickerCellTrigger', 'DatePickerNext', 'DatePickerPrev', 'DatePickerAnchor', 'DatePickerArrow', 'DatePickerClose'] },42dateRangeField: { category: 'Date', description: 'Date range input (alpha)', components: ['DateRangeFieldRoot', 'DateRangeFieldInput'] },43dateRangePicker: { category: 'Date', description: 'Date range picker (alpha)', components: ['DateRangePickerRoot', 'DateRangePickerField', 'DateRangePickerInput', 'DateRangePickerTrigger', 'DateRangePickerContent', 'DateRangePickerCalendar', 'DateRangePickerHeader', 'DateRangePickerHeading', 'DateRangePickerGrid', 'DateRangePickerCell', 'DateRangePickerCellTrigger', 'DateRangePickerNext', 'DateRangePickerPrev'] },44rangeCalendar: { category: 'Date', description: 'Calendar for date ranges (alpha)', components: ['RangeCalendarRoot', 'RangeCalendarHeader', 'RangeCalendarHeading', 'RangeCalendarGrid', 'RangeCalendarCell', 'RangeCalendarCellTrigger', 'RangeCalendarNext', 'RangeCalendarPrev'] },45timeField: { category: 'Date', description: 'Time input field (alpha)', components: ['TimeFieldRoot', 'TimeFieldInput'] },4647accordion: { category: 'Disclosure', description: 'Collapsible content sections', components: ['AccordionRoot', 'AccordionItem', 'AccordionHeader', 'AccordionTrigger', 'AccordionContent'] },48collapsible: { category: 'Disclosure', description: 'Single collapsible panel', components: ['CollapsibleRoot', 'CollapsibleTrigger', 'CollapsibleContent'] },4950alertDialog: { category: 'Overlay', description: 'Modal dialog requiring action', components: ['AlertDialogRoot', 'AlertDialogTrigger', 'AlertDialogPortal', 'AlertDialogOverlay', 'AlertDialogContent', 'AlertDialogTitle', 'AlertDialogDescription', 'AlertDialogCancel', 'AlertDialogAction'] },51dialog: { category: 'Overlay', description: 'Modal dialog', components: ['DialogRoot', 'DialogTrigger', 'DialogPortal', 'DialogOverlay', 'DialogContent', 'DialogTitle', 'DialogDescription', 'DialogClose'] },52hoverCard: { category: 'Overlay', description: 'Card shown on hover', components: ['HoverCardRoot', 'HoverCardTrigger', 'HoverCardPortal', 'HoverCardContent', 'HoverCardArrow'] },53popover: { category: 'Overlay', description: 'Floating content panel', components: ['PopoverRoot', 'PopoverTrigger', 'PopoverPortal', 'PopoverContent', 'PopoverArrow', 'PopoverClose', 'PopoverAnchor'] },54tooltip: { category: 'Overlay', description: 'Informational hover tip', components: ['TooltipProvider', 'TooltipRoot', 'TooltipTrigger', 'TooltipPortal', 'TooltipContent', 'TooltipArrow'] },55toast: { category: 'Overlay', description: 'Temporary notifications', components: ['ToastProvider', 'ToastRoot', 'ToastViewport', 'ToastTitle', 'ToastDescription', 'ToastAction', 'ToastClose', 'ToastPortal'] },5657contextMenu: { category: 'Menu', description: 'Right-click context menu', components: ['ContextMenuRoot', 'ContextMenuTrigger', 'ContextMenuPortal', 'ContextMenuContent', 'ContextMenuItem', 'ContextMenuCheckboxItem', 'ContextMenuRadioGroup', 'ContextMenuRadioItem', 'ContextMenuItemIndicator', 'ContextMenuLabel', 'ContextMenuGroup', 'ContextMenuSeparator', 'ContextMenuSub', 'ContextMenuSubTrigger', 'ContextMenuSubContent', 'ContextMenuArrow'] },58dropdownMenu: { category: 'Menu', description: 'Dropdown action menu', components: ['DropdownMenuRoot', 'DropdownMenuTrigger', 'DropdownMenuPortal', 'DropdownMenuContent', 'DropdownMenuItem', 'DropdownMenuCheckboxItem', 'DropdownMenuRadioGroup', 'DropdownMenuRadioItem', 'DropdownMenuItemIndicator', 'DropdownMenuLabel', 'DropdownMenuGroup', 'DropdownMenuSeparator', 'DropdownMenuSub', 'DropdownMenuSubTrigger', 'DropdownMenuSubContent', 'DropdownMenuArrow'] },59menubar: { category: 'Menu', description: 'Horizontal menu bar', components: ['MenubarRoot', 'MenubarMenu', 'MenubarTrigger', 'MenubarPortal', 'MenubarContent', 'MenubarItem', 'MenubarCheckboxItem', 'MenubarRadioGroup', 'MenubarRadioItem', 'MenubarItemIndicator', 'MenubarLabel', 'MenubarGroup', 'MenubarSeparator', 'MenubarSub', 'MenubarSubTrigger', 'MenubarSubContent', 'MenubarArrow'] },60navigationMenu: { category: 'Menu', description: 'Site navigation menu', components: ['NavigationMenuRoot', 'NavigationMenuList', 'NavigationMenuItem', 'NavigationMenuTrigger', 'NavigationMenuContent', 'NavigationMenuLink', 'NavigationMenuIndicator', 'NavigationMenuViewport', 'NavigationMenuSub'] },6162avatar: { category: 'Data', description: 'User image with fallback', components: ['AvatarRoot', 'AvatarImage', 'AvatarFallback'] },63pagination: { category: 'Data', description: 'Page navigation', components: ['PaginationRoot', 'PaginationList', 'PaginationListItem', 'PaginationFirst', 'PaginationPrev', 'PaginationNext', 'PaginationLast', 'PaginationEllipsis'] },64progress: { category: 'Data', description: 'Progress indicator', components: ['ProgressRoot', 'ProgressIndicator'] },65scrollArea: { category: 'Data', description: 'Custom scrollbar container', components: ['ScrollAreaRoot', 'ScrollAreaViewport', 'ScrollAreaScrollbar', 'ScrollAreaThumb', 'ScrollAreaCorner'] },66separator: { category: 'Data', description: 'Visual divider', components: ['Separator'] },67splitter: { category: 'Data', description: 'Resizable split panels', components: ['SplitterGroup', 'SplitterPanel', 'SplitterResizeHandle'] },68stepper: { category: 'Data', description: 'Multi-step progress indicator', components: ['StepperRoot', 'StepperItem', 'StepperTrigger', 'StepperTitle', 'StepperDescription', 'StepperIndicator', 'StepperSeparator'] },69tabs: { category: 'Data', description: 'Tabbed content panels', components: ['TabsRoot', 'TabsList', 'TabsTrigger', 'TabsContent', 'TabsIndicator'] },70tree: { category: 'Data', description: 'Hierarchical tree view', components: ['TreeRoot', 'TreeItem', 'TreeVirtualizer'] },7172aspectRatio: { category: 'Layout', description: 'Maintain aspect ratio', components: ['AspectRatio'] },73toolbar: { category: 'Layout', description: 'Toolbar with buttons/toggles', components: ['ToolbarRoot', 'ToolbarButton', 'ToolbarLink', 'ToolbarToggleGroup', 'ToolbarToggleItem', 'ToolbarSeparator'] },74configProvider: { category: 'Utility', description: 'Global config context', components: ['ConfigProvider'] },75focusScope: { category: 'Utility', description: 'Focus trap container', components: ['FocusScope'] },76presence: { category: 'Utility', description: 'Animation presence control', components: ['Presence'] },77primitive: { category: 'Utility', description: 'Base element wrapper', components: ['Primitive', 'Slot'] },78visuallyHidden: { category: 'Utility', description: 'Screen reader only content', components: ['VisuallyHidden'] },79}8081const COMPOSABLES = [82{ name: 'useEmitAsProps', description: 'Convert emit functions to props for passing to child components' },83{ name: 'useFilter', description: 'Filter items based on search query with customizable matching' },84{ name: 'useForwardProps', description: 'Forward props to child components while filtering out handled ones' },85{ name: 'useForwardPropsEmits', description: 'Combine useForwardProps and useEmitAsProps' },86{ name: 'useForwardExpose', description: 'Forward exposed methods/refs from child components' },87{ name: 'useId', description: 'Generate unique IDs for accessibility attributes' },88{ name: 'useDateFormatter', description: 'Format dates with locale support' },89{ name: 'useDirection', description: 'Get/set text direction (ltr/rtl)' },90{ name: 'useLocale', description: 'Get/set locale for internationalization' },91]9293async function fetchMeta(componentName: string): Promise<{ props: PropMeta[], emits: EmitMeta[], slots: SlotMeta[] }> {94const url = `${BASE_URL}/docs/content/meta/${componentName}.md`95try {96const res = await fetch(url)97if (!res.ok)98return { props: [], emits: [], slots: [] }99const text = await res.text()100return parseMeta(text)101}102catch { return { props: [], emits: [], slots: [] } }103}104105function parseMeta(content: string): { props: PropMeta[], emits: EmitMeta[], slots: SlotMeta[] } {106const props: PropMeta[] = []107const emits: EmitMeta[] = []108const slots: SlotMeta[] = []109const propsMatch = content.match(/<PropsTable\s+:data="(\[[\s\S]*?\])"\s*\/>/)110const emitsMatch = content.match(/<EmitsTable\s+:data="(\[[\s\S]*?\])"\s*\/>/)111const slotsMatch = content.match(/<SlotsTable\s+:data="(\[[\s\S]*?\])"\s*\/>/)112if (propsMatch) {113try {114props.push(...JSON.parse(propsMatch[1].replace(/'/g, '"')))115}116catch {}117}118if (emitsMatch) {119try {120emits.push(...JSON.parse(emitsMatch[1].replace(/'/g, '"')))121}122catch {}123}124if (slotsMatch) {125try {126slots.push(...JSON.parse(slotsMatch[1].replace(/'/g, '"')))127}128catch {}129}130return { props, emits, slots }131}132133const escapeMarkdown = (str: string) => str.replace(/\|/g, '\\|').replace(/\n/g, ' ')134135function truncateType(type: string, max = 50) {136const c = type.replace(/\s+/g, ' ').trim()137return c.length > max ? `${c.slice(0, max - 3)}...` : c138}139const toKebab = (str: string) => str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()140141async function generateGroupFile(groupName: string, group: { category: string, description: string, components: string[] }): Promise<string> {142const lines: string[] = []143const title = groupName.charAt(0).toUpperCase() + groupName.slice(1).replace(/([A-Z])/g, ' $1').trim()144lines.push(`# ${title}`)145lines.push('')146lines.push(group.description)147lines.push('')148lines.push(`**Parts:** ${group.components.map(c => `\`${c}\``).join(', ')}`)149lines.push('')150151for (const comp of group.components) {152const meta = await fetchMeta(comp)153if (meta.props.length === 0 && meta.emits.length === 0 && meta.slots.length === 0)154continue155156lines.push(`## ${comp}`)157lines.push('')158159if (meta.props.length > 0) {160lines.push('### Props')161lines.push('| Prop | Type | Default |')162lines.push('|------|------|---------|')163for (const p of meta.props) {164const type = escapeMarkdown(truncateType(p.type))165const def = p.default ? `\`${escapeMarkdown(p.default)}\`` : '-'166lines.push(`| \`${p.name}\`${p.required ? '*' : ''} | \`${type}\` | ${def} |`)167}168lines.push('')169}170171if (meta.emits.length > 0) {172lines.push('### Emits')173lines.push('| Event | Payload |')174lines.push('|-------|---------|')175for (const e of meta.emits) {176lines.push(`| \`${e.name}\` | \`${escapeMarkdown(truncateType(e.type))}\` |`)177}178lines.push('')179}180181if (meta.slots.length > 0) {182lines.push('### Slots')183lines.push('| Slot | Type |')184lines.push('|------|------|')185for (const s of meta.slots) {186lines.push(`| \`${s.name}\` | \`${escapeMarkdown(truncateType(s.type))}\` |`)187}188lines.push('')189}190}191return lines.join('\n')192}193194async function main() {195const __dirname = dirname(fileURLToPath(import.meta.url))196const baseDir = join(__dirname, '..')197const componentsDir = join(baseDir, 'components')198mkdirSync(componentsDir, { recursive: true })199200console.log('Generating Reka UI component docs...')201202// Generate index203const index: string[] = []204index.push('# Components')205index.push('')206index.push('> Auto-generated. Run `npx tsx skills/reka-ui/scripts/generate-components.ts` to update.')207index.push('')208209const categories: Record<string, string[]> = {}210for (const [name, group] of Object.entries(COMPONENT_GROUPS)) {211if (!categories[group.category])212categories[group.category] = []213categories[group.category].push(name)214}215216for (const [cat, groupNames] of Object.entries(categories)) {217index.push(`## ${cat}`)218index.push('')219index.push('| Component | Description | File |')220index.push('|-----------|-------------|------|')221for (const name of groupNames) {222const g = COMPONENT_GROUPS[name]223const file = `components/${toKebab(name)}.md`224index.push(`| **${name}** | ${g.description} | \`${file}\` |`)225}226index.push('')227}228229index.push('## Composables')230index.push('')231index.push('| Composable | Description |')232index.push('|------------|-------------|')233for (const c of COMPOSABLES) {234index.push(`| \`${c.name}\` | ${c.description} |`)235}236index.push('')237238writeFileSync(join(baseDir, 'components.md'), index.join('\n'))239console.log('✓ Generated components.md (index)')240241// Generate per-group files242for (const [name, group] of Object.entries(COMPONENT_GROUPS)) {243const content = await generateGroupFile(name, group)244const filename = `${toKebab(name)}.md`245writeFileSync(join(componentsDir, filename), content)246console.log(`✓ Generated components/${filename}`)247}248249console.log(`\nDone! Generated ${Object.keys(COMPONENT_GROUPS).length + 1} files.`)250}251252main().catch(console.error)253