Compare commits
No commits in common. "main" and "worktree-primevue-to-shadcn-vue" have entirely different histories.
main
...
worktree-p
9 changed files with 13 additions and 209 deletions
|
|
@ -15,6 +15,10 @@
|
|||
"./style": {
|
||||
"import": "./src/assets/css/tailwind.css",
|
||||
"types": "./src/index.d.ts"
|
||||
},
|
||||
"./theme": {
|
||||
"import": "./src/assets/css/tailwind.css",
|
||||
"types": "./src/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
--border-lo: #38342a;
|
||||
|
||||
--primary: #d4b278;
|
||||
--primary-fg: #fff8e8;
|
||||
--primary-fg: #0e0c08;
|
||||
|
||||
--warn: #e08840;
|
||||
--warn-subtle: #281808;
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
--info: #88aec8;
|
||||
--info-subtle: #101e2c;
|
||||
--info-border: #1e3850;
|
||||
--info-fg: #fff8e8;
|
||||
--info-fg: #0a1828;
|
||||
|
||||
--neutral: #7a6e60;
|
||||
--neutral-fg: #fff8e8;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { cva } from 'class-variance-authority'
|
|||
export { default as Badge } from './Badge.vue'
|
||||
|
||||
export const badgeVariants = cva(
|
||||
'inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 text-xs font-serif font-semibold [&>svg]:size-3',
|
||||
'inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 text-xs font-semibold [&>svg]:size-3',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { cva } from 'class-variance-authority'
|
|||
export { default as Button } from './Button.vue'
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg border border-transparent font-serif font-medium transition-all outline-none disabled:pointer-events-none disabled:opacity-50 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
|
||||
'inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg border border-transparent font-medium transition-all outline-none disabled:pointer-events-none disabled:opacity-50 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
|||
|
|
@ -1,200 +0,0 @@
|
|||
import { h, type SetupContext } from 'vue'
|
||||
|
||||
export type LibComponent =
|
||||
| { new (...args: unknown[]): unknown } // Constructor components
|
||||
| { (...args: unknown[]): unknown } // Functional components
|
||||
| Record<string, unknown> // Object components
|
||||
|
||||
export type Props<T> = T extends { new (...args: unknown[]): { $props: infer P } }
|
||||
? P
|
||||
: T extends { props: infer P }
|
||||
? P
|
||||
: string
|
||||
|
||||
export type Key<T> = T extends { new (...args: unknown[]): { $props: infer P } }
|
||||
? keyof P
|
||||
: T extends { props: infer P }
|
||||
? keyof P
|
||||
: string
|
||||
|
||||
// Try to extract unstyled type from component
|
||||
// If extraction fails, accept any value via unknown
|
||||
type ExtractUnstyled<T> = T extends { props: infer P }
|
||||
? P extends { unstyled: infer U }
|
||||
? U
|
||||
: unknown
|
||||
: T extends { new (...args: unknown[]): { $props: infer P } }
|
||||
? P extends { unstyled: infer U }
|
||||
? U
|
||||
: unknown
|
||||
: unknown
|
||||
|
||||
export type Themeable<
|
||||
T extends LibComponent,
|
||||
K extends Key<T> = Key<T>,
|
||||
I extends Key<T> = Key<T>,
|
||||
> = {
|
||||
component: T
|
||||
propMutator?: (props: Record<Key<T>, unknown>) => Record<Key<T>, unknown>
|
||||
classes?: string[]
|
||||
styles?: string[]
|
||||
injectionKeys?: {
|
||||
class?: K
|
||||
style?: I
|
||||
}
|
||||
unstyled?: ExtractUnstyled<T>
|
||||
slots?: Record<string, LibComponent>
|
||||
}
|
||||
|
||||
export type ThemeLibrary<
|
||||
T extends Record<string, Themeable<LibComponent>> = Record<string, Themeable<LibComponent>>,
|
||||
> = {
|
||||
components: T
|
||||
}
|
||||
|
||||
export class ThemeEngine {
|
||||
readonly lib: ThemeLibrary
|
||||
|
||||
constructor(library: ThemeLibrary) {
|
||||
this.lib = library
|
||||
}
|
||||
|
||||
getComponent<I extends LibComponent>(key: keyof ThemeLibrary['components']): I {
|
||||
const { component, classes, styles, injectionKeys, propMutator, unstyled, slots } =
|
||||
this.lib.components[key]
|
||||
|
||||
// Build a MINIMAL pass-through wrapper rather than cloning the wrapped
|
||||
// component. Cloning dragged along the component's `props`, `emits` and
|
||||
// `extends` chain — which made Vue treat listeners like `onUpdate:modelValue`
|
||||
// as the wrapper's own declared events and withhold them from `ctx.attrs`,
|
||||
// silently breaking two-way `v-model`. By declaring only the engine's own
|
||||
// props here, every other prop, listener and attribute falls through
|
||||
// `ctx.attrs` and is forwarded verbatim to the real component below.
|
||||
const componentObj: Record<string, unknown> = {
|
||||
name: `Nych${String(key)}`,
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
injectedClasses: { default: classes },
|
||||
injectedStyles: { default: styles },
|
||||
stylePlug: { type: String },
|
||||
unstyled: { default: unstyled },
|
||||
},
|
||||
}
|
||||
|
||||
const applyThemeProps = (props: Record<string, unknown>) => {
|
||||
const mergedProps = { ...props }
|
||||
|
||||
const stylePlug =
|
||||
typeof mergedProps['stylePlug'] === 'string' ? mergedProps['stylePlug'] : undefined
|
||||
|
||||
const defaultClassValues = Array.isArray(mergedProps['injectedClasses'])
|
||||
? mergedProps['injectedClasses']
|
||||
: []
|
||||
|
||||
const defaultStyleValues = Array.isArray(mergedProps['injectedStyles'])
|
||||
? mergedProps['injectedStyles']
|
||||
: []
|
||||
|
||||
const classKey = injectionKeys?.class ?? 'class'
|
||||
const styleKey = injectionKeys?.style ?? 'style'
|
||||
|
||||
const classValue = mergedProps[classKey] ?? []
|
||||
const styleValue =
|
||||
typeof mergedProps[styleKey] === 'string'
|
||||
? mergedProps[styleKey]
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
let classArray: string[] | undefined = undefined
|
||||
|
||||
if (typeof classValue === 'object') {
|
||||
if (Array.isArray(classValue)) {
|
||||
classArray = classValue
|
||||
} else if (classKey !== 'pt') {
|
||||
// Support Vue's object-form class syntax: { 'my-class': condition }
|
||||
classArray = Object.entries(classValue as Record<string, boolean>)
|
||||
.filter(([, active]) => Boolean(active))
|
||||
.map(([name]) => name)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof classValue === 'string') {
|
||||
classArray = classValue.split(' ')
|
||||
}
|
||||
|
||||
if (!classArray) {
|
||||
classArray = []
|
||||
}
|
||||
|
||||
const finalClasses = stylePlug
|
||||
? [...classArray, ...defaultClassValues]
|
||||
.map((cls) => (cls.startsWith(`${stylePlug}-`) ? cls : `${stylePlug}-${cls}`))
|
||||
.join(' ')
|
||||
: [...classArray, ...defaultClassValues].join(' ')
|
||||
const finalStyles = [...styleValue, ...defaultStyleValues].join('; ')
|
||||
|
||||
if (classKey === 'pt' || styleKey === 'pt') {
|
||||
mergedProps['unstyled'] = true
|
||||
const existingPt =
|
||||
typeof mergedProps[classKey] === 'object'
|
||||
? (mergedProps[classKey] as Record<string, unknown>)
|
||||
: {}
|
||||
const existingRoot = (existingPt.root as { class?: string; style?: string }) ?? {}
|
||||
mergedProps[classKey] = {
|
||||
...existingPt,
|
||||
root: {
|
||||
...existingRoot,
|
||||
class: [existingRoot.class, finalClasses].filter(Boolean).join(' '),
|
||||
style: [existingRoot.style, finalStyles].filter(Boolean).join('; '),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
mergedProps[classKey] = finalClasses
|
||||
}
|
||||
|
||||
if (styleKey !== 'pt') {
|
||||
mergedProps[styleKey] = finalStyles
|
||||
}
|
||||
|
||||
if (propMutator) {
|
||||
// Supports both in-place mutation (returns same ref) and returning a new props object
|
||||
const mutated = propMutator(mergedProps as Record<Key<typeof component>, unknown>)
|
||||
if (mutated !== mergedProps) {
|
||||
Object.assign(mergedProps, mutated)
|
||||
}
|
||||
}
|
||||
|
||||
return mergedProps
|
||||
}
|
||||
|
||||
// Always wrap via a render function so the original component runs its own
|
||||
// setup naturally through Vue's component system with the modified props.
|
||||
// Slots are merged here so no extra wrapper component layer is needed.
|
||||
componentObj.setup = (props: Record<string, unknown>, ctx: SetupContext) => {
|
||||
return () => {
|
||||
const themeSlots: Record<string, unknown> = {}
|
||||
for (const [name, slotComponent] of Object.entries(slots || {})) {
|
||||
themeSlots[name] = (scope?: Record<string, unknown>) => [h(slotComponent, scope || {})]
|
||||
}
|
||||
|
||||
const originalSlots = ctx.slots || {}
|
||||
const mergedSlots = { ...themeSlots, ...originalSlots }
|
||||
|
||||
// Merge attrs into props so the engine sees class/style from fallthrough attrs
|
||||
// before merging theme values — prevents ctx.attrs from overwriting engine output.
|
||||
// Running inside the render function also keeps mergedProps reactive to prop/attr changes.
|
||||
const mergedProps = applyThemeProps({ ...props, ...ctx.attrs })
|
||||
|
||||
for (const key of ['injectedClasses', 'injectedStyles', 'stylePlug']) {
|
||||
delete mergedProps[key as keyof typeof mergedProps]
|
||||
}
|
||||
|
||||
return h(component as LibComponent, mergedProps, mergedSlots)
|
||||
}
|
||||
}
|
||||
|
||||
return componentObj as I
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'
|
|||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const cssPath = resolve(import.meta.dirname, '../src/assets/css/tailwind.css')
|
||||
const cssPath = resolve(process.cwd(), 'src/assets/css/tailwind.css')
|
||||
|
||||
describe('tailwind token mapping', () => {
|
||||
const css = readFileSync(cssPath, 'utf-8')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import { useGlobals } from 'storybook/preview-api'
|
||||
import '@nychthemeron/library/style'
|
||||
import '@nychthemeron/library/theme'
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export const WithLabels: Story = {
|
|||
return { honey, wine, laurel }
|
||||
},
|
||||
template: `
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem; font-family: var(--font-serif); color: var(--text-body);">
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem; font-family: var(--font-sans); color: var(--text-body);">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<NychCheckbox v-model="honey" /> Honey
|
||||
</label>
|
||||
|
|
@ -85,7 +85,7 @@ export const Group: Story = {
|
|||
return { honey, wine, laurel }
|
||||
},
|
||||
template: `
|
||||
<div style="font-family: var(--font-serif); color: var(--text-body);">
|
||||
<div style="font-family: var(--font-sans); color: var(--text-body);">
|
||||
<NychCheckboxGroup name="offerings">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<NychCheckbox v-model="honey" /> Honey
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const WithLabels: Story = {
|
|||
return { god }
|
||||
},
|
||||
template: `
|
||||
<div style="font-family: var(--font-serif); color: var(--text-body);">
|
||||
<div style="font-family: var(--font-sans); color: var(--text-body);">
|
||||
<NychRadioGroup v-model="god">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<NychRadioGroupItem value="apollo" /> Apollo
|
||||
|
|
|
|||
Loading…
Reference in a new issue