Base UI Set of Components #5
20 changed files with 1776 additions and 17 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -46,3 +46,10 @@ packages/*/storybook-static
|
|||
# Storybook
|
||||
.storybook/node_modules
|
||||
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",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.60.0",
|
||||
"playwright": "^1.60.0",
|
||||
"prettier": "3.8.3",
|
||||
"primevue": "^4.5.5",
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
|
|
@ -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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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/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/node": "^25.9.1",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"primevue": "^4.5.5",
|
||||
"@vitest/eslint-plugin": "^1.6.19",
|
||||
"@vitest/coverage-v8": "^4.1.8",
|
||||
"@vitest/eslint-plugin": "^1.6.19",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"@vue/test-utils": "^2.4.10",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
|
|
@ -20,7 +19,9 @@
|
|||
"jsdom": "^29.1.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.60.0",
|
||||
"playwright": "^1.60.0",
|
||||
"prettier": "3.8.3",
|
||||
"primevue": "^4.5.5",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.0.16",
|
||||
"vite-plugin-vue-devtools": "^8.1.2",
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@
|
|||
color: var(--danger-fg);
|
||||
}
|
||||
|
||||
|
||||
.nych-loading-icon .wreath path {
|
||||
fill: var(--primary-fg);
|
||||
}
|
||||
|
|
@ -93,3 +92,636 @@ button[data-p~='loading'] > .nych-loading-icon {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,14 +63,22 @@ export class ThemeEngine {
|
|||
const { component, classes, styles, injectionKeys, propMutator, unstyled, slots } =
|
||||
this.lib.components[key]
|
||||
|
||||
const componentObj = { ...(component as Record<string, unknown>) }
|
||||
|
||||
componentObj.props = {
|
||||
...((componentObj.props as Record<string, unknown>) || {}),
|
||||
injectedClasses: { default: classes },
|
||||
injectedStyles: { default: styles },
|
||||
stylePlug: { type: String },
|
||||
unstyled: { default: unstyled },
|
||||
// Build a MINIMAL pass-through wrapper rather than cloning the wrapped
|
||||
// component. Cloning dragged along the component's `props`, `emits` and
|
||||
// `extends` chain — which made Vue treat listeners like `onUpdate:modelValue`
|
||||
// as the wrapper's own declared events and withhold them from `ctx.attrs`,
|
||||
// silently breaking two-way `v-model`. By declaring only the engine's own
|
||||
// props here, every other prop, listener and attribute falls through
|
||||
// `ctx.attrs` and is forwarded verbatim to the real component below.
|
||||
const componentObj: Record<string, unknown> = {
|
||||
name: `Nych${String(key)}`,
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
injectedClasses: { default: classes },
|
||||
injectedStyles: { default: styles },
|
||||
stylePlug: { type: String },
|
||||
unstyled: { default: unstyled },
|
||||
},
|
||||
}
|
||||
|
||||
const applyThemeProps = (props: Record<string, unknown>) => {
|
||||
|
|
|
|||
|
|
@ -6,4 +6,20 @@ export {
|
|||
type Key,
|
||||
type Props,
|
||||
} 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 { Button as PrimeButton } from 'primevue'
|
||||
import { ThemeLibrary, type Themeable, ThemeEngine } from '../engine'
|
||||
import {
|
||||
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'
|
||||
|
||||
/**
|
||||
* 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> = {
|
||||
component: PrimeButton,
|
||||
unstyled: true,
|
||||
|
|
@ -42,6 +90,51 @@ const nychthemeron: ThemeLibrary = {
|
|||
components: {
|
||||
Button: buttonThemeable,
|
||||
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 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 = (): {
|
||||
Button: typeof PrimeButton
|
||||
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
|
||||
} => ({
|
||||
Button,
|
||||
LoadingIcon,
|
||||
InputText,
|
||||
Textarea,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
RadioButton,
|
||||
RadioButtonGroup,
|
||||
ToggleSwitch,
|
||||
Select,
|
||||
Card,
|
||||
Dialog,
|
||||
Message,
|
||||
Tag,
|
||||
install(app: App) {
|
||||
app.component('NychButton', Button)
|
||||
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 { mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
import { h } from 'vue'
|
||||
import { h, ref } from 'vue'
|
||||
|
||||
import { ThemeEngine, ThemeLibrary, type Themeable } from '../src/engine'
|
||||
|
||||
|
|
@ -564,6 +565,40 @@ describe('ThemeEngine', () => {
|
|||
expect(vnode).toBeDefined()
|
||||
})
|
||||
|
||||
it('forwards update:modelValue so v-model works on wrapped components', async () => {
|
||||
// Regression: a cloned wrapper inherited the component's emits/extends, so
|
||||
// Vue withheld onUpdate:modelValue from ctx.attrs and the engine never
|
||||
// forwarded it — breaking two-way v-model on every wrapped component.
|
||||
const Emitter = {
|
||||
name: 'Emitter',
|
||||
props: { modelValue: { type: String, default: '' } },
|
||||
emits: ['update:modelValue'],
|
||||
//eslint-disable-next-line
|
||||
setup(_props: Record<string, unknown>, { emit }: { emit: (e: string, v: string) => void }) {
|
||||
return () => h('button', { onClick: () => emit('update:modelValue', 'changed') }, 'x')
|
||||
},
|
||||
}
|
||||
|
||||
const theme: ThemeLibrary = { components: { Emitter: { component: Emitter } } }
|
||||
const engine = new ThemeEngine(theme)
|
||||
const Wrapped = engine.getComponent('Emitter')
|
||||
|
||||
const Parent = {
|
||||
components: { Wrapped },
|
||||
setup() {
|
||||
const val = ref('initial')
|
||||
return { val }
|
||||
},
|
||||
template: `<div><Wrapped v-model="val" /><span class="out">{{ val }}</span></div>`,
|
||||
}
|
||||
|
||||
const wrapper = mount(Parent)
|
||||
expect(wrapper.find('.out').text()).toBe('initial')
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.find('.out').text()).toBe('changed')
|
||||
})
|
||||
|
||||
it('merges theme slots with original component slots', () => {
|
||||
const ThemeSlot = {
|
||||
name: 'ThemeSlot',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
import { describe, it, expect, vi } from 'vitest'
|
||||
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
|
||||
const callSetup = (component: any, props: Record<string, unknown>) => {
|
||||
|
|
@ -92,3 +108,50 @@ describe('LoadingIcon', () => {
|
|||
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 { 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',
|
||||
|
|
|
|||
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