├── .prettierignore ├── .npmrc ├── .prettierrc ├── assets ├── hero-dark.gif └── hero-light.gif ├── .gitignore ├── src ├── components │ ├── skeleton.ts │ ├── color-picker.ts │ ├── config.ts │ ├── icon.ts │ ├── chip.ts │ ├── modal.ts │ ├── collapsible.ts │ ├── card.ts │ ├── progress.ts │ ├── link.ts │ ├── slider.ts │ ├── textarea.ts │ ├── tooltip.ts │ ├── checkbox.ts │ ├── avatar-group.ts │ ├── popover.ts │ ├── switch.ts │ ├── separator.ts │ ├── button-group.ts │ ├── accordion.ts │ ├── carousel.ts │ ├── drawer.ts │ ├── input-number.ts │ ├── select.ts │ ├── kbd.ts │ ├── form-field.ts │ ├── radio-group.ts │ ├── toast.ts │ ├── slideover.ts │ ├── alert.ts │ ├── pin-input.ts │ ├── avatar.ts │ ├── input.ts │ ├── input-menu.ts │ ├── tabs.ts │ ├── index.ts │ ├── calendar.ts │ ├── badge.ts │ ├── pagination.ts │ ├── stepper.ts │ ├── button.ts │ ├── breadcrumb.ts │ ├── select-menu.ts │ ├── command-palette.ts │ ├── dropdown-menu.ts │ └── navigation-menu.ts ├── nuxt-ui.ts ├── nuxt-ui-pro.ts ├── pro-components │ ├── page-accordion.ts │ ├── page-columns.ts │ ├── color-mode-switch.ts │ ├── color-mode-select.ts │ ├── content-search.ts │ ├── blog-posts.ts │ ├── pricing-plans.ts │ ├── content-search-button.ts │ ├── page-grid.ts │ ├── locale-select.ts │ ├── error.ts │ ├── page-feature.ts │ ├── page-card.ts │ ├── page-anchors.ts │ ├── page-header.ts │ ├── footer.ts │ ├── page-links.ts │ ├── user.ts │ ├── page-logos.ts │ ├── banner.ts │ ├── content-surround.ts │ ├── page-cta.ts │ ├── page-aside.ts │ ├── index.ts │ ├── page-hero.ts │ ├── page-section.ts │ ├── header.ts │ ├── footer-columns.ts │ ├── blog-post.ts │ ├── content-toc.ts │ ├── pricing-plan.ts │ └── content-navigation.ts ├── utils.ts └── types.ts ├── eslint.config.js ├── tsconfig.json ├── .vscode └── settings.json ├── LICENSE.md ├── README.md └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shell-emulator=true 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /assets/hero-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Justineo/tempad-dev-plugin-nuxt-ui/HEAD/assets/hero-dark.gif -------------------------------------------------------------------------------- /assets/hero-light.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Justineo/tempad-dev-plugin-nuxt-ui/HEAD/assets/hero-light.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | lib-cov 8 | logs 9 | node_modules 10 | temp 11 | -------------------------------------------------------------------------------- /src/components/skeleton.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { h } from '../utils' 3 | 4 | export type SkeletonProperties = { 5 | '◆ LoadingState': '100%' | '50%' 6 | } 7 | 8 | export function Skeleton(_: DesignComponent) { 9 | return h('USkeleton', {}, {}) 10 | } 11 | -------------------------------------------------------------------------------- /src/nuxt-ui.ts: -------------------------------------------------------------------------------- 1 | import { definePlugin } from '@tempad-dev/plugins' 2 | import { transformComponent } from './components' 3 | 4 | export const plugin = definePlugin({ 5 | name: 'Nuxt UI', 6 | code: { 7 | component: { 8 | title: 'Component', 9 | lang: 'vue', 10 | transformComponent, 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /src/nuxt-ui-pro.ts: -------------------------------------------------------------------------------- 1 | import { definePlugin } from '@tempad-dev/plugins' 2 | import { transformComponent } from './pro-components' 3 | 4 | export const plugin = definePlugin({ 5 | name: 'Nuxt UI Pro', 6 | code: { 7 | component: { 8 | title: 'Component', 9 | lang: 'vue', 10 | transformComponent, 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import deMorgan from 'eslint-plugin-de-morgan' 3 | 4 | export default antfu( 5 | { 6 | type: 'lib', 7 | rules: { 8 | 'ts/consistent-type-definitions': 'off', 9 | 'ts/explicit-function-return-type': 'off', 10 | }, 11 | markdown: false, 12 | }, 13 | deMorgan.configs.recommended, 14 | ) 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "typeRoots": ["node_modules/@types", "node_modules/@figma"], 9 | "strict": true, 10 | "strictNullChecks": true, 11 | "noUncheckedIndexedAccess": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "skipDefaultLibCheck": true, 15 | "skipLibCheck": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pro-components/page-accordion.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { omit } from '@s-libs/micro-dash' 3 | import { Accordion } from '../components/accordion' 4 | import { h } from '../utils' 5 | 6 | // eslint-disable-next-line ts/no-empty-object-type 7 | export type PageAccordionProperties = {} 8 | 9 | export function PageAccordion(component: DesignComponent) { 10 | const accordion = Accordion(component) 11 | 12 | return h('UPageAccordion', omit(accordion.props, 'ui'), {}) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/color-picker.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { cleanPropNames, h } from '../utils' 3 | 4 | export type ColorPickerProperties = { 5 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 6 | '🚦 State': 'Default' | 'Disabled' 7 | } 8 | 9 | export function ColorPicker(component: DesignComponent) { 10 | const { size, state } = cleanPropNames(component.properties) 11 | 12 | return h( 13 | 'UColorPicker', 14 | { 15 | size, 16 | disabled: state === 'Disabled', 17 | }, 18 | { 19 | size: 'md', 20 | disabled: false, 21 | }, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/config.ts: -------------------------------------------------------------------------------- 1 | export const ui = { 2 | icons: { 3 | arrowRight: 'i-lucide-arrow-right', 4 | arrowLeft: 'i-lucide-arrow-left', 5 | check: 'i-lucide-check', 6 | chevronDoubleRight: 'i-lucide-chevrons-right', 7 | chevronDown: 'i-lucide-chevron-down', 8 | chevronDoubleLeft: 'i-lucide-chevrons-left', 9 | chevronRight: 'i-lucide-chevron-right', 10 | chevronLeft: 'i-lucide-chevron-left', 11 | close: 'i-lucide-x', 12 | ellipsis: 'i-lucide-ellipsis', 13 | minus: 'i-lucide-minus', 14 | plus: 'i-lucide-plus', 15 | search: 'i-lucide-search', 16 | success: 'i-lucide-circle-check', 17 | }, 18 | } as const 19 | -------------------------------------------------------------------------------- /src/pro-components/page-columns.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { PageCardProperties } from './page-card' 3 | import { findAll } from '@tempad-dev/plugins' 4 | import { h } from '../utils' 5 | import { PageCard } from './page-card' 6 | 7 | export type PageColumnsProperties = { 8 | '🖥️ Device': 'Desktop' | 'Mobile' 9 | } 10 | 11 | export function PageColumns(component: DesignComponent) { 12 | const cards = findAll>(component, { 13 | type: 'INSTANCE', 14 | name: 'PageCard', 15 | }).map(PageCard) 16 | 17 | return h('UPageColumns', {}, {}, cards) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/icon.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { h } from '../utils' 3 | 4 | // eslint-disable-next-line ts/no-empty-object-type 5 | export type IconProperties = {} 6 | 7 | export function Icon(component: DesignComponent) { 8 | return h( 9 | 'UIcon', 10 | { 11 | name: getIconName(component.name), 12 | }, 13 | {}, 14 | ) 15 | } 16 | 17 | // close -> i-lucide-close 18 | // simple-icons/x -> i-simple-icons-x 19 | export function getIconName(name?: string): string | undefined { 20 | if (!name) { 21 | return undefined 22 | } 23 | 24 | if (name.includes('/')) { 25 | const [prefix, iconName] = name.split('/') 26 | return `i-${prefix}-${iconName}` 27 | } 28 | 29 | return `i-lucide-${name}` 30 | } 31 | -------------------------------------------------------------------------------- /src/components/chip.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { cleanPropNames, h, toLowerCase } from '../utils' 3 | 4 | export type ChipProperties = { 5 | '↳ Label': string 6 | Color: 'Primary' | 'Secondary' | 'Success' | 'Info' | 'Warning' | 'Error' 7 | '📏 Size': '3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' 8 | '👁️ IsLabel': 'True' | 'False' 9 | } 10 | 11 | export function Chip(component: DesignComponent) { 12 | const { color, size, isLabel, label } = cleanPropNames(component.properties) 13 | 14 | return h( 15 | 'UChip', 16 | { 17 | text: isLabel === 'True' ? label : undefined, 18 | color: toLowerCase(color), 19 | size, 20 | }, 21 | { 22 | color: 'primary', 23 | size: 'md', 24 | }, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/pro-components/color-mode-switch.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { SwitchProperties } from '../components/switch' 3 | import { pick } from '@s-libs/micro-dash' 4 | import { findChild } from '@tempad-dev/plugins' 5 | import { Switch } from '../components/switch' 6 | import { h } from '../utils' 7 | 8 | export type ColorModeSwitchProperties = { 9 | ColorMode: 'Dark' | 'Light' 10 | } 11 | 12 | export function ColorModeSwitch(component: DesignComponent) { 13 | const switcher = findChild>(component, { 14 | type: 'INSTANCE', 15 | name: 'Switch', 16 | }) 17 | 18 | const { props = {} } = switcher ? Switch(switcher) : {} 19 | 20 | return h('UColorModeSwitch', pick(props, 'disabled', 'color', 'size'), {}) 21 | } 22 | -------------------------------------------------------------------------------- /src/pro-components/color-mode-select.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { SelectMenuProperties } from '../components/select-menu' 3 | import { omit } from '@s-libs/micro-dash' 4 | import { findChild } from '@tempad-dev/plugins' 5 | import { SelectMenu } from '../components/select-menu' 6 | import { h } from '../utils' 7 | 8 | // eslint-disable-next-line ts/no-empty-object-type 9 | export type ColorModeSelectProperties = {} 10 | 11 | export function ColorModeSelect(component: DesignComponent) { 12 | const menu = findChild>(component, { 13 | type: 'INSTANCE', 14 | name: 'SelectMenu', 15 | }) 16 | 17 | const { props = {} } = menu ? SelectMenu(menu) : {} 18 | 19 | return h('UColorModeSelect', omit(props, 'items', 'icon'), {}) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/modal.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, LOREM_IPSUM_TITLE, renderSlot } from '../utils' 3 | 4 | export type ModalProperties = { 5 | '👁️ Footer': boolean 6 | '↳ BodySlot': DesignComponent 7 | '👁️ Header': boolean 8 | '👁️ Body': boolean 9 | '↳ FooterSlot': DesignComponent 10 | '↳ HeaderSlot': DesignComponent 11 | '👁️ Background': 'False' | 'True' 12 | } 13 | 14 | export function Modal(component: DesignComponent) { 15 | const { header, body, footer } = cleanPropNames(component.properties) 16 | 17 | return h('UModal', {}, {}, [ 18 | ...(header ? [renderSlot('header', [LOREM_IPSUM_TITLE])] : []), 19 | ...(body ? [renderSlot('body', [LOREM_IPSUM_TEXT])] : []), 20 | ...(footer ? [renderSlot('footer', [LOREM_IPSUM_TITLE])] : []), 21 | ]) 22 | } 23 | -------------------------------------------------------------------------------- /src/pro-components/content-search.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { CommandPaletteProperties } from '../components/command-palette' 3 | import { findChild } from '@tempad-dev/plugins' 4 | import { CommandPalette } from '../components/command-palette' 5 | import { h } from '../utils' 6 | 7 | // eslint-disable-next-line ts/no-empty-object-type 8 | export type ContentSearchProperties = {} 9 | 10 | export function ContentSearch(component: DesignComponent) { 11 | const commandPalette = findChild>(component, { 12 | type: 'INSTANCE', 13 | name: 'CommandPalette', 14 | }) 15 | 16 | const props = commandPalette ? CommandPalette(commandPalette).props : {} 17 | 18 | return h( 19 | 'UContentSearch', 20 | { 21 | ...props, 22 | }, 23 | {}, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/collapsible.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from './button' 3 | import { findChild } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h } from '../utils' 5 | import { Button, BUTTON_NAMES } from './button' 6 | 7 | export type CollapsibleProperties = { 8 | '❖ Slot': DesignComponent 9 | '👁️ Open': boolean 10 | '◆ Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 11 | } 12 | 13 | export function Collapsible(component: DesignComponent) { 14 | const { open } = cleanPropNames(component.properties) 15 | 16 | const button = findChild>(component, { 17 | type: 'INSTANCE', 18 | name: BUTTON_NAMES, 19 | }) 20 | 21 | return h( 22 | 'UCollapsible', 23 | { 24 | open, 25 | }, 26 | { 27 | open: false, 28 | }, 29 | button ? [Button(button)] : [], 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/pro-components/blog-posts.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { BlogPostProperties } from './blog-post' 3 | import { findAll } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, toLowerCase } from '../utils' 5 | import { renderBlogPostItem } from './blog-post' 6 | 7 | export type BlogPostsProperties = { 8 | Orientation: 'Horizontal' | 'Vertical' 9 | } 10 | 11 | export function BlogPosts(component: DesignComponent) { 12 | const { orientation } = cleanPropNames(component.properties) 13 | 14 | const posts = findAll>(component, { 15 | type: 'INSTANCE', 16 | name: 'BlogPost', 17 | }).map((item) => renderBlogPostItem(item)) 18 | 19 | return h( 20 | 'UBlogPosts', 21 | { 22 | orientation: toLowerCase(orientation), 23 | posts, 24 | }, 25 | { 26 | orientation: 'horizontal', 27 | }, 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/card.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, FrameNode } from '@tempad-dev/plugins' 2 | import { findOne } from '@tempad-dev/plugins' 3 | import { h, LOREM_IPSUM_TEXT, LOREM_IPSUM_TITLE, renderSlot } from '../utils' 4 | 5 | export type CardProperties = { 6 | '❖ BodySlot': DesignComponent 7 | '❖ FooterSlot': DesignComponent 8 | '❖ HeaderSlot': DesignComponent 9 | } 10 | 11 | export function Card(component: DesignComponent) { 12 | const header = findOne(component, { 13 | type: 'FRAME', 14 | name: 'Header', 15 | }) 16 | const body = findOne(component, { 17 | type: 'FRAME', 18 | name: 'Body', 19 | }) 20 | const footer = findOne(component, { 21 | type: 'FRAME', 22 | name: 'Footer', 23 | }) 24 | 25 | return h('UCard', {}, {}, [ 26 | ...(header ? [renderSlot('header', [LOREM_IPSUM_TITLE])] : []), 27 | ...(body ? [LOREM_IPSUM_TEXT] : []), 28 | ...(footer ? [renderSlot('footer', [LOREM_IPSUM_TITLE])] : []), 29 | ]) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/progress.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { cleanPropNames, h, toLowerCase } from '../utils' 3 | 4 | export type ProgressProperties = { 5 | '👁️ Indicator': boolean 6 | '🎨 Color': 'Neutral' | 'Primary' | 'Secondary' | 'Success' | 'Info' | 'Warning' | 'Error' 7 | '📏 Size': '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' 8 | '⇅ Orientation': 'Horizontal' | 'Vertical' 9 | '◆ Value': '0%' | '25%' | '50%' | '75%' | '100%' 10 | } 11 | 12 | export function Progress(component: DesignComponent) { 13 | const { color, size, orientation, value, indicator } = cleanPropNames(component.properties) 14 | 15 | return h( 16 | 'UProgress', 17 | { 18 | modelValue: Number.parseInt(value, 10), 19 | status: indicator, 20 | size, 21 | color: toLowerCase(color), 22 | orientation: toLowerCase(orientation), 23 | }, 24 | { 25 | status: false, 26 | size: 'md', 27 | color: 'primary', 28 | orientation: 'horizontal', 29 | }, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/link.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { cleanPropNames, h, toKebabCase, toLowerCase } from '../utils' 3 | 4 | export type LinkProperties = { 5 | '𝐓 Label': string 6 | '🎨 Color': 'Neutral' | 'Primary' 7 | '🚦 State': 'Default' | 'Disabled' | 'Hover' 8 | } 9 | 10 | export function Link(component: DesignComponent) { 11 | const { color, state, label } = cleanPropNames(component.properties) 12 | 13 | return h( 14 | 'ULink', 15 | { 16 | active: toLowerCase(color) === 'primary', 17 | disabled: state === 'Disabled', 18 | }, 19 | { 20 | active: false, 21 | disabled: false, 22 | }, 23 | [label], 24 | ) 25 | } 26 | 27 | export function getLinkTo(label: string, type: 'path' | 'external' | 'hash' = 'path') { 28 | const path = toKebabCase(label) 29 | 30 | switch (type) { 31 | case 'external': 32 | return `https://example.com/${path}` 33 | case 'hash': 34 | return `#${path}` 35 | default: 36 | return `/${path}` 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pro-components/pricing-plans.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { PricingPlanProperties } from './pricing-plan' 3 | import { findAll } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, toLowerCase } from '../utils' 5 | import { renderPricingPlanItem } from './pricing-plan' 6 | 7 | export type PricingPlansProperties = { 8 | '⇅ Orientation': 'Vertical' | 'Horizontal' 9 | '👁️ Compact': 'false' | 'true' 10 | } 11 | 12 | export function PricingPlans(component: DesignComponent) { 13 | const { orientation, compact } = cleanPropNames(component.properties) 14 | 15 | const plans = findAll>(component, { 16 | type: 'INSTANCE', 17 | name: 'PricingPlan', 18 | }).map((plan) => renderPricingPlanItem(plan)) 19 | 20 | return h( 21 | 'UPricingPlans', 22 | { 23 | plans, 24 | orientation: toLowerCase(orientation), 25 | compact: compact === 'true', 26 | }, 27 | { 28 | orientation: 'horizontal', 29 | compact: false, 30 | }, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Auto fix 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit", 5 | "source.organizeImports": "never" 6 | }, 7 | 8 | // Silent the stylistic rules in you IDE, but still auto fix them 9 | "eslint.rules.customizations": [ 10 | { "rule": "style/*", "severity": "off" }, 11 | { "rule": "*-indent", "severity": "off" }, 12 | { "rule": "*-spacing", "severity": "off" }, 13 | { "rule": "*-spaces", "severity": "off" }, 14 | { "rule": "*-order", "severity": "off" }, 15 | { "rule": "*-dangle", "severity": "off" }, 16 | { "rule": "*-newline", "severity": "off" }, 17 | { "rule": "*quotes", "severity": "off" }, 18 | { "rule": "*semi", "severity": "off" } 19 | ], 20 | 21 | // Enable eslint for all supported languages 22 | "eslint.validate": [ 23 | "javascript", 24 | "javascriptreact", 25 | "typescript", 26 | "typescriptreact", 27 | "vue", 28 | "html", 29 | "markdown", 30 | "json", 31 | "jsonc", 32 | "yaml" 33 | ], 34 | "editor.formatOnSave": true, 35 | "eslint.useFlatConfig": true 36 | } 37 | -------------------------------------------------------------------------------- /src/components/slider.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { cleanPropNames, h, toLowerCase } from '../utils' 3 | 4 | export type SliderProperties = { 5 | '👁️ Indicator2': boolean 6 | '🎨 Color': 'Error' | 'Neutral' | 'Primary' 7 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 8 | '⇅ Orientation': 'Horizontal' | 'Vertical' 9 | '🚦 State': 'Default' | 'Disabled' 10 | '◆ IndicatorPosition': '0' | '25' | '50' | '75' | '100' 11 | } 12 | 13 | export function Slider(component: DesignComponent) { 14 | const { color, size, orientation, state, indicatorPosition, indicator2 } = cleanPropNames(component.properties) 15 | 16 | const value = Number(indicatorPosition) 17 | 18 | return h( 19 | 'USlider', 20 | { 21 | modelValue: indicator2 ? [0, value] : value, 22 | color: toLowerCase(color), 23 | size, 24 | orientation: toLowerCase(orientation), 25 | disabled: state === 'Disabled', 26 | }, 27 | { 28 | color: 'primary', 29 | size: 'md', 30 | orientation: 'horizontal', 31 | disabled: false, 32 | }, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/components/textarea.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import { cleanPropNames, h, toLowerCase } from '../utils' 3 | 4 | export type TextareaProperties = { 5 | '↳ PlaceholderSlot': string 6 | '👁️ Placeholder': boolean 7 | '↳ CompletedLabel': string 8 | '👁️ Completed': boolean 9 | '🎨 Color': 'Error' | 'Neutral' | 'Primary' 10 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 11 | '◆ Variant': 'Outline' | 'Soft' | 'Subtle' | 'Ghost' | 'None' 12 | '🚦 State': 'Default' | 'Hover' | 'Focus' | 'Disabled' 13 | } 14 | 15 | export function Textarea(component: DesignComponent) { 16 | const { color, size, variant, state, placeholder, placeholderSlot } = cleanPropNames(component.properties) 17 | 18 | return h( 19 | 'UTextarea', 20 | { 21 | placeholder: placeholder ? placeholderSlot : undefined, 22 | color: toLowerCase(color), 23 | variant: toLowerCase(variant), 24 | size, 25 | disabled: state === 'Disabled', 26 | }, 27 | { 28 | color: 'primary', 29 | variant: 'outline', 30 | size: 'md', 31 | disabled: false, 32 | }, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025-Present GU Yiling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pro-components/content-search-button.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from '../components/button' 3 | import type { ButtonProps } from '../types' 4 | import { findChild } from '@tempad-dev/plugins' 5 | import { Button, BUTTON_NAMES } from '../components/button' 6 | import { ui } from '../components/config' 7 | import { h } from '../utils' 8 | 9 | // eslint-disable-next-line ts/no-empty-object-type 10 | export type ContentSearchButtonProperties = {} 11 | 12 | export function ContentSearchButton(component: DesignComponent) { 13 | const button = findChild>(component, { 14 | type: 'INSTANCE', 15 | name: BUTTON_NAMES, 16 | }) 17 | 18 | const props: Partial = button ? Button(button).props : {} 19 | 20 | const { icon, label, color, variant, size, disabled } = props 21 | 22 | return h( 23 | 'UContentSearchButton', 24 | { 25 | icon, 26 | label, 27 | color, 28 | variant, 29 | size, 30 | disabled, 31 | }, 32 | { 33 | icon: ui.icons.search, 34 | color: 'neutral', 35 | variant: 'ghost', 36 | }, 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/tooltip.ts: -------------------------------------------------------------------------------- 1 | import type { TooltipProps } from '@nuxt/ui' 2 | import type { DesignComponent } from '@tempad-dev/plugins' 3 | import { cleanPropNames, h, pickOverrides, toLowerCase } from '../utils' 4 | import { getKbdItems } from './kbd' 5 | 6 | const SIDE_MAP = { 7 | top: 'bottom', 8 | right: 'left', 9 | bottom: 'top', 10 | left: 'right', 11 | none: 'bottom', 12 | } as const 13 | 14 | export type TooltipProperties = { 15 | '◆ ArrowPlacement': 'Bottom' | 'Left' | 'None' | 'Right' | 'Top' 16 | '𝐓 Label': string 17 | } 18 | 19 | export function Tooltip(component: DesignComponent) { 20 | const { properties } = component 21 | 22 | const { arrowPlacement, label } = cleanPropNames(properties) 23 | 24 | const content: TooltipProps['content'] = pickOverrides( 25 | { 26 | side: SIDE_MAP[toLowerCase(arrowPlacement)], 27 | }, 28 | { 29 | side: 'bottom', 30 | }, 31 | ) 32 | 33 | const kbds = getKbdItems(component, { 34 | size: 'sm', 35 | }) 36 | 37 | return h( 38 | 'UTooltip', 39 | { 40 | text: label, 41 | content, 42 | arrow: arrowPlacement !== 'None', 43 | kbds, 44 | }, 45 | { 46 | arrow: false, 47 | }, 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/pro-components/page-grid.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 2 | import type { PageCardProperties } from './page-card' 3 | import type { PageFeatureProperties } from './page-feature' 4 | import { findAll } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h } from '../utils' 6 | import { PageCard } from './page-card' 7 | import { PageFeature } from './page-feature' 8 | 9 | export type PageGridProperties = { 10 | '⇅ Orientation': 'Horizontal' | 'Vertical' 11 | '◆ Variant': 'Card' | 'Feature' 12 | } 13 | 14 | export function PageGrid(component: DesignComponent) { 15 | const { variant } = cleanPropNames(component.properties) 16 | 17 | const children: DevComponent['children'] = [] 18 | 19 | if (variant === 'Card') { 20 | children.push( 21 | ...findAll>(component, { 22 | type: 'INSTANCE', 23 | name: 'PageCard', 24 | }).map(PageCard), 25 | ) 26 | } else if (variant === 'Feature') { 27 | children.push( 28 | ...findAll>(component, { 29 | type: 'INSTANCE', 30 | name: 'PageFeature', 31 | }).map(PageFeature), 32 | ) 33 | } 34 | 35 | return h('UPageGrid', {}, {}, children) 36 | } 37 | -------------------------------------------------------------------------------- /src/pro-components/locale-select.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { SelectMenuProperties } from '../components/select-menu' 3 | import { pick } from '@s-libs/micro-dash' 4 | import { findChild } from '@tempad-dev/plugins' 5 | import { SelectMenu } from '../components/select-menu' 6 | import { h } from '../utils' 7 | 8 | // eslint-disable-next-line ts/no-empty-object-type 9 | export type LocaleSelectProperties = {} 10 | 11 | export function LocaleSelect(component: DesignComponent) { 12 | const menu = findChild>(component, { 13 | type: 'INSTANCE', 14 | name: 'SelectMenu', 15 | }) 16 | 17 | const { props = {} } = menu ? SelectMenu(menu) : {} 18 | 19 | return h( 20 | 'ULocaleSelect', 21 | // https://github.com/nuxt/ui-pro/blob/f9e87ea77d9c4ffdbfcb9f627f541a63cd8ffa09/src/runtime/components/locale/LocaleSelect.vue#L4-L6 22 | { 23 | locales: [], 24 | ...pick( 25 | props, 26 | 'color', 27 | 'variant', 28 | 'size', 29 | 'trailingIcon', 30 | 'selectedIcon', 31 | 'content', 32 | 'arrow', 33 | 'portal', 34 | 'disabled', 35 | 'ui', 36 | ), 37 | }, 38 | {}, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/pro-components/error.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from '../components/button' 3 | import { findChild } from '@tempad-dev/plugins' 4 | import { BUTTON_NAMES, renderButtonItem } from '../components/button' 5 | import { cleanPropNames, h } from '../utils' 6 | 7 | export type ErrorProperties = { 8 | '𝐓 ErrorMessage': string 9 | '𝐓 StatusMessage': string 10 | '𝐓 StatusCode': string 11 | } 12 | 13 | export function Error(component: DesignComponent) { 14 | const { errorMessage: message, statusMessage, statusCode } = cleanPropNames(component.properties) 15 | 16 | const button = findChild>(component, { 17 | type: 'INSTANCE', 18 | name: BUTTON_NAMES, 19 | }) 20 | 21 | const clear = button 22 | ? renderButtonItem(button, { 23 | size: 'lg', 24 | color: 'primary', 25 | variant: 'solid', 26 | label: 'Back to home', 27 | }) 28 | : false 29 | 30 | return h( 31 | 'UError', 32 | { 33 | error: { 34 | statusCode, 35 | statusMessage, 36 | message, 37 | }, 38 | clear: clear ? (Object.keys(clear).length > 0 ? clear : true) : false, 39 | }, 40 | { 41 | clear: true, 42 | }, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/pro-components/page-feature.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { IconProperties } from '../components/icon' 3 | import type { PageFeatureProps } from '../types' 4 | import { getIconName } from '../components/icon' 5 | import { cleanPropNames, h, toLowerCase } from '../utils' 6 | 7 | export type PageFeatureProperties = { 8 | '👁️ Description': boolean 9 | '↳ Description': string 10 | '𝐓 Title': string 11 | '↳ IconName': DesignComponent 12 | '👁️ Icon': boolean 13 | '⇅ Orientation': 'Horizontal' | 'Vertical' 14 | } 15 | 16 | export function PageFeature(component: DesignComponent) { 17 | const { showDescription, description, title, iconName, icon, orientation } = cleanPropNames(component.properties, { 18 | '👁️ Description': 'showDescription', 19 | }) 20 | 21 | return h( 22 | 'UPageFeature', 23 | { 24 | icon: icon ? getIconName(iconName.name) : undefined, 25 | title, 26 | description: (showDescription && description) || undefined, 27 | orientation: toLowerCase(orientation), 28 | }, 29 | { 30 | orientation: 'horizontal', 31 | }, 32 | ) 33 | } 34 | 35 | export function renderPageFeatureItem(feature: DesignComponent): PageFeatureProps { 36 | return PageFeature(feature).props 37 | } 38 | -------------------------------------------------------------------------------- /src/components/checkbox.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { IconProperties } from './icon' 3 | import { cleanPropNames, h, toLowerCase } from '../utils' 4 | import { ui } from './config' 5 | import { getIconName } from './icon' 6 | 7 | export type CheckboxProperties = { 8 | '🙂 Icon': DesignComponent 9 | '𝐓 Label': string 10 | '↳ DescriptionSlot': string 11 | '👁️ Description': boolean 12 | '👁️ Required': boolean 13 | '🎨 Color': 'Neutral' | 'Primary' | 'Error' 14 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 15 | '🚦 State': 'Default' | 'Checked' | 'Focus' | 'Disabled' 16 | } 17 | 18 | export function Checkbox(component: DesignComponent) { 19 | const { color, size, state, label, description, descriptionSlot, required, icon } = cleanPropNames( 20 | component.properties, 21 | ) 22 | 23 | return h( 24 | 'UCheckbox', 25 | { 26 | label, 27 | description: description ? descriptionSlot : undefined, 28 | color: toLowerCase(color), 29 | size, 30 | icon: getIconName(icon.name), 31 | disabled: state === 'Disabled', 32 | required, 33 | }, 34 | { 35 | color: 'primary', 36 | size: 'md', 37 | icon: ui.icons.check, 38 | disabled: false, 39 | required: false, 40 | }, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/components/avatar-group.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { AvatarProps } from '../types' 3 | import type { AvatarProperties } from './avatar' 4 | import { findChildren } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h } from '../utils' 6 | import { Avatar } from './avatar' 7 | 8 | export type AvatarGroupProperties = { 9 | Size: '3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' 10 | } 11 | 12 | export function AvatarGroup(component: DesignComponent) { 13 | const { size } = cleanPropNames(component.properties) 14 | 15 | const avatars = findChildren>(component, { 16 | type: 'INSTANCE', 17 | name: 'Avatar', 18 | }).map((child) => Avatar(child)) 19 | 20 | avatars.forEach((avatar) => { 21 | if ('size' in avatar.props) { 22 | const { size, ...props } = avatar.props 23 | avatar.props = props 24 | } 25 | }) 26 | 27 | const last = avatars.at(-1) 28 | const lastProps = last?.props as AvatarProps | undefined 29 | const overflow = !Number.isNaN(Number.parseInt(lastProps?.alt || '', 10)) 30 | 31 | return h( 32 | 'UAvatarGroup', 33 | { 34 | size, 35 | max: overflow ? avatars.length - 1 : undefined, 36 | }, 37 | { 38 | size: 'md', 39 | }, 40 | overflow ? avatars.slice(0, -1) : avatars, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # tempad-dev-plugin-nuxt-ui 4 | 5 | Nuxt UI plugins for [TemPad Dev](https://github.com/ecomfe/tempad-dev). 6 | 7 | This plugin allows you to view component code, specifically tailored for Nuxt UI. 8 | 9 | 10 | 11 | 12 | Click the add button in the plugins section, enter "@nuxt" and press enter to install. 13 | 14 | 15 | ## Installation 16 | 17 | 1. Install [TemPad Dev](https://chromewebstore.google.com/detail/tempad-dev/lgoeakbaikpkihoiphamaeopmliaimpc) from Chrome Web Store. 18 | 19 | 2. Install `@nuxt` or `@nuxt/pro` in TemPad Dev's plugins section. 20 | 21 | > [!Note] 22 | > 23 | > `@nuxt/pro` contains all components in `@nuxt`, and 31 more components for Nuxt UI Pro. 24 | > 25 | > If you are using the free version of Nuxt UI, you can install `@nuxt` for smaller plugin size. 26 | 27 | ## Features 28 | 29 | - **Component Codegen**: Convert design components to [Nuxt UI](https://ui3.nuxt.dev/) code. 30 | 31 | e.g. `InputOutline` to ``: 32 | 33 | ```vue 34 | 35 | ``` 36 | 37 | ## License 38 | 39 | [MIT](./LICENSE) License © 2025-Present [GU Yiling](https://github.com/Justineo) 40 | -------------------------------------------------------------------------------- /src/pro-components/page-card.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { IconProperties } from '../components/icon' 3 | import { getIconName } from '../components/icon' 4 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, toLowerCase } from '../utils' 5 | 6 | export type PageCardProperties = { 7 | '👁️ Slot': boolean 8 | '↳ Slot': DesignComponent 9 | '𝐓 Description': string 10 | '𝐓 Title': string 11 | '↳ IconName': DesignComponent 12 | '👁️ Icon': boolean 13 | '◆ Variant': 'Ghost' | 'Outline' | 'Soft' | 'Subtle' | 'Solid' | 'Naked' 14 | '⇅ Orientation': 'Horizontal' | 'Vertical' 15 | '🚦 State': 'Default' | 'Hover' 16 | '👁️ Reverse': 'False' | 'True' 17 | } 18 | 19 | export function PageCard(component: DesignComponent) { 20 | const { title, description, icon, iconName, variant, orientation, reverse, showSlot } = cleanPropNames( 21 | component.properties, 22 | { 23 | '👁️ Slot': 'showSlot', 24 | }, 25 | ) 26 | 27 | return h( 28 | 'UPageCard', 29 | { 30 | icon: icon ? getIconName(iconName.name) : undefined, 31 | title, 32 | description, 33 | orientation: toLowerCase(orientation), 34 | reverse: reverse === 'True', 35 | variant: toLowerCase(variant), 36 | }, 37 | { 38 | orientation: 'vertical', 39 | reverse: false, 40 | variant: 'outline', 41 | }, 42 | showSlot ? [LOREM_IPSUM_TEXT] : [], 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/popover.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 2 | import type { PopoverProps } from '../types' 3 | import type { ButtonProperties } from './button' 4 | import { findChild } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, pickOverrides, renderSlot, toLowerCase } from '../utils' 6 | import { Button, BUTTON_NAMES } from './button' 7 | 8 | export type PopoverProperties = { 9 | '👁️ IsOpen': boolean 10 | '◆ Slot': DesignComponent 11 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 12 | '⇅ Position': 'Bottom' | 'Left' | 'Right' | 'Top' 13 | '👁️ Arrow': 'False' | 'True' 14 | } 15 | 16 | export function Popover(component: DesignComponent) { 17 | const { position, arrow } = cleanPropNames(component.properties) 18 | 19 | const content: PopoverProps['content'] = pickOverrides( 20 | { 21 | side: toLowerCase(position), 22 | }, 23 | { 24 | side: 'bottom', 25 | }, 26 | ) 27 | 28 | const children: DevComponent['children'] = [renderSlot('content', [LOREM_IPSUM_TEXT])] 29 | 30 | const trigger = findChild>(component, { 31 | type: 'INSTANCE', 32 | name: BUTTON_NAMES, 33 | }) 34 | 35 | if (trigger) { 36 | children.unshift(Button(trigger)) 37 | } 38 | 39 | return h( 40 | 'UPopover', 41 | { 42 | content, 43 | arrow: arrow === 'True', 44 | }, 45 | { 46 | arrow: false, 47 | }, 48 | children, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/pro-components/page-anchors.ts: -------------------------------------------------------------------------------- 1 | import type { PageAnchor } from '@nuxt/ui-pro/runtime/components/PageAnchors.vue' 2 | import type { DesignComponent } from '@tempad-dev/plugins' 3 | import type { IconProperties } from '../components/icon' 4 | import { findChildren } from '@tempad-dev/plugins' 5 | import { getIconName } from '../components/icon' 6 | import { getLinkTo } from '../components/link' 7 | import { cleanPropNames, h } from '../utils' 8 | 9 | export type PageAnchorsItemProperties = { 10 | '𝐓 Label': string 11 | '👁️ External': boolean 12 | '🙂 IconLeadingName': DesignComponent 13 | '🚦 State': 'Default' | 'Hover' | 'Active' 14 | } 15 | 16 | export function renderPageAnchorsItem(item: DesignComponent): PageAnchor { 17 | const { label, external, iconLeadingName } = cleanPropNames(item.properties) 18 | 19 | return { 20 | label, 21 | icon: iconLeadingName ? getIconName(iconLeadingName.name) : undefined, 22 | external, 23 | to: getLinkTo(label, external ? 'external' : 'hash'), 24 | } 25 | } 26 | 27 | // eslint-disable-next-line ts/no-empty-object-type 28 | export type PageAnchorsProperties = {} 29 | 30 | export function PageAnchors(component: DesignComponent) { 31 | const links = findChildren>(component, { 32 | type: 'INSTANCE', 33 | name: 'PageAnchorsItem', 34 | }).map((item) => renderPageAnchorsItem(item)) 35 | 36 | return h( 37 | 'UPageAnchors', 38 | { 39 | links, 40 | }, 41 | {}, 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/pro-components/page-header.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from '../components/button' 3 | import { queryAll } from '@tempad-dev/plugins' 4 | import { BUTTON_NAMES, renderButtonItem } from '../components/button' 5 | import { getLinkTo } from '../components/link' 6 | import { cleanPropNames, h } from '../utils' 7 | 8 | export type PageHeaderProperties = { 9 | '👁️ Links': boolean 10 | '𝐓 Description': string 11 | '𝐓 Title': string 12 | '↳ Headline': string 13 | '👁️ Headline': boolean 14 | '🖥️ Device': 'Desktop' | 'Mobile' 15 | } 16 | 17 | export function PageHeader(component: DesignComponent) { 18 | const { 19 | title, 20 | description, 21 | headline, 22 | showHeadline, 23 | links: showLinks, 24 | } = cleanPropNames(component.properties, { 25 | '👁️ Headline': 'showHeadline', 26 | }) 27 | 28 | const links = queryAll>(component, [ 29 | { query: 'all', type: 'FRAME', name: 'Links' }, 30 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 31 | ]) 32 | .map((button) => 33 | renderButtonItem(button, { 34 | color: 'neutral', 35 | variant: 'outline', 36 | }), 37 | ) 38 | .map((link) => ({ 39 | ...link, 40 | to: getLinkTo(link.label || ''), 41 | })) 42 | 43 | return h( 44 | 'UPageHeader', 45 | { 46 | headline: (showHeadline && headline) || undefined, 47 | title, 48 | description, 49 | links: showLinks ? links : undefined, 50 | }, 51 | {}, 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/pro-components/footer.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from '../components/button' 3 | import type { NavigationMenuProperties } from '../components/navigation-menu' 4 | import { queryAll, queryOne } from '@tempad-dev/plugins' 5 | import { Button, BUTTON_NAMES } from '../components/button' 6 | import { NavigationMenu } from '../components/navigation-menu' 7 | import { h, renderSlot } from '../utils' 8 | 9 | export type FooterProperties = { 10 | '👁️ Top': boolean 11 | '👁️ Default': boolean 12 | '👁️ RightSlot': boolean 13 | '👁️ Bottom': boolean 14 | '👁️ LeftSlot': boolean 15 | '🖥️ Device': 'Mobile' | 'Desktop' 16 | } 17 | 18 | export function Footer(component: DesignComponent) { 19 | const left = queryOne(component, [ 20 | { query: 'child', type: 'FRAME', name: 'LeftSlot' }, 21 | { query: 'child', type: 'TEXT' }, 22 | ])?.characters 23 | 24 | const right = queryAll>(component, [ 25 | { query: 'child', type: 'FRAME', name: 'RightSlot' }, 26 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 27 | ]).map((button) => Button(button)) 28 | 29 | const menu = queryOne>(component, [ 30 | { query: 'child', type: 'FRAME', name: 'Default' }, 31 | { query: 'child', type: 'INSTANCE', name: 'NavigationMenu' }, 32 | ]) 33 | 34 | return h('UFooter', {}, {}, [ 35 | ...(left ? [renderSlot('left', [left])] : []), 36 | ...(menu ? [NavigationMenu(menu)] : []), 37 | ...(right.length ? [renderSlot('right', right)] : []), 38 | ]) 39 | } 40 | -------------------------------------------------------------------------------- /src/pro-components/page-links.ts: -------------------------------------------------------------------------------- 1 | import type { PageLink } from '@nuxt/ui-pro/runtime/components/PageLinks.vue' 2 | import type { DesignComponent } from '@tempad-dev/plugins' 3 | import type { IconProperties } from '../components/icon' 4 | import { queryAll } from '@tempad-dev/plugins' 5 | import { getIconName } from '../components/icon' 6 | import { getLinkTo } from '../components/link' 7 | import { cleanPropNames, h } from '../utils' 8 | 9 | export type PageLinksItemProperties = { 10 | '𝐓 Label': string 11 | '↳ IconLeadingName': DesignComponent 12 | '🙂 IconLeading': boolean 13 | '👁️ External': boolean 14 | } 15 | 16 | export function renderPageLinksItem(item: DesignComponent): PageLink { 17 | const { label, external, iconLeading, iconLeadingName } = cleanPropNames(item.properties) 18 | 19 | return { 20 | label, 21 | icon: iconLeading ? getIconName(iconLeadingName.name) : undefined, 22 | external, 23 | to: getLinkTo(label, external ? 'external' : 'path'), 24 | } 25 | } 26 | 27 | export type PageLinksProperties = { 28 | '↳ Title': string 29 | '👁️ Title': boolean 30 | } 31 | 32 | export function PageLinks(component: DesignComponent) { 33 | const { title, showTitle } = cleanPropNames(component.properties, { 34 | '👁️ Title': 'showTitle', 35 | }) 36 | 37 | const links = queryAll>(component, [ 38 | { query: 'one', type: 'FRAME', name: 'Links' }, 39 | { query: 'children', type: 'INSTANCE', name: 'PageLinksItem' }, 40 | ]).map((item) => renderPageLinksItem(item)) 41 | 42 | return h( 43 | 'UPageLinks', 44 | { 45 | title: (showTitle && title) || undefined, 46 | links, 47 | }, 48 | {}, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/components/switch.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { IconProperties } from './icon' 3 | import { cleanPropNames, h, toLowerCase } from '../utils' 4 | import { getIconName } from './icon' 5 | 6 | export type SwitchProperties = { 7 | '𝐓 Description': string 8 | '↳ Description': boolean 9 | '𝐓 Title': string 10 | '👁️ Required': boolean 11 | '👁️ ActiveIcon': boolean 12 | '↳ ActiveIconName': DesignComponent 13 | '👁️ DefaultIcon': boolean 14 | '↳ DefaultIconName': DesignComponent 15 | '🎨 Color': 'Error' | 'Neutral' | 'Primary' | 'Secondary' | 'Success' | 'Info' | 'Warning' 16 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 17 | '🚦 State': 'Active' | 'Default' | 'Disabled' | 'Focus' 18 | } 19 | 20 | export function Switch(component: DesignComponent) { 21 | const { 22 | color, 23 | size, 24 | state, 25 | title, 26 | showDescription, 27 | description, 28 | defaultIcon, 29 | defaultIconName, 30 | activeIcon, 31 | activeIconName, 32 | required, 33 | } = cleanPropNames(component.properties, { 34 | '↳ Description': 'showDescription', 35 | }) 36 | 37 | return h( 38 | 'USwitch', 39 | { 40 | label: title, 41 | description: (showDescription && description) || undefined, 42 | color: toLowerCase(color), 43 | size, 44 | checkedIcon: defaultIcon ? getIconName(activeIconName.name) : undefined, 45 | uncheckedIcon: activeIcon ? getIconName(defaultIconName.name) : undefined, 46 | disabled: state === 'Disabled', 47 | required, 48 | }, 49 | { 50 | color: 'primary', 51 | size: 'md', 52 | disabled: false, 53 | required: false, 54 | }, 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/components/separator.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { AvatarProperties } from './avatar' 3 | import type { IconProperties } from './icon' 4 | import { findChild } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h, toLowerCase } from '../utils' 6 | import { renderAvatarItem } from './avatar' 7 | import { getIconName } from './icon' 8 | 9 | export type SeparatorProperties = { 10 | '𝐓 Span': string 11 | '🙂 IconName': DesignComponent 12 | '🎨 Color': 'Neutral' | 'Primary' 13 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 14 | '⇅ Orientation': 'Horizontal' | 'Vertical' 15 | '◆ Separator': 'Solid' | 'Dashed' | 'Dotted' 16 | '◆ Slot': 'Avatar' | 'Icon' | 'Span' 17 | } 18 | 19 | export function Separator(component: DesignComponent) { 20 | const { color, size, orientation, separator, slot, iconName, span } = cleanPropNames(component.properties) 21 | 22 | const avatar = 23 | slot === 'Avatar' 24 | ? findChild>(component, { 25 | type: 'INSTANCE', 26 | name: 'Avatar', 27 | }) 28 | : undefined 29 | 30 | const { chip, ...avatarItem } = avatar ? renderAvatarItem(avatar) : {} 31 | 32 | return h( 33 | 'USeparator', 34 | { 35 | label: (slot === 'Span' && span) || undefined, 36 | icon: (slot === 'Icon' && getIconName(iconName.name)) || undefined, 37 | avatar: Object.keys(avatarItem).length > 0 ? avatarItem : undefined, 38 | color: toLowerCase(color), 39 | size, 40 | type: toLowerCase(separator), 41 | orientation: toLowerCase(orientation), 42 | }, 43 | { 44 | color: 'neutral', 45 | size: 'xs', 46 | type: 'solid', 47 | orientation: 'horizontal', 48 | }, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/pro-components/user.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { AvatarProperties } from '../components/avatar' 3 | import type { UserProps } from '../types' 4 | import { omit } from '@s-libs/micro-dash' 5 | import { findChild } from '@tempad-dev/plugins' 6 | import { renderAvatarItem } from '../components/avatar' 7 | import { cleanPropNames, h, toLowerCase } from '../utils' 8 | 9 | export type UserProperties = { 10 | '👁️ Description': boolean 11 | '↳ Description': string 12 | '𝐓 Name': string 13 | '📏 Size': '3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' 14 | '⇅ Orientation': 'Horizontal' | 'Vertical' 15 | '👁️ Chip': 'False' | 'True' 16 | } 17 | 18 | export function User(component: DesignComponent) { 19 | const { showDescription, description, name, size, orientation } = cleanPropNames(component.properties, { 20 | '👁️ Description': 'showDescription', 21 | }) 22 | 23 | const avatar = findChild>(component, { type: 'INSTANCE', name: 'Avatar' }) 24 | const { chip, ...avatarItem } = avatar ? omit(renderAvatarItem(avatar), 'size') : {} 25 | 26 | return h( 27 | 'UUser', 28 | { 29 | name, 30 | description: (showDescription && description) || undefined, 31 | avatar: Object.keys(avatarItem).length > 0 ? avatarItem : undefined, 32 | chip: omit(chip, 'size', 'inset'), 33 | size: toLowerCase(size), 34 | orientation: toLowerCase(orientation), 35 | }, 36 | { 37 | size: 'md', 38 | orientation: 'horizontal', 39 | }, 40 | ) 41 | } 42 | 43 | export function renderUserItem(user: DesignComponent): Partial { 44 | const { 45 | props: { orientation, ...rest }, 46 | } = User(user) 47 | 48 | return rest 49 | } 50 | -------------------------------------------------------------------------------- /src/components/button-group.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from './button' 3 | import type { InputProperties } from './input' 4 | import { findChild, findChildren, queryOne } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h, toLowerCase } from '../utils' 6 | import { Button, BUTTON_NAMES, renderButtonChild } from './button' 7 | import { Input, INPUT_NAMES } from './input' 8 | 9 | export type ButtonGroupProperties = { 10 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 11 | '◆ Variant': 'Buttons' | 'Input + button' 12 | '⇅ Orientation': 'Vertical' | 'Horizontal' 13 | } 14 | 15 | export function ButtonGroup(component: DesignComponent) { 16 | const { variant, size, orientation } = cleanPropNames(component.properties) 17 | 18 | const children: DevComponent['children'] = [] 19 | 20 | if (variant === 'Buttons') { 21 | const buttons = findChildren>(component, { 22 | type: 'INSTANCE', 23 | 24 | name: BUTTON_NAMES, 25 | }) 26 | 27 | children.push(...buttons.map((button) => renderButtonChild(button))) 28 | } else { 29 | const input = queryOne>(component, [ 30 | { query: 'child', type: 'INSTANCE', name: 'Input' }, 31 | { query: 'child', type: 'INSTANCE', name: INPUT_NAMES }, 32 | ]) 33 | if (input) { 34 | children.push(Input(input)) 35 | } 36 | 37 | const button = findChild>(component, { 38 | type: 'INSTANCE', 39 | name: BUTTON_NAMES, 40 | }) 41 | if (button) { 42 | children.push(Button(button)) 43 | } 44 | } 45 | 46 | return h( 47 | 'UButtonGroup', 48 | { 49 | size, 50 | orientation: toLowerCase(orientation), 51 | }, 52 | { 53 | size: 'md', 54 | orientation: 'horizontal', 55 | }, 56 | children, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/pro-components/page-logos.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { AvatarItem } from '../components/avatar' 3 | import type { IconProperties } from '../components/icon' 4 | import { queryAll } from '@tempad-dev/plugins' 5 | import { getRandomAvatar } from '../components/avatar' 6 | import { getIconName } from '../components/icon' 7 | import { cleanPropNames, h } from '../utils' 8 | 9 | export type PageLogosProperties = { 10 | '↳ Title': string 11 | '❖ Slot8': DesignComponent 12 | '❖ Slot7': DesignComponent 13 | '❖ Slot6': DesignComponent 14 | '❖ Slot5': DesignComponent 15 | '👁️ Title': boolean 16 | '❖ Slot4': DesignComponent 17 | '❖ Slot3': DesignComponent 18 | '❖ Slot2': DesignComponent 19 | '❖ Slot1': DesignComponent 20 | '◆ Variant': 'Icons' | 'Logos' 21 | '👁️ Overlay': 'False' | 'True' 22 | } 23 | 24 | export function PageLogos(component: DesignComponent) { 25 | const { title, showTitle, variant, overlay } = cleanPropNames(component.properties, { 26 | '👁️ Title': 'showTitle', 27 | }) 28 | 29 | const items: (string | Required)[] = [] 30 | if (variant === 'Icons') { 31 | const icons = queryAll>(component, [ 32 | { query: 'child', type: 'FRAME', name: 'Icons' }, 33 | { query: 'children', type: 'INSTANCE' }, 34 | ]) 35 | .map((icon) => getIconName(icon.name)) 36 | .filter((name) => name != null) 37 | items.push(...icons) 38 | } else { 39 | const logos = queryAll>(component, [ 40 | { query: 'child', type: 'FRAME', name: 'Logos' }, 41 | { query: 'children', type: 'INSTANCE' }, 42 | ]).map(() => getRandomAvatar()) 43 | items.push(...logos) 44 | } 45 | 46 | return h( 47 | 'UPageLogos', 48 | { 49 | title: (showTitle && title) || undefined, 50 | items, 51 | marquee: overlay === 'True', 52 | }, 53 | { 54 | marquee: false, 55 | }, 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/components/accordion.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { IconProperties } from './icon' 3 | import { findChildren } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, LOREM_IPSUM_TITLE, pickOverrides } from '../utils' 5 | import { ui } from './config' 6 | import { getIconName } from './icon' 7 | 8 | // eslint-disable-next-line ts/no-empty-object-type 9 | export type AccordionProperties = {} 10 | 11 | export type CollapsiblePanelProperties = { 12 | '🙂 IconTrailingName': DesignComponent 13 | '𝐓 Label': string 14 | '𝐓 Description': string 15 | '👁️ IconLeading': boolean 16 | '↳ IconLeadingName': DesignComponent 17 | '◆ State': 'Closed' | 'Disable' | 'Focus' | 'Open' 18 | } 19 | 20 | export function Accordion(component: DesignComponent) { 21 | const panels = findChildren>(component, { 22 | type: 'INSTANCE', 23 | name: ['Collapsible_panel', 'PageAccordionItem'], 24 | }) 25 | 26 | let openCount = 0 27 | 28 | const items = panels.map((panel) => { 29 | const { state, label, description, iconLeading, iconLeadingName, iconTrailingName } = cleanPropNames( 30 | panel.properties, 31 | ) 32 | 33 | if (state === 'Open') { 34 | openCount++ 35 | } 36 | 37 | return pickOverrides( 38 | { 39 | label: label || LOREM_IPSUM_TITLE, 40 | content: description || LOREM_IPSUM_TEXT, 41 | icon: iconLeading ? getIconName(iconLeadingName.name) : undefined, 42 | trailingIcon: getIconName(iconTrailingName.name), 43 | disabled: state === 'Disable', 44 | }, 45 | { 46 | disabled: false, 47 | trailingIcon: ui.icons.chevronDown, 48 | }, 49 | ) 50 | }) 51 | 52 | return h( 53 | 'UAccordion', 54 | { 55 | items, 56 | type: openCount > 1 ? 'multiple' : 'single', 57 | }, 58 | { 59 | type: 'single', 60 | }, 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tempad-dev-plugin-nuxt-ui", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "packageManager": "pnpm@9.14.0", 7 | "description": "Nuxt UI plugins for TemPad Dev.", 8 | "author": "Justineo ", 9 | "license": "MIT", 10 | "homepage": "https://github.com/Justineo/tempad-dev-plugin-nuxt-ui#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Justineo/tempad-dev-plugin-nuxt-ui.git" 14 | }, 15 | "bugs": "https://github.com/Justineo/tempad-dev-plugin-nuxt-ui/issues", 16 | "keywords": [], 17 | "sideEffects": false, 18 | "scripts": { 19 | "build": "unbuild", 20 | "dev": "unbuild --stub", 21 | "lint": "eslint .", 22 | "lint:fix": "eslint . --fix", 23 | "format": "prettier . --write", 24 | "typecheck": "tsc --noEmit", 25 | "prepare": "simple-git-hooks" 26 | }, 27 | "devDependencies": { 28 | "@antfu/eslint-config": "^4.3.0", 29 | "@antfu/ni": "^0.23.2", 30 | "@antfu/utils": "^0.7.10", 31 | "@figma/plugin-typings": "^1.107.0", 32 | "@nuxt/content": "^3.2.1", 33 | "@nuxt/ui": "3.0.0-beta.2", 34 | "@nuxt/ui-pro": "3.0.0-beta.2", 35 | "@s-libs/micro-dash": "^18.0.0", 36 | "@tempad-dev/plugins": "^0.5.0", 37 | "@types/node": "^22.13.5", 38 | "eslint": "^9.21.0", 39 | "eslint-plugin-de-morgan": "^1.1.0", 40 | "lint-staged": "^15.4.3", 41 | "pnpm": "^9.15.5", 42 | "prettier": "^3.5.2", 43 | "reka-ui": "1.0.0-alpha.11", 44 | "simple-git-hooks": "^2.11.1", 45 | "tailwind-variants": "^0.3.1", 46 | "typescript": "^5.7.3", 47 | "unbuild": "^3.3.1", 48 | "vite": "^6.1.1", 49 | "vue": "^3.5.13", 50 | "vue-component-type-helpers": "^2.2.2", 51 | "vue-router": "^4.5.0" 52 | }, 53 | "simple-git-hooks": { 54 | "pre-commit": "pnpm lint-staged && pnpm build && git add dist" 55 | }, 56 | "lint-staged": { 57 | "*": [ 58 | "pnpm lint:fix", 59 | "pnpm format" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/carousel.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { CarouselProps } from '../types' 3 | import type { ButtonProperties } from './button' 4 | import { queryAll } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h } from '../utils' 6 | import { BUTTON_NAMES, renderButtonItem } from './button' 7 | import { ui } from './config' 8 | 9 | export type CarouselProperties = { 10 | '👁️ Prev/Next': boolean 11 | '👁️ Pagination': boolean 12 | '◆ Variant': 'Default' | 'Fade' | 'Single' 13 | '⇅ DotsPosition': 'Bottom' | 'Top' 14 | '⇅ Prev/Next Position': 'Bottom' | 'Center' | 'Top' 15 | } 16 | 17 | export function Carousel(component: DesignComponent) { 18 | const { variant, pagination, prevNext } = cleanPropNames(component.properties) 19 | 20 | let arrowProps: Pick = {} 21 | if (prevNext) { 22 | const [prevButton, nextButton] = queryAll>(component, [ 23 | { query: 'child', type: 'FRAME', name: 'Carousel + prev/next' }, 24 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 25 | ]) 26 | const defaults = { 27 | size: 'md', 28 | color: 'neutral', 29 | variant: 'link', 30 | } as const 31 | const { icon: prevIcon, ...prev } = prevButton 32 | ? renderButtonItem(prevButton, { 33 | ...defaults, 34 | icon: ui.icons.arrowLeft, 35 | }) 36 | : {} 37 | const { icon: nextIcon, ...next } = nextButton 38 | ? renderButtonItem(nextButton, { 39 | ...defaults, 40 | icon: ui.icons.arrowRight, 41 | }) 42 | : {} 43 | 44 | arrowProps = { 45 | prev, 46 | next, 47 | prevIcon, 48 | nextIcon, 49 | } 50 | } 51 | 52 | return h( 53 | 'UCarousel', 54 | { 55 | items: [], 56 | arrows: prevNext, 57 | dots: pagination, 58 | fade: variant === 'Fade', 59 | ...arrowProps, 60 | }, 61 | { 62 | arrows: false, 63 | dots: false, 64 | fade: false, 65 | }, 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/pro-components/banner.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from '../components/button' 3 | import type { IconProperties } from '../components/icon' 4 | import { findChildren } from '@tempad-dev/plugins' 5 | import { BUTTON_NAMES, renderButtonItem } from '../components/button' 6 | import { ui } from '../components/config' 7 | import { getIconName } from '../components/icon' 8 | import { cleanPropNames, h, toLowerCase } from '../utils' 9 | 10 | export type BannerProperties = { 11 | '👁️ Action2': boolean 12 | '👁️ IconLeading': boolean 13 | '𝐓 Title': string 14 | '👁️ Action1': boolean 15 | '👁️ Close': boolean 16 | '↳ IconLeadingName': DesignComponent 17 | '🎨 Color': 'Neutral' | 'Primary' | 'Success' | 'Info' | 'Warning' | 'Error' | 'Secondary' 18 | '🖥️ Device': 'Desktop' | 'Mobile' 19 | '🚦 State': 'Default' | 'Hover' 20 | } 21 | 22 | export function Banner(component: DesignComponent) { 23 | const { iconLeading, iconLeadingName, close: showClose, title, color } = cleanPropNames(component.properties) 24 | 25 | const buttons = findChildren>(component, { 26 | type: 'INSTANCE', 27 | name: BUTTON_NAMES, 28 | }) 29 | 30 | const actions = (showClose ? buttons.slice(0, -1) : buttons).map((button) => 31 | renderButtonItem(button, { color: 'neutral', size: 'xs' }), 32 | ) 33 | 34 | const { icon: closeIcon, ...close } = showClose 35 | ? renderButtonItem(buttons.at(-1)!, { 36 | size: 'md', 37 | color: 'neutral', 38 | variant: 'ghost', 39 | }) 40 | : {} 41 | 42 | return h( 43 | 'UBanner', 44 | { 45 | color: toLowerCase(color), 46 | icon: iconLeading ? getIconName(iconLeadingName.name) : undefined, 47 | title, 48 | actions: actions.length > 0 ? actions : undefined, 49 | close: showClose && Object.keys(close).length > 0 ? close : showClose, 50 | closeIcon, 51 | }, 52 | { 53 | color: 'primary', 54 | close: false, 55 | closeIcon: ui.icons.close, 56 | }, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/pro-components/content-surround.ts: -------------------------------------------------------------------------------- 1 | import type { ContentSurroundLink } from '@nuxt/ui-pro/runtime/components/content/ContentSurround.vue' 2 | import type { DesignComponent } from '@tempad-dev/plugins' 3 | import type { IconProperties } from '../components/icon' 4 | import { findChildren, queryOne } from '@tempad-dev/plugins' 5 | import { ui } from '../components/config' 6 | import { getIconName } from '../components/icon' 7 | import { getLinkTo } from '../components/link' 8 | import { cleanPropNames, h } from '../utils' 9 | 10 | export type ContentSurroundItemProperties = { 11 | '𝐓 Description': string 12 | '𝐓 Title': string 13 | '⇅ Align': 'Left' | 'Right' 14 | '🚦State': 'Default' | 'Focus' | 'Hover' 15 | } 16 | 17 | export function renderContentSurroundLink( 18 | component: DesignComponent, 19 | ): ContentSurroundLink { 20 | const { title, description } = cleanPropNames(component.properties) 21 | 22 | const iconNode = queryOne>(component, [ 23 | { query: 'child', type: 'FRAME', name: 'Icon' }, 24 | { query: 'child', type: 'INSTANCE' }, 25 | ]) 26 | 27 | const icon = iconNode ? getIconName(iconNode.name) : undefined 28 | 29 | return { 30 | title, 31 | description, 32 | path: getLinkTo(title, 'hash'), 33 | icon, 34 | } 35 | } 36 | 37 | // eslint-disable-next-line ts/no-empty-object-type 38 | export type ContentSurroundProperties = {} 39 | 40 | export function ContentSurround(component: DesignComponent) { 41 | const links = findChildren>(component, { 42 | type: 'INSTANCE', 43 | name: 'ContentSurroundItem', 44 | }).map((item) => renderContentSurroundLink(item)) 45 | 46 | const { icon: prevIcon, ...prev } = links[0]! 47 | const { icon: nextIcon, ...next } = links[1]! 48 | 49 | return h( 50 | 'UContentSurround', 51 | { 52 | prevIcon, 53 | nextIcon, 54 | surround: [prev, next], 55 | }, 56 | { 57 | prevIcon: ui.icons.arrowLeft, 58 | nextIcon: ui.icons.arrowRight, 59 | }, 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/pro-components/page-cta.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from '../components/button' 3 | import { queryAll } from '@tempad-dev/plugins' 4 | import { BUTTON_NAMES, renderButtonItem } from '../components/button' 5 | import { getLinkTo } from '../components/link' 6 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, toLowerCase } from '../utils' 7 | 8 | export type PageCTAProperties = { 9 | '↳ Slot': DesignComponent 10 | '👁️ Slot': boolean 11 | '👁️ Links': boolean 12 | '↳ Description': string 13 | '👁️ Description': boolean 14 | '𝐓 Title': string 15 | '🖥️ Device': 'Desktop' | 'Mobile' 16 | '◆ Variant': 'Outline' | 'Naked' | 'Soft' | 'Solid' | 'Subtle' 17 | '⇅ Orientation': 'Horizontal' | 'Vertical' 18 | '👁️ Reverse': 'False' | 'True' 19 | } 20 | 21 | export function PageCTA(component: DesignComponent) { 22 | const { 23 | title, 24 | description, 25 | orientation, 26 | reverse, 27 | variant, 28 | links: showLinks, 29 | showDescription, 30 | showSlot, 31 | } = cleanPropNames(component.properties, { 32 | '👁️ Slot': 'showSlot', 33 | '👁️ Description': 'showDescription', 34 | }) 35 | 36 | const links = showLinks 37 | ? queryAll>(component, [ 38 | { query: 'one', type: 'FRAME', name: 'Links' }, 39 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 40 | ]) 41 | .map((button) => renderButtonItem(button, { size: 'lg' })) 42 | .map((link) => ({ 43 | ...link, 44 | to: getLinkTo(link.label || ''), 45 | })) 46 | : [] 47 | 48 | return h( 49 | 'UPageCTA', 50 | { 51 | title, 52 | description: showDescription ? description : undefined, 53 | orientation: toLowerCase(orientation), 54 | reverse: reverse === 'True', 55 | variant: toLowerCase(variant), 56 | links, 57 | }, 58 | { 59 | orientation: 'vertical', 60 | reverse: false, 61 | variant: 'outline', 62 | }, 63 | showSlot ? [LOREM_IPSUM_TEXT] : [], 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/components/drawer.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from './button' 3 | import { queryAll } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, renderSlot, toLowerCase } from '../utils' 5 | import { Button, BUTTON_NAMES } from './button' 6 | 7 | export type DrawerProperties = { 8 | '◆ ContainerSlot': DesignComponent 9 | '👁️ Heading': boolean 10 | '↳ Description': string 11 | '👁️ Description': boolean 12 | '👁️ Handle': boolean 13 | '𝐓 Title': string 14 | '👁️ Buttons': boolean 15 | '⇅ Direction': 'Bottom' | 'Top' | 'Right' | 'Left' 16 | '👁️ Overlay': 'True' | 'False' 17 | } 18 | 19 | export function Drawer(component: DesignComponent) { 20 | const { 21 | direction, 22 | overlay, 23 | handle, 24 | heading, 25 | title, 26 | showDescription, 27 | description, 28 | buttons: showButtons, 29 | } = cleanPropNames(component.properties, { 30 | '👁️ Description': 'showDescription', 31 | }) 32 | 33 | const children: DevComponent['children'] = [renderSlot('body', [LOREM_IPSUM_TEXT])] 34 | 35 | if (showButtons) { 36 | const buttons = queryAll>(component, [ 37 | { query: 'one', type: 'FRAME', name: 'Buttons' }, 38 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 39 | ]) 40 | 41 | if (buttons.length > 0) { 42 | const buttonComponents = buttons.map((button) => { 43 | const buttonComponent = Button(button) 44 | buttonComponent.props.class = 'justify-center' 45 | return buttonComponent 46 | }) 47 | 48 | children.push(renderSlot('footer', buttonComponents)) 49 | } 50 | } 51 | 52 | return h( 53 | 'UDrawer', 54 | { 55 | title: heading ? title : undefined, 56 | description: showDescription ? description : undefined, 57 | overlay: overlay === 'True', 58 | handle, 59 | direction: toLowerCase(direction), 60 | }, 61 | { 62 | overlay: true, 63 | handle: true, 64 | direction: 'bottom', 65 | }, 66 | children, 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/pro-components/page-aside.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent, FrameNode } from '@tempad-dev/plugins' 2 | import type { SeparatorProperties } from '../components/separator' 3 | import type { ContentNavigationProperties } from './content-navigation' 4 | import type { ContentSearchButtonProperties } from './content-search-button' 5 | import type { PageAnchorsProperties } from './page-anchors' 6 | import { findChild } from '@tempad-dev/plugins' 7 | import { Separator } from '../components/separator' 8 | import { h, renderSlot } from '../utils' 9 | import { ContentNavigation } from './content-navigation' 10 | import { ContentSearchButton } from './content-search-button' 11 | import { PageAnchors } from './page-anchors' 12 | 13 | // eslint-disable-next-line ts/no-empty-object-type 14 | export type PageAsideProperties = {} 15 | 16 | export function PageAside(component: DesignComponent) { 17 | const children: DevComponent['children'] = [] 18 | 19 | const top = findChild(component, { 20 | type: 'FRAME', 21 | name: 'Top', 22 | }) 23 | 24 | if (top) { 25 | const search = findChild>(top, { 26 | type: 'INSTANCE', 27 | name: 'ContentSearchButton', 28 | }) 29 | const anchors = findChild>(top, { 30 | type: 'INSTANCE', 31 | name: 'PageAnchors', 32 | }) 33 | if (search || anchors) { 34 | const topSlot = renderSlot('top', [ 35 | ...(search ? [ContentSearchButton(search)] : []), 36 | ...(anchors ? [PageAnchors(anchors)] : []), 37 | ]) 38 | children.push(topSlot) 39 | } 40 | } 41 | 42 | const sep = findChild>(component, { 43 | type: 'INSTANCE', 44 | name: 'Separator', 45 | }) 46 | const nav = findChild>(component, { 47 | type: 'INSTANCE', 48 | name: 'ContentNavigation', 49 | }) 50 | if (sep || nav) { 51 | const content = [...(sep ? [Separator(sep)] : []), ...(nav ? [ContentNavigation(nav)] : [])] 52 | children.push(...content) 53 | } 54 | 55 | return h('UPageAside', {}, {}, children) 56 | } 57 | -------------------------------------------------------------------------------- /src/components/input-number.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from './button' 3 | import { findChildren } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, toLowerCase } from '../utils' 5 | import { BUTTON_NAMES, renderButtonItem } from './button' 6 | import { ui } from './config' 7 | 8 | export type InputNumberProperties = { 9 | '👁️ Completed': boolean 10 | '↳ CompletedText': string 11 | '↳ PlaceholderText': string 12 | '👁️ Placeholder': boolean 13 | '🎨 Color': 'Neutral' | 'Primary' | 'Error' 14 | '◆ Variant': 'Ghost' | 'None' | 'Outline' | 'Soft' | 'Subtle' 15 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 16 | '⇅ Orientation': 'Horizontal' | 'Vertical' 17 | '🚦 State': 'Default' | 'Focus (or hover)' | 'Disabled' 18 | '✧ Highlight': 'False' | 'True' 19 | } 20 | 21 | export function InputNumber(component: DesignComponent) { 22 | const { color, variant, size, orientation, state, highlight, placeholder, placeholderText } = cleanPropNames( 23 | component.properties, 24 | ) 25 | 26 | const [dec, inc] = findChildren>(component, { 27 | type: 'INSTANCE', 28 | name: BUTTON_NAMES, 29 | }).map((button) => 30 | renderButtonItem(button, { 31 | variant: 'link', 32 | square: true, 33 | size, 34 | }), 35 | ) 36 | 37 | const { icon: decrementIcon, ...decrement } = dec || {} 38 | const { icon: incrementIcon, ...increment } = inc || {} 39 | 40 | return h( 41 | 'UInputNumber', 42 | { 43 | placeholder: (placeholder && placeholderText) || undefined, 44 | color: toLowerCase(color), 45 | variant: toLowerCase(variant), 46 | size, 47 | highlight: highlight === 'True', 48 | orientation: toLowerCase(orientation), 49 | increment, 50 | incrementIcon, 51 | decrement, 52 | decrementIcon, 53 | disabled: state === 'Disabled', 54 | }, 55 | { 56 | color: 'primary', 57 | variant: 'outline', 58 | size: 'md', 59 | highlight: false, 60 | orientation: 'horizontal', 61 | incrementIcon: ui.icons.plus, 62 | decrementIcon: ui.icons.minus, 63 | disabled: false, 64 | }, 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/components/select.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { SelectProps } from '../types' 3 | import type { IconProperties } from './icon' 4 | import { cleanPropNames, h, toLowerCase } from '../utils' 5 | import { getRandomAvatar } from './avatar' 6 | import { ui } from './config' 7 | import { getIconName } from './icon' 8 | 9 | const SELECT_VARIANT_MAP: Record = { 10 | SelectOutline: 'outline', 11 | SelectSoft: 'soft', 12 | SelectNone: 'none', 13 | SelectGhost: 'ghost', 14 | SelectSubtle: 'subtle', 15 | } 16 | 17 | type SelectName = keyof typeof SELECT_VARIANT_MAP 18 | 19 | export const SELECT_NAMES = Object.keys(SELECT_VARIANT_MAP) as SelectName[] 20 | 21 | export type SelectProperties = { 22 | '🙂 IconTrailingName': DesignComponent 23 | '🙂 IconLeadingName': DesignComponent 24 | '👁️ Completed': boolean 25 | '↳ CompletedLabel': string 26 | '👁️ Placeholder': boolean 27 | '↳ PlaceholderLabel': string 28 | '🎨 Color': 'Neutral' | 'Primary' | 'Error' 29 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 30 | '🚦 State': 'Default' | 'Focus' | 'Disabled' 31 | '◆ LeadingSlot': 'Icon' | 'Avatar' | 'None' | 'Span' 32 | } 33 | 34 | export function Select(component: DesignComponent, defaults: Partial = {}) { 35 | const variant = SELECT_VARIANT_MAP[component.name] 36 | 37 | const { color, size, state, leadingSlot, placeholder, placeholderLabel, iconLeadingName, iconTrailingName } = 38 | cleanPropNames(component.properties) 39 | 40 | const icon = leadingSlot === 'Icon' ? getIconName(iconLeadingName.name) : undefined 41 | const trailingIcon = getIconName(iconTrailingName.name) 42 | 43 | const avatar = leadingSlot === 'Avatar' ? getRandomAvatar() : undefined 44 | 45 | return h( 46 | 'USelect', 47 | { 48 | placeholder: placeholder ? placeholderLabel : undefined, 49 | color: toLowerCase(color), 50 | variant, 51 | size, 52 | icon, 53 | trailingIcon, 54 | avatar, 55 | disabled: state === 'Disabled', 56 | }, 57 | { 58 | color: 'primary', 59 | variant: 'outline', 60 | size: 'md', 61 | trailingIcon: ui.icons.chevronDown, 62 | disabled: false, 63 | ...defaults, 64 | }, 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/components/kbd.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, TextNode } from '@tempad-dev/plugins' 2 | import type { KbdProps } from '../types' 3 | import { findChild, queryAll } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, pickOverrides, toLowerCase } from '../utils' 5 | 6 | export const kbdGlyphsMap: Record = { 7 | '⌘': 'meta', 8 | '⌃': 'ctrl', 9 | '⌥': 'alt', 10 | '⊞': 'win', 11 | '⇧': 'shift', 12 | '↵': 'enter', 13 | '⌦': 'delete', 14 | '⌫': 'backspace', 15 | '⎋': 'escape', 16 | '⇥': 'tab', 17 | '⇪': 'capslock', 18 | '↑': 'arrowup', 19 | '→': 'arrowright', 20 | '↓': 'arrowdown', 21 | '←': 'arrowleft', 22 | '⇞': 'pageup', 23 | '⇟': 'pagedown', 24 | '↖': 'home', 25 | '↘': 'end', 26 | } 27 | 28 | export type KbdProperties = { 29 | '📏 Size': 'sm' | 'md' | 'lg' 30 | '◆ Variant': 'Solid' | 'Subtle' | 'Outline' 31 | '𝐓 Label': '←' | '↑' | '→' | '↓' | '↖' | '↘' | '⇧' | '⇪' | '⌃' | '⌘' | '⌥' | '⌦' | '⌫' | '⎇' | '⎋' 32 | } 33 | 34 | export function Kbd(component: DesignComponent) { 35 | const { size, variant } = cleanPropNames(component.properties) 36 | 37 | const text = findChild(component, { 38 | type: 'TEXT', 39 | })?.characters 40 | 41 | return h( 42 | 'UKbd', 43 | { 44 | value: text ? kbdGlyphsMap[text] || text : undefined, 45 | variant: toLowerCase(variant), 46 | size, 47 | }, 48 | { 49 | variant: 'outline', 50 | size: 'md', 51 | }, 52 | ) 53 | } 54 | 55 | export function renderKbdItem( 56 | kbd: DesignComponent, 57 | defaults: Partial = {}, 58 | ): Partial { 59 | const { props } = Kbd(kbd) 60 | 61 | return pickOverrides(props, defaults) 62 | } 63 | 64 | export function getKbdItems( 65 | menuItem: DesignComponent, 66 | defaults: Partial = {}, 67 | ): string[] | KbdProps[] | undefined { 68 | const kbds = queryAll>(menuItem, [ 69 | { query: 'child', type: 'FRAME', name: 'Kbd' }, 70 | { query: 'children', type: 'INSTANCE', name: 'Kbd' }, 71 | ]).map((kbd) => renderKbdItem(kbd, defaults)) 72 | 73 | if (kbds.length === 0) { 74 | return undefined 75 | } 76 | 77 | if (kbds.every((kbd) => Object.keys(kbd).length === 1 && kbd.value)) { 78 | return kbds.map((kbd) => kbd.value!) 79 | } 80 | 81 | return kbds.length > 0 ? kbds : undefined 82 | } 83 | -------------------------------------------------------------------------------- /src/pro-components/index.ts: -------------------------------------------------------------------------------- 1 | import type { RenderFn } from '../types' 2 | import { COMPONENT_MAP } from '../components' 3 | 4 | import { createTransformComponent } from '../utils' 5 | import { Banner } from './banner' 6 | import { BlogPost } from './blog-post' 7 | import { BlogPosts } from './blog-posts' 8 | import { ColorModeSelect } from './color-mode-select' 9 | import { ColorModeSwitch } from './color-mode-switch' 10 | import { ContentNavigation } from './content-navigation' 11 | import { ContentSearch } from './content-search' 12 | import { ContentSearchButton } from './content-search-button' 13 | import { ContentSurround } from './content-surround' 14 | import { ContentToc } from './content-toc' 15 | import { Error } from './error' 16 | import { Footer } from './footer' 17 | import { FooterColumns } from './footer-columns' 18 | import { Header } from './header' 19 | import { LocaleSelect } from './locale-select' 20 | import { PageAccordion } from './page-accordion' 21 | import { PageAnchors } from './page-anchors' 22 | import { PageAside } from './page-aside' 23 | import { PageCard } from './page-card' 24 | import { PageColumns } from './page-columns' 25 | import { PageCTA } from './page-cta' 26 | import { PageFeature } from './page-feature' 27 | import { PageGrid } from './page-grid' 28 | import { PageHeader } from './page-header' 29 | import { PageHero } from './page-hero' 30 | import { PageLinks } from './page-links' 31 | import { PageLogos } from './page-logos' 32 | import { PageSection } from './page-section' 33 | import { PricingPlan } from './pricing-plan' 34 | import { PricingPlans } from './pricing-plans' 35 | import { User } from './user' 36 | 37 | export const PRO_COMPONENT_MAP: Record = { 38 | ...COMPONENT_MAP, 39 | Banner, 40 | BlogPost, 41 | BlogPosts, 42 | ColorModeSelect, 43 | ColorModeSwitch, 44 | ContentNavigation, 45 | ContentSearch, 46 | ContentSearchButton, 47 | ContentSurround, 48 | ContentToc, 49 | Error, 50 | Footer, 51 | FooterColumns, 52 | Header, 53 | LocalSelect: LocaleSelect, 54 | LocaleSelect, 55 | PageAccordion, 56 | PageAnchors, 57 | PageAside, 58 | PageCard, 59 | PageColumns, 60 | PageCTA, 61 | PageFeature, 62 | PageGrid, 63 | PageHeader, 64 | PageHero, 65 | PageLinks, 66 | PageLogos, 67 | PageSection, 68 | PricingPlan, 69 | PricingPlans, 70 | User, 71 | } 72 | 73 | export const transformComponent = createTransformComponent(PRO_COMPONENT_MAP) 74 | -------------------------------------------------------------------------------- /src/components/form-field.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 2 | import type { InputProperties } from './input' 3 | import type { InputNumberProperties } from './input-number' 4 | import type { PinInputProperties } from './pin-input' 5 | import { findOne } from '@tempad-dev/plugins' 6 | import { cleanPropNames, h } from '../utils' 7 | import { Input, INPUT_NAMES } from './input' 8 | import { InputNumber } from './input-number' 9 | import { PinInput } from './pin-input' 10 | 11 | export type FormFieldProperties = { 12 | '👁️ Description': boolean 13 | '👁️ Help': boolean 14 | '👁️ Hint': boolean 15 | '👁️ Required': boolean 16 | '↳ HintSlot': string 17 | '↳ DescriptionSlot': string 18 | '↳ HelpSlot': string 19 | '𝐓 Label': string 20 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 21 | '◆ Input': 'Input' | 'InputNumber' | 'PinInput' 22 | '👁️ Error': 'False' | 'True' 23 | } 24 | 25 | export function FormField(component: DesignComponent) { 26 | const { size, input, error, label, required, hint, hintSlot, help, helpSlot, description, descriptionSlot } = 27 | cleanPropNames(component.properties) 28 | 29 | const children: DevComponent['children'] = [] 30 | 31 | if (input === 'Input') { 32 | const i = findOne>(component, { 33 | type: 'INSTANCE', 34 | name: INPUT_NAMES, 35 | }) 36 | 37 | if (i) { 38 | children.push(Input(i)) 39 | } 40 | } else if (input === 'InputNumber') { 41 | const i = findOne>(component, { 42 | type: 'INSTANCE', 43 | name: 'InputNumber', 44 | }) 45 | 46 | if (i) { 47 | children.push(InputNumber(i)) 48 | } 49 | } else if (input === 'PinInput') { 50 | const i = findOne>(component, { 51 | type: 'INSTANCE', 52 | name: 'PinInput', 53 | }) 54 | 55 | if (i) { 56 | children.push(PinInput(i)) 57 | } 58 | } 59 | 60 | return h( 61 | 'UFormField', 62 | { 63 | label, 64 | description: (description && descriptionSlot) || undefined, 65 | help: (error === 'False' && help && helpSlot) || undefined, 66 | error: error === 'True' && (helpSlot || true), 67 | hint: (hint && hintSlot) || undefined, 68 | size, 69 | required, 70 | }, 71 | { 72 | error: false, 73 | size: 'md', 74 | required: false, 75 | }, 76 | children, 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /src/components/radio-group.ts: -------------------------------------------------------------------------------- 1 | import type { RadioGroupItem } from '@nuxt/ui' 2 | import type { DesignComponent } from '@tempad-dev/plugins' 3 | import type { RadioGroupProps } from '../types' 4 | import { findAll } from '@tempad-dev/plugins' 5 | import { cleanPropNames, getFirst, h, pickOverrides, toLowerCase } from '../utils' 6 | 7 | export type RadioProperties = { 8 | '𝐓 Label': string 9 | '👁️ Description': boolean 10 | '↳ DescriptionSlot': string 11 | '🎨 Color': 'Neutral' | 'Error' | 'Primary' | 'Secondary' | 'Success' | 'Info' | 'Warning' 12 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 13 | '🚦 State': 'Default' | 'Disabled' | 'Focus' | 'Selected' 14 | } 15 | 16 | type RadioGroupItemExtra = Pick 17 | 18 | export function renderRadioItem(item: DesignComponent): RadioGroupItem & RadioGroupItemExtra { 19 | const { color, state, label, description, descriptionSlot } = cleanPropNames(item.properties) 20 | 21 | return pickOverrides( 22 | { 23 | label, 24 | description: (description && descriptionSlot) || undefined, 25 | disabled: state === 'Disabled', 26 | // These should be omitted in final `items` 27 | color: toLowerCase(color), 28 | }, 29 | { 30 | color: 'primary', 31 | disabled: false, 32 | }, 33 | ) 34 | } 35 | 36 | export type RadioGroupProperties = { 37 | '𝐓 Legend': string 38 | '👁️ Required': boolean 39 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 40 | '⇅ Align': 'Horizontal' | 'Vertical' 41 | } 42 | 43 | export function RadioGroup(component: DesignComponent) { 44 | const { size, align, legend, required } = cleanPropNames(component.properties) 45 | 46 | const items = findAll>(component, { 47 | type: 'INSTANCE', 48 | name: 'Radio', 49 | }).map((item) => renderRadioItem(item)) 50 | 51 | const color = getFirst(items, 'color') 52 | 53 | const disabled = items.every((item) => item.disabled) 54 | if (disabled) { 55 | items.forEach((item) => { 56 | delete item.disabled 57 | }) 58 | } 59 | 60 | return h( 61 | 'URadioGroup', 62 | { 63 | legend, 64 | items, 65 | size, 66 | color, 67 | orientation: toLowerCase(align), 68 | disabled, 69 | required, 70 | }, 71 | { 72 | size: 'md', 73 | color: 'primary', 74 | orientation: 'horizontal', 75 | disabled: false, 76 | required: false, 77 | }, 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/components/toast.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, FrameNode } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from './button' 3 | import type { IconProperties } from './icon' 4 | import { findChild, queryAll } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h, toLowerCase } from '../utils' 6 | import { getRandomAvatar } from './avatar' 7 | import { BUTTON_NAMES, renderButtonItem } from './button' 8 | import { ui } from './config' 9 | import { getIconName } from './icon' 10 | 11 | export type ToastProperties = { 12 | '🙂 LeadingIconName': DesignComponent 13 | '𝐓 Description': string 14 | '𝐓 Title': string 15 | '🎨 Color': 'Primary' | 'Neutral' | 'Secondary' | 'Success' | 'Info' | 'Warning' | 'Error' 16 | '◆ LeadingSlot': 'Avatar' | 'Icon' | 'None' 17 | '👁️ Description': 'False' | 'True' 18 | '👁️ Actions': 'False' | 'True' 19 | '◆ Progress': '0' | '100' 20 | } 21 | 22 | export function Toast(component: DesignComponent) { 23 | const { color, leadingSlot, description, title, showDescription, leadingIconName } = cleanPropNames( 24 | component.properties, 25 | { 26 | '👁️ Description': 'showDescription', 27 | }, 28 | ) 29 | 30 | const content = findChild(component, { 31 | type: 'FRAME', 32 | name: 'Content', 33 | }) 34 | const button = findChild>(content || component, { 35 | type: 'INSTANCE', 36 | name: BUTTON_NAMES, 37 | }) 38 | const close = button 39 | ? renderButtonItem(button, { 40 | size: 'md', 41 | color: 'neutral', 42 | variant: 'link', 43 | }) 44 | : false 45 | const closeIcon = close ? close.icon : undefined 46 | 47 | const actions = queryAll>(component, [ 48 | { query: 'one', type: 'FRAME', name: 'Actions' }, 49 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 50 | ]).map((button) => 51 | renderButtonItem(button, { 52 | size: 'xs', 53 | }), 54 | ) 55 | 56 | return h( 57 | 'UToast', 58 | { 59 | title, 60 | description: (showDescription && description) || undefined, 61 | icon: leadingSlot === 'Icon' ? getIconName(leadingIconName.name) : undefined, 62 | avatar: leadingSlot === 'Avatar' ? getRandomAvatar() : undefined, 63 | color: toLowerCase(color), 64 | close, 65 | closeIcon, 66 | actions: actions.length > 0 ? actions : undefined, 67 | }, 68 | { 69 | color: 'primary', 70 | close: true, 71 | closeIcon: ui.icons.close, 72 | }, 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/pro-components/page-hero.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 2 | import type { BadgeProperties } from '../components/badge' 3 | import type { ButtonProperties } from '../components/button' 4 | import { queryAll, queryOne } from '@tempad-dev/plugins' 5 | import { Badge } from '../components/badge' 6 | import { BUTTON_NAMES, renderButtonItem } from '../components/button' 7 | import { getLinkTo } from '../components/link' 8 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, renderSlot, toLowerCase } from '../utils' 9 | 10 | export type PageHeroProperties = { 11 | '↳ Slot': DesignComponent 12 | '👁️ Slot': boolean 13 | '↳ Description': string 14 | '𝐓 Title': string 15 | '👁️ Links': boolean 16 | '👁️ Description': boolean 17 | '👁️ Headline': boolean 18 | '⇅ Orientation': 'Vertical' | 'Horizontal' 19 | '🖥️ Device': 'Desktop' | 'Mobile' 20 | } 21 | 22 | export function PageHero(component: DesignComponent) { 23 | const { 24 | title, 25 | showDescription, 26 | description, 27 | headline: showHeadline, 28 | showSlot, 29 | links: showLinks, 30 | orientation, 31 | } = cleanPropNames(component.properties, { 32 | '👁️ Slot': 'showSlot', 33 | '👁️ Description': 'showDescription', 34 | }) 35 | 36 | const links = queryAll>(component, [ 37 | { query: 'one', type: 'FRAME', name: 'Links' }, 38 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 39 | ]) 40 | .map((button) => renderButtonItem(button, { size: 'xl' })) 41 | .map((link) => ({ 42 | ...link, 43 | to: getLinkTo(link.label || ''), 44 | })) 45 | 46 | const children: DevComponent['children'] = [] 47 | 48 | if (showHeadline) { 49 | const badge = queryOne>(component, [ 50 | { query: 'one', type: 'FRAME', name: 'Top' }, 51 | { query: 'one', type: 'INSTANCE', name: 'Badge' }, 52 | ]) 53 | if (badge) { 54 | children.push(renderSlot('headline', [Badge(badge)])) 55 | } 56 | } 57 | 58 | if (showSlot) { 59 | children.push(LOREM_IPSUM_TEXT) 60 | } 61 | 62 | return h( 63 | 'UPageHero', 64 | { 65 | // Headline doesn't support text content yet. 66 | // headline: (showHeadline && headline) || undefined, 67 | title, 68 | description: (showDescription && description) || undefined, 69 | links: showLinks ? links : undefined, 70 | orientation: toLowerCase(orientation), 71 | }, 72 | { 73 | orientation: 'vertical', 74 | }, 75 | children, 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /src/components/slideover.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent, FrameNode, TextNode } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from './button' 3 | import { findChild, findChildren, findOne } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, LOREM_IPSUM_TITLE, renderSlot, toLowerCase } from '../utils' 5 | import { BUTTON_NAMES, renderButtonItem } from './button' 6 | import { ui } from './config' 7 | 8 | export type SlideoverProperties = { 9 | '◆ FooterSlot': DesignComponent 10 | '◆ BodySlot': DesignComponent 11 | '◆ Variant': 'Bottom' | 'Top' | 'Left' | 'Right' 12 | '👁️ Overlay': 'True' | 'False' 13 | } 14 | 15 | export function Slideover(component: DesignComponent) { 16 | const { variant, overlay } = cleanPropNames(component.properties) 17 | 18 | const header = findOne(component, { 19 | type: 'FRAME', 20 | name: 'Header', 21 | }) 22 | 23 | const body = findOne(component, { 24 | type: 'FRAME', 25 | name: 'Body', 26 | }) 27 | 28 | const footer = findOne(component, { 29 | type: 'FRAME', 30 | name: 'Footer', 31 | }) 32 | 33 | const titleAndDesc = findOne(component, { 34 | type: 'FRAME', 35 | name: 'Title and description', 36 | }) 37 | 38 | const closeButton = header 39 | ? findChild>(header, { 40 | type: 'INSTANCE', 41 | name: BUTTON_NAMES, 42 | }) 43 | : undefined 44 | const closeProps = closeButton 45 | ? renderButtonItem(closeButton, { 46 | size: 'md', 47 | color: 'neutral', 48 | variant: 'ghost', 49 | }) 50 | : false 51 | 52 | const { icon: closeIcon, square, ...close } = closeProps || {} 53 | 54 | const [title, desc] = titleAndDesc 55 | ? findChildren(titleAndDesc, { 56 | type: 'TEXT', 57 | }) 58 | : [] 59 | 60 | const children: DevComponent['children'] = [] 61 | 62 | if (body) { 63 | children.push(renderSlot('body', [LOREM_IPSUM_TEXT])) 64 | } 65 | 66 | if (footer) { 67 | children.push(renderSlot('footer', [LOREM_IPSUM_TITLE])) 68 | } 69 | 70 | return h( 71 | 'USlideover', 72 | { 73 | title: title?.characters, 74 | description: desc?.characters, 75 | overlay: overlay === 'True', 76 | side: toLowerCase(variant), 77 | close: closeProps ? (Object.keys(close).length > 0 ? close : true) : false, 78 | closeIcon, 79 | }, 80 | { 81 | overlay: true, 82 | side: 'right', 83 | close: true, 84 | closeIcon: ui.icons.close, 85 | }, 86 | children, 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /src/components/alert.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from './button' 3 | import type { IconProperties } from './icon' 4 | import { findChild, queryAll } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h, toLowerCase } from '../utils' 6 | import { getRandomAvatar } from './avatar' 7 | import { BUTTON_NAMES, renderButtonItem } from './button' 8 | import { ui } from './config' 9 | import { getIconName } from './icon' 10 | 11 | export type AlertProperties = { 12 | '𝐓 Title': string 13 | '👁️ Icon': boolean 14 | '↳ IconName': DesignComponent 15 | '𝐓 Description': string 16 | '👁️ CloseButton': boolean 17 | '🎨 Color': 'Neutral' | 'Primary' | 'Secondary' | 'Success' | 'Info' | 'Warning' | 'Error' 18 | '◆ Variant': 'Solid' | 'Outline' | 'Soft' | 'Subtle' 19 | '◆ LeadingSlot': 'Avatar' | 'Icon' 20 | '👁️ Description': 'True' | 'False' 21 | '👁️ Action': 'False' | 'True' 22 | } 23 | 24 | export function Alert(component: DesignComponent) { 25 | const { color, variant, leadingSlot, showDescription, action, title, description, closeButton, icon, iconName } = 26 | cleanPropNames(component.properties, { 27 | '👁️ Description': 'showDescription', 28 | }) 29 | 30 | const button = findChild>(component, { 31 | type: 'INSTANCE', 32 | name: BUTTON_NAMES, 33 | }) 34 | const close = 35 | closeButton && button 36 | ? renderButtonItem(button, { 37 | size: 'md', 38 | color: 'neutral', 39 | variant: 'link', 40 | }) 41 | : false 42 | const closeIcon = close ? close.icon : undefined 43 | 44 | const actions = 45 | action === 'True' 46 | ? queryAll>(component, [ 47 | { query: 'one', type: 'FRAME', name: 'Actions' }, 48 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 49 | ]).map((button) => 50 | renderButtonItem(button, { 51 | size: 'xs', 52 | }), 53 | ) 54 | : [] 55 | 56 | return h( 57 | 'UAlert', 58 | { 59 | title, 60 | description: (showDescription && description) || undefined, 61 | icon: leadingSlot === 'Icon' && icon ? getIconName(iconName.name) : undefined, 62 | avatar: leadingSlot === 'Avatar' ? getRandomAvatar() : undefined, 63 | color: toLowerCase(color), 64 | variant: toLowerCase(variant), 65 | close, 66 | closeIcon, 67 | actions: actions.length > 0 ? actions : undefined, 68 | }, 69 | { 70 | color: 'primary', 71 | variant: 'solid', 72 | close: false, 73 | closeIcon: ui.icons.close, 74 | }, 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/components/pin-input.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { PinInputProps } from '../types' 3 | import { findChildren } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, toLowerCase } from '../utils' 5 | 6 | export type PinInputItemProperties = { 7 | '👁️ Completed': boolean 8 | '↳ CompletedText': string 9 | '👁️ Placeholder': boolean 10 | '👁️ Mask': boolean 11 | '↳ PlaceholderText': string 12 | '🎨 Color': 'Neutral' | 'Primary' | 'Error' 13 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 14 | '◆ Variant': 'Outline' | 'Soft' | 'Subtle' | 'Ghost' | 'None' 15 | '🚦State': 'Default' | 'Disabled' | 'Focus (or hover)' 16 | '✧ Highlight': 'False' | 'True' 17 | } 18 | 19 | type PinInputItem = Pick< 20 | PinInputProps, 21 | 'color' | 'variant' | 'size' | 'highlight' | 'type' | 'disabled' | 'placeholder' | 'mask' 22 | > & { value?: string } 23 | 24 | export function renderPinInputItem(item: DesignComponent): PinInputItem { 25 | const { color, variant, state, highlight, placeholder, placeholderText, completed, completedText, mask } = 26 | cleanPropNames(item.properties) 27 | 28 | return { 29 | color: toLowerCase(color), 30 | variant: toLowerCase(variant), 31 | highlight: highlight === 'True', 32 | disabled: state === 'Disabled', 33 | placeholder: (placeholder && placeholderText) || undefined, 34 | value: (completed && completedText) || undefined, 35 | mask, 36 | } 37 | } 38 | 39 | export type PinInputProperties = { 40 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 41 | } 42 | 43 | export function PinInput(component: DesignComponent) { 44 | const items: PinInputItem[] = findChildren>(component, { 45 | type: 'INSTANCE', 46 | name: 'PinInputItem', 47 | }).map((item) => renderPinInputItem(item)) 48 | 49 | const type: PinInputProps['type'] = items.some((item) => item.value && !/^\d$/.test(item.value)) ? 'text' : 'number' 50 | 51 | const placeholder: PinInputProps['placeholder'] = items.find((item) => !!item.placeholder)?.placeholder 52 | const mask: PinInputProps['mask'] = items.some((item) => item.mask) 53 | 54 | const { size } = cleanPropNames(component.properties) 55 | const { color, variant, highlight, disabled } = items[0]! 56 | 57 | return h( 58 | 'UPinInput', 59 | { 60 | color, 61 | variant, 62 | size, 63 | length: items.length, 64 | highlight, 65 | type, 66 | disabled, 67 | placeholder, 68 | mask, 69 | }, 70 | { 71 | color: 'primary', 72 | variant: 'outline', 73 | size: 'md', 74 | length: 5, 75 | highlight: false, 76 | type: 'text', 77 | disabled: false, 78 | mask: false, 79 | }, 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/components/avatar.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, TextNode } from '@tempad-dev/plugins' 2 | import type { AvatarProps, ChipProps } from '../types' 3 | import type { IconProperties } from './icon' 4 | import { findChild } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h, toLowerCase } from '../utils' 6 | import { getIconName } from './icon' 7 | 8 | export type AvatarProperties = { 9 | '🙂 IconName': DesignComponent 10 | '📏 Size': '3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' 11 | '◆ Variant': 'Image' | 'Icon' | 'Alt' 12 | '◆ ChipPosition': 'Bottom-left' | 'Bottom-right' | 'None' | 'Top-left' | 'Top-right' 13 | } 14 | 15 | export function Avatar(component: DesignComponent, defaults: Partial = {}) { 16 | const { variant, size, iconName, chipPosition } = cleanPropNames(component.properties) 17 | 18 | const altText = findChild(component, { 19 | type: 'TEXT', 20 | })?.characters 21 | 22 | const Avatar = h( 23 | 'UAvatar', 24 | { 25 | icon: variant === 'Icon' ? getIconName(iconName.name) : undefined, 26 | alt: variant === 'Alt' ? altText : undefined, 27 | ...(variant === 'Image' ? getRandomAvatar() : {}), 28 | size, 29 | }, 30 | { 31 | size: 'md', 32 | ...defaults, 33 | }, 34 | ) 35 | 36 | if (chipPosition === 'None') { 37 | return Avatar 38 | } 39 | 40 | const position = toLowerCase(chipPosition) 41 | 42 | return h( 43 | 'UChip', 44 | { 45 | inset: true, 46 | position, 47 | }, 48 | { 49 | position: 'top-right', 50 | }, 51 | [Avatar], 52 | ) 53 | } 54 | 55 | const USERNAMES = [ 56 | 'benjamincanac', 57 | 'romhml', 58 | 'smarroufin', 59 | 'atinux', 60 | 'Haythamasalama', 61 | 'hywax', 62 | 'danielroe', 63 | 'sandros94', 64 | 'malik-jouda', 65 | 'connerblanton', 66 | 'antfu', 67 | 'Justineo', 68 | ] as const 69 | 70 | export type AvatarItem = Pick 71 | 72 | export function getAvatar(username: (typeof USERNAMES)[number]): Required { 73 | return { 74 | src: `https://github.com/${username}.png`, 75 | alt: `@${username}`, 76 | } 77 | } 78 | 79 | export function getRandomAvatar(): Required { 80 | return getAvatar(USERNAMES[Math.floor(Math.random() * USERNAMES.length)]!) 81 | } 82 | 83 | export function renderAvatarItem( 84 | avatar: DesignComponent, 85 | defaults: Partial = {}, 86 | ): Partial { 87 | const { props, children } = Avatar(avatar, defaults) 88 | 89 | const child = children[0] 90 | if (child && typeof child !== 'string' && child.name === 'UAvatar') { 91 | return { 92 | ...child.props, 93 | chip: props, 94 | } 95 | } 96 | 97 | return props as AvatarProps 98 | } 99 | -------------------------------------------------------------------------------- /src/pro-components/page-section.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from '../components/button' 3 | import type { IconProperties } from '../components/icon' 4 | import type { PageFeatureProperties } from './page-feature' 5 | import { queryAll } from '@tempad-dev/plugins' 6 | import { BUTTON_NAMES, renderButtonItem } from '../components/button' 7 | import { getIconName } from '../components/icon' 8 | import { getLinkTo } from '../components/link' 9 | import { cleanPropNames, h, LOREM_IPSUM_TEXT, toLowerCase } from '../utils' 10 | import { renderPageFeatureItem } from './page-feature' 11 | 12 | export type PageSectionProperties = { 13 | '👁️ Icon': boolean 14 | '👁️ Links': boolean 15 | '👁️ Features': boolean 16 | '👁️ Headline': boolean 17 | '𝐓 Title': string 18 | '↳ Headline': string 19 | '↳ IconName': DesignComponent 20 | '👁️ Slot': boolean 21 | '↳ Slot': DesignComponent 22 | '𝐓 Description': string 23 | '🖥️ Device': 'Desktop' | 'Mobile' 24 | '⇅ Orientation': 'Horizontal' | 'Vertical' 25 | '👁️ Reverse': 'False' | 'True' 26 | } 27 | 28 | export function PageSection(component: DesignComponent) { 29 | const { 30 | title, 31 | description, 32 | icon: showIcon, 33 | iconName, 34 | links: showLinks, 35 | features: showFeatures, 36 | showHeadline, 37 | headline, 38 | showSlot, 39 | orientation, 40 | reverse, 41 | } = cleanPropNames(component.properties, { 42 | '👁️ Headline': 'showHeadline', 43 | '👁️ Slot': 'showSlot', 44 | }) 45 | 46 | const links = showLinks 47 | ? queryAll>(component, [ 48 | { query: 'one', type: 'FRAME', name: 'Links' }, 49 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 50 | ]) 51 | .map((button) => renderButtonItem(button, { size: 'lg' })) 52 | .map((link) => ({ 53 | ...link, 54 | to: getLinkTo(link.label || ''), 55 | })) 56 | : undefined 57 | 58 | const features = showFeatures 59 | ? queryAll>(component, [ 60 | { query: 'one', type: 'FRAME', name: 'Features' }, 61 | { query: 'all', type: 'INSTANCE', name: 'PageFeature' }, 62 | ]).map((item) => renderPageFeatureItem(item)) 63 | : undefined 64 | 65 | return h( 66 | 'UPageSection', 67 | { 68 | icon: showIcon ? getIconName(iconName.name) : undefined, 69 | headline: (showHeadline && headline) || undefined, 70 | title, 71 | description, 72 | links, 73 | features, 74 | orientation: toLowerCase(orientation), 75 | reverse: reverse === 'True', 76 | }, 77 | { 78 | orientation: 'vertical', 79 | reverse: false, 80 | }, 81 | showSlot ? [LOREM_IPSUM_TEXT] : [], 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /src/components/input.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 2 | import type { InputProps } from '../types' 3 | import type { IconProperties } from './icon' 4 | import { cleanPropNames, h, renderSlot, toLowerCase } from '../utils' 5 | import { getRandomAvatar } from './avatar' 6 | import { getIconName } from './icon' 7 | 8 | const INPUT_VARIANT_MAP: Record = { 9 | InputOutline: 'outline', 10 | InputSoft: 'soft', 11 | InputNone: 'none', 12 | InputGhost: 'ghost', 13 | InputSubtle: 'subtle', 14 | } 15 | 16 | type InputName = keyof typeof INPUT_VARIANT_MAP 17 | 18 | export const INPUT_NAMES = Object.keys(INPUT_VARIANT_MAP) as InputName[] 19 | 20 | export type InputProperties = { 21 | '𝐓 Span': string 22 | '🙂 IconTrailingName': DesignComponent 23 | '🙂 IconLeadingName': DesignComponent 24 | '👁️ Completed': boolean 25 | '↳ CompletedLabel': string 26 | '👁️ Placeholder': boolean 27 | '↳ PlaceholderLabel': string 28 | '🎨 Color': 'Neutral' | 'Primary' | 'Error' 29 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 30 | '🚦 State': 'Default' | 'Disabled' | 'Focus' 31 | '◆ LeadingSlot': 'Icon' | 'Span' | 'Avatar' | 'None' 32 | '◆ TrailingSlot': 'Icon' | 'None' | 'Span' 33 | } 34 | 35 | export function Input(component: DesignComponent, defaults: Partial = {}) { 36 | const variant = INPUT_VARIANT_MAP[component.name] 37 | 38 | const { 39 | color, 40 | size, 41 | state, 42 | leadingSlot, 43 | trailingSlot, 44 | placeholder, 45 | placeholderLabel, 46 | completed, 47 | completedLabel, 48 | iconLeadingName, 49 | iconTrailingName, 50 | span, 51 | } = cleanPropNames(component.properties) 52 | 53 | const icon = leadingSlot === 'Icon' ? getIconName(iconLeadingName.name) : undefined 54 | const trailingIcon = trailingSlot === 'Icon' ? getIconName(iconTrailingName.name) : undefined 55 | 56 | const avatar = leadingSlot === 'Avatar' ? getRandomAvatar() : undefined 57 | 58 | const children: DevComponent['children'] = [] 59 | 60 | if (leadingSlot === 'Span' && span) { 61 | children.push(renderSlot('leading', [span])) 62 | } else if (trailingSlot === 'Span' && span) { 63 | children.push(renderSlot('trailing', [span])) 64 | } 65 | 66 | return h( 67 | 'UInput', 68 | { 69 | type: completed && completedLabel && /^\*+$/.test(completedLabel) ? 'password' : 'text', 70 | placeholder: placeholder ? placeholderLabel : undefined, 71 | color: toLowerCase(color), 72 | variant, 73 | size, 74 | icon, 75 | trailingIcon, 76 | avatar, 77 | disabled: state === 'Disabled', 78 | }, 79 | { 80 | type: 'text', 81 | color: 'primary', 82 | variant: 'outline', 83 | size: 'md', 84 | disabled: false, 85 | ...defaults, 86 | }, 87 | children, 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /src/components/input-menu.ts: -------------------------------------------------------------------------------- 1 | import type { InputMenuItem } from '@nuxt/ui' 2 | import type { DesignComponent, DevComponent, FrameNode, TextNode } from '@tempad-dev/plugins' 3 | import type { IconProperties } from './icon' 4 | import type { InputProperties } from './input' 5 | import { findChild, findChildren } from '@tempad-dev/plugins' 6 | import { cleanPropNames, h, pickOverrides } from '../utils' 7 | import { getRandomAvatar } from './avatar' 8 | import { getIconName } from './icon' 9 | import { Input, INPUT_NAMES } from './input' 10 | 11 | export type InputMenuItemProperties = { 12 | '🙂 IconName': DesignComponent 13 | '𝐓 Label': string 14 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 15 | '🚦 State': 'Default' | 'Selected' | 'Hover' | 'Disabled' 16 | '◆ LeadingSlot': 'Avatar' | 'Dot' | 'Icon' | 'None' 17 | } 18 | 19 | export function renderInputMenuItem(item: DesignComponent): InputMenuItem { 20 | const { state, leadingSlot, label, iconName } = cleanPropNames(item.properties) 21 | 22 | return pickOverrides( 23 | { 24 | label, 25 | icon: leadingSlot === 'Icon' ? getIconName(iconName.name) : undefined, 26 | avatar: leadingSlot === 'Avatar' ? getRandomAvatar() : undefined, 27 | chip: leadingSlot === 'Dot' ? { color: 'primary' } : undefined, 28 | disabled: state === 'Disabled', 29 | selected: state === 'Selected', 30 | }, 31 | { 32 | disabled: false, 33 | selected: false, 34 | }, 35 | ) 36 | } 37 | 38 | export type InputMenuProperties = { 39 | '👁️ isOpen': boolean 40 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 41 | } 42 | 43 | export function InputMenu(component: DesignComponent) { 44 | const { size } = cleanPropNames(component.properties) 45 | 46 | const container = findChild(component, { 47 | type: 'FRAME', 48 | name: 'InputMenu', 49 | }) 50 | 51 | const items: InputMenuItem[] = container 52 | ? findChildren>( 53 | container, 54 | (node) => 55 | (node.type === 'TEXT' && node.name === 'Title') || 56 | (node.type === 'INSTANCE' && node.name === 'InputMenuItem' && node.visible === true), 57 | ).map((item) => { 58 | if (item.type === 'TEXT') { 59 | return { 60 | label: item.characters, 61 | type: 'label', 62 | } 63 | } 64 | 65 | return renderInputMenuItem(item) 66 | }) 67 | : [] 68 | 69 | const children: DevComponent['children'] = [] 70 | 71 | const input = findChild>(component, { 72 | type: 'INSTANCE', 73 | name: INPUT_NAMES, 74 | }) 75 | 76 | const inputProps = input ? Input(input).props : {} 77 | 78 | return h( 79 | 'UInputMenu', 80 | { 81 | size, 82 | items, 83 | ...inputProps, 84 | }, 85 | { 86 | size: 'md', 87 | }, 88 | children, 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /src/components/tabs.ts: -------------------------------------------------------------------------------- 1 | import type { TabsItem } from '@nuxt/ui' 2 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 3 | import type { BadgeProperties } from './badge' 4 | import type { IconProperties } from './icon' 5 | import { findChild, findChildren } from '@tempad-dev/plugins' 6 | import { cleanPropNames, h, pickOverrides, renderSlot, toKebabCase, toLowerCase } from '../utils' 7 | import { getRandomAvatar } from './avatar' 8 | import { Badge } from './badge' 9 | import { getIconName } from './icon' 10 | 11 | export type TabsItemProperties = { 12 | '↳ IconName': DesignComponent 13 | '👁️ Icon': boolean 14 | '👁️ Avatar': boolean 15 | '𝐓 Label': string 16 | '👁️ Badge': boolean 17 | '🎨 Color': 'Neutral' | 'Primary' 18 | '◆ Variant': 'Pill' | 'Link (Horizontal)' | 'Link (Vertical)' 19 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 20 | '🚦 State': 'Default' | 'Hover' | 'Selected' | 'Focus' | 'Disabled' 21 | '◆ LeadingSlot': 'Avatar' | 'Icon' 22 | } 23 | 24 | export function renderTabsItem(item: DesignComponent): TabsItem { 25 | const { state, leadingSlot, avatar, icon, iconName, label } = cleanPropNames(item.properties) 26 | 27 | return pickOverrides( 28 | { 29 | label, 30 | icon: leadingSlot === 'Icon' && icon ? getIconName(iconName.name) : undefined, 31 | avatar: leadingSlot === 'Avatar' && avatar ? getRandomAvatar() : undefined, 32 | disabled: state === 'Disabled', 33 | }, 34 | { 35 | disabled: false, 36 | }, 37 | ) 38 | } 39 | 40 | export type TabsProperties = { 41 | '🎨 Color': 'Neutral' | 'Primary' 42 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 43 | '◆ Variant': 'Pill' | 'Link' 44 | '⇅ Align': 'Horizontal' | 'Vertical' 45 | } 46 | 47 | export function Tabs(component: DesignComponent) { 48 | const { color, size, variant, align } = cleanPropNames(component.properties) 49 | 50 | const children: DevComponent['children'] = [] 51 | const items: TabsItem[] = [] 52 | 53 | findChildren>(component, { 54 | type: 'INSTANCE', 55 | name: 'Tab', 56 | }).forEach((tab) => { 57 | const item = renderTabsItem(tab) 58 | 59 | const badge = findChild>(tab, { 60 | type: 'INSTANCE', 61 | name: 'Badge', 62 | }) 63 | const badgeComponent = badge ? Badge(badge) : undefined 64 | 65 | if (badgeComponent) { 66 | const slug = toKebabCase(item.label || '') 67 | item.slot = slug 68 | children.push(renderSlot(slug, [badgeComponent])) 69 | } 70 | 71 | items.push(item) 72 | }) 73 | 74 | return h( 75 | 'UTabs', 76 | { 77 | items, 78 | color: toLowerCase(color), 79 | variant: toLowerCase(variant), 80 | size, 81 | orientation: toLowerCase(align), 82 | }, 83 | { 84 | color: 'primary', 85 | variant: 'pill', 86 | size: 'md', 87 | orientation: 'horizontal', 88 | }, 89 | children, 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import type { TransformOptions } from '@tempad-dev/plugins' 2 | import type { RenderFn } from '../types' 3 | import { createTransformComponent, mapComponentNames } from '../utils' 4 | import { Accordion } from './accordion' 5 | import { Alert } from './alert' 6 | import { Avatar } from './avatar' 7 | import { AvatarGroup } from './avatar-group' 8 | import { Badge } from './badge' 9 | import { Breadcrumb } from './breadcrumb' 10 | import { Button, BUTTON_NAMES } from './button' 11 | import { ButtonGroup } from './button-group' 12 | import { Calendar } from './calendar' 13 | import { Card } from './card' 14 | import { Carousel } from './carousel' 15 | import { Checkbox } from './checkbox' 16 | import { Chip } from './chip' 17 | import { Collapsible } from './collapsible' 18 | import { ColorPicker } from './color-picker' 19 | import { CommandPalette } from './command-palette' 20 | import { Drawer } from './drawer' 21 | import { DropdownMenu } from './dropdown-menu' 22 | import { FormField } from './form-field' 23 | import { Icon } from './icon' 24 | import { Input, INPUT_NAMES } from './input' 25 | import { InputMenu } from './input-menu' 26 | import { InputNumber } from './input-number' 27 | import { Kbd } from './kbd' 28 | import { Link } from './link' 29 | import { Modal } from './modal' 30 | import { NavigationMenu } from './navigation-menu' 31 | import { Pagination } from './pagination' 32 | import { PinInput } from './pin-input' 33 | import { Popover } from './popover' 34 | import { Progress } from './progress' 35 | import { RadioGroup } from './radio-group' 36 | import { Select, SELECT_NAMES } from './select' 37 | import { SelectMenu } from './select-menu' 38 | import { Separator } from './separator' 39 | import { Skeleton } from './skeleton' 40 | import { Slideover } from './slideover' 41 | import { Slider } from './slider' 42 | import { Stepper } from './stepper' 43 | import { Switch } from './switch' 44 | import { Tabs } from './tabs' 45 | import { Textarea } from './textarea' 46 | import { Toast } from './toast' 47 | import { Tooltip } from './tooltip' 48 | 49 | export const COMPONENT_MAP: Record = { 50 | Accordion, 51 | Alert, 52 | Avatar, 53 | AvatarGroup, 54 | Badge, 55 | Breadcrumb, 56 | ...mapComponentNames(BUTTON_NAMES, Button), 57 | ButtonGroup, 58 | Calendar, 59 | Card, 60 | Carousel, 61 | Checkbox, 62 | Chip, 63 | Collapsible, 64 | ColorPicker, 65 | CommandPalette, 66 | Drawer, 67 | DropdownMenu, 68 | FormField, 69 | Icon, 70 | ...mapComponentNames(INPUT_NAMES, Input), 71 | InputMenu, 72 | InputNumber, 73 | Kbd, 74 | Link, 75 | Modal, 76 | NavigationMenu, 77 | Pagination, 78 | PinInput, 79 | Popover, 80 | Progress, 81 | RadioGroup, 82 | ...mapComponentNames(SELECT_NAMES, Select), 83 | SelectMenu, 84 | Separator, 85 | Skeleton, 86 | Slideover, 87 | Slider, 88 | Stepper, 89 | Switch, 90 | Tabs, 91 | TextArea: Textarea, 92 | Toast, 93 | Tooltip, 94 | } 95 | 96 | export const transformComponent: TransformOptions['transformComponent'] = createTransformComponent(COMPONENT_MAP) 97 | -------------------------------------------------------------------------------- /src/components/calendar.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { CalendarProps } from '../types' 3 | import { findAll } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, toLowerCase } from '../utils' 5 | 6 | export type CalendarItemProperties = { 7 | '𝐓 DateValue': string 8 | '👁️ Chip': boolean 9 | '🎨 Color': 'Primary' | 'Neutral' | 'Secondary' 10 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 11 | '◆ Variant': 'Data' | 'Data-disabled' | 'Data-unavailable' | 'Data-today' | 'Data-selected' 12 | '🚦 State': 'Default' | 'Highlighted' 13 | } 14 | 15 | export type CalendarItem = Pick & { 16 | selected?: boolean 17 | date: number 18 | } 19 | 20 | export function renderCalendarItem(item: DesignComponent): CalendarItem { 21 | const { color, variant, dateValue } = cleanPropNames(item.properties) 22 | 23 | const date = Number(dateValue) 24 | 25 | return { 26 | color: toLowerCase(color), 27 | selected: variant === 'Data-selected', 28 | date: Number.isNaN(date) ? 0 : date, 29 | } 30 | } 31 | 32 | export type CalendarProperties = { 33 | '👁️ yearControls': boolean 34 | '👁️ monthControls': boolean 35 | '🎨 Color': 'Primary' | 'Neutral' | 'Secondary' 36 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 37 | '◆ NumberOfMonths': '1' | '2' | '3' 38 | } 39 | 40 | export function Calendar(component: DesignComponent) { 41 | const { color, size, numberOfMonths, monthControls, yearControls } = cleanPropNames(component.properties) 42 | 43 | const months = Number(numberOfMonths) 44 | 45 | const items = findAll>(component, { 46 | type: 'INSTANCE', 47 | name: 'calendar-item', 48 | }).map((child) => renderCalendarItem(child)) 49 | 50 | const { range, multiple } = checkSelection(items) 51 | 52 | return h( 53 | 'UCalendar', 54 | { 55 | color: toLowerCase(color), 56 | size, 57 | range, 58 | multiple, 59 | numberOfMonths: Number.isNaN(months) ? 1 : months, 60 | monthControls, 61 | yearControls, 62 | }, 63 | { 64 | color: 'primary', 65 | size: 'md', 66 | range: false, 67 | multiple: false, 68 | numberOfMonths: 1, 69 | monthControls: true, 70 | yearControls: true, 71 | }, 72 | ) 73 | } 74 | 75 | function checkSelection(items: CalendarItem[]): { 76 | range: boolean 77 | multiple: boolean 78 | } { 79 | let lastSelectedIndex = -1 80 | 81 | for (let i = 0; i < items.length; i++) { 82 | if (items[i]!.selected) { 83 | // Check for non-consecutive selections 84 | if (lastSelectedIndex !== -1 && i - lastSelectedIndex > 1) { 85 | return { range: false, multiple: true } 86 | } 87 | 88 | // Check for consecutive selections 89 | if (i > 0 && items[i - 1]!.selected) { 90 | return { range: true, multiple: false } 91 | } 92 | 93 | lastSelectedIndex = i // Update the index of the last selected item 94 | } 95 | } 96 | 97 | return { range: false, multiple: false } 98 | } 99 | -------------------------------------------------------------------------------- /src/components/badge.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { BadgeProps } from '../types' 3 | import type { IconProperties } from './icon' 4 | import { cleanPropNames, h, toLowerCase } from '../utils' 5 | import { getIconName } from './icon' 6 | 7 | export type BadgeProperties = { 8 | '𝐓 Label': string 9 | '↳ IconTrailingName': DesignComponent 10 | '👁️ IconTrailing': boolean 11 | '↳ IconLeadingName': DesignComponent 12 | '👁️ IconLeading': boolean 13 | '🎨 Color': 'Neutral' | 'Primary' | 'Secondary' | 'Success' | 'Info' | 'Warning' | 'Error' 14 | '◆ Variant': 'Outline' | 'Soft' | 'Solid' | 'Subtle' 15 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 16 | '👁️ RoundedFull': 'False' | 'True' 17 | } 18 | 19 | export function Badge(component: DesignComponent, defaults: Partial = {}) { 20 | const { color, variant, size, roundedFull, label, iconLeading, iconLeadingName, iconTrailing, iconTrailingName } = 21 | cleanPropNames(component.properties) 22 | 23 | const iconProps: Partial = 24 | iconLeading && iconTrailing 25 | ? { 26 | leadingIcon: getIconName(iconLeadingName.name), 27 | trailingIcon: getIconName(iconTrailingName.name), 28 | } 29 | : { 30 | icon: iconLeading 31 | ? getIconName(iconLeadingName.name) 32 | : iconTrailing 33 | ? getIconName(iconTrailingName.name) 34 | : undefined, 35 | trailing: iconTrailing, 36 | } 37 | 38 | return h( 39 | 'UBadge', 40 | { 41 | class: roundedFull === 'True' ? 'rounded-full' : undefined, 42 | color: toLowerCase(color), 43 | variant: toLowerCase(variant), 44 | size, 45 | ...iconProps, 46 | }, 47 | { 48 | color: 'primary', 49 | variant: 'solid', 50 | size: 'md', 51 | trailing: false, 52 | ...defaults, 53 | }, 54 | label ? [label] : [], 55 | ) 56 | } 57 | 58 | export function renderBadgeItem( 59 | badge: DesignComponent, 60 | defaults: Partial, 61 | parseNumber: true, 62 | ): Partial | string | number | undefined 63 | export function renderBadgeItem( 64 | badge: DesignComponent, 65 | defaults: Partial, 66 | parseNumber?: false, 67 | ): Partial | string | undefined 68 | export function renderBadgeItem( 69 | badge: DesignComponent, 70 | defaults: Partial = {}, 71 | parseNumber = false, 72 | ): Partial | string | number | undefined { 73 | const { props, children } = Badge(badge, defaults) 74 | 75 | const label = 76 | children 77 | .map((child) => (typeof child === 'string' ? child : undefined)) 78 | .filter(Boolean) 79 | .join('') || undefined 80 | 81 | if (Object.keys(props).length === 0) { 82 | if (parseNumber && String(Number(label)) === label) { 83 | return Number(label) 84 | } 85 | return label 86 | } 87 | 88 | return { 89 | label, 90 | ...props, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/pagination.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent } from '@tempad-dev/plugins' 2 | import type { ButtonProps } from '../types' 3 | import type { ButtonProperties } from './button' 4 | import { findChildren } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h, isInteger } from '../utils' 6 | import { BUTTON_NAMES, renderButtonItem } from './button' 7 | import { ui } from './config' 8 | 9 | export type PaginationProperties = { 10 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 11 | } 12 | 13 | function parseKey(key?: string): [ButtonProps['color'], ButtonProps['variant']] { 14 | if (!key) { 15 | return [undefined, undefined] 16 | } 17 | const [colorStr, variantStr] = key.split('-') 18 | return [ 19 | colorStr === 'undefined' ? undefined : (colorStr as ButtonProps['color']), 20 | variantStr === 'undefined' ? undefined : (variantStr as ButtonProps['variant']), 21 | ] 22 | } 23 | 24 | export function Pagination(component: DesignComponent) { 25 | const { size } = cleanPropNames(component.properties) 26 | 27 | const items = findChildren>(component, { 28 | type: 'INSTANCE', 29 | name: BUTTON_NAMES, 30 | }).map((button) => renderButtonItem(button)) 31 | 32 | let controls: Partial[] = [] 33 | if (items.length >= 5) { 34 | controls = [...items] 35 | controls.splice(2, items.length - 4) 36 | controls = controls.every((control) => !isInteger(control.label || '')) ? controls : [] 37 | } 38 | 39 | const showControls = controls.length === 4 40 | const [first, prev, next, last] = controls 41 | 42 | const pages = showControls ? items.splice(2, items.length - 4) : items 43 | 44 | const ellipsis = pages.find(({ icon, label }) => icon && !/^\d+$/.test(label || '')) 45 | const pageGroups = pages.reduce( 46 | (acc, page) => { 47 | const key = `${page.color}-${page.variant}` 48 | acc[key] = (acc[key] || 0) + 1 49 | return acc 50 | }, 51 | {} as Record, 52 | ) 53 | const styles = Object.entries(pageGroups).sort(([, a], [, b]) => b - a) 54 | const [regularKey, activeKey] = styles.map(([key]) => key) 55 | 56 | const [color, variant] = parseKey(regularKey) 57 | const [activeColor, activeVariant] = parseKey(activeKey) 58 | 59 | return h( 60 | 'UPagination', 61 | { 62 | color, 63 | variant, 64 | activeColor, 65 | activeVariant, 66 | size, 67 | showControls, 68 | disabled: pages.every((page) => page.disabled), 69 | firstIcon: first?.icon, 70 | prevIcon: prev?.icon, 71 | nextIcon: next?.icon, 72 | lastIcon: last?.icon, 73 | ellipsisIcon: ellipsis?.icon, 74 | }, 75 | { 76 | color: 'neutral', 77 | variant: 'outline', 78 | activeColor: 'primary', 79 | activeVariant: 'solid', 80 | size: 'md', 81 | showControls: true, 82 | disabled: false, 83 | firstIcon: ui.icons.chevronDoubleLeft, 84 | prevIcon: ui.icons.chevronLeft, 85 | nextIcon: ui.icons.chevronRight, 86 | lastIcon: ui.icons.chevronDoubleRight, 87 | ellipsisIcon: ui.icons.ellipsis, 88 | }, 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /src/components/stepper.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, FrameNode, TextNode } from '@tempad-dev/plugins' 2 | import type { IconProperties } from './icon' 3 | import { findChild, findChildren } from '@tempad-dev/plugins' 4 | import { cleanPropNames, h, pickOverrides, toLowerCase } from '../utils' 5 | import { getIconName } from './icon' 6 | 7 | export type StepperItemProperties = { 8 | '𝐓 Span': string 9 | '🙂 IconName': DesignComponent 10 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 11 | '🎨 Color': 'Primary' | 'Neutral' | 'Secondary' | 'Success' | 'Info' | 'Warning' | 'Error' 12 | '◆ Variant': 'Icon' | 'Span' 13 | '🚦State': 'Active' | 'Active + focus' | 'Default' | 'Default + focus' | 'Disabled' 14 | } 15 | 16 | export function getStepperItem(component: DesignComponent) { 17 | const { properties } = component 18 | 19 | const { variant, state, iconName } = cleanPropNames(properties, { 20 | '🚦State': 'state', 21 | }) 22 | 23 | return pickOverrides( 24 | { 25 | icon: variant === 'Icon' ? getIconName(iconName.name) : undefined, 26 | disabled: state === 'Disabled', 27 | }, 28 | { 29 | disabled: false, 30 | }, 31 | ) 32 | } 33 | 34 | export type StepperProperties = { 35 | '↳ Description3': string 36 | '↳ Description2': string 37 | '𝐓 Title3': string 38 | '👁️ Description': boolean 39 | '↳ Description1': string 40 | '𝐓 Title2': string 41 | '𝐓 Title1': string 42 | '🎨 Color': 'Primary' | 'Neutral' | 'Secondary' | 'Success' | 'Info' | 'Warning' | 'Error' 43 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 44 | '◆ Step': '1' | '2' | '3' 45 | '⇅ Orientation': 'Horizontal' | 'Vertical' 46 | } 47 | 48 | export function Stepper(component: DesignComponent) { 49 | const { color, size, step, orientation } = cleanPropNames(component.properties) 50 | 51 | const stepContainers = findChildren(component, { 52 | type: 'FRAME', 53 | name: /^Step/, 54 | }) 55 | 56 | const items = stepContainers 57 | .map((stepContainer) => { 58 | const step = findChild>(stepContainer, { 59 | type: 'INSTANCE', 60 | name: 'Stepper_Item', 61 | }) 62 | const titleAndDesc = findChild(stepContainer, { 63 | type: 'FRAME', 64 | name: 'Title + description', 65 | }) 66 | const [title, description] = titleAndDesc 67 | ? findChildren(titleAndDesc, { 68 | type: 'TEXT', 69 | }) 70 | : [] 71 | 72 | const stepItem = step 73 | ? { 74 | title: title?.characters, 75 | description: description?.characters, 76 | ...getStepperItem(step), 77 | } 78 | : undefined 79 | 80 | return stepItem 81 | }) 82 | .filter((step) => step != null) 83 | 84 | return h( 85 | 'UStepper', 86 | { 87 | modelValue: Number(step) - 1, 88 | items, 89 | color: toLowerCase(color), 90 | size, 91 | orientation: toLowerCase(orientation), 92 | }, 93 | { 94 | color: 'primary', 95 | size: 'md', 96 | orientation: 'horizontal', 97 | }, 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /src/components/button.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, TextNode } from '@tempad-dev/plugins' 2 | import type { ButtonProps } from '../types' 3 | import type { IconProperties } from './icon' 4 | import { findOne } from '@tempad-dev/plugins' 5 | import { cleanPropNames, h, pickOverrides, toLowerCase } from '../utils' 6 | import { getRandomAvatar } from './avatar' 7 | import { getIconName } from './icon' 8 | 9 | const BUTTON_COLOR_MAP: Record = { 10 | ButtonPrimary: 'primary', 11 | ButtonSecondary: 'secondary', 12 | ButtonSuccess: 'success', 13 | ButtonInfo: 'info', 14 | ButtonWarning: 'warning', 15 | ButtonError: 'error', 16 | ButtonNeutral: 'neutral', 17 | } 18 | 19 | type ButtonName = keyof typeof BUTTON_COLOR_MAP 20 | 21 | export const BUTTON_NAMES = Object.keys(BUTTON_COLOR_MAP) as ButtonName[] 22 | 23 | export type ButtonProperties = { 24 | '👁️ Label': boolean 25 | '👁️ AvatarTrailing': boolean 26 | '↳ IconTrailingName': DesignComponent 27 | '👁️ IconTrailing': boolean 28 | '👁️ AvatarLeading': boolean 29 | '👁️ IconLeading': boolean 30 | '↳ IconLeadingName': DesignComponent 31 | '𝐓 LabelSlot': string 32 | '◆ Variant': 'Solid' | 'Outline' | 'Soft' | 'Subtle' | 'Ghost' | 'Link' 33 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 34 | '🚦 State': 'Default' | 'Disabled' | 'Focus' | 'Hover' 35 | '◆ Slot': 'Icon' | 'Avatar' 36 | '👁️ Square': 'False' | 'True' 37 | } 38 | 39 | export function Button(component: DesignComponent, defaults: Partial = {}) { 40 | const { 41 | variant, 42 | size, 43 | state, 44 | square, 45 | slot, 46 | iconLeading, 47 | iconLeadingName, 48 | iconTrailing, 49 | iconTrailingName, 50 | avatarLeading, 51 | } = cleanPropNames(component.properties) 52 | 53 | const color = BUTTON_COLOR_MAP[component.name] 54 | 55 | const icon = slot === 'Icon' && iconLeading ? getIconName(iconLeadingName.name) : undefined 56 | const trailingIcon = iconTrailing ? getIconName(iconTrailingName.name) : undefined 57 | const avatar = slot === 'Avatar' && avatarLeading ? getRandomAvatar() : undefined 58 | 59 | const label = findOne(component, { 60 | type: 'TEXT', 61 | })?.characters 62 | 63 | return h( 64 | 'UButton', 65 | { 66 | variant: toLowerCase(variant), 67 | color, 68 | size, 69 | square: square === 'True', 70 | icon, 71 | trailingIcon, 72 | avatar, 73 | disabled: state === 'Disabled', 74 | }, 75 | { 76 | color: 'primary', 77 | variant: 'solid', 78 | size: 'md', 79 | square: false, 80 | disabled: false, 81 | ...defaults, 82 | }, 83 | label ? [label] : [], 84 | ) 85 | } 86 | 87 | export function renderButtonItem( 88 | button: DesignComponent, 89 | defaults: Partial = {}, 90 | ): Partial { 91 | const { props, children } = Button(button) 92 | const label = children 93 | .map((child) => (typeof child === 'string' ? child : undefined)) 94 | .filter(Boolean) 95 | .join('') 96 | 97 | return pickOverrides( 98 | { 99 | ...(label ? { label } : {}), 100 | ...props, 101 | }, 102 | defaults, 103 | ) 104 | } 105 | 106 | export function renderButtonChild(button: DesignComponent) { 107 | const { 108 | props: { size, ...rest }, 109 | children, 110 | } = Button(button) 111 | 112 | return h('UButton', rest, {}, children) 113 | } 114 | -------------------------------------------------------------------------------- /src/components/breadcrumb.ts: -------------------------------------------------------------------------------- 1 | import type { BreadcrumbItem } from '@nuxt/ui' 2 | import type { DesignComponent, DevComponent, FrameNode, TextNode } from '@tempad-dev/plugins' 3 | import type { DropdownMenuProperties } from './dropdown-menu' 4 | import type { IconProperties } from './icon' 5 | import { findChild, findChildren, queryOne } from '@tempad-dev/plugins' 6 | import { cleanPropNames, h, isIcon, renderSlot } from '../utils' 7 | import { ui } from './config' 8 | import { DropdownMenu } from './dropdown-menu' 9 | import { getIconName } from './icon' 10 | 11 | export type BreadcrumbProperties = { 12 | '🙂 IconName3': DesignComponent 13 | '🙂 IconName2': DesignComponent 14 | '🙂 IconName1': DesignComponent 15 | '𝐓 SeparatorSlot': string 16 | '🙂 SeparatorIconName': DesignComponent 17 | '◆ LeadingSlot': 'Icon' | 'None' | 'Span' 18 | '◆ Divider': 'Icon' | 'Span' 19 | '👁️ Background': 'False' | 'True' 20 | '👁️ DropdownMenu': 'True' | 'False' 21 | } 22 | 23 | export function Breadcrumb(component: DesignComponent) { 24 | const { leadingSlot, divider, separatorIconName, separatorSlot } = cleanPropNames(component.properties) 25 | 26 | const itemNodes = findChildren>(component, { 27 | name: /^Link|^DropdownMenu/, 28 | }) 29 | 30 | const items: BreadcrumbItem[] = [] 31 | const children: DevComponent['children'] = [] 32 | 33 | itemNodes.forEach((node) => { 34 | const { type, name } = node 35 | if (type === 'FRAME' && name.startsWith('Link')) { 36 | const label = 37 | queryOne(node, [ 38 | { 39 | query: 'child', 40 | type: 'INSTANCE', 41 | name: 'Link', 42 | }, 43 | { 44 | query: 'child', 45 | type: 'TEXT', 46 | name: 'Label', 47 | }, 48 | ])?.characters || undefined 49 | 50 | const icon = leadingSlot === 'Icon' && isIcon(node.children[0]!) ? getIconName(node.children[0]!.name) : undefined 51 | 52 | items.push({ 53 | label, 54 | icon, 55 | }) 56 | } else if (type === 'INSTANCE' && name === 'Link') { 57 | items.push({ 58 | label: 59 | findChild(node, { 60 | type: 'TEXT', 61 | name: 'Label', 62 | })?.characters || undefined, 63 | }) 64 | } else if (type === 'INSTANCE' && name === 'DropdownMenu') { 65 | const menu = DropdownMenu(node, { 66 | button: { 67 | icon: undefined, 68 | ':icon': 'item.icon', 69 | }, 70 | }) 71 | 72 | if (!menu) { 73 | items.push({ 74 | icon: ui.icons.ellipsis, 75 | }) 76 | 77 | return 78 | } 79 | 80 | const { items: menuItems, ...props } = menu.props 81 | menu.props = props 82 | menu.props[':items'] = 'item.children' 83 | 84 | items.push({ 85 | icon: ui.icons.ellipsis, 86 | slot: 'dropdown', 87 | // @ts-expect-error Extra field for slot 88 | children: menuItems, 89 | }) 90 | 91 | children.push(renderSlot('dropdown', '{ item }', [menu])) 92 | } 93 | }) 94 | 95 | if (divider === 'Span' && separatorSlot) { 96 | children.push(renderSlot('separator', [separatorSlot])) 97 | } 98 | 99 | return h( 100 | 'UBreadcrumb', 101 | { 102 | items, 103 | separatorIcon: divider === 'Icon' ? getIconName(separatorIconName.name) : undefined, 104 | }, 105 | { 106 | separatorIcon: ui.icons.chevronRight, 107 | }, 108 | children, 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /src/pro-components/header.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, FrameNode } from '@tempad-dev/plugins' 2 | import type { ButtonProperties } from '../components/button' 3 | import type { DrawerProperties } from '../components/drawer' 4 | import type { ModalProperties } from '../components/modal' 5 | import type { NavigationMenuProperties } from '../components/navigation-menu' 6 | import type { SlideoverProperties } from '../components/slideover' 7 | import type { HeaderProps } from '../types' 8 | import { h as createElement, findChild, findOne, queryAll } from '@tempad-dev/plugins' 9 | import { Button, BUTTON_NAMES } from '../components/button' 10 | import { Drawer } from '../components/drawer' 11 | import { Modal } from '../components/modal' 12 | import { NavigationMenu } from '../components/navigation-menu' 13 | import { Slideover } from '../components/slideover' 14 | import { cleanPropNames, h, renderSlot, toLowerCase } from '../utils' 15 | 16 | export type HeaderProperties = { 17 | '👁️ RightSlot': boolean 18 | '👁️ Default': boolean 19 | '↳ Title': string 20 | '👁️ LeftSlot': boolean 21 | '◆ Mode': 'Desktop' | 'Drawer' | 'Slideover' | 'Modal' | 'Mobile' 22 | '◆ Variant': 'Title' | 'Logo' 23 | } 24 | 25 | function isSupportedMode(mode: string): mode is NonNullable { 26 | return ['drawer', 'slideover', 'modal'].includes(mode) 27 | } 28 | 29 | export function Header(component: DesignComponent) { 30 | const { title, leftSlot, mode: modeVariant, variant } = cleanPropNames(component.properties) 31 | 32 | const mode = toLowerCase(modeVariant) 33 | const supported = isSupportedMode(mode) 34 | 35 | const container = supported 36 | ? findChild(component, { 37 | type: 'FRAME', 38 | name: 'Header', 39 | })! 40 | : component 41 | 42 | const left = variant === 'Logo' ? createElement('Logo') : null 43 | 44 | const right = queryAll>(container, [ 45 | { query: 'child', type: 'FRAME', name: 'RightSlot' }, 46 | { query: 'children', type: 'INSTANCE', name: BUTTON_NAMES }, 47 | ]).map((button) => Button(button)) 48 | 49 | const navMenu = findOne>(component, { 50 | type: 'INSTANCE', 51 | name: 'NavigationMenu', 52 | }) 53 | 54 | let menu 55 | if (isSupportedMode(mode)) { 56 | switch (mode) { 57 | case 'modal': { 58 | const modal = findChild>(component, { 59 | type: 'INSTANCE', 60 | name: 'Modal', 61 | }) 62 | menu = (modal ? Modal(modal) : {}).props 63 | break 64 | } 65 | case 'slideover': { 66 | const slideover = findChild>(component, { 67 | type: 'INSTANCE', 68 | name: 'Slideover', 69 | }) 70 | menu = (slideover ? Slideover(slideover) : {}).props 71 | break 72 | } 73 | case 'drawer': { 74 | const drawer = findChild>(component, { 75 | type: 'INSTANCE', 76 | name: 'Drawer', 77 | }) 78 | menu = (drawer ? Drawer(drawer) : {}).props 79 | break 80 | } 81 | } 82 | } 83 | 84 | return h( 85 | 'UHeader', 86 | { 87 | title: (leftSlot && title) || '', 88 | mode: supported ? mode : undefined, 89 | menu, 90 | }, 91 | { 92 | title: 'Nuxt UI Pro', 93 | mode: 'modal', 94 | }, 95 | [ 96 | ...(left ? [renderSlot('left', [left])] : []), 97 | ...(navMenu ? [NavigationMenu(navMenu)] : []), 98 | ...(right.length ? [renderSlot('right', right)] : []), 99 | ], 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/pro-components/footer-columns.ts: -------------------------------------------------------------------------------- 1 | import type { FooterColumn, FooterColumnLink } from '@nuxt/ui-pro/runtime/components/FooterColumns.vue' 2 | import type { DesignComponent, DevComponent, FrameNode } from '@tempad-dev/plugins' 3 | import type { ButtonProperties } from '../components/button' 4 | import type { FormFieldProperties } from '../components/form-field' 5 | import type { IconProperties } from '../components/icon' 6 | import { findChildren, queryAll, queryOne } from '@tempad-dev/plugins' 7 | import { Button, BUTTON_NAMES } from '../components/button' 8 | import { FormField } from '../components/form-field' 9 | import { getIconName } from '../components/icon' 10 | import { getLinkTo } from '../components/link' 11 | import { cleanPropNames, h, renderSlot } from '../utils' 12 | 13 | export type FooterColumnLinkProperties = { 14 | '𝐓 Label': string 15 | '↳ IconLeadingName': DesignComponent 16 | '🙂 IconLeading': boolean 17 | '👁️ External': boolean 18 | '🚦State': 'Default' | 'Hover' | 'Active' 19 | } 20 | 21 | export function renderFooterColumnLink(item: DesignComponent): FooterColumnLink { 22 | const { label, iconLeadingName, iconLeading, external } = cleanPropNames(item.properties) 23 | 24 | return { 25 | label, 26 | to: getLinkTo(label, external ? 'external' : 'path'), 27 | icon: iconLeading ? getIconName(iconLeadingName.name) : undefined, 28 | external, 29 | } 30 | } 31 | 32 | export type FooterColumnsProperties = { 33 | '𝐓 TitleSection4': string 34 | '𝐓 TitleSection3': string 35 | '𝐓 TitleSection2': string 36 | '𝐓 TitleSection1': string 37 | '👁️ Column3': boolean 38 | '👁️ Column4': boolean 39 | '👁️ Newsletter': boolean 40 | '🖥️ Device': 'Desktop' | 'Mobile' 41 | } 42 | 43 | export function FooterColumns(component: DesignComponent) { 44 | const props = cleanPropNames(component.properties) 45 | 46 | const columns: FooterColumn[] = findChildren(component, { 47 | type: 'FRAME', 48 | name: /^Column /, 49 | }).map((column, index) => { 50 | const i = (index + 1) as 1 | 2 | 3 | 4 51 | const label = props[`titleSection${i}`] 52 | 53 | const children = queryAll>(column, [ 54 | { query: 'child', type: 'FRAME', name: 'Links' }, 55 | { query: 'children', type: 'INSTANCE', name: 'FooterColumnsLink' }, 56 | ]).map((item) => renderFooterColumnLink(item)) 57 | 58 | return { 59 | label, 60 | children, 61 | } 62 | }) 63 | 64 | const children: DevComponent['children'] = [] 65 | 66 | if (props.newsletter) { 67 | const field = queryOne>(component, [ 68 | { query: 'child', type: 'FRAME', name: 'Newsletter' }, 69 | { query: 'child', type: 'INSTANCE', name: 'FormField' }, 70 | ]) 71 | 72 | const button = queryOne>(component, [ 73 | { query: 'child', type: 'FRAME', name: 'Newsletter' }, 74 | { query: 'child', type: 'INSTANCE', name: BUTTON_NAMES }, 75 | ]) 76 | 77 | if (field) { 78 | const formField = FormField(field) 79 | 80 | if (button) { 81 | const buttonComponent = Button(button) 82 | const inputComponent = formField.children.find( 83 | (child) => typeof child !== 'string' && child.name === 'UInput', 84 | ) as DevComponent | undefined 85 | 86 | if (inputComponent) { 87 | inputComponent.children.push(renderSlot('trailing', [buttonComponent])) 88 | } 89 | } 90 | 91 | children.push(renderSlot('right', [formField])) 92 | } 93 | } 94 | 95 | return h( 96 | 'UFooterColumns', 97 | { 98 | columns, 99 | }, 100 | {}, 101 | children, 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /src/pro-components/blog-post.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 2 | import type { AvatarGroupProperties } from '../components/avatar-group' 3 | import type { BadgeProperties } from '../components/badge' 4 | import type { AvatarProps, BlogPostProps, UserProps } from '../types' 5 | import type { UserProperties } from './user' 6 | import { queryOne } from '@tempad-dev/plugins' 7 | import { AvatarGroup } from '../components/avatar-group' 8 | import { renderBadgeItem } from '../components/badge' 9 | import { cleanPropNames, h, toLowerCase } from '../utils' 10 | import { renderUserItem } from './user' 11 | 12 | export type BlogPostProperties = { 13 | '👁️ Image': boolean 14 | '👁️ Authors': boolean 15 | '↳ Date': string 16 | '↳ Description': string 17 | '👁️ Description': boolean 18 | '👁️ Date': boolean 19 | '👁️ Badge': boolean 20 | '𝐓 Title': string 21 | '◆ Variant': 'Outline' | 'Soft' | 'Subtle' | 'Ghost' | 'Naked' 22 | '🚦State': 'Default' | 'Hover' 23 | '⇅ Orientation': 'Horizontal' | 'Vertical' 24 | '👥 Author': 'One' | 'Multiple' 25 | } 26 | 27 | export function BlogPost(component: DesignComponent) { 28 | const { 29 | image, 30 | authors, 31 | title, 32 | showDescription, 33 | description, 34 | showDate, 35 | date, 36 | showBadge, 37 | variant, 38 | orientation, 39 | author, 40 | } = cleanPropNames(component.properties, { 41 | '👁️ Description': 'showDescription', 42 | '👁️ Date': 'showDate', 43 | '👁️ Badge': 'showBadge', 44 | }) 45 | 46 | const badgeNode = queryOne>(component, [ 47 | { query: 'one', type: 'FRAME', name: 'Date + badge' }, 48 | { query: 'one', type: 'INSTANCE', name: 'Badge' }, 49 | ]) 50 | 51 | const badge = showBadge && badgeNode ? renderBadgeItem(badgeNode, { color: 'neutral', variant: 'subtle' }) : undefined 52 | 53 | const authorItems: Partial[] = [] 54 | 55 | if (authors) { 56 | if (author === 'One') { 57 | const authorNode = queryOne>(component, [ 58 | { query: 'child', type: 'FRAME', name: 'Content' }, 59 | { query: 'child', type: 'INSTANCE', name: 'User' }, 60 | ]) 61 | 62 | if (authorNode) { 63 | authorItems.push(renderUserItem(authorNode)) 64 | } 65 | } else if (author === 'Multiple') { 66 | const authorsNode = queryOne>(component, [ 67 | { query: 'child', type: 'FRAME', name: 'Content' }, 68 | { query: 'child', type: 'INSTANCE', name: 'AvatarGroup' }, 69 | ]) 70 | 71 | if (authorsNode) { 72 | const avatarGroup = AvatarGroup(authorsNode) 73 | if (avatarGroup) { 74 | authorItems.push( 75 | ...avatarGroup.children.map((child) => { 76 | const { props } = child as DevComponent 77 | return { 78 | avatar: props, 79 | } 80 | }), 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | 87 | return h( 88 | 'UBlogPost', 89 | { 90 | title, 91 | description: showDescription ? description : undefined, 92 | date: showDate ? date : undefined, 93 | badge: typeof badge === 'number' ? String(badge) : badge, 94 | authors: authorItems, 95 | image: image ? 'https://picsum.photos/540/360' : undefined, 96 | orientation: toLowerCase(orientation), 97 | variant: toLowerCase(variant), 98 | }, 99 | { 100 | orientation: 'vertical', 101 | variant: 'outline', 102 | }, 103 | ) 104 | } 105 | 106 | export function renderBlogPostItem(post: DesignComponent): Partial { 107 | const { 108 | props: { orientation, ...rest }, 109 | } = BlogPost(post) 110 | 111 | return rest 112 | } 113 | -------------------------------------------------------------------------------- /src/components/select-menu.ts: -------------------------------------------------------------------------------- 1 | import type { SelectMenuItem } from '@nuxt/ui' 2 | import type { DesignComponent, FrameNode, TextNode } from '@tempad-dev/plugins' 3 | import type { SelectMenuProps } from '../types' 4 | import type { IconProperties } from './icon' 5 | import type { InputProperties } from './input' 6 | import type { SelectProperties } from './select' 7 | import { omit } from '@s-libs/micro-dash' 8 | import { findChild, findChildren, queryOne } from '@tempad-dev/plugins' 9 | import { cleanPropNames, getFirst, h, pickOverrides } from '../utils' 10 | import { getRandomAvatar } from './avatar' 11 | import { ui } from './config' 12 | import { getIconName } from './icon' 13 | import { Input, INPUT_NAMES } from './input' 14 | import { Select, SELECT_NAMES } from './select' 15 | 16 | export type SelectMenuItemProperties = { 17 | '🙂 IconName': DesignComponent 18 | '𝐓 Label': string 19 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 20 | '🚦 State': 'Default' | 'Selected' | 'Hover' | 'Disabled' 21 | '◆ LeadingSlot': 'Avatar' | 'Icon' | 'None' | 'Span' 22 | } 23 | 24 | export type SelectMenuItemExtra = { 25 | selected?: boolean 26 | } & Pick 27 | 28 | export function renderSelectMenuItem( 29 | item: DesignComponent, 30 | ): SelectMenuItem & SelectMenuItemExtra { 31 | const { properties } = item 32 | 33 | const { state, leadingSlot, label, iconName } = cleanPropNames(properties) 34 | 35 | const trailingIcon = 36 | state === 'Selected' ? findChild>(item, { type: 'INSTANCE' }) : undefined 37 | 38 | return pickOverrides( 39 | { 40 | label, 41 | icon: leadingSlot === 'Icon' ? getIconName(iconName.name) : undefined, 42 | avatar: leadingSlot === 'Avatar' ? getRandomAvatar() : undefined, 43 | disabled: state === 'Disabled', 44 | // These should be omitted in final `items` 45 | selected: state === 'Selected', 46 | trailingIcon: trailingIcon ? getIconName(trailingIcon.name) : undefined, 47 | }, 48 | { 49 | disabled: false, 50 | selected: false, 51 | }, 52 | ) 53 | } 54 | 55 | export type SelectMenuProperties = { 56 | '👁️ IsOpen': boolean 57 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 58 | } 59 | 60 | export function SelectMenu(component: DesignComponent) { 61 | const menu = findChild(component, { 62 | type: 'FRAME', 63 | name: 'SelectMenu', 64 | }) 65 | 66 | const container = menu 67 | ? findChild(menu, { 68 | type: 'FRAME', 69 | name: 'Container', 70 | }) 71 | : undefined 72 | 73 | const items: (SelectMenuItem & SelectMenuItemExtra)[] = container 74 | ? findChildren>( 75 | container, 76 | (node) => 77 | (node.type === 'FRAME' && node.name === 'Title') || 78 | (node.type === 'INSTANCE' && node.name === 'SelectMenuItem'), 79 | ).map((item) => { 80 | if (item.type === 'FRAME') { 81 | const text = findChild(item, { 82 | type: 'TEXT', 83 | }) 84 | return { 85 | label: text?.characters, 86 | type: 'label', 87 | } 88 | } 89 | 90 | return renderSelectMenuItem(item) 91 | }) 92 | : [] 93 | 94 | const selectedIcon = getFirst(items, 'trailingIcon') 95 | 96 | const select = findChild>(component, { 97 | type: 'INSTANCE', 98 | name: SELECT_NAMES, 99 | }) 100 | 101 | const search = menu 102 | ? queryOne>(menu, [ 103 | { 104 | query: 'child', 105 | type: 'INSTANCE', 106 | name: 'Input', 107 | }, 108 | { 109 | query: 'child', 110 | type: 'INSTANCE', 111 | name: INPUT_NAMES, 112 | }, 113 | ]) 114 | : undefined 115 | 116 | const { content, ...selectProps } = select ? Select(select).props : {} 117 | const inputProps = search ? Input(search, { placeholder: 'Search...', variant: 'none' }).props : {} 118 | 119 | return h( 120 | 'USelectMenu', 121 | { 122 | ...selectProps, 123 | items: items.map((item) => omit(item, 'selected', 'trailingIcon')), 124 | selectedIcon, 125 | searchInput: inputProps, 126 | }, 127 | { 128 | selectedIcon: ui.icons.check, 129 | }, 130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /src/components/command-palette.ts: -------------------------------------------------------------------------------- 1 | import type { CommandPaletteItem } from '@nuxt/ui' 2 | import type { DesignComponent, FrameNode } from '@tempad-dev/plugins' 3 | import type { ButtonProperties } from './button' 4 | import type { IconProperties } from './icon' 5 | import type { InputProperties } from './input' 6 | import { findChild, findChildren, findOne } from '@tempad-dev/plugins' 7 | import { cleanPropNames, h, pickOverrides, toKebabCase } from '../utils' 8 | import { getRandomAvatar } from './avatar' 9 | import { BUTTON_NAMES, renderButtonItem } from './button' 10 | import { ui } from './config' 11 | import { getIconName } from './icon' 12 | import { Input, INPUT_NAMES } from './input' 13 | import { getKbdItems } from './kbd' 14 | 15 | export type CommandPaletteItemProperties = { 16 | '👁️ Description': boolean 17 | '𝐓 Label': string 18 | '🙂 IconTrailingName': DesignComponent 19 | '↳ DescriptionSlot': string 20 | '🙂 IconLeadingName': DesignComponent 21 | '🚦 State': 'Default' | 'Disabled' | 'Hover' 22 | '◆ LeadingSlot': 'Icon' | 'Avatar' | 'None' 23 | '◆ TrailingSlot': 'None' | 'Icon' | 'Kbd' 24 | } 25 | 26 | export function renderCommandPaletteItem(item: DesignComponent): CommandPaletteItem { 27 | const { state, leadingSlot, trailingSlot, description, label, descriptionSlot, iconLeadingName, iconTrailingName } = 28 | cleanPropNames(item.properties) 29 | 30 | return pickOverrides( 31 | { 32 | label, 33 | suffix: (description && descriptionSlot) || undefined, 34 | icon: leadingSlot === 'Icon' ? getIconName(iconLeadingName.name) : undefined, 35 | avatar: leadingSlot === 'Avatar' ? getRandomAvatar() : undefined, 36 | kbds: trailingSlot === 'Kbd' ? getKbdItems(item) : undefined, 37 | active: trailingSlot === 'Icon' && iconTrailingName.name === 'check', 38 | disabled: state === 'Disabled', 39 | }, 40 | { 41 | active: false, 42 | disabled: false, 43 | }, 44 | ) 45 | } 46 | 47 | export type CommandPaletteProperties = { 48 | '◆ Open': 'Drawer' | 'Modal' | 'Default' 49 | } 50 | 51 | export function CommandPalette(component: DesignComponent) { 52 | const { open } = cleanPropNames(component.properties) 53 | 54 | const palette = 55 | open === 'Default' 56 | ? component 57 | : findChild>(component, { 58 | type: 'FRAME', 59 | name: 'CommandPalette', 60 | }) 61 | 62 | const input = palette 63 | ? findChild>(palette, { 64 | type: 'INSTANCE', 65 | name: INPUT_NAMES, 66 | }) 67 | : undefined 68 | 69 | const inputComponent = input ? Input(input) : undefined 70 | 71 | const { icon, placeholder, disabled } = inputComponent?.props || {} 72 | 73 | const containers = palette 74 | ? findChildren(palette, { 75 | type: 'FRAME', 76 | name: /^Container/, 77 | }) 78 | : [] 79 | 80 | let activeCount = 0 81 | 82 | const groups = containers.map((container) => { 83 | const title = findChild(container, { 84 | type: 'TEXT', 85 | name: 'Title', 86 | })?.characters 87 | 88 | const items = findChildren>(container, { 89 | type: 'INSTANCE', 90 | name: 'CommandPaletteItem', 91 | }).map((item) => { 92 | const menuItem = renderCommandPaletteItem(item) 93 | 94 | if (menuItem.active) { 95 | activeCount++ 96 | } 97 | 98 | return menuItem 99 | }) 100 | 101 | return { 102 | id: toKebabCase(title || ''), 103 | label: title, 104 | items, 105 | } 106 | }) 107 | 108 | const closeButton = findOne>(component, { 109 | type: 'INSTANCE', 110 | name: BUTTON_NAMES, 111 | }) 112 | 113 | const buttonProps = closeButton 114 | ? renderButtonItem(closeButton, { 115 | size: 'md', 116 | color: 'neutral', 117 | variant: 'ghost', 118 | }) 119 | : false 120 | 121 | const { icon: closeIcon, square, ...props } = buttonProps || {} 122 | 123 | return h( 124 | 'UCommandPalette', 125 | { 126 | icon, 127 | placeholder, 128 | close: closeButton ? (Object.keys(props).length > 0 ? props : true) : false, 129 | closeIcon, 130 | groups, 131 | multiple: activeCount > 1, 132 | disabled, 133 | }, 134 | { 135 | icon: ui.icons.search, 136 | placeholder: 'Type a command or search...', 137 | close: false, 138 | closeIcon: ui.icons.close, 139 | multiple: false, 140 | disabled: false, 141 | }, 142 | ) 143 | } 144 | -------------------------------------------------------------------------------- /src/pro-components/content-toc.ts: -------------------------------------------------------------------------------- 1 | import type { ContentTocLink } from '@nuxt/ui-pro/runtime/components/content/ContentToc.vue' 2 | import type { DesignComponent, DevComponent, FrameNode } from '@tempad-dev/plugins' 3 | import type { SeparatorProperties } from '../components/separator' 4 | import type { ContentTocProps } from '../types' 5 | import type { PageLinksProperties } from './page-links' 6 | import { omit } from '@s-libs/micro-dash' 7 | import { findAll, findChild, queryAll } from '@tempad-dev/plugins' 8 | import { Separator } from '../components/separator' 9 | import { cleanPropNames, getFirst, h, pickOverrides, renderSlot, toKebabCase, toLowerCase } from '../utils' 10 | import { PageLinks } from './page-links' 11 | 12 | export type ContentTocLinkProperties = { 13 | '𝐓 Label ': string 14 | '🚦State': 'Active' | 'Default' | 'Hover' 15 | '🎨 HighlightColor': 'Neutral' | 'Primary' 16 | } 17 | 18 | export function renderContentTocLink( 19 | item: DesignComponent, 20 | ): ContentTocLink & Pick { 21 | const { label, highlightColor } = cleanPropNames(item.properties) 22 | 23 | return { 24 | id: toKebabCase(label), 25 | text: label, 26 | depth: 3, 27 | ...pickOverrides( 28 | { 29 | // These should be omitted in final `links` 30 | highlightColor: toLowerCase(highlightColor), 31 | }, 32 | { 33 | highlightColor: 'primary', 34 | }, 35 | ), 36 | } 37 | } 38 | 39 | export type ContentTocItemProperties = { 40 | '🎨 Color': 'Neutral' | 'Primary' 41 | '👁️ Children': 'True' | 'False' 42 | } 43 | 44 | export function renderContentTocItem( 45 | item: DesignComponent, 46 | ): ContentTocLink & Pick { 47 | const { color } = cleanPropNames(item.properties) 48 | 49 | const link = findChild>(item, { 50 | type: 'INSTANCE', 51 | name: 'ContentTocLink', 52 | }) 53 | 54 | const { id, text, highlightColor } = link ? renderContentTocLink(link) : {} 55 | 56 | const children = queryAll>(item, [ 57 | { query: 'child', type: 'FRAME', name: 'ContentTocList' }, 58 | { query: 'children', type: 'INSTANCE', name: 'ContentTocLink' }, 59 | ]).map((item) => renderContentTocLink(item)) 60 | 61 | const linkHighlightColor = getFirst(children, 'highlightColor') 62 | 63 | return { 64 | id: id || 'label', 65 | text: text || 'Label', 66 | depth: 2, 67 | children: children.map((child) => omit(child, 'highlightColor')), 68 | // These should be omitted in final `links` 69 | ...pickOverrides( 70 | { 71 | color: toLowerCase(color), 72 | highlightColor: linkHighlightColor || highlightColor, 73 | }, 74 | { 75 | color: 'primary', 76 | highlightColor: 'primary', 77 | }, 78 | ), 79 | } 80 | } 81 | 82 | export type ContentTocProperties = { 83 | '𝐓 Title': string 84 | '🖥️ Breakpoint': 'Desktop' | 'Mobile' 85 | } 86 | 87 | export function ContentToc(component: DesignComponent) { 88 | const { title } = cleanPropNames(component.properties) 89 | 90 | const links = findAll>(component, { 91 | type: 'INSTANCE', 92 | name: 'ContentTocItem', 93 | }).map((item) => renderContentTocItem(item)) 94 | 95 | const color = getFirst(links, 'color') 96 | const highlightColor = getFirst(links, 'highlightColor') 97 | 98 | const children: DevComponent['children'] = [] 99 | 100 | const bottom = findChild(component, { 101 | type: 'FRAME', 102 | name: 'Bottom', 103 | }) 104 | if (bottom) { 105 | const sep = findChild>(bottom, { 106 | type: 'INSTANCE', 107 | name: 'Separator', 108 | }) 109 | const links = findChild>(bottom, { 110 | type: 'INSTANCE', 111 | name: 'PageLinks', 112 | }) 113 | if (sep || links) { 114 | const content = [...(sep ? [Separator(sep)] : []), ...(links ? [PageLinks(links)] : [])] 115 | children.push(renderSlot('bottom', content)) 116 | } 117 | } 118 | 119 | // `highlightColor` is calculated from children but is not used here as 120 | // the Figma component does not have a `Highlight` property yet. 121 | return h( 122 | 'UContentToc', 123 | { 124 | title, 125 | links: links.map((link) => omit(link, 'color', 'highlightColor')), 126 | color, 127 | highlightColor, 128 | }, 129 | { 130 | title: 'On this page', 131 | color: 'primary', 132 | highlightColor: 'primary', 133 | }, 134 | children, 135 | ) 136 | } 137 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DesignNode, DevComponent, TransformOptions } from '@tempad-dev/plugins' 2 | 3 | import type { CleanPropName, ComponentPropsMap, RenderFn } from './types' 4 | 5 | import { mapKeys } from '@s-libs/micro-dash' 6 | import { h as createComponent } from '@tempad-dev/plugins' 7 | import { Icon } from './components/icon' 8 | 9 | export function cleanPropNames, M extends Partial> = object>( 10 | props: T, 11 | mapping?: M, 12 | ): CleanPropName { 13 | return mapKeys(props, (_, key) => { 14 | if (mapping && key in mapping) { 15 | const mapped = mapping[key as keyof T] 16 | return mapped ?? key 17 | } 18 | const trimmedKey = (key as string).trim().replace(/^[^0-9A-Z]+/i, '') 19 | const camelizedKey = trimmedKey.replace(/[ /]+(.)/g, (_, c) => c.toUpperCase()) 20 | return camelizedKey.charAt(0).toLowerCase() + camelizedKey.slice(1) 21 | }) as CleanPropName 22 | } 23 | 24 | export function renderSlot(name: string, children: DevComponent['children']): DevComponent 25 | export function renderSlot(name: string, props: string, children: DevComponent['children']): DevComponent 26 | export function renderSlot( 27 | name: string, 28 | propsOrChildren: string | DevComponent['children'], 29 | children?: DevComponent['children'], 30 | ): DevComponent { 31 | const props = children ? (propsOrChildren as string) : null 32 | const slotChildren = children ?? (propsOrChildren as DevComponent['children']) 33 | 34 | return createComponent( 35 | 'template', 36 | { 37 | [`#${name}`]: props ?? true, 38 | }, 39 | slotChildren, 40 | ) 41 | } 42 | 43 | export function toUpperFirst(input: string): string { 44 | return input.charAt(0).toUpperCase() + input.slice(1) 45 | } 46 | 47 | export function toCamelCase(input: string): string { 48 | return input.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) 49 | } 50 | 51 | export function toKebabCase(input: string): string { 52 | return input 53 | .replace(/([a-z])([A-Z])/g, '$1-$2') 54 | .replace(/(\d)([a-z])/gi, '$1-$2') 55 | .replace(/([a-z])(\d)/gi, '$1-$2') 56 | .replace(/[_\s]/g, '-') 57 | .toLowerCase() 58 | } 59 | 60 | export function toPascalCase(input: string): string { 61 | return toUpperFirst(toCamelCase(input)) 62 | } 63 | 64 | export function toConstantCase(input: string): string { 65 | return input.toUpperCase().replace(/-/g, '_') 66 | } 67 | 68 | export function toLowerCase(input: T): Lowercase { 69 | return input.toLowerCase() as Lowercase 70 | } 71 | 72 | export function pickOverrides(obj: T, defaults: Partial): Partial { 73 | const result: Partial = {} 74 | 75 | for (const key in obj) { 76 | if (obj[key] !== undefined && obj[key] !== defaults[key]) { 77 | result[key] = obj[key] 78 | } 79 | } 80 | 81 | return result 82 | } 83 | 84 | export const LOREM_IPSUM_TITLE = 'Lorem ipsum' 85 | export const LOREM_IPSUM_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' 86 | 87 | export function h( 88 | name: K, 89 | props: Partial, 90 | defaultProps: Partial, 91 | children?: (DevComponent | string)[], 92 | ): DevComponent> { 93 | return createComponent(name, pickOverrides(props, defaultProps), children) 94 | } 95 | 96 | // Fisher–Yates shuffle 97 | export function shuffle(array: T[]): T[] { 98 | for (let i = array.length - 1; i > 0; i--) { 99 | const j = Math.floor(Math.random() * (i + 1)) 100 | ;[array[i], array[j]] = [array[j]!, array[i]!] 101 | } 102 | return array 103 | } 104 | 105 | export function getFirst(items: T[], key: K): T[K] | undefined { 106 | return items.find((item) => item[key] != null)?.[key] 107 | } 108 | 109 | export function mapComponentNames(names: readonly string[], component: RenderFn) { 110 | return names.reduce((acc, name) => ({ ...acc, [name]: component }), {}) 111 | } 112 | 113 | export function isIcon(node: DesignNode): boolean { 114 | return ( 115 | node.type === 'INSTANCE' && 116 | node.children.length === 1 && 117 | node.children[0]!.type === 'VECTOR' && 118 | node.children[0]!.name === 'Vector' 119 | ) 120 | } 121 | 122 | export function createTransformComponent( 123 | componentMap: Record, 124 | ): TransformOptions['transformComponent'] { 125 | return ({ component }) => { 126 | try { 127 | if (isIcon(component)) { 128 | // only child is a vector, assume it's an icon 129 | return Icon(component) 130 | } 131 | 132 | const render = componentMap[component.name.replaceAll(' ', '')] 133 | return render ? render(component) : '' 134 | } catch (e: unknown) { 135 | console.error(e) 136 | return '' 137 | } 138 | } 139 | } 140 | 141 | export function isInteger(input: string | number): boolean { 142 | return /^\d+$/.test(String(input)) 143 | } 144 | -------------------------------------------------------------------------------- /src/pro-components/pricing-plan.ts: -------------------------------------------------------------------------------- 1 | import type { DesignComponent, FrameNode, TextNode } from '@tempad-dev/plugins' 2 | import type { BadgeProperties } from '../components/badge' 3 | import type { ButtonProperties } from '../components/button' 4 | import type { IconProperties } from '../components/icon' 5 | import type { PricingPlanProps } from '../types' 6 | import { findChild, queryAll, queryOne } from '@tempad-dev/plugins' 7 | import { renderBadgeItem } from '../components/badge' 8 | import { BUTTON_NAMES, renderButtonItem } from '../components/button' 9 | import { ui } from '../components/config' 10 | import { getIconName } from '../components/icon' 11 | import { cleanPropNames, h, pickOverrides, toLowerCase } from '../utils' 12 | 13 | export type PricingPlanFeature = { 14 | title: string 15 | icon?: string 16 | } 17 | 18 | export type PricingPlanProperties = { 19 | '👁️ Terms': boolean 20 | '↳ Tagline': string 21 | '👁️ Tagline': boolean 22 | '👁️ BillingCycle': boolean 23 | '👁️ Features': boolean 24 | '↳ Terms': string 25 | '𝐓 Price': string 26 | '𝐓 Description': string 27 | '👁️ Discount': boolean 28 | '👁️ BillingPeriod': boolean 29 | '↳ Discount': string 30 | '👁️ Badge': boolean 31 | '𝐓 Title': string 32 | '◆ Variant': 'Outline' | 'Solid' | 'Soft' | 'Subtle' 33 | '⇅ Orientation': 'Horizontal' | 'Vertical' | 'Horizontal (Mobile' 34 | '👁️ Highlight': 'False' | 'True' 35 | } 36 | 37 | export function PricingPlan(component: DesignComponent) { 38 | const { 39 | terms, 40 | showTerms, 41 | tagline, 42 | showTagline, 43 | billingCycle: showBillingCycle, 44 | features: showFeatures, 45 | price, 46 | discount, 47 | showDiscount, 48 | billingPeriod: showBillingPeriod, 49 | badge: showBadge, 50 | title, 51 | variant, 52 | orientation, 53 | highlight, 54 | } = cleanPropNames(component.properties, { 55 | '👁️ Terms': 'showTerms', 56 | '👁️ Tagline': 'showTagline', 57 | '👁️ Discount': 'showDiscount', 58 | }) 59 | 60 | const descriptionNode = queryOne(component, [ 61 | { 62 | query: 'one', 63 | type: 'FRAME', 64 | name: 'Title + description', 65 | }, 66 | { query: 'child', type: 'TEXT' }, 67 | ]) 68 | const description = descriptionNode?.characters || undefined 69 | 70 | const badgeNode = showBadge 71 | ? queryOne>(component, [ 72 | { query: 'one', type: 'FRAME', name: 'Title + badge' }, 73 | { query: 'one', type: 'INSTANCE', name: 'Badge' }, 74 | ]) 75 | : undefined 76 | const badge = badgeNode 77 | ? renderBadgeItem(badgeNode, { 78 | color: 'primary', 79 | variant: 'subtle', 80 | }) 81 | : undefined 82 | 83 | const billings = queryAll(component, [ 84 | { query: 'one', type: 'FRAME', name: 'BillingInfos' }, 85 | { query: 'children', type: 'TEXT' }, 86 | ]).map((textNode) => textNode.characters) 87 | const billingPeriod = showBillingPeriod ? billings.shift() : undefined 88 | const billingCycle = showBillingCycle ? billings.shift() : undefined 89 | 90 | let features: string[] | PricingPlanFeature[] = queryAll(component, [ 91 | { query: 'child', type: 'FRAME', name: 'Features' }, 92 | { query: 'children', type: 'FRAME', name: /^Feature / }, 93 | ]).map((feature) => { 94 | const title = findChild(feature, { type: 'TEXT' })?.characters 95 | const iconNode = findChild>(feature, { type: 'INSTANCE' }) 96 | const icon = iconNode ? getIconName(iconNode.name) : undefined 97 | 98 | return pickOverrides({ title: title || '', icon }, { icon: ui.icons.success }) as PricingPlanFeature 99 | }) 100 | if (features.every((feature) => !feature.icon)) { 101 | features = features.map((feature) => feature.title) 102 | } 103 | 104 | const buttonNode = queryOne>(component, [ 105 | { query: 'child', type: 'FRAME', name: 'Footer' }, 106 | { query: 'child', type: 'INSTANCE', name: BUTTON_NAMES }, 107 | ]) 108 | const button = buttonNode ? renderButtonItem(buttonNode, { size: 'lg', block: true }) : undefined 109 | 110 | return h( 111 | 'UPricingPlan', 112 | { 113 | title, 114 | description, 115 | badge, 116 | billingCycle, 117 | billingPeriod, 118 | price, 119 | discount: (showDiscount && discount) || undefined, 120 | features: showFeatures ? features : undefined, 121 | button, 122 | tagline: (showTagline && tagline) || undefined, 123 | terms: (showTerms && terms) || undefined, 124 | orientation: toLowerCase(orientation), 125 | variant: toLowerCase(variant), 126 | highlight: highlight === 'True', 127 | }, 128 | { 129 | orientation: 'vertical', 130 | variant: 'outline', 131 | highlight: false, 132 | }, 133 | ) 134 | } 135 | 136 | export function renderPricingPlanItem(plan: DesignComponent): PricingPlanProps { 137 | return PricingPlan(plan).props 138 | } 139 | -------------------------------------------------------------------------------- /src/components/dropdown-menu.ts: -------------------------------------------------------------------------------- 1 | import type { DropdownMenuItem } from '@nuxt/ui' 2 | import type { DesignComponent, DevComponent, FrameNode } from '@tempad-dev/plugins' 3 | import type { ButtonProps, DropdownMenuProps } from '../types' 4 | import type { AvatarProperties } from './avatar' 5 | import type { ButtonProperties } from './button' 6 | import type { IconProperties } from './icon' 7 | import { findChild, findChildren, queryAll } from '@tempad-dev/plugins' 8 | import { cleanPropNames, h, pickOverrides } from '../utils' 9 | import { Avatar, getRandomAvatar } from './avatar' 10 | import { Button, BUTTON_NAMES } from './button' 11 | import { getIconName } from './icon' 12 | import { getKbdItems } from './kbd' 13 | 14 | export type DropdownMenuItemProperties = { 15 | '🙂 IconTrailingName': DesignComponent 16 | '🙂 IconLeadingName': DesignComponent 17 | '𝐓 Label': string 18 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 19 | '🚦 State': 'Default' | 'Hover' | 'Disabled' | 'Selected' 20 | '◆ LeadingSlot': 'Avatar' | 'Icon' | 'None' 21 | '◆ TrailingSlot': 'Icon' | 'Kbd' | 'None' 22 | } 23 | 24 | export function renderDropdownMenuItem(item: DesignComponent): DropdownMenuItem { 25 | const { properties } = item 26 | 27 | const { state, leadingSlot, trailingSlot, label, iconLeadingName, iconTrailingName } = cleanPropNames(properties) 28 | 29 | return pickOverrides( 30 | { 31 | label, 32 | icon: leadingSlot === 'Icon' ? getIconName(iconLeadingName.name) : undefined, 33 | avatar: leadingSlot === 'Avatar' ? getRandomAvatar() : undefined, 34 | kbds: trailingSlot === 'Kbd' ? getKbdItems(item) : undefined, 35 | type: trailingSlot === 'Icon' && iconTrailingName.name === 'check' ? 'checkbox' : 'link', 36 | checked: trailingSlot === 'Icon' && iconTrailingName.name === 'check', 37 | disabled: state === 'Disabled', 38 | }, 39 | { 40 | type: 'link', 41 | checked: false, 42 | disabled: false, 43 | }, 44 | ) 45 | } 46 | 47 | export type DropdownMenuProperties = { 48 | '👁️ Open': boolean 49 | '📏 Size': 'xs' | 'sm' | 'md' | 'lg' | 'xl' 50 | '◆ Variant': 'Avatar' | 'Button' 51 | '⇅ Alignment': 'Bottom-start' | 'Bottom-end' | 'Right' | 'Top-start' | 'Left' 52 | '👁️ Arrow': 'True' | 'False' 53 | } 54 | 55 | const SIDE_MAP = { 56 | 'Bottom-start': 'bottom', 57 | 'Bottom-end': 'bottom', 58 | Right: 'right', 59 | 'Top-start': 'top', 60 | Left: 'left', 61 | } as const 62 | 63 | export function DropdownMenu( 64 | component: DesignComponent, 65 | overrides: { 66 | button?: Partial 67 | } = {}, 68 | ) { 69 | const { size, variant, alignment, arrow } = cleanPropNames(component.properties) 70 | 71 | const content: DropdownMenuProps['content'] = pickOverrides( 72 | { 73 | side: SIDE_MAP[alignment], 74 | }, 75 | { 76 | side: 'bottom', 77 | }, 78 | ) 79 | 80 | const containers = queryAll(component, [ 81 | { query: 'child', type: 'FRAME', name: 'DropdownMenu' }, 82 | { query: 'children', type: 'FRAME', name: /^Container/ }, 83 | ]) 84 | 85 | const items = containers.map((container) => { 86 | const menuItems = findChildren>(container, { 87 | type: 'INSTANCE', 88 | name: 'DropdownMenuItem', 89 | }) 90 | 91 | return menuItems.map((item) => renderDropdownMenuItem(item)) 92 | }) 93 | 94 | const children: DevComponent['children'] = [] 95 | 96 | if (variant === 'Button') { 97 | const container = 98 | arrow === 'True' 99 | ? findChild(component, { 100 | type: 'FRAME', 101 | name: 'Button + arrow', 102 | }) 103 | : component 104 | 105 | const button = container 106 | ? findChild>(container, { 107 | type: 'INSTANCE', 108 | name: BUTTON_NAMES, 109 | }) 110 | : undefined 111 | 112 | if (button) { 113 | const buttonChild = Button(button) 114 | buttonChild.props = { 115 | ...buttonChild.props, 116 | ...overrides.button, 117 | } 118 | children.push(buttonChild) 119 | } 120 | } else if (variant === 'Avatar') { 121 | const container = 122 | arrow === 'True' 123 | ? findChild(component, { 124 | type: 'FRAME', 125 | name: 'Avatar + arrow', 126 | }) 127 | : component 128 | 129 | const avatar = container 130 | ? findChild>(container, { 131 | type: 'INSTANCE', 132 | name: 'Avatar', 133 | }) 134 | : undefined 135 | 136 | if (avatar) { 137 | children.push(Avatar(avatar)) 138 | } 139 | } 140 | 141 | return h( 142 | 'UDropdownMenu', 143 | { 144 | size, 145 | items, 146 | content, 147 | arrow: arrow === 'True', 148 | }, 149 | { 150 | size: 'md', 151 | arrow: false, 152 | }, 153 | children, 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /src/components/navigation-menu.ts: -------------------------------------------------------------------------------- 1 | import type { NavigationMenuChildItem, NavigationMenuItem } from '@nuxt/ui' 2 | import type { DesignComponent } from '@tempad-dev/plugins' 3 | import type { NavigationMenuProps } from '../types' 4 | import type { BadgeProperties } from './badge' 5 | import type { CollapsibleProperties } from './collapsible' 6 | import type { IconProperties } from './icon' 7 | import { omit } from '@s-libs/micro-dash' 8 | import { findAll, findChild, findChildren, queryAll, queryOne } from '@tempad-dev/plugins' 9 | import { cleanPropNames, getFirst, h, pickOverrides, toLowerCase } from '../utils' 10 | import { renderBadgeItem } from './badge' 11 | import { getIconName } from './icon' 12 | import { getLinkTo } from './link' 13 | 14 | export type NavigationMenuDropdownItemProperties = { 15 | '👁️ Description': boolean 16 | '↳ DescriptionSlot': string 17 | '𝐓 Title': string 18 | '👁️ Icon': boolean 19 | '↳ IconName': DesignComponent 20 | '🚦 State': 'Active' | 'Default' | 'Focused' | 'Hover' 21 | } 22 | 23 | export function renderNavigationMenuDropdownItem( 24 | item: DesignComponent, 25 | ): NavigationMenuChildItem { 26 | const { icon, iconName, title, description, descriptionSlot } = cleanPropNames(item.properties) 27 | 28 | return { 29 | label: title, 30 | description: (description && descriptionSlot) || undefined, 31 | icon: icon ? getIconName(iconName.name) : undefined, 32 | } 33 | } 34 | 35 | export type NavigationMenuItemProperties = { 36 | '↳ IconLeadingName': DesignComponent 37 | '𝐓 Label': string 38 | '👁️ Children': boolean 39 | '👁️ Badge': boolean 40 | '👁️ IconTrailing': boolean 41 | '👁️ IconLeading': boolean 42 | '🎨 Color': 'Neutral' | 'Primary' 43 | '⇅ Orientation': 'Horizontal' | 'Vertical' 44 | '◆ Variant': 'Link' | 'Pill' 45 | '🚦 State': 'Default' | 'Hover' | 'Disabled' | 'Focus' 46 | '👁️ Active': 'False' | 'True' 47 | '👁️ Highlight': 'False' | 'True' 48 | '👁️ External': 'False' | 'True' 49 | } 50 | 51 | type NavigationMenuItemExtra = Pick 52 | 53 | export function renderNavigationMenuItem( 54 | item: DesignComponent, 55 | ): NavigationMenuItem & NavigationMenuItemExtra { 56 | const { 57 | color, 58 | variant, 59 | state, 60 | active, 61 | highlight, 62 | iconLeading, 63 | iconLeadingName, 64 | iconTrailing, 65 | badge: showBadge, 66 | label, 67 | external: externalVariant, 68 | children: showChildren, 69 | } = cleanPropNames(item.properties) 70 | const badgeNode = showBadge 71 | ? queryOne>(item, [ 72 | { query: 'child', type: 'FRAME', name: 'Container' }, 73 | { query: 'child', type: 'INSTANCE', name: 'Badge' }, 74 | ]) 75 | : undefined 76 | 77 | const badge = badgeNode 78 | ? renderBadgeItem( 79 | badgeNode, 80 | { 81 | size: 'sm', 82 | color: 'neutral', 83 | variant: 'outline', 84 | }, 85 | true, 86 | ) 87 | : undefined 88 | 89 | const children = 90 | iconTrailing && showChildren 91 | ? findAll>(item, { 92 | type: 'INSTANCE', 93 | name: 'NavigationMenu(DropdownItem)', 94 | }).map((item) => renderNavigationMenuDropdownItem(item)) 95 | : undefined 96 | 97 | const external = externalVariant === 'True' 98 | 99 | return pickOverrides( 100 | { 101 | label, 102 | icon: iconLeading ? getIconName(iconLeadingName.name) : undefined, 103 | to: getLinkTo(label, external ? 'external' : 'path'), 104 | badge, 105 | external, 106 | children: external ? undefined : children, 107 | active: active === 'True', 108 | disabled: state === 'Disabled', 109 | // These should be omitted in final `items` 110 | variant: toLowerCase(variant), 111 | color: toLowerCase(color), 112 | highlight: highlight === 'True', 113 | }, 114 | { 115 | external: false, 116 | active: false, 117 | disabled: false, 118 | variant: 'pill', 119 | color: 'primary', 120 | highlight: false, 121 | }, 122 | ) 123 | } 124 | 125 | export type NavigationMenuProperties = { 126 | '⇅ Orientation': 'Horizontal' | 'Vertical' 127 | '👁️ Highlight': 'False' | 'True' 128 | } 129 | 130 | export function NavigationMenu(component: DesignComponent) { 131 | const { orientation, highlight } = cleanPropNames(component.properties) 132 | 133 | const items: (NavigationMenuItem & NavigationMenuItemExtra)[] = [] 134 | 135 | if (orientation === 'Horizontal') { 136 | items.push( 137 | ...findChildren>(component, { 138 | type: 'INSTANCE', 139 | name: 'NavigationMenuItem', 140 | }).map((item) => renderNavigationMenuItem(item)), 141 | ) 142 | } else { 143 | items.push( 144 | ...findChildren>(component, { 145 | type: 'INSTANCE', 146 | name: 'Collapsible', 147 | }).map((panel) => { 148 | const item = renderNavigationMenuItem( 149 | findChild>(panel, { 150 | type: 'INSTANCE', 151 | name: 'NavigationMenuItem', 152 | })!, 153 | ) 154 | const children = queryAll>(panel, [ 155 | { query: 'child', type: 'INSTANCE', name: 'NavigationMenu(ChildList)' }, 156 | { query: 'children', type: 'INSTANCE', name: 'NavigationMenuItem' }, 157 | ]).map((item) => renderNavigationMenuItem(item)) 158 | 159 | const variant = getFirst(children, 'variant') 160 | const color = getFirst(children, 'color') 161 | const highlight = getFirst(children, 'highlight') 162 | 163 | return { 164 | ...item, 165 | children: item.external ? undefined : children, 166 | 167 | // These should be omitted in final `items` 168 | variant: variant || item.variant, 169 | color: color || item.color, 170 | highlight: highlight || item.highlight, 171 | } 172 | }), 173 | ) 174 | } 175 | 176 | return h( 177 | 'UNavigationMenu', 178 | { 179 | items: items.map((item) => omit(item, 'variant', 'color', 'highlight')), 180 | color: getFirst(items, 'color'), 181 | variant: getFirst(items, 'variant'), 182 | orientation: toLowerCase(orientation), 183 | highlight: highlight === 'True' || !!getFirst(items, 'highlight'), 184 | }, 185 | { 186 | color: 'primary', 187 | variant: 'pill', 188 | orientation: 'horizontal', 189 | highlight: false, 190 | }, 191 | ) 192 | } 193 | -------------------------------------------------------------------------------- /src/pro-components/content-navigation.ts: -------------------------------------------------------------------------------- 1 | import type { ContentNavigationLink } from '@nuxt/ui-pro/runtime/components/content/ContentNavigation.vue' 2 | import type { DesignComponent } from '@tempad-dev/plugins' 3 | import type { BadgeProperties } from '../components/badge' 4 | import type { IconProperties } from '../components/icon' 5 | import type { ContentNavigationProps } from '../types' 6 | import { omit } from '@s-libs/micro-dash' 7 | import { findAll, findChild, findChildren } from '@tempad-dev/plugins' 8 | import { renderBadgeItem } from '../components/badge' 9 | import { ui } from '../components/config' 10 | import { getIconName } from '../components/icon' 11 | import { getLinkTo } from '../components/link' 12 | import { cleanPropNames, getFirst, h, pickOverrides, toLowerCase } from '../utils' 13 | 14 | type ContentNavigationItemExtra = Pick< 15 | ContentNavigationProps, 16 | 'color' | 'highlight' | 'highlightColor' | 'variant' | 'trailingIcon' 17 | > 18 | 19 | export type ContentNavigationLinkProperties = { 20 | '↳ IconLeadingName': DesignComponent 21 | '𝐓 Label': string 22 | '👁️ Badge': boolean 23 | '👁️ IconTrailing': boolean 24 | '👁️ IconLeading': boolean 25 | '🎨 Color': 'Neutral' | 'Primary' 26 | '◆ Variant': 'Link' | 'Pill' 27 | '🚦 State': 'Default' | 'Hover' | 'Disabled' | 'Focus' 28 | '👁️ Active': 'False' | 'True' 29 | '👁️ Highlight': 'False' | 'True' 30 | '👁️ External': 'False' | 'True' 31 | } 32 | 33 | export function renderContentNavigationLink( 34 | item: DesignComponent, 35 | ): ContentNavigationLink & ContentNavigationItemExtra { 36 | const { 37 | iconLeading, 38 | iconLeadingName, 39 | badge: showBadge, 40 | label, 41 | color, 42 | variant, 43 | state, 44 | active, 45 | highlight, 46 | } = cleanPropNames(item.properties) 47 | 48 | const badgeNode = showBadge 49 | ? findChild>(item, { 50 | type: 'INSTANCE', 51 | name: 'Badge', 52 | }) 53 | : undefined 54 | 55 | const badge = badgeNode 56 | ? renderBadgeItem(badgeNode, { color: 'neutral', variant: 'outline', size: 'sm' }, true) 57 | : undefined 58 | 59 | return { 60 | title: label, 61 | path: getLinkTo(label, 'hash'), 62 | ...pickOverrides( 63 | { 64 | icon: iconLeading ? getIconName(iconLeadingName.name) : undefined, 65 | badge, 66 | active: active === 'True', 67 | disabled: state === 'Disabled', 68 | // These should be omitted in final `navigation` 69 | color: toLowerCase(color), 70 | variant: toLowerCase(variant), 71 | highlight: highlight === 'True', 72 | }, 73 | { 74 | active: false, 75 | disabled: false, 76 | color: 'primary', 77 | variant: 'pill', 78 | highlight: false, 79 | }, 80 | ), 81 | } 82 | } 83 | 84 | export type ContentNavigationItemProperties = { 85 | '👁️ IconTrailing': boolean 86 | '👁️ Badge': boolean 87 | '👁️ IconLeading': boolean 88 | '𝐓 Label': string 89 | '↳ IconLeadingName': DesignComponent 90 | '👁️ Active': 'True' | 'False' 91 | '🚦State': 'Default' | 'Focus' | 'Hover' 92 | '🎨 HighlightColor': 'Neutral' | 'Primary' 93 | '◆ Variant': 'Link' | 'Pill' 94 | } 95 | 96 | export function renderContentNavigationItem( 97 | item: DesignComponent, 98 | ): ContentNavigationLink & ContentNavigationItemExtra { 99 | const { 100 | iconTrailing, 101 | badge: showBadge, 102 | iconLeading, 103 | label, 104 | iconLeadingName, 105 | active, 106 | highlightColor, 107 | variant, 108 | } = cleanPropNames(item.properties) 109 | 110 | const badgeNode = showBadge 111 | ? findChild>(item, { 112 | type: 'INSTANCE', 113 | name: 'Badge', 114 | }) 115 | : undefined 116 | 117 | const badge = badgeNode 118 | ? renderBadgeItem(badgeNode, { color: 'neutral', variant: 'outline', size: 'sm' }, true) 119 | : undefined 120 | 121 | const trailingIconNode = item.children[item.children.length - 1] as DesignComponent 122 | const trailingIcon = iconTrailing ? getIconName(trailingIconNode.name) : undefined 123 | 124 | return { 125 | title: label, 126 | path: getLinkTo(label, 'hash'), 127 | ...pickOverrides( 128 | { 129 | icon: iconLeading ? getIconName(iconLeadingName.name) : undefined, 130 | badge, 131 | active: active === 'True', 132 | // These should be omitted in final `navigation` 133 | highlightColor: toLowerCase(highlightColor), 134 | variant: toLowerCase(variant), 135 | trailingIcon, 136 | }, 137 | { 138 | active: false, 139 | highlightColor: 'primary', 140 | variant: 'pill', 141 | trailingIcon: ui.icons.chevronDown, 142 | }, 143 | ), 144 | } 145 | } 146 | 147 | export type ContentNavigationItemsProperties = { 148 | '👁️ Active': 'True' | 'False' 149 | '🎨 HighlightColor': 'Neutral' | 'Primary' 150 | } 151 | 152 | export function renderContentNavigationItems( 153 | items: DesignComponent, 154 | ): ContentNavigationLink { 155 | const item = renderContentNavigationItem( 156 | findChild>(items, { 157 | type: 'INSTANCE', 158 | name: 'ContentNavigationItem', 159 | })!, 160 | ) 161 | 162 | const children = findAll>(items, { 163 | type: 'INSTANCE', 164 | name: 'ContentNavigationLink', 165 | }).map((item) => renderContentNavigationLink(item)) 166 | 167 | const color = getFirst(children, 'color') 168 | const variant = getFirst(children, 'variant') || item.variant 169 | const highlight = getFirst(children, 'highlight') 170 | 171 | return { 172 | ...item, 173 | children: children.map( 174 | (link) => omit(link, 'color', 'variant', 'highlight') as ContentNavigationLink & ContentNavigationItemExtra, 175 | ), 176 | // These should be omitted in final `navigation`, along with `highlightColor`, `trailingIcon` 177 | color, 178 | variant, 179 | highlight, 180 | } 181 | } 182 | 183 | // eslint-disable-next-line ts/no-empty-object-type 184 | export type ContentNavigationProperties = {} 185 | 186 | export function ContentNavigation(component: DesignComponent) { 187 | const navigationItems = findChildren>(component, { 188 | type: 'INSTANCE', 189 | name: 'ContentNavigationItems', 190 | }).map((item) => renderContentNavigationItems(item)) 191 | 192 | return h( 193 | 'UContentNavigation', 194 | { 195 | navigation: navigationItems.map( 196 | (item) => 197 | omit(item, 'color', 'variant', 'highlight', 'highlightColor', 'trailingIcon') as ContentNavigationLink, 198 | ), 199 | color: getFirst(navigationItems, 'color'), 200 | highlightColor: getFirst(navigationItems, 'highlightColor'), 201 | variant: getFirst(navigationItems, 'variant'), 202 | highlight: !!getFirst(navigationItems, 'highlight'), 203 | trailingIcon: getFirst(navigationItems, 'trailingIcon'), 204 | }, 205 | { 206 | color: 'primary', 207 | highlightColor: 'primary', 208 | variant: 'pill', 209 | highlight: false, 210 | trailingIcon: ui.icons.chevronDown, 211 | }, 212 | ) 213 | } 214 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type AuthForm from '@nuxt/ui-pro/runtime/components/AuthForm.vue' 2 | import type Banner from '@nuxt/ui-pro/runtime/components/Banner.vue' 3 | import type BlogPost from '@nuxt/ui-pro/runtime/components/BlogPost.vue' 4 | import type BlogPosts from '@nuxt/ui-pro/runtime/components/BlogPosts.vue' 5 | import type ColorModeAvatar from '@nuxt/ui-pro/runtime/components/color-mode/ColorModeAvatar.vue' 6 | import type ColorModeButton from '@nuxt/ui-pro/runtime/components/color-mode/ColorModeButton.vue' 7 | import type ColorModeImage from '@nuxt/ui-pro/runtime/components/color-mode/ColorModeImage.vue' 8 | import type ColorModeSelect from '@nuxt/ui-pro/runtime/components/color-mode/ColorModeSelect.vue' 9 | import type ColorModeSwitch from '@nuxt/ui-pro/runtime/components/color-mode/ColorModeSwitch.vue' 10 | import type ContentNavigation from '@nuxt/ui-pro/runtime/components/content/ContentNavigation.vue' 11 | import type ContentSearch from '@nuxt/ui-pro/runtime/components/content/ContentSearch.vue' 12 | import type ContentSearchButton from '@nuxt/ui-pro/runtime/components/content/ContentSearchButton.vue' 13 | import type ContentSurround from '@nuxt/ui-pro/runtime/components/content/ContentSurround.vue' 14 | import type ContentToc from '@nuxt/ui-pro/runtime/components/content/ContentToc.vue' 15 | import type DashboardGroup from '@nuxt/ui-pro/runtime/components/DashboardGroup.vue' 16 | import type DashboardNavbar from '@nuxt/ui-pro/runtime/components/DashboardNavbar.vue' 17 | import type DashboardPanel from '@nuxt/ui-pro/runtime/components/DashboardPanel.vue' 18 | import type DashboardResizeHandle from '@nuxt/ui-pro/runtime/components/DashboardResizeHandle.vue' 19 | import type DashboardSearch from '@nuxt/ui-pro/runtime/components/DashboardSearch.vue' 20 | import type DashboardSearchButton from '@nuxt/ui-pro/runtime/components/DashboardSearchButton.vue' 21 | import type DashboardSidebar from '@nuxt/ui-pro/runtime/components/DashboardSidebar.vue' 22 | import type DashboardSidebarCollapse from '@nuxt/ui-pro/runtime/components/DashboardSidebarCollapse.vue' 23 | import type DashboardSidebarToggle from '@nuxt/ui-pro/runtime/components/DashboardSidebarToggle.vue' 24 | import type DashboardToolbar from '@nuxt/ui-pro/runtime/components/DashboardToolbar.vue' 25 | import type Error from '@nuxt/ui-pro/runtime/components/Error.vue' 26 | import type Footer from '@nuxt/ui-pro/runtime/components/Footer.vue' 27 | import type FooterColumns from '@nuxt/ui-pro/runtime/components/FooterColumns.vue' 28 | import type Header from '@nuxt/ui-pro/runtime/components/Header.vue' 29 | import type LocaleSelect from '@nuxt/ui-pro/runtime/components/locale/LocaleSelect.vue' 30 | import type Main from '@nuxt/ui-pro/runtime/components/Main.vue' 31 | import type Page from '@nuxt/ui-pro/runtime/components/Page.vue' 32 | import type PageAccordion from '@nuxt/ui-pro/runtime/components/PageAccordion.vue' 33 | import type PageAnchors from '@nuxt/ui-pro/runtime/components/PageAnchors.vue' 34 | import type PageAside from '@nuxt/ui-pro/runtime/components/PageAside.vue' 35 | import type PageBody from '@nuxt/ui-pro/runtime/components/PageBody.vue' 36 | import type PageCard from '@nuxt/ui-pro/runtime/components/PageCard.vue' 37 | import type PageColumns from '@nuxt/ui-pro/runtime/components/PageColumns.vue' 38 | import type PageCTA from '@nuxt/ui-pro/runtime/components/PageCTA.vue' 39 | import type PageFeature from '@nuxt/ui-pro/runtime/components/PageFeature.vue' 40 | import type PageGrid from '@nuxt/ui-pro/runtime/components/PageGrid.vue' 41 | import type PageHeader from '@nuxt/ui-pro/runtime/components/PageHeader.vue' 42 | import type PageHero from '@nuxt/ui-pro/runtime/components/PageHero.vue' 43 | import type PageLinks from '@nuxt/ui-pro/runtime/components/PageLinks.vue' 44 | import type PageList from '@nuxt/ui-pro/runtime/components/PageList.vue' 45 | import type PageLogos from '@nuxt/ui-pro/runtime/components/PageLogos.vue' 46 | import type PageMarquee from '@nuxt/ui-pro/runtime/components/PageMarquee.vue' 47 | import type PageSection from '@nuxt/ui-pro/runtime/components/PageSection.vue' 48 | import type PricingPlan from '@nuxt/ui-pro/runtime/components/PricingPlan.vue' 49 | import type PricingPlans from '@nuxt/ui-pro/runtime/components/PricingPlans.vue' 50 | import type User from '@nuxt/ui-pro/runtime/components/User.vue' 51 | import type Accordion from '@nuxt/ui/runtime/components/Accordion.vue' 52 | import type Alert from '@nuxt/ui/runtime/components/Alert.vue' 53 | import type Avatar from '@nuxt/ui/runtime/components/Avatar.vue' 54 | import type AvatarGroup from '@nuxt/ui/runtime/components/AvatarGroup.vue' 55 | import type Badge from '@nuxt/ui/runtime/components/Badge.vue' 56 | import type Breadcrumb from '@nuxt/ui/runtime/components/Breadcrumb.vue' 57 | import type Button from '@nuxt/ui/runtime/components/Button.vue' 58 | import type ButtonGroup from '@nuxt/ui/runtime/components/ButtonGroup.vue' 59 | import type Calendar from '@nuxt/ui/runtime/components/Calendar.vue' 60 | import type Card from '@nuxt/ui/runtime/components/Card.vue' 61 | import type Carousel from '@nuxt/ui/runtime/components/Carousel.vue' 62 | import type Checkbox from '@nuxt/ui/runtime/components/Checkbox.vue' 63 | import type Chip from '@nuxt/ui/runtime/components/Chip.vue' 64 | import type Collapsible from '@nuxt/ui/runtime/components/Collapsible.vue' 65 | import type ColorPicker from '@nuxt/ui/runtime/components/ColorPicker.vue' 66 | import type CommandPalette from '@nuxt/ui/runtime/components/CommandPalette.vue' 67 | import type Container from '@nuxt/ui/runtime/components/Container.vue' 68 | import type ContextMenu from '@nuxt/ui/runtime/components/ContextMenu.vue' 69 | import type Drawer from '@nuxt/ui/runtime/components/Drawer.vue' 70 | import type DropdownMenu from '@nuxt/ui/runtime/components/DropdownMenu.vue' 71 | import type Form from '@nuxt/ui/runtime/components/Form.vue' 72 | import type FormField from '@nuxt/ui/runtime/components/FormField.vue' 73 | import type Icon from '@nuxt/ui/runtime/components/Icon.vue' 74 | import type Input from '@nuxt/ui/runtime/components/Input.vue' 75 | import type InputMenu from '@nuxt/ui/runtime/components/InputMenu.vue' 76 | import type InputNumber from '@nuxt/ui/runtime/components/InputNumber.vue' 77 | import type Kbd from '@nuxt/ui/runtime/components/Kbd.vue' 78 | import type Link from '@nuxt/ui/runtime/components/Link.vue' 79 | import type Modal from '@nuxt/ui/runtime/components/Modal.vue' 80 | import type NavigationMenu from '@nuxt/ui/runtime/components/NavigationMenu.vue' 81 | import type Pagination from '@nuxt/ui/runtime/components/Pagination.vue' 82 | import type PinInput from '@nuxt/ui/runtime/components/PinInput.vue' 83 | import type Popover from '@nuxt/ui/runtime/components/Popover.vue' 84 | import type Progress from '@nuxt/ui/runtime/components/Progress.vue' 85 | import type RadioGroup from '@nuxt/ui/runtime/components/RadioGroup.vue' 86 | import type Select from '@nuxt/ui/runtime/components/Select.vue' 87 | import type SelectMenu from '@nuxt/ui/runtime/components/SelectMenu.vue' 88 | import type Separator from '@nuxt/ui/runtime/components/Separator.vue' 89 | import type Skeleton from '@nuxt/ui/runtime/components/Skeleton.vue' 90 | import type Slideover from '@nuxt/ui/runtime/components/Slideover.vue' 91 | import type Slider from '@nuxt/ui/runtime/components/Slider.vue' 92 | import type Stepper from '@nuxt/ui/runtime/components/Stepper.vue' 93 | import type Switch from '@nuxt/ui/runtime/components/Switch.vue' 94 | import type Table from '@nuxt/ui/runtime/components/Table.vue' 95 | import type Tabs from '@nuxt/ui/runtime/components/Tabs.vue' 96 | import type Textarea from '@nuxt/ui/runtime/components/Textarea.vue' 97 | import type Toast from '@nuxt/ui/runtime/components/Toast.vue' 98 | import type Tooltip from '@nuxt/ui/runtime/components/Tooltip.vue' 99 | import type { DesignComponent, DevComponent } from '@tempad-dev/plugins' 100 | import type { VNodeProps } from 'vue' 101 | 102 | import type { ComponentProps as CP } from 'vue-component-type-helpers' 103 | 104 | type TrimLeft = S extends ` ${infer Rest}` ? TrimLeft : S 105 | type TrimRight = S extends `${infer Rest} ` ? TrimRight : S 106 | type Trim = TrimLeft> 107 | 108 | // prettier-ignore 109 | type RemovePrefix = S extends `${infer First}${infer Rest}` 110 | ? First extends 111 | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' 112 | ? S 113 | : RemovePrefix 114 | : S 115 | 116 | type Camelize = S extends `${infer Head}${' ' | '/'}${infer Rest}` 117 | ? `${Head}${Camelize>}` 118 | : S 119 | 120 | type LowerFirst = T extends `${infer First}${infer Rest}` ? `${Lowercase}${Rest}` : T 121 | 122 | export type CleanPropName> = object> = { 123 | [K in keyof T as K extends keyof M 124 | ? Extract 125 | : LowerFirst>>>]: T[K] 126 | } 127 | 128 | type MapComponentProps = T & { 129 | [K in keyof T as K extends `on${infer Rest}` ? `@${CamelToKebab}` : `:${CamelToKebab}`]: string 130 | } 131 | 132 | export type ComponentProps = MapComponentProps, keyof VNodeProps>> 133 | 134 | export type AccordionProps = ComponentProps 135 | export type AlertProps = ComponentProps 136 | export type AvatarGroupProps = ComponentProps 137 | export type AvatarProps = ComponentProps 138 | export type BadgeProps = ComponentProps 139 | export type BreadcrumbProps = ComponentProps 140 | export type ButtonProps = ComponentProps 141 | export type ButtonGroupProps = ComponentProps 142 | export type CalendarProps = ComponentProps 143 | export type CardProps = ComponentProps 144 | export type CarouselProps = ComponentProps 145 | export type CheckboxProps = ComponentProps 146 | export type ChipProps = ComponentProps 147 | export type CollapsibleProps = ComponentProps 148 | export type ColorPickerProps = ComponentProps 149 | export type CommandPaletteProps = ComponentProps 150 | export type ContainerProps = ComponentProps 151 | export type ContextMenuProps = ComponentProps 152 | export type DrawerProps = ComponentProps 153 | export type DropdownMenuProps = ComponentProps 154 | export type FormProps = ComponentProps 155 | export type FormFieldProps = ComponentProps 156 | export type IconProps = ComponentProps 157 | export type InputProps = ComponentProps 158 | export type InputMenuProps = ComponentProps 159 | export type InputNumberProps = ComponentProps 160 | export type KbdProps = ComponentProps 161 | export type LinkProps = ComponentProps 162 | export type ModalProps = ComponentProps 163 | export type NavigationMenuProps = ComponentProps 164 | export type PaginationProps = ComponentProps 165 | export type PinInputProps = ComponentProps 166 | export type PopoverProps = ComponentProps 167 | export type ProgressProps = ComponentProps 168 | export type RadioGroupProps = ComponentProps 169 | export type SelectProps = ComponentProps 170 | export type SelectMenuProps = ComponentProps 171 | export type SeparatorProps = ComponentProps 172 | export type SkeletonProps = ComponentProps 173 | export type SlideoverProps = ComponentProps 174 | export type SliderProps = ComponentProps 175 | export type StepperProps = ComponentProps 176 | export type SwitchProps = ComponentProps 177 | export type TableProps = ComponentProps 178 | export type TabsProps = ComponentProps 179 | export type TextareaProps = ComponentProps 180 | export type ToastProps = ComponentProps 181 | export type TooltipProps = ComponentProps 182 | export type AuthFormProps = ComponentProps 183 | export type BannerProps = ComponentProps 184 | export type BlogPostProps = ComponentProps 185 | export type BlogPostsProps = ComponentProps 186 | export type ColorModeAvatarProps = ComponentProps 187 | export type ColorModeButtonProps = ComponentProps 188 | export type ColorModeImageProps = ComponentProps 189 | export type ColorModeSelectProps = ComponentProps 190 | export type ColorModeSwitchProps = ComponentProps 191 | export type ContentNavigationProps = ComponentProps 192 | export type ContentSearchProps = ComponentProps 193 | export type ContentSearchButtonProps = ComponentProps 194 | export type ContentSurroundProps = ComponentProps 195 | export type ContentTocProps = ComponentProps 196 | export type DashboardGroupProps = ComponentProps 197 | export type DashboardNavbarProps = ComponentProps 198 | export type DashboardPanelProps = ComponentProps 199 | export type DashboardResizeHandleProps = ComponentProps 200 | export type DashboardSearchProps = ComponentProps 201 | export type DashboardSearchButtonProps = ComponentProps 202 | export type DashboardSidebarProps = ComponentProps 203 | export type DashboardSidebarCollapseProps = ComponentProps 204 | export type DashboardSidebarToggleProps = ComponentProps 205 | export type DashboardToolbarProps = ComponentProps 206 | export type ErrorProps = ComponentProps 207 | export type FooterProps = ComponentProps 208 | export type FooterColumnsProps = ComponentProps 209 | export type HeaderProps = ComponentProps 210 | export type LocaleSelectProps = ComponentProps 211 | export type MainProps = ComponentProps 212 | export type PageProps = ComponentProps 213 | export type PageAccordionProps = ComponentProps 214 | export type PageAnchorsProps = ComponentProps 215 | export type PageAsideProps = ComponentProps 216 | export type PageBodyProps = ComponentProps 217 | export type PageCardProps = ComponentProps 218 | export type PageColumnsProps = ComponentProps 219 | export type PageCTAProps = ComponentProps 220 | export type PageFeatureProps = ComponentProps 221 | export type PageGridProps = ComponentProps 222 | export type PageHeaderProps = ComponentProps 223 | export type PageHeroProps = ComponentProps 224 | export type PageLinksProps = ComponentProps 225 | export type PageListProps = ComponentProps 226 | export type PageLogosProps = ComponentProps 227 | export type PageMarqueeProps = ComponentProps 228 | export type PageSectionProps = ComponentProps 229 | export type PricingPlanProps = ComponentProps 230 | export type PricingPlansProps = ComponentProps 231 | export type UserProps = ComponentProps 232 | 233 | type CamelToKebab = T extends `${infer First}${infer Rest}` 234 | ? `${Lowercase}${CamelToKebabInner}` 235 | : T 236 | 237 | type CamelToKebabInner = T extends `${infer First}${infer Rest}` 238 | ? First extends Uppercase 239 | ? `-${Lowercase}${CamelToKebabInner}` 240 | : `${First}${CamelToKebabInner}` 241 | : T 242 | 243 | export type RenderFn = (component: DesignComponent) => DevComponent 244 | 245 | export interface ComponentPropsMap { 246 | UAccordion: AccordionProps 247 | UAlert: AlertProps 248 | UAvatarGroup: AvatarGroupProps 249 | UAvatar: AvatarProps | ChipProps 250 | UBadge: BadgeProps 251 | UBreadcrumb: BreadcrumbProps 252 | UButton: ButtonProps 253 | UButtonGroup: ButtonGroupProps 254 | UCalendar: CalendarProps 255 | UCard: CardProps 256 | UCarousel: CarouselProps 257 | UCheckbox: CheckboxProps 258 | UChip: ChipProps 259 | UCollapsible: CollapsibleProps 260 | UColorPicker: ColorPickerProps 261 | UCommandPalette: CommandPaletteProps 262 | UContainer: ContainerProps 263 | UContextMenu: ContextMenuProps 264 | UDrawer: DrawerProps 265 | UDropdownMenu: DropdownMenuProps 266 | UForm: FormProps 267 | UFormField: FormFieldProps 268 | UIcon: IconProps 269 | UInput: InputProps 270 | UInputMenu: InputMenuProps 271 | UInputNumber: InputNumberProps 272 | UKbd: KbdProps 273 | ULink: LinkProps 274 | UModal: ModalProps 275 | UNavigationMenu: NavigationMenuProps 276 | UPagination: PaginationProps 277 | UPinInput: PinInputProps 278 | UPopover: PopoverProps 279 | UProgress: ProgressProps 280 | URadioGroup: RadioGroupProps 281 | USelect: SelectProps 282 | USelectMenu: SelectMenuProps 283 | USeparator: SeparatorProps 284 | USkeleton: SkeletonProps 285 | USlideover: SlideoverProps 286 | USlider: SliderProps 287 | UStepper: StepperProps 288 | USwitch: SwitchProps 289 | UTable: TableProps 290 | UTabs: TabsProps 291 | UTextarea: TextareaProps 292 | UToast: ToastProps 293 | UTooltip: TooltipProps 294 | 295 | UAuthForm: AuthFormProps 296 | UBanner: BannerProps 297 | UBlogPost: BlogPostProps 298 | UBlogPosts: BlogPostsProps 299 | UColorModeAvatar: ColorModeAvatarProps 300 | UColorModeButton: ColorModeButtonProps 301 | UColorModeImage: ColorModeImageProps 302 | UColorModeSelect: ColorModeSelectProps 303 | UColorModeSwitch: ColorModeSwitchProps 304 | UContentNavigation: ContentNavigationProps 305 | UContentSearch: ContentSearchProps 306 | UContentSearchButton: ContentSearchButtonProps 307 | UContentSurround: ContentSurroundProps 308 | UContentToc: ContentTocProps 309 | UDashboardGroup: DashboardGroupProps 310 | UDashboardNavbar: DashboardNavbarProps 311 | UDashboardPanel: DashboardPanelProps 312 | UDashboardResizeHandle: DashboardResizeHandleProps 313 | UDashboardSearch: DashboardSearchProps 314 | UDashboardSearchButton: DashboardSearchButtonProps 315 | UDashboardSidebar: DashboardSidebarProps 316 | UDashboardSidebarCollapse: DashboardSidebarCollapseProps 317 | UDashboardSidebarToggle: DashboardSidebarToggleProps 318 | UDashboardToolbar: DashboardToolbarProps 319 | UError: ErrorProps 320 | UFooter: FooterProps 321 | UFooterColumns: FooterColumnsProps 322 | UHeader: HeaderProps 323 | ULocaleSelect: LocaleSelectProps 324 | UMain: MainProps 325 | UPage: PageProps 326 | UPageAccordion: PageAccordionProps 327 | UPageAnchors: PageAnchorsProps 328 | UPageAside: PageAsideProps 329 | UPageBody: PageBodyProps 330 | UPageCard: PageCardProps 331 | UPageColumns: PageColumnsProps 332 | UPageCTA: PageCTAProps 333 | UPageFeature: PageFeatureProps 334 | UPageGrid: PageGridProps 335 | UPageHeader: PageHeaderProps 336 | UPageHero: PageHeroProps 337 | UPageLinks: PageLinksProps 338 | UPageList: PageListProps 339 | UPageLogos: PageLogosProps 340 | UPageMarquee: PageMarqueeProps 341 | UPageSection: PageSectionProps 342 | UPricingPlan: PricingPlanProps 343 | UPricingPlans: PricingPlansProps 344 | UUser: UserProps 345 | } 346 | --------------------------------------------------------------------------------