feat: Base set of UI components
This commit is contained in:
parent
a4ec81967f
commit
ddfc01f917
20 changed files with 1776 additions and 17 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -46,3 +46,10 @@ packages/*/storybook-static
|
||||||
# Storybook
|
# Storybook
|
||||||
.storybook/node_modules
|
.storybook/node_modules
|
||||||
node_modules/.cache
|
node_modules/.cache
|
||||||
|
|
||||||
|
# Local browser-verification tooling (Playwright + extracted libs + screenshots)
|
||||||
|
.tmp/
|
||||||
|
.bun-cache/
|
||||||
|
.pw-browsers/
|
||||||
|
.pw-libs/
|
||||||
|
.shots/
|
||||||
|
|
|
||||||
9
bun.lock
9
bun.lock
|
|
@ -22,6 +22,7 @@
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
"oxlint": "~1.60.0",
|
"oxlint": "~1.60.0",
|
||||||
|
"playwright": "^1.60.0",
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.8.3",
|
||||||
"primevue": "^4.5.5",
|
"primevue": "^4.5.5",
|
||||||
"typescript": "~6.0.3",
|
"typescript": "~6.0.3",
|
||||||
|
|
@ -797,7 +798,7 @@
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
|
@ -1051,6 +1052,10 @@
|
||||||
|
|
||||||
"pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="],
|
"pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="],
|
||||||
|
|
||||||
|
"playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="],
|
||||||
|
|
||||||
|
"playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="],
|
||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||||
|
|
@ -1413,6 +1418,8 @@
|
||||||
|
|
||||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
|
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"vue/@vue/compiler-dom": ["@vue/compiler-dom@3.5.35", "", { "dependencies": { "@vue/compiler-core": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA=="],
|
"vue/@vue/compiler-dom": ["@vue/compiler-dom@3.5.35", "", { "dependencies": { "@vue/compiler-core": "3.5.35", "@vue/shared": "3.5.35" } }, "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA=="],
|
||||||
|
|
||||||
"vue/@vue/server-renderer": ["@vue/server-renderer@3.5.35", "", { "dependencies": { "@vue/compiler-ssr": "3.5.35", "@vue/shared": "3.5.35" }, "peerDependencies": { "vue": "3.5.35" } }, "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA=="],
|
"vue/@vue/server-renderer": ["@vue/server-renderer@3.5.35", "", { "dependencies": { "@vue/compiler-ssr": "3.5.35", "@vue/shared": "3.5.35" }, "peerDependencies": { "vue": "3.5.35" } }, "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA=="],
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@
|
||||||
"@types/jsdom": "^28.0.3",
|
"@types/jsdom": "^28.0.3",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"primevue": "^4.5.5",
|
|
||||||
"@vitest/eslint-plugin": "^1.6.19",
|
|
||||||
"@vitest/coverage-v8": "^4.1.8",
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
|
"@vitest/eslint-plugin": "^1.6.19",
|
||||||
"@vue/eslint-config-typescript": "^14.7.0",
|
"@vue/eslint-config-typescript": "^14.7.0",
|
||||||
"@vue/test-utils": "^2.4.10",
|
"@vue/test-utils": "^2.4.10",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
|
@ -20,7 +19,9 @@
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
"oxlint": "~1.60.0",
|
"oxlint": "~1.60.0",
|
||||||
|
"playwright": "^1.60.0",
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.8.3",
|
||||||
|
"primevue": "^4.5.5",
|
||||||
"typescript": "~6.0.3",
|
"typescript": "~6.0.3",
|
||||||
"vite": "^8.0.16",
|
"vite": "^8.0.16",
|
||||||
"vite-plugin-vue-devtools": "^8.1.2",
|
"vite-plugin-vue-devtools": "^8.1.2",
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@
|
||||||
color: var(--danger-fg);
|
color: var(--danger-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.nych-loading-icon .wreath path {
|
.nych-loading-icon .wreath path {
|
||||||
fill: var(--primary-fg);
|
fill: var(--primary-fg);
|
||||||
}
|
}
|
||||||
|
|
@ -93,3 +92,636 @@ button[data-p~='loading'] > .nych-loading-icon {
|
||||||
height: 40px !important;
|
height: 40px !important;
|
||||||
font-size: 20pt !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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,22 @@ export class ThemeEngine {
|
||||||
const { component, classes, styles, injectionKeys, propMutator, unstyled, slots } =
|
const { component, classes, styles, injectionKeys, propMutator, unstyled, slots } =
|
||||||
this.lib.components[key]
|
this.lib.components[key]
|
||||||
|
|
||||||
const componentObj = { ...(component as Record<string, unknown>) }
|
// Build a MINIMAL pass-through wrapper rather than cloning the wrapped
|
||||||
|
// component. Cloning dragged along the component's `props`, `emits` and
|
||||||
componentObj.props = {
|
// `extends` chain — which made Vue treat listeners like `onUpdate:modelValue`
|
||||||
...((componentObj.props as Record<string, unknown>) || {}),
|
// as the wrapper's own declared events and withhold them from `ctx.attrs`,
|
||||||
injectedClasses: { default: classes },
|
// silently breaking two-way `v-model`. By declaring only the engine's own
|
||||||
injectedStyles: { default: styles },
|
// props here, every other prop, listener and attribute falls through
|
||||||
stylePlug: { type: String },
|
// `ctx.attrs` and is forwarded verbatim to the real component below.
|
||||||
unstyled: { default: unstyled },
|
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 applyThemeProps = (props: Record<string, unknown>) => {
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,20 @@ export {
|
||||||
type Key,
|
type Key,
|
||||||
type Props,
|
type Props,
|
||||||
} from './engine'
|
} from './engine'
|
||||||
export { createNychthemeron, Button, LoadingIcon } from './lib'
|
export {
|
||||||
|
createNychthemeron,
|
||||||
|
Button,
|
||||||
|
LoadingIcon,
|
||||||
|
InputText,
|
||||||
|
Textarea,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
|
RadioButton,
|
||||||
|
RadioButtonGroup,
|
||||||
|
ToggleSwitch,
|
||||||
|
Select,
|
||||||
|
Card,
|
||||||
|
Dialog,
|
||||||
|
Message,
|
||||||
|
Tag,
|
||||||
|
} from './lib'
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,56 @@
|
||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
import { Button as PrimeButton } from 'primevue'
|
import {
|
||||||
import { ThemeLibrary, type Themeable, ThemeEngine } from '../engine'
|
Button as PrimeButton,
|
||||||
|
InputText as PrimeInputText,
|
||||||
|
Textarea as PrimeTextarea,
|
||||||
|
Checkbox as PrimeCheckbox,
|
||||||
|
CheckboxGroup as PrimeCheckboxGroup,
|
||||||
|
RadioButton as PrimeRadioButton,
|
||||||
|
RadioButtonGroup as PrimeRadioButtonGroup,
|
||||||
|
ToggleSwitch as PrimeToggleSwitch,
|
||||||
|
Select as PrimeSelect,
|
||||||
|
Card as PrimeCard,
|
||||||
|
Dialog as PrimeDialog,
|
||||||
|
Message as PrimeMessage,
|
||||||
|
Tag as PrimeTag,
|
||||||
|
} from 'primevue'
|
||||||
|
import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine'
|
||||||
import { NychLoadingIcon } from '../components'
|
import { NychLoadingIcon } from '../components'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API.
|
||||||
|
*
|
||||||
|
* The engine injects the base class (`nych-<base>`) onto `pt.root`. This factory layers a
|
||||||
|
* `nych-<base>-<section>` class onto every additional section so multi-part components
|
||||||
|
* (Checkbox box, Select overlay, Card body, …) can be fully styled in unstyled mode, where
|
||||||
|
* PrimeVue emits no classes of its own.
|
||||||
|
*/
|
||||||
|
const ptThemeable = <T extends LibComponent>(
|
||||||
|
component: T,
|
||||||
|
base: string,
|
||||||
|
sections: string[] = [],
|
||||||
|
): Themeable<T> => ({
|
||||||
|
component,
|
||||||
|
unstyled: true as Themeable<T>['unstyled'],
|
||||||
|
classes: [`nych-${base}`],
|
||||||
|
injectionKeys: { class: 'pt' as Key<T> },
|
||||||
|
propMutator: ((props: Record<string, unknown>) => {
|
||||||
|
const pt = (props.pt as Record<string, { class?: string }>) ?? {}
|
||||||
|
|
||||||
|
for (const section of sections) {
|
||||||
|
const existing = pt[section]?.class
|
||||||
|
pt[section] = {
|
||||||
|
...(pt[section] ?? {}),
|
||||||
|
class: [existing, `nych-${base}-${section}`].filter(Boolean).join(' '),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.pt = pt
|
||||||
|
|
||||||
|
return props
|
||||||
|
}) as Themeable<T>['propMutator'],
|
||||||
|
})
|
||||||
|
|
||||||
const buttonThemeable: Themeable<typeof PrimeButton> = {
|
const buttonThemeable: Themeable<typeof PrimeButton> = {
|
||||||
component: PrimeButton,
|
component: PrimeButton,
|
||||||
unstyled: true,
|
unstyled: true,
|
||||||
|
|
@ -42,6 +90,51 @@ const nychthemeron: ThemeLibrary = {
|
||||||
components: {
|
components: {
|
||||||
Button: buttonThemeable,
|
Button: buttonThemeable,
|
||||||
LoadingIcon: loadingIconThemeable,
|
LoadingIcon: loadingIconThemeable,
|
||||||
|
InputText: ptThemeable(PrimeInputText, 'input'),
|
||||||
|
Textarea: ptThemeable(PrimeTextarea, 'textarea'),
|
||||||
|
Checkbox: ptThemeable(PrimeCheckbox, 'checkbox', ['box', 'input', 'icon']),
|
||||||
|
CheckboxGroup: ptThemeable(PrimeCheckboxGroup, 'checkbox-group'),
|
||||||
|
RadioButton: ptThemeable(PrimeRadioButton, 'radio', ['box', 'input', 'icon']),
|
||||||
|
RadioButtonGroup: ptThemeable(PrimeRadioButtonGroup, 'radio-group'),
|
||||||
|
ToggleSwitch: ptThemeable(PrimeToggleSwitch, 'toggleswitch', ['input', 'slider', 'handle']),
|
||||||
|
Select: ptThemeable(PrimeSelect, 'select', [
|
||||||
|
'label',
|
||||||
|
'dropdown',
|
||||||
|
'dropdownIcon',
|
||||||
|
'overlay',
|
||||||
|
'header',
|
||||||
|
'listContainer',
|
||||||
|
'list',
|
||||||
|
'option',
|
||||||
|
'optionLabel',
|
||||||
|
'emptyMessage',
|
||||||
|
]),
|
||||||
|
Card: ptThemeable(PrimeCard, 'card', [
|
||||||
|
'header',
|
||||||
|
'body',
|
||||||
|
'caption',
|
||||||
|
'title',
|
||||||
|
'subtitle',
|
||||||
|
'content',
|
||||||
|
'footer',
|
||||||
|
]),
|
||||||
|
Dialog: ptThemeable(PrimeDialog, 'dialog', [
|
||||||
|
'mask',
|
||||||
|
'header',
|
||||||
|
'title',
|
||||||
|
'headerActions',
|
||||||
|
'content',
|
||||||
|
'footer',
|
||||||
|
]),
|
||||||
|
Message: ptThemeable(PrimeMessage, 'message', [
|
||||||
|
'contentWrapper',
|
||||||
|
'content',
|
||||||
|
'icon',
|
||||||
|
'text',
|
||||||
|
'closeButton',
|
||||||
|
'closeIcon',
|
||||||
|
]),
|
||||||
|
Tag: ptThemeable(PrimeTag, 'tag', ['label', 'icon']),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,16 +142,69 @@ const _engine = new ThemeEngine(nychthemeron)
|
||||||
|
|
||||||
export const Button: typeof PrimeButton = _engine.getComponent<typeof PrimeButton>('Button')
|
export const Button: typeof PrimeButton = _engine.getComponent<typeof PrimeButton>('Button')
|
||||||
export const LoadingIcon = _engine.getComponent<typeof NychLoadingIcon>('LoadingIcon')
|
export const LoadingIcon = _engine.getComponent<typeof NychLoadingIcon>('LoadingIcon')
|
||||||
|
export const InputText: typeof PrimeInputText =
|
||||||
|
_engine.getComponent<typeof PrimeInputText>('InputText')
|
||||||
|
export const Textarea: typeof PrimeTextarea = _engine.getComponent<typeof PrimeTextarea>('Textarea')
|
||||||
|
export const Checkbox: typeof PrimeCheckbox = _engine.getComponent<typeof PrimeCheckbox>('Checkbox')
|
||||||
|
export const CheckboxGroup: typeof PrimeCheckboxGroup =
|
||||||
|
_engine.getComponent<typeof PrimeCheckboxGroup>('CheckboxGroup')
|
||||||
|
export const RadioButton: typeof PrimeRadioButton =
|
||||||
|
_engine.getComponent<typeof PrimeRadioButton>('RadioButton')
|
||||||
|
export const RadioButtonGroup: typeof PrimeRadioButtonGroup =
|
||||||
|
_engine.getComponent<typeof PrimeRadioButtonGroup>('RadioButtonGroup')
|
||||||
|
export const ToggleSwitch: typeof PrimeToggleSwitch =
|
||||||
|
_engine.getComponent<typeof PrimeToggleSwitch>('ToggleSwitch')
|
||||||
|
export const Select: typeof PrimeSelect = _engine.getComponent<typeof PrimeSelect>('Select')
|
||||||
|
export const Card: typeof PrimeCard = _engine.getComponent<typeof PrimeCard>('Card')
|
||||||
|
export const Dialog: typeof PrimeDialog = _engine.getComponent<typeof PrimeDialog>('Dialog')
|
||||||
|
export const Message: typeof PrimeMessage = _engine.getComponent<typeof PrimeMessage>('Message')
|
||||||
|
export const Tag: typeof PrimeTag = _engine.getComponent<typeof PrimeTag>('Tag')
|
||||||
|
|
||||||
export const createNychthemeron = (): {
|
export const createNychthemeron = (): {
|
||||||
Button: typeof PrimeButton
|
Button: typeof PrimeButton
|
||||||
LoadingIcon: typeof NychLoadingIcon
|
LoadingIcon: typeof NychLoadingIcon
|
||||||
|
InputText: typeof PrimeInputText
|
||||||
|
Textarea: typeof PrimeTextarea
|
||||||
|
Checkbox: typeof PrimeCheckbox
|
||||||
|
CheckboxGroup: typeof PrimeCheckboxGroup
|
||||||
|
RadioButton: typeof PrimeRadioButton
|
||||||
|
RadioButtonGroup: typeof PrimeRadioButtonGroup
|
||||||
|
ToggleSwitch: typeof PrimeToggleSwitch
|
||||||
|
Select: typeof PrimeSelect
|
||||||
|
Card: typeof PrimeCard
|
||||||
|
Dialog: typeof PrimeDialog
|
||||||
|
Message: typeof PrimeMessage
|
||||||
|
Tag: typeof PrimeTag
|
||||||
install: (app: App) => void
|
install: (app: App) => void
|
||||||
} => ({
|
} => ({
|
||||||
Button,
|
Button,
|
||||||
LoadingIcon,
|
LoadingIcon,
|
||||||
|
InputText,
|
||||||
|
Textarea,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
|
RadioButton,
|
||||||
|
RadioButtonGroup,
|
||||||
|
ToggleSwitch,
|
||||||
|
Select,
|
||||||
|
Card,
|
||||||
|
Dialog,
|
||||||
|
Message,
|
||||||
|
Tag,
|
||||||
install(app: App) {
|
install(app: App) {
|
||||||
app.component('NychButton', Button)
|
app.component('NychButton', Button)
|
||||||
app.component('NychLoadingIcon', LoadingIcon)
|
app.component('NychLoadingIcon', LoadingIcon)
|
||||||
|
app.component('NychInputText', InputText)
|
||||||
|
app.component('NychTextarea', Textarea)
|
||||||
|
app.component('NychCheckbox', Checkbox)
|
||||||
|
app.component('NychCheckboxGroup', CheckboxGroup)
|
||||||
|
app.component('NychRadioButton', RadioButton)
|
||||||
|
app.component('NychRadioButtonGroup', RadioButtonGroup)
|
||||||
|
app.component('NychToggleSwitch', ToggleSwitch)
|
||||||
|
app.component('NychSelect', Select)
|
||||||
|
app.component('NychCard', Card)
|
||||||
|
app.component('NychDialog', Dialog)
|
||||||
|
app.component('NychMessage', Message)
|
||||||
|
app.component('NychTag', Tag)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import { h } from 'vue'
|
import { h, ref } from 'vue'
|
||||||
|
|
||||||
import { ThemeEngine, ThemeLibrary, type Themeable } from '../src/engine'
|
import { ThemeEngine, ThemeLibrary, type Themeable } from '../src/engine'
|
||||||
|
|
||||||
|
|
@ -564,6 +565,40 @@ describe('ThemeEngine', () => {
|
||||||
expect(vnode).toBeDefined()
|
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', () => {
|
it('merges theme slots with original component slots', () => {
|
||||||
const ThemeSlot = {
|
const ThemeSlot = {
|
||||||
name: 'ThemeSlot',
|
name: 'ThemeSlot',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
import { describe, it, expect, vi } from 'vitest'
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
import { createNychthemeron, Button, LoadingIcon } from '../src/index'
|
import {
|
||||||
|
createNychthemeron,
|
||||||
|
Button,
|
||||||
|
LoadingIcon,
|
||||||
|
InputText,
|
||||||
|
Textarea,
|
||||||
|
Checkbox,
|
||||||
|
RadioButton,
|
||||||
|
CheckboxGroup,
|
||||||
|
RadioButtonGroup,
|
||||||
|
ToggleSwitch,
|
||||||
|
Select,
|
||||||
|
Card,
|
||||||
|
Dialog,
|
||||||
|
Message,
|
||||||
|
Tag,
|
||||||
|
} from '../src/index'
|
||||||
|
|
||||||
//eslint-disable-next-line
|
//eslint-disable-next-line
|
||||||
const callSetup = (component: any, props: Record<string, unknown>) => {
|
const callSetup = (component: any, props: Record<string, unknown>) => {
|
||||||
|
|
@ -92,3 +108,50 @@ describe('LoadingIcon', () => {
|
||||||
expect(LoadingIcon).toBeDefined()
|
expect(LoadingIcon).toBeDefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('themed pt components', () => {
|
||||||
|
const cases = [
|
||||||
|
{ name: 'InputText', component: InputText, base: 'nych-input' },
|
||||||
|
{ name: 'Textarea', component: Textarea, base: 'nych-textarea' },
|
||||||
|
{ name: 'Checkbox', component: Checkbox, base: 'nych-checkbox' },
|
||||||
|
{ name: 'CheckboxGroup', component: CheckboxGroup, base: 'nych-checkbox-group' },
|
||||||
|
{ name: 'RadioButton', component: RadioButton, base: 'nych-radio' },
|
||||||
|
{ name: 'RadioButtonGroup', component: RadioButtonGroup, base: 'nych-radio-group' },
|
||||||
|
{ name: 'ToggleSwitch', component: ToggleSwitch, base: 'nych-toggleswitch' },
|
||||||
|
{ name: 'Select', component: Select, base: 'nych-select' },
|
||||||
|
{ name: 'Card', component: Card, base: 'nych-card' },
|
||||||
|
{ name: 'Dialog', component: Dialog, base: 'nych-dialog' },
|
||||||
|
{ name: 'Message', component: Message, base: 'nych-message' },
|
||||||
|
{ name: 'Tag', component: Tag, base: 'nych-tag' },
|
||||||
|
]
|
||||||
|
|
||||||
|
it.each(cases)('$name is a valid Vue component', ({ component }) => {
|
||||||
|
expect(component).toBeDefined()
|
||||||
|
expect(typeof (component as Record<string, unknown>).setup).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(cases)('$name injects its base class onto pt.root', ({ component, base }) => {
|
||||||
|
const result = callSetup(component, { pt: {} })
|
||||||
|
expect(result.pt?.root?.class).toContain(base)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(cases)('$name does not leak engine internal props', ({ component }) => {
|
||||||
|
const result = callSetup(component, { pt: {} })
|
||||||
|
expect(result).not.toHaveProperty('injectedClasses')
|
||||||
|
expect(result).not.toHaveProperty('injectedStyles')
|
||||||
|
expect(result).not.toHaveProperty('stylePlug')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Checkbox themes its box, input and icon sections', () => {
|
||||||
|
const result = callSetup(Checkbox, { pt: {} })
|
||||||
|
expect(result.pt?.box?.class).toContain('nych-checkbox-box')
|
||||||
|
expect(result.pt?.input?.class).toContain('nych-checkbox-input')
|
||||||
|
expect(result.pt?.icon?.class).toContain('nych-checkbox-icon')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Select themes its overlay and option sections', () => {
|
||||||
|
const result = callSetup(Select, { pt: {} })
|
||||||
|
expect(result.pt?.overlay?.class).toContain('nych-select-overlay')
|
||||||
|
expect(result.pt?.option?.class).toContain('nych-select-option')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import type { Preview } from '@storybook/vue3-vite'
|
import type { Preview } from '@storybook/vue3-vite'
|
||||||
|
import { setup } from '@storybook/vue3-vite'
|
||||||
|
import PrimeVue from 'primevue/config'
|
||||||
import '@nychthemeron/library/theme'
|
import '@nychthemeron/library/theme'
|
||||||
|
|
||||||
|
setup((app) => {
|
||||||
|
app.use(PrimeVue, { unstyled: true })
|
||||||
|
})
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
|
|
|
||||||
73
packages/playground/stories/Card.stories.ts
Normal file
73
packages/playground/stories/Card.stories.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import { Card as NychCard, Button as NychButton, Tag as NychTag } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Card',
|
||||||
|
component: NychCard,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof NychCard>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychCard },
|
||||||
|
template: `
|
||||||
|
<NychCard style="width: 22rem;">
|
||||||
|
<template #title>Elysium</template>
|
||||||
|
<template #subtitle>The blessed fields</template>
|
||||||
|
<template #content>
|
||||||
|
A resting place reserved for the heroic and the virtuous, where the
|
||||||
|
air is mild and the laurel never wilts.
|
||||||
|
</template>
|
||||||
|
</NychCard>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithFooter: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychCard, NychButton },
|
||||||
|
template: `
|
||||||
|
<NychCard style="width: 22rem;">
|
||||||
|
<template #title>Passage of the Styx</template>
|
||||||
|
<template #subtitle>One obol required</template>
|
||||||
|
<template #content>
|
||||||
|
Charon ferries the souls of the dead across the river that divides
|
||||||
|
the world of the living from the world of the dead.
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<NychButton label="Pay the toll" />
|
||||||
|
<NychButton label="Turn back" severity="secondary" />
|
||||||
|
</template>
|
||||||
|
</NychCard>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithHeaderAndTag: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychCard, NychTag },
|
||||||
|
template: `
|
||||||
|
<NychCard style="width: 22rem;">
|
||||||
|
<template #header>
|
||||||
|
<div style="height: 120px; background: linear-gradient(135deg, var(--surface-2), var(--surface-3));"></div>
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<span style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
Oracle of Delphi <NychTag value="Sacred" severity="info" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
Seekers travelled from across the Aegean to hear the Pythia speak the
|
||||||
|
will of Apollo in riddling verse.
|
||||||
|
</template>
|
||||||
|
</NychCard>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
111
packages/playground/stories/Checkbox.stories.ts
Normal file
111
packages/playground/stories/Checkbox.stories.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { Checkbox as NychCheckbox, CheckboxGroup as NychCheckboxGroup } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Checkbox',
|
||||||
|
component: NychCheckbox,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
disabled: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Disable the checkbox',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NychCheckbox>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychCheckbox },
|
||||||
|
setup() {
|
||||||
|
const checked = ref(false)
|
||||||
|
return { checked }
|
||||||
|
},
|
||||||
|
template: `<NychCheckbox v-model="checked" binary />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checked: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychCheckbox },
|
||||||
|
setup() {
|
||||||
|
const checked = ref(true)
|
||||||
|
return { checked }
|
||||||
|
},
|
||||||
|
template: `<NychCheckbox v-model="checked" binary />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychCheckbox },
|
||||||
|
setup() {
|
||||||
|
const checked = ref(true)
|
||||||
|
return { checked }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; gap: 1rem;">
|
||||||
|
<NychCheckbox :modelValue="false" binary disabled />
|
||||||
|
<NychCheckbox :modelValue="true" binary disabled />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithLabels: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychCheckbox },
|
||||||
|
setup() {
|
||||||
|
const offerings = ref(['honey'])
|
||||||
|
return { offerings }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.75rem; font-family: var(--font-sans); color: var(--text-body);">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychCheckbox v-model="offerings" value="honey" /> Honey
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychCheckbox v-model="offerings" value="wine" /> Wine
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychCheckbox v-model="offerings" value="laurel" /> Laurel
|
||||||
|
</label>
|
||||||
|
<p style="color: var(--text-muted); margin-top: 0.25rem;">Offerings: {{ offerings.join(', ') || 'none' }}</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckboxGroup holds the array v-model and shares a name across children.
|
||||||
|
export const Group: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychCheckbox, NychCheckboxGroup },
|
||||||
|
setup() {
|
||||||
|
const offerings = ref(['honey'])
|
||||||
|
return { offerings }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="font-family: var(--font-sans); color: var(--text-body);">
|
||||||
|
<NychCheckboxGroup v-model="offerings" name="offerings">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychCheckbox value="honey" /> Honey
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychCheckbox value="wine" /> Wine
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychCheckbox value="laurel" /> Laurel
|
||||||
|
</label>
|
||||||
|
</NychCheckboxGroup>
|
||||||
|
<p style="color: var(--text-muted); margin-top: 0.75rem;">Offerings: {{ offerings.join(', ') || 'none' }}</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
79
packages/playground/stories/Dialog.stories.ts
Normal file
79
packages/playground/stories/Dialog.stories.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { Dialog as NychDialog, Button as NychButton } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Dialog',
|
||||||
|
component: NychDialog,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof NychDialog>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychDialog, NychButton },
|
||||||
|
setup() {
|
||||||
|
const visible = ref(false)
|
||||||
|
return { visible }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<NychButton label="Summon the Oracle" @click="visible = true" />
|
||||||
|
<NychDialog v-model:visible="visible" modal header="Oracle of Delphi" style="width: 28rem;">
|
||||||
|
<p style="margin: 0;">
|
||||||
|
Seekers travelled from across the Aegean to hear the Pythia speak the
|
||||||
|
will of Apollo in riddling verse. Approach, and ask your question.
|
||||||
|
</p>
|
||||||
|
<template #footer>
|
||||||
|
<NychButton label="Withdraw" severity="secondary" @click="visible = false" />
|
||||||
|
<NychButton label="Ask" @click="visible = false" />
|
||||||
|
</template>
|
||||||
|
</NychDialog>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendered open by default so the dialog surface is visible in docs/snapshots.
|
||||||
|
export const Open: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychDialog, NychButton },
|
||||||
|
setup() {
|
||||||
|
const visible = ref(true)
|
||||||
|
return { visible }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<NychDialog v-model:visible="visible" modal header="Passage of the Styx" style="width: 28rem;">
|
||||||
|
<p style="margin: 0;">
|
||||||
|
Charon ferries the souls of the dead across the river that divides the
|
||||||
|
world of the living from the world of the dead. One obol is required.
|
||||||
|
</p>
|
||||||
|
<template #footer>
|
||||||
|
<NychButton label="Turn back" severity="secondary" @click="visible = false" />
|
||||||
|
<NychButton label="Pay the toll" @click="visible = false" />
|
||||||
|
</template>
|
||||||
|
</NychDialog>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithoutFooter: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychDialog },
|
||||||
|
setup() {
|
||||||
|
const visible = ref(true)
|
||||||
|
return { visible }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<NychDialog v-model:visible="visible" modal header="An Omen" style="width: 26rem;">
|
||||||
|
<p style="margin: 0;">A crow has settled upon the western gate. Interpret it as you will.</p>
|
||||||
|
</NychDialog>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
93
packages/playground/stories/InputText.stories.ts
Normal file
93
packages/playground/stories/InputText.stories.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { InputText as NychInputText } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/InputText',
|
||||||
|
component: NychInputText,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
size: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['small', undefined, 'large'],
|
||||||
|
description: 'Input size',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: { type: 'text' },
|
||||||
|
description: 'Placeholder text',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Disable the input',
|
||||||
|
},
|
||||||
|
invalid: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Mark the input as invalid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NychInputText>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
const modelTemplate = (attrs: string) => ({
|
||||||
|
components: { NychInputText },
|
||||||
|
setup() {
|
||||||
|
const value = ref('')
|
||||||
|
return { value }
|
||||||
|
},
|
||||||
|
template: `<NychInputText v-model="value" ${attrs} />`,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => modelTemplate('placeholder="Speak, traveller…"'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Filled: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychInputText },
|
||||||
|
setup() {
|
||||||
|
const value = ref('Charon')
|
||||||
|
return { value }
|
||||||
|
},
|
||||||
|
template: `<NychInputText v-model="value" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Invalid: Story = {
|
||||||
|
render: () => modelTemplate('placeholder="Required" invalid'),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychInputText },
|
||||||
|
setup() {
|
||||||
|
const value = ref('Sealed')
|
||||||
|
return { value }
|
||||||
|
},
|
||||||
|
template: `<NychInputText v-model="value" disabled />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sizes: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychInputText },
|
||||||
|
setup() {
|
||||||
|
const a = ref('')
|
||||||
|
const b = ref('')
|
||||||
|
const c = ref('')
|
||||||
|
return { a, b, c }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 1rem; width: 16rem;">
|
||||||
|
<NychInputText v-model="a" size="small" placeholder="Small" />
|
||||||
|
<NychInputText v-model="b" placeholder="Normal" />
|
||||||
|
<NychInputText v-model="c" size="large" placeholder="Large" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
86
packages/playground/stories/Message.stories.ts
Normal file
86
packages/playground/stories/Message.stories.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import { Message as NychMessage } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Message',
|
||||||
|
component: NychMessage,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
severity: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['info', 'success', 'warn', 'error', 'secondary'],
|
||||||
|
description: 'Message severity',
|
||||||
|
},
|
||||||
|
closable: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Show a close button',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NychMessage>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Info: Story = {
|
||||||
|
args: { severity: 'info' },
|
||||||
|
render: (args) => ({
|
||||||
|
components: { NychMessage },
|
||||||
|
setup: () => ({ args }),
|
||||||
|
template: `<NychMessage v-bind="args" style="width: 24rem;">The Oracle has spoken.</NychMessage>`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Success: Story = {
|
||||||
|
args: { severity: 'success' },
|
||||||
|
render: (args) => ({
|
||||||
|
components: { NychMessage },
|
||||||
|
setup: () => ({ args }),
|
||||||
|
template: `<NychMessage v-bind="args" style="width: 24rem;">The offering was accepted.</NychMessage>`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Warn: Story = {
|
||||||
|
args: { severity: 'warn' },
|
||||||
|
render: (args) => ({
|
||||||
|
components: { NychMessage },
|
||||||
|
setup: () => ({ args }),
|
||||||
|
template: `<NychMessage v-bind="args" style="width: 24rem;">The ferryman grows impatient.</NychMessage>`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Error: Story = {
|
||||||
|
args: { severity: 'error' },
|
||||||
|
render: (args) => ({
|
||||||
|
components: { NychMessage },
|
||||||
|
setup: () => ({ args }),
|
||||||
|
template: `<NychMessage v-bind="args" style="width: 24rem;">The gods have refused your plea.</NychMessage>`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Closable: Story = {
|
||||||
|
args: { severity: 'info', closable: true },
|
||||||
|
render: (args) => ({
|
||||||
|
components: { NychMessage },
|
||||||
|
setup: () => ({ args }),
|
||||||
|
template: `<NychMessage v-bind="args" style="width: 24rem;">This omen may be dismissed.</NychMessage>`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllSeverities: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychMessage },
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.75rem; width: 24rem;">
|
||||||
|
<NychMessage severity="info">The Oracle has spoken.</NychMessage>
|
||||||
|
<NychMessage severity="success">The offering was accepted.</NychMessage>
|
||||||
|
<NychMessage severity="warn">The ferryman grows impatient.</NychMessage>
|
||||||
|
<NychMessage severity="error">The gods have refused your plea.</NychMessage>
|
||||||
|
<NychMessage severity="secondary">A neutral notice from the keeper.</NychMessage>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
109
packages/playground/stories/RadioButton.stories.ts
Normal file
109
packages/playground/stories/RadioButton.stories.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
RadioButton as NychRadioButton,
|
||||||
|
RadioButtonGroup as NychRadioButtonGroup,
|
||||||
|
} from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/RadioButton',
|
||||||
|
component: NychRadioButton,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
disabled: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Disable the radio button',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NychRadioButton>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychRadioButton },
|
||||||
|
setup() {
|
||||||
|
const realm = ref('underworld')
|
||||||
|
return { realm }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; gap: 1rem;">
|
||||||
|
<NychRadioButton v-model="realm" value="underworld" />
|
||||||
|
<NychRadioButton v-model="realm" value="sky" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychRadioButton },
|
||||||
|
setup() {
|
||||||
|
const realm = ref('sky')
|
||||||
|
return { realm }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; gap: 1rem;">
|
||||||
|
<NychRadioButton :modelValue="'underworld'" value="sky" disabled />
|
||||||
|
<NychRadioButton :modelValue="'sky'" value="sky" disabled />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithLabels: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychRadioButton },
|
||||||
|
setup() {
|
||||||
|
const god = ref('apollo')
|
||||||
|
return { god }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.75rem; font-family: var(--font-sans); color: var(--text-body);">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychRadioButton v-model="god" value="apollo" /> Apollo
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychRadioButton v-model="god" value="hades" /> Hades
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychRadioButton v-model="god" value="hermes" /> Hermes
|
||||||
|
</label>
|
||||||
|
<p style="color: var(--text-muted); margin-top: 0.25rem;">Chosen: {{ god }}</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// RadioButtonGroup binds the v-model once on the group and shares a name across
|
||||||
|
// children — the children only carry a `value`.
|
||||||
|
export const Group: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychRadioButton, NychRadioButtonGroup },
|
||||||
|
setup() {
|
||||||
|
const god = ref('apollo')
|
||||||
|
return { god }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="font-family: var(--font-sans); color: var(--text-body);">
|
||||||
|
<NychRadioButtonGroup v-model="god" name="god">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychRadioButton value="apollo" /> Apollo
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychRadioButton value="hades" /> Hades
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<NychRadioButton value="hermes" /> Hermes
|
||||||
|
</label>
|
||||||
|
</NychRadioButtonGroup>
|
||||||
|
<p style="color: var(--text-muted); margin-top: 0.75rem;">Chosen: {{ god }}</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
77
packages/playground/stories/Select.stories.ts
Normal file
77
packages/playground/stories/Select.stories.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { Select as NychSelect } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Select',
|
||||||
|
component: NychSelect,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
placeholder: {
|
||||||
|
control: { type: 'text' },
|
||||||
|
description: 'Placeholder text',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Disable the select',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NychSelect>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
const RIVERS = ['Styx', 'Acheron', 'Cocytus', 'Phlegethon', 'Lethe']
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychSelect },
|
||||||
|
setup() {
|
||||||
|
const river = ref(null)
|
||||||
|
return { river, RIVERS }
|
||||||
|
},
|
||||||
|
template: `<NychSelect v-model="river" :options="RIVERS" placeholder="Choose a river" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Selected: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychSelect },
|
||||||
|
setup() {
|
||||||
|
const river = ref('Lethe')
|
||||||
|
return { river, RIVERS }
|
||||||
|
},
|
||||||
|
template: `<NychSelect v-model="river" :options="RIVERS" placeholder="Choose a river" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychSelect },
|
||||||
|
setup() {
|
||||||
|
const river = ref('Styx')
|
||||||
|
return { river, RIVERS }
|
||||||
|
},
|
||||||
|
template: `<NychSelect v-model="river" :options="RIVERS" disabled />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ObjectOptions: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychSelect },
|
||||||
|
setup() {
|
||||||
|
const god = ref(null)
|
||||||
|
const gods = [
|
||||||
|
{ name: 'Apollo', domain: 'apollo' },
|
||||||
|
{ name: 'Hades', domain: 'hades' },
|
||||||
|
{ name: 'Hermes', domain: 'hermes' },
|
||||||
|
]
|
||||||
|
return { god, gods }
|
||||||
|
},
|
||||||
|
template: `<NychSelect v-model="god" :options="gods" optionLabel="name" optionValue="domain" placeholder="Choose a god" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
55
packages/playground/stories/Tag.stories.ts
Normal file
55
packages/playground/stories/Tag.stories.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import { Tag as NychTag } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Tag',
|
||||||
|
component: NychTag,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
severity: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['secondary', 'success', 'info', 'warn', 'danger', 'contrast'],
|
||||||
|
description: 'Tag color severity',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
control: { type: 'text' },
|
||||||
|
description: 'Tag label',
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Pill-shaped tag',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NychTag>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: { value: 'Mortal' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Rounded: Story = {
|
||||||
|
args: { value: 'Immortal', severity: 'info', rounded: true },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Severities: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychTag },
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
|
||||||
|
<NychTag value="Primary" />
|
||||||
|
<NychTag value="Secondary" severity="secondary" />
|
||||||
|
<NychTag value="Info" severity="info" />
|
||||||
|
<NychTag value="Success" severity="success" />
|
||||||
|
<NychTag value="Warn" severity="warn" />
|
||||||
|
<NychTag value="Danger" severity="danger" />
|
||||||
|
<NychTag value="Contrast" severity="contrast" />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
80
packages/playground/stories/Textarea.stories.ts
Normal file
80
packages/playground/stories/Textarea.stories.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { Textarea as NychTextarea } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Textarea',
|
||||||
|
component: NychTextarea,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
placeholder: {
|
||||||
|
control: { type: 'text' },
|
||||||
|
description: 'Placeholder text',
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
control: { type: 'number' },
|
||||||
|
description: 'Visible rows',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Disable the textarea',
|
||||||
|
},
|
||||||
|
invalid: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Mark the textarea as invalid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NychTextarea>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychTextarea },
|
||||||
|
setup() {
|
||||||
|
const value = ref('')
|
||||||
|
return { value }
|
||||||
|
},
|
||||||
|
template: `<NychTextarea v-model="value" rows="4" placeholder="Inscribe your oath…" style="width: 22rem;" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Filled: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychTextarea },
|
||||||
|
setup() {
|
||||||
|
const value = ref(
|
||||||
|
'Whoever crosses the Styx leaves the world of light behind, and is bound to the laws of the deep.',
|
||||||
|
)
|
||||||
|
return { value }
|
||||||
|
},
|
||||||
|
template: `<NychTextarea v-model="value" rows="4" style="width: 22rem;" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Invalid: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychTextarea },
|
||||||
|
setup() {
|
||||||
|
const value = ref('')
|
||||||
|
return { value }
|
||||||
|
},
|
||||||
|
template: `<NychTextarea v-model="value" rows="4" invalid placeholder="Required" style="width: 22rem;" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychTextarea },
|
||||||
|
setup() {
|
||||||
|
const value = ref('This scroll is sealed.')
|
||||||
|
return { value }
|
||||||
|
},
|
||||||
|
template: `<NychTextarea v-model="value" rows="4" disabled style="width: 22rem;" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
75
packages/playground/stories/ToggleSwitch.stories.ts
Normal file
75
packages/playground/stories/ToggleSwitch.stories.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { ToggleSwitch as NychToggleSwitch } from '@nychthemeron/library'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/ToggleSwitch',
|
||||||
|
component: NychToggleSwitch,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
disabled: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Disable the switch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof NychToggleSwitch>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychToggleSwitch },
|
||||||
|
setup() {
|
||||||
|
const on = ref(false)
|
||||||
|
return { on }
|
||||||
|
},
|
||||||
|
template: `<NychToggleSwitch v-model="on" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const On: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychToggleSwitch },
|
||||||
|
setup() {
|
||||||
|
const on = ref(true)
|
||||||
|
return { on }
|
||||||
|
},
|
||||||
|
template: `<NychToggleSwitch v-model="on" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychToggleSwitch },
|
||||||
|
setup() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div style="display: flex; gap: 1rem;">
|
||||||
|
<NychToggleSwitch :modelValue="false" disabled />
|
||||||
|
<NychToggleSwitch :modelValue="true" disabled />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithLabel: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { NychToggleSwitch },
|
||||||
|
setup() {
|
||||||
|
const nocturnal = ref(true)
|
||||||
|
return { nocturnal }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.75rem; font-family: var(--font-sans); color: var(--text-body); cursor: pointer;">
|
||||||
|
<NychToggleSwitch v-model="nocturnal" />
|
||||||
|
{{ nocturnal ? 'Hades (night)' : 'Apollo (day)' }}
|
||||||
|
</label>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue