Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions integrations/mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## [Unreleased]

## [1.2.0] - 2025-09-04

### Added

- Added `get_component_props` tool to retrieve component props/properties for specific Ark UI components

## [1.1.2] - 2025-09-03

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion integrations/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ark-ui/mcp",
"description": "The official MCP server for Ark UI",
"version": "1.1.2",
"version": "1.2.0",
"main": "dist/stdio.js",
"type": "module",
"license": "MIT",
Expand Down
23 changes: 23 additions & 0 deletions integrations/mcp/src/lib/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
GetComponentPropsResponse,
GetExampleResponse,
GetStyleGuideResponse,
ListComponentExamplesResponse,
Expand Down Expand Up @@ -75,3 +76,25 @@ export async function getStyleGuide(component: string): Promise<GetStyleGuideRes

return response.json() as Promise<GetStyleGuideResponse>
}

export async function getComponentProps({
framework,
component,
}: {
framework: string
component: string
}): Promise<GetComponentPropsResponse> {
const response = await fetch(`https://ark-ui.com/api/types/${framework}/${component}`)

if (!response.ok) {
throw new Error(`Failed to fetch component props: ${response.status} ${response.statusText}`)
}

const props = await response.json()

return {
framework,
component,
props,
}
}
6 changes: 6 additions & 0 deletions integrations/mcp/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export interface GetStyleGuideResponse {
cssVar?: Record<string, string>
}

export interface GetComponentPropsResponse {
framework: string
component: string
props: Record<string, any>
}

