chore: remove PrimeVue and the ThemeEngine, migration complete
- Delete packages/library/src/engine/ and engine.spec.ts; LoadingIcon is now exported directly instead of routed through a generic theming engine. - Remove primevue from every package.json (root devDependencies, library peerDependencies, playground dependencies) and from vite.config.ts's externalized/global Rollup config; add reka-ui as the library's peer dep. - Delete the hand-written nychthemeron.css/theme.css, now fully superseded by the Tailwind entry CSS; both library exports (./style, ./theme) point at it. - Drop the PrimeVue plugin install from Storybook's preview.ts. - Add a `@source "../../";` directive to tailwind.css: Tailwind's automatic content detection is scoped to whichever Vite project runs it, so when the playground consumes this CSS file it never scanned the sibling packages/library/src tree for class usage -- every component rendered with zero utility classes applied until this was added (verified via the compiled Storybook CSS output going from ~2KB/no .bg-primary rule to ~40KB with the full utility set, and the pre-fix screenshot that surfaced it: buttons and other components with a real background color showing as bare, near-invisible text on white). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7bb554aeb0
commit
ff02f7ec72
13 changed files with 13 additions and 2043 deletions
17
bun.lock
17
bun.lock
|
|
@ -24,7 +24,6 @@
|
|||
"oxlint": "~1.69.0",
|
||||
"playwright": "^1.60.0",
|
||||
"prettier": "3.8.4",
|
||||
"primevue": "^4.5.5",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"vite-plugin-vue-devtools": "^8.1.2",
|
||||
|
|
@ -42,7 +41,6 @@
|
|||
"@vueuse/core": "^14.3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"reka-ui": "^2.10.1",
|
||||
"shadcn-vue": "^2.7.4",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.2",
|
||||
|
|
@ -54,7 +52,7 @@
|
|||
"vite-plugin-dts": "^5.0.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"primevue": "^4.5.0",
|
||||
"reka-ui": "^2.10.0",
|
||||
"vue": "^3.5.0",
|
||||
},
|
||||
},
|
||||
|
|
@ -64,7 +62,6 @@
|
|||
"dependencies": {
|
||||
"@nychthemeron/library": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"primevue": "^4.5.0",
|
||||
"vue": "^3.5.32",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -433,16 +430,6 @@
|
|||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@primeuix/styled": ["@primeuix/styled@0.7.4", "", { "dependencies": { "@primeuix/utils": "^0.6.1" } }, "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ=="],
|
||||
|
||||
"@primeuix/styles": ["@primeuix/styles@2.0.3", "", { "dependencies": { "@primeuix/styled": "^0.7.4" } }, "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw=="],
|
||||
|
||||
"@primeuix/utils": ["@primeuix/utils@0.6.4", "", {}, "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg=="],
|
||||
|
||||
"@primevue/core": ["@primevue/core@4.5.5", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/utils": "^0.6.2" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-JpkXhq1ddc70JdsC3CC4dM+UbeeWuCW/8DpS9dNBfrOk824TLSlRlMEGFyVKqRMn5WPQvYLiy3xXfLQeNdSqhQ=="],
|
||||
|
||||
"@primevue/icons": ["@primevue/icons@4.5.5", "", { "dependencies": { "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.5" } }, "sha512-eteOhTdAOXEYE9qW1AOrBBgDxQ2szHJxSkEK1XVdV2TKxGM5FQf03Ovms0VDyZTc16XBIgvwYjXJQS0BPbhPaA=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="],
|
||||
|
|
@ -1415,8 +1402,6 @@
|
|||
|
||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||
|
||||
"primevue": ["primevue@4.5.5", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^2.0.3", "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.5", "@primevue/icons": "4.5.5" } }, "sha512-Kv5REIewCdP806QaoU+4nBXfmpzOGFKkZ9qH4KsL6MjiAQVc4PUzypt8erl4r3Vzh3nr3aWZIxkxYRRsLGiX2A=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
|
||||
"promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="],
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
"oxlint": "~1.69.0",
|
||||
"playwright": "^1.60.0",
|
||||
"prettier": "3.8.4",
|
||||
"primevue": "^4.5.5",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"vite-plugin-vue-devtools": "^8.1.2",
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@
|
|||
"types": "./src/index.d.ts"
|
||||
},
|
||||
"./style": {
|
||||
"import": "./src/assets/css/nychthemeron.css",
|
||||
"import": "./src/assets/css/tailwind.css",
|
||||
"types": "./src/index.d.ts"
|
||||
},
|
||||
"./theme": {
|
||||
"import": "./src/assets/theme/theme.css",
|
||||
"import": "./src/assets/css/tailwind.css",
|
||||
"types": "./src/index.d.ts"
|
||||
}
|
||||
},
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0",
|
||||
"primevue": "^4.5.0"
|
||||
"reka-ui": "^2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^2.0.0",
|
||||
|
|
@ -45,7 +45,6 @@
|
|||
"@vueuse/core": "^14.3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"reka-ui": "^2.10.1",
|
||||
"shadcn-vue": "^2.7.4",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.2",
|
||||
|
|
|
|||
|
|
@ -1,169 +0,0 @@
|
|||
/* ============================================================
|
||||
nychthemeron.css
|
||||
Toggle themes by setting data-theme on <html>:
|
||||
<html data-theme="hades">
|
||||
<html data-theme="apollo">
|
||||
============================================================ */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500&display=swap');
|
||||
|
||||
/* ── HADES ───────────────────────────────────────────────── */
|
||||
[data-theme='hades'] {
|
||||
/* surfaces */
|
||||
--surface-0: #18160f;
|
||||
--surface-1: #28241a;
|
||||
--surface-2: #2e2a1e;
|
||||
--surface-3: #322e22;
|
||||
--track: #32302a;
|
||||
|
||||
/* borders */
|
||||
--border: #4a4538;
|
||||
--border-hi: #5a5040;
|
||||
--border-lo: #38342a;
|
||||
|
||||
/* primary */
|
||||
--primary: #d4b278;
|
||||
--primary-fg: #0e0c08;
|
||||
|
||||
/* semantic */
|
||||
--warn: #e08840; /* phlegethon */
|
||||
--warn-subtle: #281808;
|
||||
--warn-border: #4a2c10;
|
||||
--warn-fg: #fff8e8;
|
||||
|
||||
--danger: #7a1a1a; /* plague */
|
||||
--danger-subtle: #200808;
|
||||
--danger-border: #3a1010;
|
||||
--danger-fg: #fff8e8;
|
||||
|
||||
--info: #88aec8; /* cocytus */
|
||||
--info-subtle: #101e2c;
|
||||
--info-border: #1e3850;
|
||||
--info-fg: #0a1828;
|
||||
|
||||
--neutral: #7a6e60; /* acheron */
|
||||
--neutral-fg: #fff8e8;
|
||||
|
||||
--success: #6aaa7a; /* laurel */
|
||||
--success-subtle: #0e2214;
|
||||
--success-border: #1e4228;
|
||||
--success-fg: #fff8e8;
|
||||
|
||||
/* text */
|
||||
--text-high: #f4f0e8;
|
||||
--text-body: #d0c8b8;
|
||||
--text-muted: #908878;
|
||||
--text-dim: #807868;
|
||||
--text-label: #c0b8a8;
|
||||
|
||||
/* focus ring */
|
||||
--focus-ring: 0 0 0 3px rgba(212, 178, 120, 0.65);
|
||||
|
||||
/* fonts */
|
||||
--font-serif: 'Cinzel', Georgia, serif;
|
||||
--font-sans: 'IBM Plex Sans', system-ui, sans-serif;
|
||||
--font-mono: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
/* ── APOLLO ──────────────────────────────────────────────── */
|
||||
[data-theme='apollo'] {
|
||||
/* surfaces */
|
||||
--surface-0: #faf7f2;
|
||||
--surface-1: #f4f0e8;
|
||||
--surface-2: #eee9df;
|
||||
--surface-3: #e4ddd0;
|
||||
--track: #e4ddd0;
|
||||
|
||||
/* borders */
|
||||
--border: #bab2a0;
|
||||
--border-hi: #c0b8a8;
|
||||
--border-lo: #ccc4b4;
|
||||
|
||||
/* primary */
|
||||
--primary: #b8860b;
|
||||
--primary-fg: #fff8e8;
|
||||
|
||||
/* semantic */
|
||||
--warn: #9a6020; /* harmony */
|
||||
--warn-subtle: #fdf3e0;
|
||||
--warn-border: #e8d0a0;
|
||||
--warn-fg: #fff8e8;
|
||||
|
||||
--danger: #7a1a1a; /* plague */
|
||||
--danger-subtle: #faeaea;
|
||||
--danger-border: #c8a0a0;
|
||||
--danger-fg: #fff8e8;
|
||||
|
||||
--info: #3a5a8a; /* oracle */
|
||||
--info-subtle: #e8eef8;
|
||||
--info-border: #b0c4e0;
|
||||
--info-fg: #fff8e8;
|
||||
|
||||
--neutral: #8a8070; /* marble */
|
||||
--neutral-fg: #fff8e8;
|
||||
|
||||
--success: #5a7a40; /* laurel */
|
||||
--success-subtle: #eef4e8;
|
||||
--success-border: #c0d4a8;
|
||||
--success-fg: #fff8e8;
|
||||
|
||||
/* text */
|
||||
--text-high: #1a1610;
|
||||
--text-body: #3a3020;
|
||||
--text-muted: #5a5040;
|
||||
--text-dim: #8a8070;
|
||||
--text-label: #5a5040;
|
||||
|
||||
/* focus ring */
|
||||
--focus-ring: 0 0 0 3px rgba(184, 134, 11, 0.65);
|
||||
|
||||
/* fonts */
|
||||
--font-serif: 'Cinzel', Georgia, serif;
|
||||
--font-sans: 'IBM Plex Sans', system-ui, sans-serif;
|
||||
--font-mono: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
/* ── BASE RESET ──────────────────────────────────────────── */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
[class^='nych-'],
|
||||
[class^='nych-']::before,
|
||||
[class^='nych-']::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
[class^='nych-']:focus-visible {
|
||||
outline: 2px solid transparent; /* visible in forced-colors / Windows High Contrast */
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--surface-0);
|
||||
color: var(--text-body);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@keyframes nych-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.nych-loading-icon .wreath {
|
||||
transform-origin: 340px 180px;
|
||||
animation: nych-spin 2s linear infinite;
|
||||
}
|
||||
|
|
@ -12,6 +12,12 @@
|
|||
@import "tw-animate-css";
|
||||
@import "shadcn-vue/tailwind.css";
|
||||
|
||||
/* Explicit source: this file is consumed by sibling workspace packages (e.g.
|
||||
packages/playground) whose own Vite root Tailwind's automatic content
|
||||
detection never walks into, so class usage in src/components/** would
|
||||
otherwise be invisible to the scanner. */
|
||||
@source "../../";
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-sans);
|
||||
--font-serif: var(--font-serif);
|
||||
|
|
|
|||
|
|
@ -1,727 +0,0 @@
|
|||
@import url('../css/nychthemeron.css');
|
||||
|
||||
/* Restore serif font for nych- components even if consumer overrides html font-family */
|
||||
[class^='nych-'] {
|
||||
font-family: var(--font-serif);
|
||||
}
|
||||
|
||||
[class^='nych-']:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
[class^='nych-button'] {
|
||||
padding: 7px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 10px var(--text-high);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
[class^='nych-button']:hover:not(:disabled) {
|
||||
box-shadow: 0px 0px 8px var(--text-high);
|
||||
}
|
||||
|
||||
[class^='nych-button'][data-p~='loading'] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
[class^='nych-button'][data-p~='loading'] span {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
[class^='nych-'][class$='-primary'] {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
[class^='nych-'][class$='-secondary'],
|
||||
[class^='nych-'][class$='-muted'] {
|
||||
background-color: var(--neutral);
|
||||
color: var(--neutral-fg);
|
||||
}
|
||||
|
||||
[class^='nych-'][class$='-info'] {
|
||||
background-color: var(--info);
|
||||
color: var(--info-fg);
|
||||
}
|
||||
|
||||
[class^='nych-'][class$='-success'] {
|
||||
background-color: var(--success);
|
||||
color: var(--success-fg);
|
||||
}
|
||||
|
||||
[class^='nych-'][class$='-warning'] {
|
||||
background-color: var(--warn);
|
||||
color: var(--warn-fg);
|
||||
}
|
||||
|
||||
[class^='nych-'][class$='-danger'],
|
||||
[class^='nych-'][class$='-error'] {
|
||||
background-color: var(--danger);
|
||||
color: var(--danger-fg);
|
||||
}
|
||||
|
||||
.nych-loading-icon .wreath path {
|
||||
fill: var(--primary-fg);
|
||||
}
|
||||
|
||||
.nych-loading-icon .wreath line {
|
||||
stroke: var(--text-high);
|
||||
}
|
||||
|
||||
button[data-p~='loading'] > .nych-loading-icon {
|
||||
margin-right: 5px;
|
||||
margin-top: 0px;
|
||||
height: 25px;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
[class^='nych-'][data-p~='small'] :not(svg, svg *) {
|
||||
height: 15px !important;
|
||||
font-size: 8pt !important;
|
||||
}
|
||||
|
||||
[class^='nych-'][data-p~='normal'] :not(svg, svg *) {
|
||||
height: 25px !important;
|
||||
font-size: 12pt !important;
|
||||
}
|
||||
|
||||
[class^='nych-'][data-p~='large'] :not(svg, svg *) {
|
||||
height: 40px !important;
|
||||
font-size: 20pt !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Text inputs — InputText, Textarea
|
||||
============================================================ */
|
||||
.nych-input,
|
||||
.nych-textarea {
|
||||
font-family: var(--font-sans);
|
||||
width: 100%;
|
||||
padding: 8px 11px;
|
||||
color: var(--text-body);
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.nych-input::placeholder,
|
||||
.nych-textarea::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.nych-input:hover:not(:disabled),
|
||||
.nych-textarea:hover:not(:disabled) {
|
||||
border-color: var(--border-hi);
|
||||
}
|
||||
|
||||
.nych-input:focus,
|
||||
.nych-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.nych-input[data-p~='invalid'],
|
||||
.nych-textarea[data-p~='invalid'] {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.nych-input[data-p~='small'] {
|
||||
padding: 5px 8px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.nych-input[data-p~='large'] {
|
||||
padding: 11px 13px;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.nych-textarea {
|
||||
resize: vertical;
|
||||
min-height: 84px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Checkbox
|
||||
============================================================ */
|
||||
.nych-checkbox,
|
||||
.nych-radio {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nych-checkbox-input,
|
||||
.nych-radio-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nych-checkbox-box,
|
||||
.nych-radio-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid var(--border);
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.nych-checkbox-box {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nych-radio-box {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nych-checkbox:hover .nych-checkbox-box,
|
||||
.nych-radio:hover .nych-radio-box {
|
||||
border-color: var(--border-hi);
|
||||
}
|
||||
|
||||
.nych-checkbox[data-p-checked='true'] .nych-checkbox-box {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.nych-checkbox-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
color: var(--primary-fg);
|
||||
fill: var(--primary-fg);
|
||||
stroke: var(--primary-fg);
|
||||
}
|
||||
|
||||
.nych-checkbox[data-p-disabled='true'] .nych-checkbox-box,
|
||||
.nych-radio[data-p-disabled='true'] .nych-radio-box {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Group containers (CheckboxGroup / RadioButtonGroup) — layout only. */
|
||||
.nych-checkbox-group,
|
||||
.nych-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.nych-checkbox-input:focus-visible + .nych-checkbox-box,
|
||||
.nych-radio-input:focus-visible + .nych-radio-box {
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
/* Keep the visually-hidden native control hidden even when disabled — the
|
||||
global [class^='nych-']:disabled opacity would otherwise reveal it. */
|
||||
.nych-checkbox .nych-checkbox-input,
|
||||
.nych-radio .nych-radio-input,
|
||||
.nych-toggleswitch .nych-toggleswitch-input {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Radio button
|
||||
============================================================ */
|
||||
.nych-radio-icon {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.nych-radio[data-p-checked='true'] .nych-radio-box {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.nych-radio[data-p-checked='true'] .nych-radio-icon {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Toggle switch
|
||||
============================================================ */
|
||||
.nych-toggleswitch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nych-toggleswitch-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nych-toggleswitch-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--track);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.nych-toggleswitch-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transform: translateY(-50%);
|
||||
background-color: var(--text-muted);
|
||||
border-radius: 50%;
|
||||
transition:
|
||||
left 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.nych-toggleswitch[data-p-checked='true'] .nych-toggleswitch-slider {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.nych-toggleswitch[data-p-checked='true'] .nych-toggleswitch-handle {
|
||||
left: 25px;
|
||||
background-color: var(--primary-fg);
|
||||
}
|
||||
|
||||
.nych-toggleswitch-input:focus-visible + .nych-toggleswitch-slider {
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.nych-toggleswitch[data-p-disabled='true'] .nych-toggleswitch-slider,
|
||||
.nych-toggleswitch[data-p-disabled='true'] .nych-toggleswitch-handle {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Select
|
||||
============================================================ */
|
||||
.nych-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 13rem;
|
||||
font-family: var(--font-sans);
|
||||
padding: 8px 11px;
|
||||
color: var(--text-body);
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.nych-select:hover {
|
||||
border-color: var(--border-hi);
|
||||
}
|
||||
|
||||
.nych-select[data-p~='focus'] {
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.nych-select[data-p~='disabled'] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.nych-select-label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nych-select-label[data-p~='placeholder'] {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.nych-select-dropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.nych-select-dropdownIcon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.nych-select-overlay {
|
||||
font-family: var(--font-sans);
|
||||
margin-top: 4px;
|
||||
background-color: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nych-select-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
max-height: 14rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nych-select-option {
|
||||
padding: 8px 11px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-body);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.nych-select-option[data-p-focused='true'],
|
||||
.nych-select-option:hover {
|
||||
background-color: var(--surface-3);
|
||||
}
|
||||
|
||||
.nych-select-option[data-p-selected='true'] {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
.nych-select-emptyMessage {
|
||||
padding: 8px 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Card
|
||||
============================================================ */
|
||||
.nych-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
|
||||
overflow: hidden;
|
||||
color: var(--text-body);
|
||||
}
|
||||
|
||||
.nych-card-header :where(img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nych-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.nych-card-caption {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nych-card-title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text-high);
|
||||
}
|
||||
|
||||
.nych-card-subtitle {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.nych-card-content {
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.nych-card-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Message
|
||||
============================================================ */
|
||||
.nych-message {
|
||||
color: var(--text-body);
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nych-message-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 14px;
|
||||
}
|
||||
|
||||
.nych-message-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.nych-message-text {
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.nych-message-closeButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.nych-message-closeButton:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(128, 128, 128, 0.18);
|
||||
}
|
||||
|
||||
.nych-message-closeIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* Severity tints are mixed from the semantic colour into the surface so they
|
||||
read consistently in both the dark (Hades) and light (Apollo) themes,
|
||||
rather than relying on the very-dark *-subtle tokens that muddied Hades. */
|
||||
.nych-message[data-p~='info'] {
|
||||
background-color: color-mix(in srgb, var(--info) 15%, var(--surface-1));
|
||||
border-color: color-mix(in srgb, var(--info) 30%, var(--border));
|
||||
border-left-color: var(--info);
|
||||
}
|
||||
.nych-message[data-p~='info'] .nych-message-icon {
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.nych-message[data-p~='success'] {
|
||||
background-color: color-mix(in srgb, var(--success) 15%, var(--surface-1));
|
||||
border-color: color-mix(in srgb, var(--success) 30%, var(--border));
|
||||
border-left-color: var(--success);
|
||||
}
|
||||
.nych-message[data-p~='success'] .nych-message-icon {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.nych-message[data-p~='warn'] {
|
||||
background-color: color-mix(in srgb, var(--warn) 16%, var(--surface-1));
|
||||
border-color: color-mix(in srgb, var(--warn) 32%, var(--border));
|
||||
border-left-color: var(--warn);
|
||||
}
|
||||
.nych-message[data-p~='warn'] .nych-message-icon {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.nych-message[data-p~='error'] {
|
||||
background-color: color-mix(in srgb, var(--danger) 20%, var(--surface-1));
|
||||
border-color: color-mix(in srgb, var(--danger) 38%, var(--border));
|
||||
border-left-color: var(--danger);
|
||||
}
|
||||
.nych-message[data-p~='error'] .nych-message-icon {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.nych-message[data-p~='secondary'] {
|
||||
background-color: var(--surface-2);
|
||||
border-left-color: var(--neutral);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Tag
|
||||
============================================================ */
|
||||
.nych-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
.nych-tag[data-p~='rounded'] {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.nych-tag-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.nych-tag[data-p~='secondary'] {
|
||||
background-color: var(--neutral);
|
||||
color: var(--neutral-fg);
|
||||
}
|
||||
|
||||
.nych-tag[data-p~='info'] {
|
||||
background-color: var(--info);
|
||||
color: var(--info-fg);
|
||||
}
|
||||
|
||||
.nych-tag[data-p~='success'] {
|
||||
background-color: var(--success);
|
||||
color: var(--success-fg);
|
||||
}
|
||||
|
||||
.nych-tag[data-p~='warn'] {
|
||||
background-color: var(--warn);
|
||||
color: var(--warn-fg);
|
||||
}
|
||||
|
||||
.nych-tag[data-p~='danger'] {
|
||||
background-color: var(--danger);
|
||||
color: var(--danger-fg);
|
||||
}
|
||||
|
||||
.nych-tag[data-p~='contrast'] {
|
||||
background-color: var(--text-high);
|
||||
color: var(--surface-0);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Dialog
|
||||
============================================================ */
|
||||
.nych-dialog-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.nych-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 92vw;
|
||||
max-height: 90vh;
|
||||
color: var(--text-body);
|
||||
background-color: var(--surface-1);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nych-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-lo);
|
||||
}
|
||||
|
||||
.nych-dialog-title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.3rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text-high);
|
||||
}
|
||||
|
||||
.nych-dialog-headerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nych-dialog-headerActions button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.nych-dialog-headerActions button:hover {
|
||||
color: var(--text-high);
|
||||
background-color: var(--surface-3);
|
||||
}
|
||||
|
||||
.nych-dialog-headerActions button svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.nych-dialog-content {
|
||||
font-family: var(--font-sans);
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nych-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--border-lo);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,3 @@
|
|||
export {
|
||||
ThemeEngine,
|
||||
type ThemeLibrary,
|
||||
type Themeable,
|
||||
type LibComponent,
|
||||
type Key,
|
||||
type Props,
|
||||
} from './engine'
|
||||
export {
|
||||
createNychthemeron,
|
||||
Button,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { App } from 'vue'
|
||||
import { ThemeLibrary, type Themeable, ThemeEngine } from '../engine'
|
||||
import {
|
||||
NychLoadingIcon,
|
||||
Button,
|
||||
|
|
@ -89,20 +88,7 @@ export {
|
|||
DialogScrollContent,
|
||||
}
|
||||
|
||||
const loadingIconThemeable: Themeable<typeof NychLoadingIcon> = {
|
||||
component: NychLoadingIcon,
|
||||
unstyled: false,
|
||||
}
|
||||
|
||||
const nychthemeron: ThemeLibrary = {
|
||||
components: {
|
||||
LoadingIcon: loadingIconThemeable,
|
||||
},
|
||||
}
|
||||
|
||||
const _engine = new ThemeEngine(nychthemeron)
|
||||
|
||||
export const LoadingIcon = _engine.getComponent<typeof NychLoadingIcon>('LoadingIcon')
|
||||
export const LoadingIcon = NychLoadingIcon
|
||||
|
||||
export const createNychthemeron = (): {
|
||||
Button: typeof Button
|
||||
|
|
|
|||
|
|
@ -1,894 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
import { h, ref } from 'vue'
|
||||
|
||||
import { ThemeEngine, ThemeLibrary, type Themeable } from '../src/engine'
|
||||
|
||||
const getTheme = () => {
|
||||
const button: Themeable<typeof Button> = {
|
||||
classes: ['test-class'],
|
||||
styles: ['width: 50px'],
|
||||
propMutator: (props) => {
|
||||
if (props.variant) {
|
||||
const pt = props.pt as { root: { class?: string } }
|
||||
|
||||
const classes = pt.root.class?.split(' ') ?? []
|
||||
|
||||
classes.push(`test-${props.variant}`)
|
||||
|
||||
pt.root.class = classes.join(' ')
|
||||
|
||||
props.pt = pt
|
||||
}
|
||||
|
||||
return props
|
||||
},
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
component: Button,
|
||||
}
|
||||
|
||||
const buttonTheme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: button,
|
||||
},
|
||||
}
|
||||
return buttonTheme
|
||||
}
|
||||
|
||||
const getSimpleTheme = () => {
|
||||
const button: Themeable<typeof Button> = {
|
||||
classes: ['btn-default'],
|
||||
styles: ['padding: 8px'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
component: Button,
|
||||
}
|
||||
|
||||
return {
|
||||
components: {
|
||||
Button: button,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
const callSetup = (component: any, props: Record<string, unknown>) => {
|
||||
// Apply prop defaults from the component definition
|
||||
const mergedProps: Record<string, unknown> = {}
|
||||
|
||||
if (component.props) {
|
||||
for (const [key, propDef] of Object.entries(component.props)) {
|
||||
if (typeof propDef === 'object' && propDef !== null && 'default' in propDef) {
|
||||
mergedProps[key] = propDef.default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge in user-provided props
|
||||
Object.assign(mergedProps, props)
|
||||
|
||||
if (component.setup) {
|
||||
const result = component.setup(mergedProps, { slots: {}, attrs: {} })
|
||||
// Engine always returns a render function; call it and return the vnode's props
|
||||
// so tests can inspect the final merged props passed to the original component.
|
||||
if (typeof result === 'function') {
|
||||
const vnode = result()
|
||||
return (vnode?.props as Record<string, unknown>) || {}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return mergedProps
|
||||
}
|
||||
|
||||
describe('ThemeEngine', () => {
|
||||
it('instantiates properly', () => {
|
||||
const theme = getTheme()
|
||||
const engine = new ThemeEngine(theme)
|
||||
|
||||
expect(engine).not.toBeNull()
|
||||
expect(engine.lib).toBe(theme)
|
||||
})
|
||||
|
||||
it('returns a wrapped component', () => {
|
||||
const theme = getTheme()
|
||||
const engine = new ThemeEngine(theme)
|
||||
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
expect(themedButton).not.toBeNull()
|
||||
expect(themedButton.props).toBeDefined()
|
||||
expect(themedButton.setup).toBeDefined()
|
||||
})
|
||||
|
||||
it('injects classes from theme definition', () => {
|
||||
const theme = getSimpleTheme()
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.class).toContain('btn-default')
|
||||
})
|
||||
|
||||
it('injects styles from theme definition', () => {
|
||||
const theme = getSimpleTheme()
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.style).toContain('padding: 8px')
|
||||
})
|
||||
|
||||
it('applies propMutator to modify component props', () => {
|
||||
const theme = getTheme()
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {},
|
||||
variant: 'primary',
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.class).toContain('test-primary')
|
||||
})
|
||||
|
||||
it('handles PT injection key for PrimeVue components', () => {
|
||||
const theme = getTheme()
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt).toBeDefined()
|
||||
expect(result.pt.root).toBeDefined()
|
||||
expect(result.pt.root.class).toBeDefined()
|
||||
expect(result.pt.root.style).toBeDefined()
|
||||
})
|
||||
|
||||
it('applies class prefix when stylePlug is provided', () => {
|
||||
const simpleTheme = getSimpleTheme()
|
||||
const engine = new ThemeEngine(simpleTheme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
stylePlug: 'custom',
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.class).toContain('custom-btn-default')
|
||||
})
|
||||
|
||||
it('merges user-provided PT with theme-injected classes', () => {
|
||||
const simpleTheme = getSimpleTheme()
|
||||
const engine = new ThemeEngine(simpleTheme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {
|
||||
label: {
|
||||
class: 'user-label-class',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Theme classes should be injected to root
|
||||
expect(result.pt?.root?.class).toContain('btn-default')
|
||||
// User-provided PT structure should be preserved
|
||||
expect(result.pt?.label?.class).toBe('user-label-class')
|
||||
})
|
||||
|
||||
it('preserves existing PT structure when merging', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {
|
||||
root: { class: 'existing' },
|
||||
label: { class: 'label-class' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.pt?.root).toBeDefined()
|
||||
expect(result.pt?.label?.class).toBe('label-class')
|
||||
})
|
||||
|
||||
it('preserves theme classes when user provides pt.root', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['theme-class'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: { root: { class: 'user-root' } },
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.class).toContain('theme-class')
|
||||
expect(result.pt?.root?.class).toContain('user-root')
|
||||
})
|
||||
|
||||
it('handles missing injectionKeys by defaulting to class and style', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['default-class'],
|
||||
styles: ['color: red'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {})
|
||||
|
||||
expect(result.class).toBe('default-class')
|
||||
expect(result.style).toBe('color: red')
|
||||
})
|
||||
|
||||
it('supports unstyled prop passthrough', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
unstyled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
expect(themedButton.props.unstyled.default).toBe(true)
|
||||
})
|
||||
|
||||
it('combines multiple classes in correct order', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['class-a', 'class-b', 'class-c'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.class).toBe('class-a class-b class-c')
|
||||
})
|
||||
|
||||
it('combines multiple styles correctly', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
styles: ['padding: 8px', 'margin: 4px', 'border: 1px solid'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.style).toBe('padding: 8px; margin: 4px; border: 1px solid')
|
||||
})
|
||||
|
||||
it('properly injects injectedClasses and injectedStyles props', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['injected-class'],
|
||||
styles: ['injected-style'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
expect(themedButton.props.injectedClasses).toBeDefined()
|
||||
expect(themedButton.props.injectedStyles).toBeDefined()
|
||||
expect(themedButton.props.injectedClasses.default).toEqual(['injected-class'])
|
||||
expect(themedButton.props.injectedStyles.default).toEqual(['injected-style'])
|
||||
})
|
||||
|
||||
it('applies non-PT class injection correctly', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['my-class'],
|
||||
styles: ['my-style'],
|
||||
injectionKeys: {
|
||||
class: 'class',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {})
|
||||
|
||||
expect(result.class).toBe('my-class')
|
||||
expect(result.style).toBe('my-style')
|
||||
})
|
||||
|
||||
it('handles object-form class values', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['theme-class'],
|
||||
injectionKeys: {
|
||||
class: 'class',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
class: { 'active-class': true, 'inactive-class': false },
|
||||
})
|
||||
|
||||
expect(result.class).toContain('active-class')
|
||||
expect(result.class).not.toContain('inactive-class')
|
||||
expect(result.class).toContain('theme-class')
|
||||
})
|
||||
|
||||
it('merges array and string classes correctly', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['theme-class'],
|
||||
injectionKeys: {
|
||||
class: 'class',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result1 = callSetup(themedButton, {
|
||||
class: ['user-class-1', 'user-class-2'],
|
||||
})
|
||||
|
||||
expect(result1.class).toContain('user-class-1')
|
||||
expect(result1.class).toContain('user-class-2')
|
||||
expect(result1.class).toContain('theme-class')
|
||||
|
||||
const result2 = callSetup(themedButton, {
|
||||
class: 'user-string-class',
|
||||
})
|
||||
|
||||
expect(result2.class).toContain('user-string-class')
|
||||
expect(result2.class).toContain('theme-class')
|
||||
})
|
||||
|
||||
it('wraps components with custom slots', () => {
|
||||
const MockSlot = { name: 'MockSlot', template: '<div>Mock Slot</div>' }
|
||||
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
slots: {
|
||||
icon: MockSlot,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
expect(themedButton.setup).toBeDefined()
|
||||
expect(themedButton.setup({}, {})).toBeDefined()
|
||||
})
|
||||
|
||||
it('slot wrapper returns a render function', () => {
|
||||
const MockSlot = { name: 'MockSlot', template: '<div>Mock Slot</div>' }
|
||||
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
slots: {
|
||||
icon: MockSlot,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const setupResult = themedButton.setup({}, {})
|
||||
|
||||
expect(typeof setupResult).toBe('function')
|
||||
})
|
||||
|
||||
it('combines slots with classes and styles', () => {
|
||||
const MockSlot = { name: 'MockSlot' }
|
||||
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['btn-class'],
|
||||
styles: ['color: blue'],
|
||||
slots: {
|
||||
icon: MockSlot,
|
||||
},
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
// With slots, setup returns a render function, not props
|
||||
const setupResult = themedButton.setup(
|
||||
{
|
||||
injectedClasses: ['btn-class'],
|
||||
injectedStyles: ['color: blue'],
|
||||
pt: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
expect(typeof setupResult).toBe('function')
|
||||
})
|
||||
|
||||
it('renders original component with modified props via h()', () => {
|
||||
const ComponentWithSetup = {
|
||||
props: { class: String },
|
||||
//eslint-disable-next-line
|
||||
setup(props: Record<string, unknown>) {
|
||||
return () => h('div', { class: props.class })
|
||||
},
|
||||
}
|
||||
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Custom: {
|
||||
component: ComponentWithSetup,
|
||||
classes: ['custom-class'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedComponent = engine.getComponent('Custom')
|
||||
|
||||
const result = callSetup(themedComponent, {})
|
||||
|
||||
// Engine wraps via h() — vnode props contain the theme-injected class
|
||||
expect(result.class).toBe('custom-class')
|
||||
})
|
||||
|
||||
it('slot functions are created for each slot component', () => {
|
||||
const MockSlot = {
|
||||
name: 'MockSlot',
|
||||
//eslint-disable-next-line
|
||||
setup() {
|
||||
return () => h('span', 'slot content')
|
||||
},
|
||||
}
|
||||
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
slots: {
|
||||
icon: MockSlot,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const renderFn = themedButton.setup({}, { slots: {} })
|
||||
|
||||
expect(typeof renderFn).toBe('function')
|
||||
const vnode = renderFn()
|
||||
expect(vnode).toBeDefined()
|
||||
})
|
||||
|
||||
it('forwards update:modelValue so v-model works on wrapped components', async () => {
|
||||
// Regression: a cloned wrapper inherited the component's emits/extends, so
|
||||
// Vue withheld onUpdate:modelValue from ctx.attrs and the engine never
|
||||
// forwarded it — breaking two-way v-model on every wrapped component.
|
||||
const Emitter = {
|
||||
name: 'Emitter',
|
||||
props: { modelValue: { type: String, default: '' } },
|
||||
emits: ['update:modelValue'],
|
||||
//eslint-disable-next-line
|
||||
setup(_props: Record<string, unknown>, { emit }: { emit: (e: string, v: string) => void }) {
|
||||
return () => h('button', { onClick: () => emit('update:modelValue', 'changed') }, 'x')
|
||||
},
|
||||
}
|
||||
|
||||
const theme: ThemeLibrary = { components: { Emitter: { component: Emitter } } }
|
||||
const engine = new ThemeEngine(theme)
|
||||
const Wrapped = engine.getComponent('Emitter')
|
||||
|
||||
const Parent = {
|
||||
components: { Wrapped },
|
||||
setup() {
|
||||
const val = ref('initial')
|
||||
return { val }
|
||||
},
|
||||
template: `<div><Wrapped v-model="val" /><span class="out">{{ val }}</span></div>`,
|
||||
}
|
||||
|
||||
const wrapper = mount(Parent)
|
||||
expect(wrapper.find('.out').text()).toBe('initial')
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.find('.out').text()).toBe('changed')
|
||||
})
|
||||
|
||||
it('merges theme slots with original component slots', () => {
|
||||
const ThemeSlot = {
|
||||
name: 'ThemeSlot',
|
||||
//eslint-disable-next-line
|
||||
setup() {
|
||||
return () => h('span', 'theme slot')
|
||||
},
|
||||
}
|
||||
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
slots: {
|
||||
icon: ThemeSlot,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const originalSlots = {
|
||||
label: () => h('span', 'label slot'),
|
||||
}
|
||||
|
||||
const renderFn = themedButton.setup({}, { slots: originalSlots })
|
||||
|
||||
expect(typeof renderFn).toBe('function')
|
||||
const vnode = renderFn()
|
||||
expect(vnode).toBeDefined()
|
||||
})
|
||||
|
||||
it('handles non-string style values by defaulting to empty', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
styles: ['theme-style'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
style: { someObject: 'value' },
|
||||
pt: {},
|
||||
})
|
||||
|
||||
// When style is not a string, it defaults to empty array, so only theme styles are applied
|
||||
expect(result.pt?.root?.style).toBe('theme-style')
|
||||
})
|
||||
|
||||
it('preserves already-prefixed classes', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['my-class', 'custom-already-prefixed'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
stylePlug: 'custom',
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.class).toContain('custom-my-class')
|
||||
expect(result.pt?.root?.class).toContain('custom-already-prefixed')
|
||||
})
|
||||
|
||||
it('handles styleKey as pt injection key', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
styles: ['style-value'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
style: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.style).toContain('style-value')
|
||||
})
|
||||
|
||||
it('slot wrapper works with non-function modifiedSetup', () => {
|
||||
const MockSlot = {
|
||||
name: 'MockSlot',
|
||||
//eslint-disable-next-line
|
||||
setup() {
|
||||
return () => h('span', 'slot')
|
||||
},
|
||||
}
|
||||
|
||||
const ComponentWithoutSetup = {
|
||||
name: 'ComponentWithoutSetup',
|
||||
template: '<div></div>',
|
||||
}
|
||||
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Custom: {
|
||||
component: ComponentWithoutSetup,
|
||||
slots: {
|
||||
icon: MockSlot,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedComponent: any = engine.getComponent('Custom')
|
||||
|
||||
const renderFn = themedComponent.setup({}, { slots: {} })
|
||||
|
||||
expect(typeof renderFn).toBe('function')
|
||||
const vnode = renderFn()
|
||||
expect(vnode).toBeDefined()
|
||||
})
|
||||
|
||||
it('does not pass internal engine props to the wrapped component', () => {
|
||||
const theme = getSimpleTheme()
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, { pt: {}, stylePlug: 'custom' })
|
||||
|
||||
expect(result).not.toHaveProperty('injectedClasses')
|
||||
expect(result).not.toHaveProperty('injectedStyles')
|
||||
expect(result).not.toHaveProperty('stylePlug')
|
||||
})
|
||||
|
||||
it('handles classKey that is not pt', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['my-class'],
|
||||
styles: ['my-style'],
|
||||
injectionKeys: {
|
||||
class: 'customClass',
|
||||
style: 'customStyle',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {})
|
||||
|
||||
expect(result.customClass).toBe('my-class')
|
||||
expect(result.customStyle).toBe('my-style')
|
||||
})
|
||||
|
||||
it('spreads existing pt object when merging theme props', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['theme-class'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
pt: {
|
||||
label: { class: 'label-class', style: 'color: red' },
|
||||
icon: { class: 'icon-class' },
|
||||
tooltip: { content: 'help text' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.class).toContain('theme-class')
|
||||
expect(result.pt?.label?.class).toBe('label-class')
|
||||
expect(result.pt?.label?.style).toBe('color: red')
|
||||
expect(result.pt?.icon?.class).toBe('icon-class')
|
||||
expect(result.pt?.tooltip?.content).toBe('help text')
|
||||
})
|
||||
|
||||
it('handles style as string and splits it correctly', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
styles: ['margin: 10px'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {
|
||||
style: 'padding: 5px; color: blue',
|
||||
pt: {},
|
||||
})
|
||||
|
||||
expect(result.pt?.root?.style).toContain('padding: 5px')
|
||||
expect(result.pt?.root?.style).toContain('color: blue')
|
||||
expect(result.pt?.root?.style).toContain('margin: 10px')
|
||||
})
|
||||
|
||||
it('creates pt structure when pt is not provided', () => {
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
classes: ['btn-class'],
|
||||
styles: ['btn-style'],
|
||||
injectionKeys: {
|
||||
class: 'pt',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
const themedButton = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const result = callSetup(themedButton, {})
|
||||
|
||||
expect(result.pt).toBeDefined()
|
||||
expect(result.pt?.root?.class).toBe('btn-class')
|
||||
expect(result.pt?.root?.style).toBe('btn-style')
|
||||
})
|
||||
|
||||
it('slot functions created with proper scope handling', () => {
|
||||
const mockSlotComponent = {
|
||||
name: 'IconSlot',
|
||||
//eslint-disable-next-line
|
||||
setup(props: Record<string, unknown>) {
|
||||
return () => h('i', `Icon: ${props.name}`)
|
||||
},
|
||||
}
|
||||
|
||||
const theme: ThemeLibrary = {
|
||||
components: {
|
||||
Button: {
|
||||
component: Button,
|
||||
slots: {
|
||||
icon: mockSlotComponent,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const engine = new ThemeEngine(theme)
|
||||
//eslint-disable-next-line
|
||||
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
||||
|
||||
const renderFn = themedButton.setup({}, { slots: {} })
|
||||
|
||||
expect(typeof renderFn).toBe('function')
|
||||
const vnode = renderFn()
|
||||
expect(vnode).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
|
@ -20,11 +20,11 @@ export default defineConfig({
|
|||
fileName: () => 'index.js',
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue', 'primevue'],
|
||||
external: ['vue', 'reka-ui'],
|
||||
output: {
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
primevue: 'PrimeVue',
|
||||
'reka-ui': 'RekaUI',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import { setup } from '@storybook/vue3-vite'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import '@nychthemeron/library/theme'
|
||||
|
||||
setup((app) => {
|
||||
app.use(PrimeVue, { unstyled: true })
|
||||
})
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
"dependencies": {
|
||||
"@nychthemeron/library": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"primevue": "^4.5.0",
|
||||
"vue": "^3.5.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue