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": {
|
"./style": {
|
||||||
"import": "./src/assets/css/tailwind.css",
|
"import": "./src/assets/css/tailwind.css",
|
||||||
"types": "./src/index.d.ts"
|
"types": "./src/index.d.ts"
|
||||||
|
},
|
||||||
|
"./theme": {
|
||||||
|
"import": "./src/assets/css/tailwind.css",
|
||||||
|
"types": "./src/index.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
--border-lo: #38342a;
|
--border-lo: #38342a;
|
||||||
|
|
||||||
--primary: #d4b278;
|
--primary: #d4b278;
|
||||||
--primary-fg: #fff8e8;
|
--primary-fg: #0e0c08;
|
||||||
|
|
||||||
--warn: #e08840;
|
--warn: #e08840;
|
||||||
--warn-subtle: #281808;
|
--warn-subtle: #281808;
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
--info: #88aec8;
|
--info: #88aec8;
|
||||||
--info-subtle: #101e2c;
|
--info-subtle: #101e2c;
|
||||||
--info-border: #1e3850;
|
--info-border: #1e3850;
|
||||||
--info-fg: #fff8e8;
|
--info-fg: #0a1828;
|
||||||
|
|
||||||
--neutral: #7a6e60;
|
--neutral: #7a6e60;
|
||||||
--neutral-fg: #fff8e8;
|
--neutral-fg: #fff8e8;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { cva } from 'class-variance-authority'
|
||||||
export { default as Badge } from './Badge.vue'
|
export { default as Badge } from './Badge.vue'
|
||||||
|
|
||||||
export const badgeVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { cva } from 'class-variance-authority'
|
||||||
export { default as Button } from './Button.vue'
|
export { default as Button } from './Button.vue'
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
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 { readFileSync } from 'node:fs'
|
||||||
import { resolve } from 'node:path'
|
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', () => {
|
describe('tailwind token mapping', () => {
|
||||||
const css = readFileSync(cssPath, 'utf-8')
|
const css = readFileSync(cssPath, 'utf-8')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Preview } from '@storybook/vue3-vite'
|
import type { Preview } from '@storybook/vue3-vite'
|
||||||
import { useGlobals } from 'storybook/preview-api'
|
import { useGlobals } from 'storybook/preview-api'
|
||||||
import '@nychthemeron/library/style'
|
import '@nychthemeron/library/theme'
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export const WithLabels: Story = {
|
||||||
return { honey, wine, laurel }
|
return { honey, wine, laurel }
|
||||||
},
|
},
|
||||||
template: `
|
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;">
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
<NychCheckbox v-model="honey" /> Honey
|
<NychCheckbox v-model="honey" /> Honey
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -85,7 +85,7 @@ export const Group: Story = {
|
||||||
return { honey, wine, laurel }
|
return { honey, wine, laurel }
|
||||||
},
|
},
|
||||||
template: `
|
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">
|
<NychCheckboxGroup name="offerings">
|
||||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
<NychCheckbox v-model="honey" /> Honey
|
<NychCheckbox v-model="honey" /> Honey
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export const WithLabels: Story = {
|
||||||
return { god }
|
return { god }
|
||||||
},
|
},
|
||||||
template: `
|
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">
|
<NychRadioGroup v-model="god">
|
||||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
<NychRadioGroupItem value="apollo" /> Apollo
|
<NychRadioGroupItem value="apollo" /> Apollo
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue