From ff02f7ec720ee99300183c12517aad7eb6c487f6 Mon Sep 17 00:00:00 2001 From: Matthew L McPeak Date: Sat, 11 Jul 2026 15:07:59 -0400 Subject: [PATCH] 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 --- bun.lock | 17 +- package.json | 1 - packages/library/package.json | 7 +- .../library/src/assets/css/nychthemeron.css | 169 ---- packages/library/src/assets/css/tailwind.css | 6 + packages/library/src/assets/theme/theme.css | 727 -------------- packages/library/src/engine/index.ts | 200 ---- packages/library/src/index.ts | 8 - packages/library/src/lib/index.ts | 16 +- packages/library/tests/engine.spec.ts | 894 ------------------ packages/library/vite.config.ts | 4 +- packages/playground/.storybook/preview.ts | 6 - packages/playground/package.json | 1 - 13 files changed, 13 insertions(+), 2043 deletions(-) delete mode 100644 packages/library/src/assets/css/nychthemeron.css delete mode 100644 packages/library/src/assets/theme/theme.css delete mode 100644 packages/library/src/engine/index.ts delete mode 100644 packages/library/tests/engine.spec.ts diff --git a/bun.lock b/bun.lock index 57f6333..6248fff 100644 --- a/bun.lock +++ b/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=="], diff --git a/package.json b/package.json index 77b52fd..3c84a09 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/library/package.json b/packages/library/package.json index 1f3309a..39e6b0d 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -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", diff --git a/packages/library/src/assets/css/nychthemeron.css b/packages/library/src/assets/css/nychthemeron.css deleted file mode 100644 index 6f1d9f0..0000000 --- a/packages/library/src/assets/css/nychthemeron.css +++ /dev/null @@ -1,169 +0,0 @@ -/* ============================================================ - nychthemeron.css - Toggle themes by setting data-theme on : - - - ============================================================ */ - -@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; -} diff --git a/packages/library/src/assets/css/tailwind.css b/packages/library/src/assets/css/tailwind.css index d1b5d94..fc9c430 100644 --- a/packages/library/src/assets/css/tailwind.css +++ b/packages/library/src/assets/css/tailwind.css @@ -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); diff --git a/packages/library/src/assets/theme/theme.css b/packages/library/src/assets/theme/theme.css deleted file mode 100644 index 10adb38..0000000 --- a/packages/library/src/assets/theme/theme.css +++ /dev/null @@ -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); -} diff --git a/packages/library/src/engine/index.ts b/packages/library/src/engine/index.ts deleted file mode 100644 index a516e6f..0000000 --- a/packages/library/src/engine/index.ts +++ /dev/null @@ -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 // Object components - -export type Props = T extends { new (...args: unknown[]): { $props: infer P } } - ? P - : T extends { props: infer P } - ? P - : string - -export type Key = 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 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 = Key, - I extends Key = Key, -> = { - component: T - propMutator?: (props: Record, unknown>) => Record, unknown> - classes?: string[] - styles?: string[] - injectionKeys?: { - class?: K - style?: I - } - unstyled?: ExtractUnstyled - slots?: Record -} - -export type ThemeLibrary< - T extends Record> = Record>, -> = { - components: T -} - -export class ThemeEngine { - readonly lib: ThemeLibrary - - constructor(library: ThemeLibrary) { - this.lib = library - } - - getComponent(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 = { - name: `Nych${String(key)}`, - inheritAttrs: false, - props: { - injectedClasses: { default: classes }, - injectedStyles: { default: styles }, - stylePlug: { type: String }, - unstyled: { default: unstyled }, - }, - } - - const applyThemeProps = (props: Record) => { - 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) - .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) - : {} - 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, 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, ctx: SetupContext) => { - return () => { - const themeSlots: Record = {} - for (const [name, slotComponent] of Object.entries(slots || {})) { - themeSlots[name] = (scope?: Record) => [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 - } -} diff --git a/packages/library/src/index.ts b/packages/library/src/index.ts index babe237..26005c2 100644 --- a/packages/library/src/index.ts +++ b/packages/library/src/index.ts @@ -1,11 +1,3 @@ -export { - ThemeEngine, - type ThemeLibrary, - type Themeable, - type LibComponent, - type Key, - type Props, -} from './engine' export { createNychthemeron, Button, diff --git a/packages/library/src/lib/index.ts b/packages/library/src/lib/index.ts index 67f78ae..10005db 100644 --- a/packages/library/src/lib/index.ts +++ b/packages/library/src/lib/index.ts @@ -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 = { - component: NychLoadingIcon, - unstyled: false, -} - -const nychthemeron: ThemeLibrary = { - components: { - LoadingIcon: loadingIconThemeable, - }, -} - -const _engine = new ThemeEngine(nychthemeron) - -export const LoadingIcon = _engine.getComponent('LoadingIcon') +export const LoadingIcon = NychLoadingIcon export const createNychthemeron = (): { Button: typeof Button diff --git a/packages/library/tests/engine.spec.ts b/packages/library/tests/engine.spec.ts deleted file mode 100644 index e2795b1..0000000 --- a/packages/library/tests/engine.spec.ts +++ /dev/null @@ -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 = { - 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 = { - 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) => { - // Apply prop defaults from the component definition - const mergedProps: Record = {} - - 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) || {} - } - 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('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('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('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('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('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('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('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('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('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('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('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('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('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('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('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('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('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: '
Mock Slot
' } - - const theme: ThemeLibrary = { - components: { - Button: { - component: Button, - slots: { - icon: MockSlot, - }, - }, - }, - } - - const engine = new ThemeEngine(theme) - //eslint-disable-next-line - const themedButton: any = engine.getComponent('Button') - - expect(themedButton.setup).toBeDefined() - expect(themedButton.setup({}, {})).toBeDefined() - }) - - it('slot wrapper returns a render function', () => { - const MockSlot = { name: 'MockSlot', template: '
Mock Slot
' } - - const theme: ThemeLibrary = { - components: { - Button: { - component: Button, - slots: { - icon: MockSlot, - }, - }, - }, - } - - const engine = new ThemeEngine(theme) - //eslint-disable-next-line - const themedButton: any = engine.getComponent('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('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) { - 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('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, { 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: `
{{ val }}
`, - } - - 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('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('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('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('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: '
', - } - - 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('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('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('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('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('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) { - 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('Button') - - const renderFn = themedButton.setup({}, { slots: {} }) - - expect(typeof renderFn).toBe('function') - const vnode = renderFn() - expect(vnode).toBeDefined() - }) -}) diff --git a/packages/library/vite.config.ts b/packages/library/vite.config.ts index f37ef8f..34c6aae 100644 --- a/packages/library/vite.config.ts +++ b/packages/library/vite.config.ts @@ -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', }, }, }, diff --git a/packages/playground/.storybook/preview.ts b/packages/playground/.storybook/preview.ts index f5d2053..da1aae2 100644 --- a/packages/playground/.storybook/preview.ts +++ b/packages/playground/.storybook/preview.ts @@ -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', diff --git a/packages/playground/package.json b/packages/playground/package.json index e5d3b3f..3640cdf 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -10,7 +10,6 @@ "dependencies": { "@nychthemeron/library": "workspace:*", "@tailwindcss/vite": "^4.3.2", - "primevue": "^4.5.0", "vue": "^3.5.32" }, "devDependencies": {