export interface Example {
id: string
filename: string
Expand Down
37 changes: 37 additions & 0 deletions integrations/mcp/src/tools/get-component-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from 'zod'
import { fetchComponentList, getComponentProps } from '../lib/fetch.js'
import { FRAMEWORKS, type Tool } from '../lib/types.js'

export const getComponentPropsTool: Tool<{ componentList: string[] }> = {
name: 'get_component_props',
description:
'Get the props/properties for a specific Ark UI component in a given framework. This tool retrieves detailed information about the available props for the specified component.',
ctx: async () => {
const componentList = await fetchComponentList('react') // Default to 'react' for initial context
return { componentList }
},
async exec(server, { name, description, ctx }) {
server.tool(
name,
description,
{
framework: z.enum(FRAMEWORKS).describe('The framework type to get component props for.'),
component: z
.enum(ctx.componentList as [string, ...string[]])
.describe('The name of the component to get props for.'),
},
async ({ framework, component }) => {
const componentProps = await getComponentProps({ framework, component })

return {
content: [
{
type: 'text',
text: JSON.stringify(componentProps, null, 2),
},
],
}
},
)
},
}
3 changes: 2 additions & 1 deletion integrations/mcp/src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import type { Tool, ToolConfig } from '../lib/types.js'
import { getComponentPropsTool } from './get-component-props.js'
import { getExampleTool } from './get-example.js'
import { listComponentsTool } from './list-components.js'
import { listExamplesTool } from './list-examples.js'
import { stylingGuideTool } from './styling-guide.js'

const tools: Tool[] = [listComponentsTool, listExamplesTool, getExampleTool, stylingGuideTool]
const tools: Tool[] = [listComponentsTool, listExamplesTool, getExampleTool, getComponentPropsTool, stylingGuideTool]

const registeredToolCache = new Map<string, Tool>()

Expand Down
5 changes: 4 additions & 1 deletion packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,20 +189,23 @@
"@sveltejs/package": "2.5.0",
"@sveltejs/vite-plugin-svelte": "6.1.3",
"@tanstack/svelte-form": "1.19.2",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.8.0",
"@testing-library/svelte": "5.2.8",
"@testing-library/user-event": "14.6.1",
"@vitest/coverage-v8": "3.2.4",
"clean-package": "2.2.0",
"image-conversion": "2.1.1",
"jsdom": "26.1.0",
"lucide-svelte": "0.541.0",
"storybook": "9.1.3",
"svelte": "5.38.3",
"svelte-check": "4.3.1",
"tslib": "2.8.1",
"typescript": "5.9.2",
"vite": "7.1.3",
"vitest": "3.2.4"
"vitest": "3.2.4",
"vitest-axe": "1.0.0-pre.5"
},
"peerDependencies": {
"svelte": ">=5.20.0"
Expand Down
30 changes: 30 additions & 0 deletions packages/svelte/src/lib/components/accordion/accordion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { render, screen } from '@testing-library/svelte'
import { axe } from 'vitest-axe'
import { describe, expect, it } from 'vitest'
import ComponentUnderTest from './examples/basic.svelte'

describe('Accordion', () => {
it('should have no accessibility violations', async () => {
const { container } = render(ComponentUnderTest)
const results = await axe(container)
expect(results).toHaveNoViolations()
})

it('should render accordion items', async () => {
render(ComponentUnderTest)
expect(screen.getByText('What is React?')).toBeInTheDocument()
expect(screen.getByText('What is Svelte?')).toBeInTheDocument()
})

it('should show default expanded item', () => {
render(ComponentUnderTest)

// React is open by default
const reactContent = screen.getByText('React is a JavaScript library for building user interfaces.')
expect(reactContent).toBeVisible()

// Vue should be closed
const vueContent = screen.getByText('Vue is a JavaScript library for building user interfaces.')
expect(vueContent).not.toBeVisible()
})
})
29 changes: 29 additions & 0 deletions packages/svelte/src/lib/components/select/select.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { render, screen } from '@testing-library/svelte'
import user from '@testing-library/user-event'
import { axe } from 'vitest-axe'
import { describe, expect, it } from 'vitest'
import ComponentUnderTest from './examples/basic.svelte'

describe('Select', () => {
it('should have no accessibility violations', async () => {
const { container } = render(ComponentUnderTest)
const results = await axe(container)
expect(results).toHaveNoViolations()
})

it('should render select with placeholder', () => {
render(ComponentUnderTest)
expect(screen.getByText('Select a Framework')).toBeInTheDocument()
expect(screen.getByText('Framework')).toBeInTheDocument()
})

it('should open dropdown when clicked', async () => {
render(ComponentUnderTest)
const trigger = screen.getByRole('combobox')

await user.click(trigger)

expect(screen.getByRole('option', { name: 'React' })).toBeInTheDocument()
expect(screen.getByRole('option', { name: 'Svelte' })).toBeInTheDocument()
})
})
132 changes: 132 additions & 0 deletions packages/svelte/src/lib/lucide-optimize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Pre-compile regex for better performance
const LUCIDE_IMPORT_PATTERN = /([ \t]*)import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]lucide-svelte['"]/g

/**
* SourceMap interface for transformation output
*/
interface SourceMap {
version: number
sources: string[]
names: string[]
sourceRoot?: string
sourcesContent?: string[]
mappings: string
file: string
}

/**
* Plugin interface matching Vite/Rollup plugin structure
*/
interface SveltePlugin {
name: string
transform: (code: string, id: string) => TransformResult | null | undefined
}

/**
* Result of the transform operation
*/
interface TransformResult {
code: string
map?: SourceMap | null
}

/**
* Creates a Vite/Svelte plugin that optimizes lucide-svelte imports by converting
* destructured imports to direct imports for better tree-shaking
*
* @returns A Svelte plugin that transforms lucide-svelte imports
*/
export function lucideOptimizeImports(): SveltePlugin {
return {
name: 'lucide-svelte-optimizer',
transform(sourceCode: string, filePath: string): TransformResult | null | undefined {
if (!isValidInput(sourceCode, filePath)) return null

try {
// Quick check if the file contains lucide-svelte imports
if (!sourceCode.includes('lucide-svelte')) return null

const { transformedCode, hasChanges } = transformLucideImports(sourceCode)

if (hasChanges) {
return {
code: transformedCode,
map: null, // No source maps in this implementation
}
}

return null
} catch (error) {
handleTransformError(error)
return null
}
},
}
}

/**
* Validates the input parameters for processing
*/
function isValidInput(code: string, id: string): boolean {
return Boolean(code && id)
}

/**
* Transforms lucide-svelte imports from destructured to individual imports
*/
function transformLucideImports(sourceCode: string): { transformedCode: string; hasChanges: boolean } {
let hasChanges = false

const transformedCode = sourceCode.replace(
LUCIDE_IMPORT_PATTERN,
(match: string, indentation: string, importNames: string): string => {
if (!importNames.trim()) return match

const semicolonAtEnd = match.endsWith(';')
const individualImports = convertToIndividualImports(importNames, indentation, semicolonAtEnd)

if (individualImports) {
hasChanges = true
return individualImports
}

return match
},
)

return { transformedCode, hasChanges }
}

/**
* Converts a comma-separated list of imports to individual import statements
*/
function convertToIndividualImports(importNames: string, indentation: string, withSemicolon: boolean): string {
return importNames
.split(',')
.map((name) => name.trim())
.filter(Boolean)
.map((name) => {
const kebabCasePath = convertToKebabCase(name)
const semicolon = withSemicolon ? ';' : ''
return `${indentation}import ${name} from 'lucide-svelte/icons/${kebabCasePath}'${semicolon}`
})
.join('\n')
}

/**
* Converts a camelCase or PascalCase string to kebab-case
*/
function convertToKebabCase(str: string): string {
return str
.replace(/Icon$/, '') // Remove 'Icon' suffix
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase()
}

/**
* Handles and logs transformation errors
*/
function handleTransformError(error: unknown): void {
const typedError = error instanceof Error ? error : new Error(String(error))
console.error('Error in lucide-svelte-optimizer plugin:', typedError)
}
Loading