feat(ui): Sidebar nav

This commit is contained in:
Brian McGee
2025-06-10 11:28:11 +01:00
parent 6314dfdb3b
commit 0603c13db9
40 changed files with 1150 additions and 238 deletions

View File

@@ -8,7 +8,6 @@ const config: StorybookConfig = {
"@storybook/addon-links",
"@storybook/addon-docs",
"@storybook/addon-a11y",
"@storybook/addon-onboarding",
],
async viteFinal(config) {
return mergeConfig(config, {

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M27 38H6V17H10V13H13.5V9H37.5V13H41V24H27V27H34V31H30.5V34.5H27V38ZM16.5 20.5H20V17H16.5V20.5Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M27 38H6V17H10V13H13.5V9H37.5V13H41V24H27V27H34V31H30.5V34.5H27V38ZM16.5 20.5H20V17H16.5V20.5Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 221 B

View File

@@ -1,25 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="42" viewBox="0 0 35 42" fill="none">
<rect y="6" width="6" height="6" fill="black"/>
<rect x="6" y="6" width="6" height="6" fill="black"/>
<rect x="12" y="6" width="6" height="6" fill="black"/>
<rect x="6" y="12" width="6" height="6" fill="black"/>
<rect x="18" y="18" width="6" height="6" fill="black"/>
<rect x="18" y="12" width="6" height="6" fill="black"/>
<rect x="12" y="24" width="6" height="6" fill="black"/>
<rect x="12" y="18" width="6" height="6" fill="black"/>
<rect x="12" y="12" width="6" height="6" fill="black"/>
<rect width="6" height="6" fill="black"/>
<rect x="6" width="6" height="6" fill="black"/>
<rect x="24" y="18" width="6" height="6" fill="black"/>
<rect y="12" width="6" height="6" fill="black"/>
<rect x="6" y="18" width="6" height="6" fill="black"/>
<rect y="18" width="6" height="6" fill="black"/>
<rect y="24" width="6" height="6" fill="black"/>
<rect y="30" width="6" height="6" fill="black"/>
<rect y="36" width="6" height="6" fill="black"/>
<rect x="6" y="24" width="6" height="6" fill="black"/>
<rect x="18" y="24" width="6" height="6" fill="black"/>
<rect x="24" y="24" width="6" height="6" fill="black"/>
<rect x="29" y="24" width="6" height="6" fill="black"/>
<rect x="6" y="30" width="6" height="6" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="42" viewBox="0 0 35 42" fill="currentColor">
<rect y="6" width="6" height="6"/>
<rect x="6" y="6" width="6" height="6"/>
<rect x="12" y="6" width="6" height="6"/>
<rect x="6" y="12" width="6" height="6"/>
<rect x="18" y="18" width="6" height="6"/>
<rect x="18" y="12" width="6" height="6"/>
<rect x="12" y="24" width="6" height="6"/>
<rect x="12" y="18" width="6" height="6"/>
<rect x="12" y="12" width="6" height="6"/>
<rect width="6" height="6"/>
<rect x="6" width="6" height="6"/>
<rect x="24" y="18" width="6" height="6"/>
<rect y="12" width="6" height="6"/>
<rect x="6" y="18" width="6" height="6"/>
<rect y="18" width="6" height="6"/>
<rect y="24" width="6" height="6"/>
<rect y="30" width="6" height="6"/>
<rect y="36" width="6" height="6"/>
<rect x="6" y="24" width="6" height="6"/>
<rect x="18" y="24" width="6" height="6"/>
<rect x="24" y="24" width="6" height="6"/>
<rect x="29" y="24" width="6" height="6"/>
<rect x="6" y="30" width="6" height="6"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M20.2002 12.7998H23V15.5996H25.8008V14.2002H28.6006V10H34.2002V12.7998H37V15.5996H39.8008V24H37V26.7998H34.2002V29.5996H31.4004V32.4004H28.6006V35.2002H25.8008V38H23V35.2002H20.2002V32.4004H17.4004V29.5996H14.6006V26.7998H11.8008V24H9V15.5996H11.8008V12.7998H14.6006V10H20.2002V12.7998Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M20.2002 12.7998H23V15.5996H25.8008V14.2002H28.6006V10H34.2002V12.7998H37V15.5996H39.8008V24H37V26.7998H34.2002V29.5996H31.4004V32.4004H28.6006V35.2002H25.8008V38H23V35.2002H20.2002V32.4004H17.4004V29.5996H14.6006V26.7998H11.8008V24H9V15.5996H11.8008V12.7998H14.6006V10H20.2002V12.7998Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 418 B

After

Width:  |  Height:  |  Size: 413 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M38.666 5V39.667H38.667V5H43V44H4V5H38.666ZM12.666 35.334H16.999V31H12.666V35.334ZM29.999 35.334H34.333V31H29.999V35.334ZM21.333 26.667H25.666V22.334H21.333V26.667ZM12.666 18H16.999V13.667H12.666V18ZM29.999 18H34.333V13.667H29.999V18Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M38.666 5V39.667H38.667V5H43V44H4V5H38.666ZM12.666 35.334H16.999V31H12.666V35.334ZM29.999 35.334H34.333V31H29.999V35.334ZM21.333 26.667H25.666V22.334H21.333V26.667ZM12.666 18H16.999V13.667H12.666V18ZM29.999 18H34.333V13.667H29.999V18Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 361 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M38 42H10V38H6V10H10V6H38V10H42V38H38V42ZM18 32H30V28H18V32ZM14 28H18V24H14V28ZM30 28H34V24H30V28ZM16 20H20V16H16V20ZM28 20H32V16H28V20Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M38 42H10V38H6V10H10V6H38V10H42V38H38V42ZM18 32H30V28H18V32ZM14 28H18V24H14V28ZM30 28H34V24H30V28ZM16 20H20V16H16V20ZM28 20H32V16H28V20Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M42 42H14V38H38V14H42V42ZM34 6V34H6V6H34ZM18 18H14V22H18V26H22V22H26V18H22V14H18V18Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M42 42H14V38H38V14H42V42ZM34 6V34H6V6H34ZM18 18H14V22H18V26H22V22H26V18H22V14H18V18Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 216 B

After

Width:  |  Height:  |  Size: 211 B

View File

@@ -1,13 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="27" viewBox="0 0 38 27" fill="none">
<rect x="4.46155" y="4.15381" width="4.15385" height="4.15385" fill="black"/>
<rect x="29.3846" y="4.15381" width="4.15385" height="4.15385" fill="black"/>
<rect x="8.61539" width="4.15385" height="4.15385" fill="black"/>
<rect x="33.5385" width="4.15385" height="4.15385" fill="black"/>
<rect x="0.307678" width="4.15385" height="4.15385" fill="black"/>
<rect x="25.2308" width="4.15385" height="4.15385" fill="black"/>
<rect x="0.307678" y="8.30762" width="4.15385" height="4.15385" fill="black"/>
<rect x="25.2308" y="8.30762" width="4.15385" height="4.15385" fill="black"/>
<rect x="8.61539" y="8.30762" width="4.15385" height="4.15385" fill="black"/>
<rect x="33.5385" y="8.30762" width="4.15385" height="4.15385" fill="black"/>
<rect x="4" y="23" width="30" height="4" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="27" viewBox="0 0 38 27" fill="currentColor">
<rect x="4.46155" y="4.15381" width="4.15385" height="4.15385"/>
<rect x="29.3846" y="4.15381" width="4.15385" height="4.15385"/>
<rect x="8.61539" width="4.15385" height="4.15385"/>
<rect x="33.5385" width="4.15385" height="4.15385"/>
<rect x="0.307678" width="4.15385" height="4.15385"/>
<rect x="25.2308" width="4.15385" height="4.15385"/>
<rect x="0.307678" y="8.30762" width="4.15385" height="4.15385"/>
<rect x="25.2308" y="8.30762" width="4.15385" height="4.15385"/>
<rect x="8.61539" y="8.30762" width="4.15385" height="4.15385"/>
<rect x="33.5385" y="8.30762" width="4.15385" height="4.15385"/>
<rect x="4" y="23" width="30" height="4"/>
</svg>

Before

Width:  |  Height:  |  Size: 936 B

After

Width:  |  Height:  |  Size: 801 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M39.2002 39.2002H43V43H39.2002V39.2021H35.3994V35.4014H39.2002V39.2002ZM27.7998 8.80078H31.5996V31.6016H27.7998V35.4004H12.6006V12.6016H20.2002V8.80078H12.6006V5H27.7998V8.80078ZM35.4004 35.4004H31.6006V31.6006H35.4004V35.4004ZM12.5996 12.5996H8.7998V20.2002H12.5996V31.6016H8.7998V27.8008H5V12.5996H8.7998V8.80078H12.5996V12.5996ZM35.4004 27.8008H31.6006V12.5996H35.4004V27.8008Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M39.2002 39.2002H43V43H39.2002V39.2021H35.3994V35.4014H39.2002V39.2002ZM27.7998 8.80078H31.5996V31.6016H27.7998V35.4004H12.6006V12.6016H20.2002V8.80078H12.6006V5H27.7998V8.80078ZM35.4004 35.4004H31.6006V31.6006H35.4004V35.4004ZM12.5996 12.5996H8.7998V20.2002H12.5996V31.6016H8.7998V27.8008H5V12.5996H8.7998V8.80078H12.5996V12.5996ZM35.4004 27.8008H31.6006V12.5996H35.4004V27.8008Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -1,8 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="23" viewBox="0 0 36 23" fill="none">
<rect x="27" width="22.5" height="4.5" transform="rotate(90 27 0)" fill="black"/>
<rect x="31.5" y="4.5" width="13.5" height="4.5" transform="rotate(90 31.5 4.5)" fill="black"/>
<rect x="36" y="9" width="4.5" height="4.5" transform="rotate(90 36 9)" fill="black"/>
<rect width="22.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 9 0)" fill="black"/>
<rect width="13.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 4.5 4.5)" fill="black"/>
<rect width="4.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 0 9)" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="23" viewBox="0 0 36 23" fill="currentColor">
<rect x="27" width="22.5" height="4.5" transform="rotate(90 27 0)"/>
<rect x="31.5" y="4.5" width="13.5" height="4.5" transform="rotate(90 31.5 4.5)"/>
<rect x="36" y="9" width="4.5" height="4.5" transform="rotate(90 36 9)"/>
<rect width="22.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 9 0)"/>
<rect width="13.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 4.5 4.5)"/>
<rect width="4.5" height="4.5" transform="matrix(-4.37114e-08 1 1 4.37114e-08 0 9)"/>
</svg>

Before

Width:  |  Height:  |  Size: 694 B

After

Width:  |  Height:  |  Size: 624 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M39.4004 31.2002H35.7998V34.7998H32.2002V38.3994H28.5996V42H25V38.3994H21.4004V34.7998H17.7998V31.2002H14.2002V27.6006H39.4004V31.2002ZM28.5996 13.2002H32.2002V16.7998H35.7998V20.3994H39.4004V24H43V27.5996H10.5996V24H7V9.60059H28.5996V13.2002ZM14.2002 13.2002V16.7998H17.7998V13.2002H14.2002ZM25 9.59961H7V6H25V9.59961Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path d="M39.4004 31.2002H35.7998V34.7998H32.2002V38.3994H28.5996V42H25V38.3994H21.4004V34.7998H17.7998V31.2002H14.2002V27.6006H39.4004V31.2002ZM28.5996 13.2002H32.2002V16.7998H35.7998V20.3994H39.4004V24H43V27.5996H10.5996V24H7V9.60059H28.5996V13.2002ZM14.2002 13.2002V16.7998H17.7998V13.2002H14.2002ZM25 9.59961H7V6H25V9.59961Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8 0H28.4V9.6H18.8V0ZM11.6 12H35.6V16.8H30.8V33.6V48H26V33.6H21.2V48H16.4V33.6V16.8H11.6V12ZM6.8 7.2V12H11.6V7.2H6.8ZM6.8 7.2L2 7.2V2.4H6.8V7.2ZM40.4 7.2V12H35.6V7.2H40.4ZM40.4 7.2L40.4 2.4H45.2V7.2L40.4 7.2Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8 0H28.4V9.6H18.8V0ZM11.6 12H35.6V16.8H30.8V33.6V48H26V33.6H21.2V48H16.4V33.6V16.8H11.6V12ZM6.8 7.2V12H11.6V7.2H6.8ZM6.8 7.2L2 7.2V2.4H6.8V7.2ZM40.4 7.2V12H35.6V7.2H40.4ZM40.4 7.2L40.4 2.4H45.2V7.2L40.4 7.2Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -15,6 +15,7 @@
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@solidjs/testing-library": "^0.8.10",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"solid-js": "^1.9.7",
@@ -126,7 +127,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@@ -268,7 +268,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -366,7 +365,6 @@
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -2037,6 +2035,27 @@
"solid-js": "^1.8.6"
}
},
"node_modules/@solidjs/testing-library": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@solidjs/testing-library/-/testing-library-0.8.10.tgz",
"integrity": "sha512-qdeuIerwyq7oQTIrrKvV0aL9aFeuwTd86VYD3afdq5HYEwoox1OBTJy4y8A3TFZr8oAR0nujYgCzY/8wgHGfeQ==",
"license": "MIT",
"dependencies": {
"@testing-library/dom": "^10.4.0"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@solidjs/router": ">=0.9.0",
"solid-js": ">=1.0.0"
},
"peerDependenciesMeta": {
"@solidjs/router": {
"optional": true
}
}
},
"node_modules/@storybook/addon-a11y": {
"version": "9.0.12",
"resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-9.0.12.tgz",
@@ -2305,7 +2324,6 @@
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.10.4",
@@ -2409,7 +2427,6 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
@@ -3161,7 +3178,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dequal": "^2.0.3"
@@ -3989,7 +4005,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4011,7 +4026,6 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-serializer": {
@@ -5323,7 +5337,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -5644,7 +5657,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
@@ -6505,7 +6517,6 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1",
@@ -6520,7 +6531,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -6622,7 +6632,6 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
},
"node_modules/read-cache": {

View File

@@ -67,6 +67,7 @@
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@solidjs/testing-library": "^0.8.10",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"solid-js": "^1.9.7",

View File

@@ -2,7 +2,6 @@ import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Button, ButtonProps } from "./Button";
import { Component } from "solid-js";
import { expect, fn, waitFor } from "storybook/test";
import { PlayFunctionContext } from "storybook/internal/csf";
import { StoryContext } from "@kachurun/storybook-solid-vite";
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
@@ -150,7 +149,7 @@ export const Primary: Story = {
hierarchy: "primary",
onAction: fn(async () => {
// wait 500 ms to simulate an action
await new Promise((resolve) => setTimeout(resolve, 500));
await new Promise((resolve) => setTimeout(resolve, 1000));
// randomly fail to check that the loading state still returns to normal
if (Math.random() > 0.5) {
throw new Error("Action failure");
@@ -159,6 +158,7 @@ export const Primary: Story = {
},
parameters: {
test: {
// increase test timeout to allow for the loading action
mockTimers: true,
},
},
@@ -205,14 +205,17 @@ export const Primary: Story = {
});
// wait for the action handler to finish
await waitFor(async () => {
// the loading class should be removed
await expect(button).not.toHaveClass("loading");
// the loader should be hidden
await expect(loader.clientWidth).toEqual(0);
// the pointer should be normal
await expect(getCursorStyle(button)).toEqual("pointer");
});
await waitFor(
async () => {
// the loading class should be removed
await expect(button).not.toHaveClass("loading");
// the loader should be hidden
await expect(loader.clientWidth).toEqual(0);
// the pointer should be normal
await expect(getCursorStyle(button)).toEqual("pointer");
},
{ timeout: 1500 },
);
});
}
},

View File

@@ -1,4 +1,4 @@
import { splitProps, type JSX, createSignal, Show } from "solid-js";
import { splitProps, type JSX, createSignal } from "solid-js";
import cx from "classnames";
import { Typography } from "../Typography/Typography";
import { Button as KobalteButton } from "@kobalte/core/button";

View File

@@ -0,0 +1,15 @@
div.divider {
@apply bg-inv-2;
&.inverted {
@apply bg-def-3;
}
&.horizontal {
@apply w-full h-px;
}
&.vertical {
@apply h-full w-px;
}
}

View File

@@ -0,0 +1,47 @@
import { Meta, StoryObj } from "@kachurun/storybook-solid";
import { Divider, DividerProps } from "@/src/components/v2/Divider/Divider";
const meta: Meta<DividerProps> = {
title: "Components/Divider",
component: Divider,
};
export default meta;
type Story = StoryObj<DividerProps>;
export const Default: Story = {};
export const Horizontal: Story = {
args: {
orientation: "horizontal",
},
};
export const HorizontalInverted: Story = {
args: {
inverted: true,
...Horizontal.args,
},
};
export const Vertical: Story = {
args: {
orientation: "vertical",
},
decorators: [
(Story: Story) => (
<div class="h-32 w-full">
<Story />
</div>
),
],
};
export const VerticalInverted: Story = {
args: {
inverted: true,
...Vertical.args,
},
decorators: [...Vertical.decorators],
};

View File

@@ -0,0 +1,16 @@
import "./Divider.css";
import cx from "classnames";
export type Orientation = "horizontal" | "vertical";
export interface DividerProps {
inverted?: boolean;
orientation?: Orientation;
}
export const Divider = (props: DividerProps) => {
const inverted = props.inverted || false;
const orientation = props.orientation || "horizontal";
return <div class={cx("divider", orientation, { inverted: inverted })} />;
};

View File

@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import type { Meta, StoryObj, StoryContext } from "@kachurun/storybook-solid";
import { Component, For } from "solid-js";
import Icon, { IconProps, IconVariant } from "./Icon";
import cx from "classnames";
const iconVariants: IconVariant[] = [
"ClanIcon",
@@ -59,6 +60,13 @@ const IconExamples: Component<IconProps> = (props) => (
const meta: Meta<IconProps> = {
title: "Components/Icon",
component: IconExamples,
decorators: [
(Story: StoryObj, context: StoryContext<IconProps>) => (
<div class={cx(context.args.inverted || false ? "bg-inv-acc-3" : "")}>
<Story />
</div>
),
],
};
export default meta;
@@ -67,6 +75,64 @@ type Story = StoryObj<IconProps>;
export const Default: Story = {};
export const Primary: Story = {
args: {
color: "primary",
},
};
export const Secondary: Story = {
args: {
color: "secondary",
},
};
export const Tertiary: Story = {
args: {
color: "tertiary",
},
};
export const Quaternary: Story = {
args: {
color: "quaternary",
},
};
export const PrimaryInverted: Story = {
args: {
...Primary.args,
inverted: true,
},
};
export const SecondaryInverted: Story = {
args: {
...Secondary.args,
inverted: true,
},
};
export const TertiaryInverted: Story = {
args: {
...Tertiary.args,
inverted: true,
},
};
export const QuaternaryInverted: Story = {
args: {
...Quaternary.args,
inverted: true,
},
};
export const Inverted: Story = {
args: {
inverted: true,
},
};
export const Large: Story = {
args: {
width: "2rem",

View File

@@ -1,5 +1,5 @@
import cx from "classnames";
import { Component, JSX, Show, splitProps } from "solid-js";
import { Component, JSX, splitProps } from "solid-js";
import ArrowBottom from "@/icons/arrow-bottom.svg";
import ArrowLeft from "@/icons/arrow-left.svg";
import ArrowRight from "@/icons/arrow-right.svg";
@@ -45,9 +45,10 @@ import Offline from "@/icons/offline.svg";
import Switch from "@/icons/switch.svg";
import Tag from "@/icons/tag.svg";
import Machine from "@/icons/machine.svg";
import Loader from "@/icons/loader.svg";
import { Dynamic } from "solid-js/web";
import { Color, fgClass } from "../colors";
const icons = {
AI,
ArrowBottom,
@@ -98,24 +99,43 @@ const icons = {
export type IconVariant = keyof typeof icons;
const viewBoxes: Partial<Record<IconVariant, string>> = {
ClanIcon: "0 0 72 89",
Offline: "0 0 38 27",
};
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
icon: IconVariant;
class?: string;
size?: number | string;
color?: Color;
inverted?: boolean;
}
const Icon: Component<IconProps> = (props) => {
const [local, iconProps] = splitProps(props, ["icon", "class"]);
const [local, iconProps] = splitProps(props, [
"icon",
"color",
"class",
"size",
"inverted",
]);
const IconComponent = () => icons[local.icon];
// we need to adjust the view box for certain icons
const viewBox = () => viewBoxes[local.icon] ?? "0 0 48 48";
return IconComponent() ? (
<Dynamic
component={IconComponent()}
class={cx("icon", local.class)}
width={iconProps.size || "1em"}
height={iconProps.size || "1em"}
viewBox="0 0 48 48"
class={cx("icon", local.class, fgClass(local.color, local.inverted), {
inverted: local.inverted,
})}
data-icon-name={local.icon}
width={local.size || "1em"}
height={local.size || "1em"}
viewBox={viewBox()}
ref={iconProps.ref}
{...iconProps}
/>

View File

@@ -0,0 +1,19 @@
span.tag-status {
@apply flex items-center gap-1;
.indicator {
@apply w-1.5 h-1.5 rounded-full m-1.5;
}
&.online > .indicator {
background-color: #0ae856; /* todo get from theme */
}
&.offline > .indicator {
background-color: #ff2c78; /* todo get from theme */
}
&.installed > .indicator {
background-color: var(--clr-fg-inv-3);
}
}

View File

@@ -0,0 +1,73 @@
import {
MachineStatus,
TagStatusProps,
} from "@/src/components/v2/MachineStatus/MachineStatus";
import { Meta, StoryObj } from "@kachurun/storybook-solid";
const meta: Meta<TagStatusProps> = {
title: "Components/MachineStatus",
component: MachineStatus,
decorators: [
(Story: StoryObj) => (
<div class="p-5 bg-inv-1">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<TagStatusProps>;
export const Online: Story = {
args: {
status: "Online",
},
};
export const Offline: Story = {
args: {
status: "Offline",
},
};
export const Installed: Story = {
args: {
status: "Installed",
},
};
export const NotInstalled: Story = {
args: {
status: "Not Installed",
},
};
export const OnlineWithLabel: Story = {
args: {
...Online.args,
label: true,
},
};
export const OfflineWithLabel: Story = {
args: {
...Offline.args,
label: true,
},
};
export const InstalledWithLabel: Story = {
args: {
...Installed.args,
label: true,
},
};
export const NotInstalledWithLabel: Story = {
args: {
...NotInstalled.args,
label: true,
},
};

View File

@@ -0,0 +1,42 @@
import "./MachineStatus.css";
import { Badge } from "@kobalte/core/badge";
import cx from "classnames";
import { Show } from "solid-js";
import Icon from "../Icon/Icon";
import { Typography } from "@/src/components/v2/Typography/Typography";
export type MachineStatus =
| "Online"
| "Offline"
| "Installed"
| "Not Installed";
export interface TagStatusProps {
label?: boolean;
status: MachineStatus;
}
export const MachineStatus = (props: TagStatusProps) => (
<Badge
class={cx("tag-status", {
online: props.status == "Online",
offline: props.status == "Offline",
installed: props.status == "Installed",
"not-installed": props.status == "Not Installed",
})}
textValue={props.status}
>
{props.label && (
<Typography hierarchy="label" size="xs" weight="medium" inverted={true}>
{props.status}
</Typography>
)}
<Show
when={props.status == "Not Installed"}
fallback={<div class="indicator" />}
>
<Icon icon="Offline" inverted={true} />
</Show>
</Badge>
);

View File

@@ -0,0 +1,10 @@
div.sidebar {
@apply h-full w-auto max-w-60 border-none;
& > div.header {
}
& > div.body {
@apply pt-4 pb-3 px-2;
}
}

View File

@@ -0,0 +1,109 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
createMemoryHistory,
MemoryRouter,
Route,
RouteSectionProps,
} from "@solidjs/router";
import {
SidebarNav,
SidebarNavProps,
} from "@/src/components/v2/Sidebar/SidebarNav";
import { Suspense } from "solid-js";
import { StoryContext } from "@kachurun/storybook-solid-vite";
const sidebarNavProps: SidebarNavProps = {
clanLinks: [
{ label: "Brian's Clan", path: "/clan/1" },
{ label: "Dave's Clan", path: "/clan/2" },
{ label: "Mic92's Clan", path: "/clan/3" },
],
clanDetail: {
label: "Brian's Clan",
settingsPath: "/clan/1/settings",
machines: [
{
label: "Backup & Home",
path: "/clan/1/machine/backup",
serviceCount: 3,
status: "Online",
},
{
label: "Raspberry Pi",
path: "/clan/1/machine/pi",
serviceCount: 1,
status: "Offline",
},
{
label: "Mom's Laptop",
path: "/clan/1/machine/moms-laptop",
serviceCount: 2,
status: "Installed",
},
{
label: "Dad's Laptop",
path: "/clan/1/machine/dads-laptop",
serviceCount: 4,
status: "Not Installed",
},
],
},
extraSections: [
{
label: "Tools",
links: [
{ label: "Borgbackup", path: "/clan/1/service/borgbackup" },
{ label: "Syncthing", path: "/clan/1/service/syncthing" },
{ label: "Mumble", path: "/clan/1/service/mumble" },
{ label: "Minecraft", path: "/clan/1/service/minecraft" },
],
},
{
label: "Links",
links: [
{ label: "GitHub", path: "https://github.com/brian-the-dev" },
{ label: "Twitter", path: "https://twitter.com/brian_the_dev" },
{
label: "LinkedIn",
path: "https://www.linkedin.com/in/brian-the-dev/",
},
{
label: "Instagram",
path: "https://www.instagram.com/brian_the_dev/",
},
],
},
],
};
const meta: Meta<RouteSectionProps> = {
title: "Components/Sidebar/Nav",
component: SidebarNav,
render: (_: never, context: StoryContext<SidebarNavProps>) => {
const history = createMemoryHistory();
history.set({ value: "/clan/1/machine/backup" });
return (
<div style="height: 670px;">
<MemoryRouter
history={history}
root={(props) => (
<Suspense>
<SidebarNav {...sidebarNavProps} />
</Suspense>
)}
>
<Route path="/clan/1/machine/backup" component={(props) => <></>} />
</MemoryRouter>
</div>
);
},
};
export default meta;
type Story = StoryObj<RouteSectionProps>;
export const Default: Story = {
args: {},
};

View File

@@ -0,0 +1,47 @@
import "./SidebarNav.css";
import { SidebarNavHeader } from "@/src/components/v2/Sidebar/SidebarNavHeader";
import { SidebarNavBody } from "@/src/components/v2/Sidebar/SidebarNavBody";
import { MachineStatus } from "@/src/components/v2/MachineStatus/MachineStatus";
export interface LinkProps {
path: string;
label?: string;
}
export interface SectionProps {
label: string;
links: LinkProps[];
}
export interface MachineProps {
label: string;
path: string;
status: MachineStatus;
serviceCount: number;
}
export interface ClanLinkProps {
label: string;
path: string;
}
export interface ClanProps {
label: string;
settingsPath: string;
machines: MachineProps[];
}
export interface SidebarNavProps {
clanDetail: ClanProps;
clanLinks: ClanLinkProps[];
extraSections: SectionProps[];
}
export const SidebarNav = (props: SidebarNavProps) => {
return (
<div class="sidebar">
<SidebarNavHeader {...props} />
<SidebarNavBody {...props} />
</div>
);
};

View File

@@ -0,0 +1,127 @@
div.sidebar-body {
@apply py-4 px-2 h-full;
@apply border border-inv-3 rounded-bl-md rounded-br-md;
&::-webkit-scrollbar {
display: none;
}
overflow-y: auto;
scrollbar-width: none;
scrollbar-color: theme(colors.primary.700) theme(colors.primary.600);
background: linear-gradient(
180deg,
var(--clr-bg-inv-1) 0%,
var(--clr-bg-inv-3) 100%
);
@apply backdrop-blur-sm;
.accordion {
@apply w-full mb-4;
&:last-child {
@apply mb-0;
}
& > .item {
@apply py-3 px-1.5 bg-inv-3 rounded-md mb-4;
&:last-child {
@apply mb-0;
}
& > .header {
@apply flex mb-4 px-2;
& > .trigger {
@apply inline-flex items-center justify-between w-full;
&:focus-visible {
@apply z-10;
outline: 2px solid hsl(200 98% 39%);
outline-offset: 2px;
}
& > .icon {
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
&[data-expanded] > .icon {
transform: rotate(180deg);
}
.section-title {
@apply uppercase;
}
}
}
& > .content {
@apply overflow-hidden flex flex-col;
animation: slideAccordionUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
&[data-expanded] {
animation: slideAccordionDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
nav * {
@apply outline-none;
}
nav > a {
@apply block w-full px-2 py-1.5 min-h-7 my-2 rounded-md;
&:first-child {
@apply mt-0;
}
&:last-child {
@apply mb-0;
}
&:focus-visible {
background: linear-gradient(
90deg,
theme(colors.secondary.900),
60%,
theme(colors.secondary.600) 100%
);
}
&:hover {
@apply bg-inv-acc-2;
}
&:active {
@apply bg-inv-acc-3;
}
&.active {
@apply bg-inv-acc-2;
}
}
}
}
}
}
@keyframes slideAccordionDown {
from {
height: 0;
}
to {
height: var(--kb-accordion-content-height);
}
}
@keyframes slideAccordionUp {
from {
height: var(--kb-accordion-content-height);
}
to {
height: 0;
}
}

View File

@@ -0,0 +1,138 @@
import "./SidebarNavBody.css";
import { A } from "@solidjs/router";
import { Accordion } from "@kobalte/core/accordion";
import Icon from "../Icon/Icon";
import { Typography } from "@/src/components/v2/Typography/Typography";
import {
MachineProps,
SidebarNavProps,
} from "@/src/components/v2/Sidebar/SidebarNav";
import { For } from "solid-js";
import { MachineStatus } from "@/src/components/v2/MachineStatus/MachineStatus";
const MachineRoute = (props: MachineProps) => (
<div class="flex w-full flex-col gap-2">
<div class="flex flex-row items-center justify-between">
<Typography
hierarchy="label"
size="xs"
weight="bold"
color="primary"
inverted={true}
>
{props.label}
</Typography>
<MachineStatus status={props.status} />
</div>
<div class="flex w-full flex-row items-center gap-1">
<Icon icon="Flash" size="0.75rem" inverted={true} color="tertiary" />
<Typography
hierarchy="label"
family="mono"
size="s"
inverted={true}
color="primary"
>
{props.serviceCount}
</Typography>
</div>
</div>
);
export const SidebarNavBody = (props: SidebarNavProps) => {
const sectionLabels = props.extraSections.map((section) => section.label);
// controls which sections are open by default
// we want them all to be open by default
const defaultAccordionValues = ["your-machines", ...sectionLabels];
return (
<div class="sidebar-body">
<Accordion
class="accordion"
multiple
defaultValue={defaultAccordionValues}
>
<Accordion.Item class="item" value="your-machines">
<Accordion.Header class="header">
<Accordion.Trigger class="trigger">
<Typography
class="section-title"
hierarchy="label"
family="mono"
size="xs"
inverted={true}
color="tertiary"
>
Your Machines
</Typography>
<Icon
icon="CaretDown"
color="tertiary"
inverted={true}
size="0.75rem"
/>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="content">
<nav>
<For each={props.clanDetail.machines}>
{(machine) => (
<A href={machine.path}>
<MachineRoute {...machine} />
</A>
)}
</For>
</nav>
</Accordion.Content>
</Accordion.Item>
<For each={props.extraSections}>
{(section) => (
<Accordion.Item class="item" value={section.label}>
<Accordion.Header class="header">
<Accordion.Trigger class="trigger">
<Typography
class="section-title"
hierarchy="label"
family="mono"
size="xs"
inverted={true}
color="tertiary"
>
{section.label}
</Typography>
<Icon
icon="CaretDown"
color="tertiary"
inverted={true}
size="0.75rem"
/>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="content">
<nav>
<For each={section.links || []}>
{(link) => (
<A href={link.path}>
<Typography
hierarchy="body"
size="xs"
weight="bold"
color="primary"
inverted={true}
>
{link.label}
</Typography>
</A>
)}
</For>
</nav>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
);
};

View File

@@ -0,0 +1,91 @@
div.sidebar-header {
@apply flex items-center justify-center w-full px-1 py-1;
@apply border border-inv-3 rounded-md rounded-bl-none rounded-br-none;
background: linear-gradient(
90deg,
var(--clr-bg-inv-3) 0%,
var(--clr-bg-inv-4) 100%
);
& > .dropdown-trigger {
@apply flex items-center justify-between flex-grow px-1 py-1;
@apply rounded-tl-md rounded-tr-md;
@apply border border-transparent border-b-0;
transition: all 250ms ease-in-out;
div.title {
@apply flex items-center gap-2 justify-start;
& > .clan-icon {
@apply rounded-full bg-inv-4 w-7 h-7;
}
}
.icon[data-icon-name="CaretDown"] {
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
&[data-expanded] {
@apply bg-def-1 border-def-2;
.icon[data-icon-name="CaretDown"] {
transform: rotate(180deg);
}
}
}
}
.sidebar-dropdown-content {
@apply flex flex-col w-full px-2 py-1.5;
@apply bg-def-1 rounded-bl-md rounded-br-md;
@apply border border-def-2;
animation: sidebarNavContentHide 250ms ease-in forwards;
.dropdown-item {
@apply flex items-center justify-start w-full px-1.5 py-2 gap-2 rounded;
&:hover {
@apply bg-def-acc-2 cursor-pointer;
}
}
.dropdown-group {
@apply flex flex-col gap-2;
@apply px-1;
.dropdown-group-label {
}
.dropdown-group-items {
@apply rounded px-1 py-1.5 bg-def-2;
}
}
}
.sidebar-dropdown-content[data-expanded] {
animation: sidebarNavContentShow 250ms ease-out;
}
@keyframes sidebarNavContentShow {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes sidebarNavContentHide {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.96);
}
}

View File

@@ -0,0 +1,99 @@
import "./SidebarNavHeader.css";
import Icon from "@/src/components/v2/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router";
import { Typography } from "../Typography/Typography";
import { createSignal, For } from "solid-js";
import {
ClanLinkProps,
ClanProps,
} from "@/src/components/v2/Sidebar/SidebarNav";
export interface SidebarHeaderProps {
clanDetail: ClanProps;
clanLinks: ClanLinkProps[];
}
export const SidebarNavHeader = (props: SidebarHeaderProps) => {
const navigate = useNavigate();
const [open, setOpen] = createSignal(false);
const firstChar = props.clanDetail.label.charAt(0);
return (
<div class="sidebar-header">
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="title">
<div class="clan-icon">
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={true}
>
{firstChar.toUpperCase()}
</Typography>
</div>
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={!open()}
>
{props.clanDetail.label}
</Typography>
</div>
<DropdownMenu.Icon>
<Icon icon={"CaretDown"} inverted={!open()} size="0.75rem" />
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigate(props.clanDetail.settingsPath)}
>
<Icon
icon="Settings"
size="0.75rem"
inverted={true}
color="tertiary"
/>
<Typography hierarchy="label" size="xs" weight="medium">
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
>
YOUR CLANS
</Typography>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={props.clanLinks}>
{(clan) => (
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigate(clan.path)}
>
<Typography hierarchy="label" size="xs" weight="medium">
{clan.label}
</Typography>
</DropdownMenu.Item>
)}
</For>
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
);
};

View File

@@ -1,151 +1,151 @@
/* Body */
.typography {
&.font-weight-normal {
&.weight-normal {
font-weight: 400;
}
&.font-weight-medium {
&.weight-medium {
font-weight: 500;
}
&.font-weight-bold {
&.weight-bold {
font-weight: 600;
}
&.font-body {
&.font-family-regular {
&.body {
&.family-regular {
font-family: "Archivo", sans-serif;
}
&.font-family-condensed {
&.family-condensed {
font-family: "Archivo SemiCondensed", sans-serif;
}
&.font-size-default {
&.size-default {
font-size: 1rem;
line-height: 1.32;
letter-spacing: 0.02rem;
}
&.font-size-s {
&.size-s {
font-size: 0.875rem;
line-height: 1.32;
letter-spacing: 0.0175rem;
}
&.font-size-xs {
&.size-xs {
font-size: 0.75rem;
line-height: 1.32;
letter-spacing: 0.0225rem;
}
&.font-size-xxs {
&.size-xxs {
font-size: 0.6875rem;
line-height: 1.32;
letter-spacing: 0.00688rem;
}
}
&.font-label {
&.font-family-condensed {
&.label {
&.family-condensed {
font-family: "Archivo SemiCondensed", sans-serif;
&.font-size-default {
&.size-default {
font-size: 0.875rem;
line-height: 1.32;
line-height: 1;
letter-spacing: 0.0175rem;
}
&.font-size-s {
&.size-s {
font-size: 0.8125rem;
line-height: 1.32;
line-height: 1;
letter-spacing: 0.0175rem;
}
&.font-size-xs {
&.size-xs {
font-size: 0.75rem;
line-height: 1.32;
line-height: 1;
letter-spacing: 0.0075rem;
}
}
&.font-family-mono {
&.family-mono {
font-family: "Commit Mono", monospace;
&.font-size-default {
&.size-default {
font-size: 0.8125rem;
line-height: 0;
line-height: 1;
letter-spacing: normal;
}
&.font-size-s {
&.size-s {
font-size: 0.75rem;
line-height: 0;
line-height: 1;
letter-spacing: normal;
}
&.font-size-xs {
&.size-xs {
font-size: 0.6875rem;
line-height: 0;
line-height: 1;
letter-spacing: normal;
}
}
}
&.font-title {
&.font-family-regular {
&.title {
&.family-regular {
font-family: "Archivo", sans-serif;
}
&.font-size-default {
&.size-default {
font-size: 1.125rem;
line-height: 124%;
letter-spacing: 0.03375rem;
}
&.font-size-m {
&.size-m {
font-size: 1.25rem;
line-height: 124%;
letter-spacing: 0.0375rem;
}
&.font-size-l {
&.size-l {
font-size: 1.375rem;
line-height: 124%;
letter-spacing: 0.04125rem;
}
}
&.font-headline {
&.font-family-regular {
&.headline {
&.family-regular {
font-family: "Archivo", sans-serif;
}
&.font-size-default {
&.size-default {
font-size: 1.5rem;
line-height: 116%;
letter-spacing: 0.015rem;
}
&.font-size-m {
&.size-m {
font-size: 1.75rem;
line-height: 116%;
letter-spacing: 0.0175rem;
}
&.font-size-l {
&.size-l {
font-size: 2rem;
line-height: 116%;
letter-spacing: 0.06rem;
}
}
&.font-teaser {
&.font-family-regular {
&.teaser {
&.family-regular {
font-family: "Archivo", sans-serif;
}
&.font-size-default {
&.size-default {
font-size: 3rem;
line-height: normal;
letter-spacing: -0.06rem;

View File

@@ -14,9 +14,7 @@ There are two fonts being used within our typography system:
## UI Components
When creating UI components that a user will interact with,
you must use the condensed form of `Body`, `Label` and `Label Mono`.
<DocsStory of={TypographyStories.BodyCondensed} />
you **must use** `Label` or `Label Mono`.
<DocsStory of={TypographyStories.LabelCondensed} />

View File

@@ -1,14 +1,8 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
AllowedSizes,
Color,
Family,
Hierarchy,
Typography,
Weight,
} from "./Typography";
import { Family, Hierarchy, Typography, Weight } from "./Typography";
import { Component, For, Show } from "solid-js";
import { AllColors } from "@/src/components/v2/colors";
interface TypographyExamplesProps {
weights: Weight[];
@@ -19,14 +13,6 @@ interface TypographyExamplesProps {
inverted?: boolean;
}
const colors: (Color | "inherit")[] = [
"inherit",
"primary",
"secondary",
"tertiary",
"quaternary",
];
const TypographyExamples: Component<TypographyExamplesProps> = (props) => (
<table
class="w-full min-w-max table-auto text-left"
@@ -59,7 +45,7 @@ const TypographyExamples: Component<TypographyExamplesProps> = (props) => (
</Typography>
</Show>
<Show when={props.colors}>
<For each={colors}>
<For each={AllColors}>
{(color) => (
<>
<Typography

View File

@@ -1,36 +1,14 @@
import { type JSX, mergeProps } from "solid-js";
import { type JSX } from "solid-js";
import { Dynamic } from "solid-js/web";
import cx from "classnames";
import "./Typography.css";
import { Color, fgClass } from "@/src/components/v2/colors";
export type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
export type Color = "primary" | "secondary" | "tertiary" | "quaternary";
export type Hierarchy = "body" | "title" | "headline" | "label" | "teaser";
export type Weight = "normal" | "medium" | "bold";
export type Family = "regular" | "condensed" | "mono";
const colorMap: Record<Color, string> = {
primary: cx("fg-def-1"),
secondary: cx("fg-def-2"),
tertiary: cx("fg-def-3"),
quaternary: cx("fg-def-4"),
};
const invertedColorMap: Record<Color, string> = {
primary: cx("fg-inv-1"),
secondary: cx("fg-inv-2"),
tertiary: cx("fg-inv-3"),
quaternary: cx("fg-inv-4"),
};
const colorFor = (color: Color | "inherit" = "primary", inverted = false) => {
if (color === "inherit") {
return "text-inherit";
}
return inverted ? invertedColorMap[color] : colorMap[color];
};
// type Size = "default" | "xs" | "s" | "m" | "l";
interface SizeForHierarchy {
body: {
@@ -63,30 +41,30 @@ export type AllowedSizes<H extends Hierarchy> = keyof SizeForHierarchy[H];
const sizeHierarchyMap: SizeForHierarchy = {
body: {
default: cx("font-size-default"),
s: cx("font-size-s"),
xs: cx("font-size-xs"),
xxs: cx("font-size-xxs"),
default: cx("size-default"),
s: cx("size-s"),
xs: cx("size-xs"),
xxs: cx("size-xxs"),
},
headline: {
default: cx("font-size-default"),
m: cx("font-size-m"),
l: cx("font-size-l"),
default: cx("size-default"),
m: cx("size-m"),
l: cx("size-l"),
},
title: {
default: cx("font-size-default"),
// xs: cx("font-size-xs"),
// s: cx("font-size-s"),
m: cx("font-size-m"),
l: cx("font-size-l"),
default: cx("size-default"),
// xs: cx("size-xs"),
// s: cx("size-s"),
m: cx("size-m"),
l: cx("size-l"),
},
label: {
default: cx("font-size-default"),
s: cx("font-size-s"),
xs: cx("font-size-xs"),
default: cx("size-default"),
s: cx("size-s"),
xs: cx("size-xs"),
},
teaser: {
default: cx("font-size-default"),
default: cx("size-default"),
},
};
@@ -99,50 +77,43 @@ const defaultFamilyMap: Record<Hierarchy, Family> = {
};
const weightMap: Record<Weight, string> = {
normal: cx("font-weight-normal"),
medium: cx("font-weight-medium"),
bold: cx("font-weight-bold"),
normal: cx("weight-normal"),
medium: cx("weight-medium"),
bold: cx("weight-bold"),
};
interface _TypographyProps<H extends Hierarchy> {
hierarchy: H;
size: AllowedSizes<H>;
color?: Color | "inherit";
color?: Color;
children: JSX.Element;
weight?: Weight;
family?: Family;
inverted?: boolean;
tag?: Tag;
class?: string;
classList?: Record<string, boolean>;
}
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
const family = () =>
`font-family-${props.family || defaultFamilyMap[props.hierarchy]}`;
const color = () => colorFor(props.color, props.inverted);
const classList = mergeProps(props.classList, {
"font-body": props.hierarchy === "body" || !props.hierarchy,
"font-label": props.hierarchy === "label",
"font-title": props.hierarchy === "title",
"font-headline": props.hierarchy === "headline",
"font-teaser": props.hierarchy === "teaser",
});
`family-${props.family || defaultFamilyMap[props.hierarchy]}`;
const hierarchy = () => props.hierarchy || "body";
const size = () => sizeHierarchyMap[props.hierarchy][props.size] as string;
const weight = () => weightMap[props.weight || "normal"];
const color = () => fgClass(props.color, props.inverted);
return (
<Dynamic
class={cx(
"typography",
color(),
hierarchy(),
family(),
weightMap[props.weight || "normal"],
sizeHierarchyMap[props.hierarchy][props.size] as string,
weight(),
size(),
color(),
props.class,
)}
component={props.tag || "span"}
classList={classList}
>
{props.children}
</Dynamic>

View File

@@ -0,0 +1,41 @@
export type Color =
| "primary"
| "secondary"
| "tertiary"
| "quaternary"
| "inherit";
export const AllColors: Color[] = [
"primary",
"secondary",
"tertiary",
"quaternary",
"inherit",
];
const colorMap: Record<Color, string> = {
primary: "fg-def-1",
secondary: "fg-def-2",
tertiary: "fg-def-3",
quaternary: "fg-def-4",
inherit: "text-inherit",
};
const invertedColorMap: Record<Color, string> = {
primary: "fg-inv-1",
secondary: "fg-inv-2",
tertiary: "fg-inv-3",
quaternary: "fg-inv-4",
inherit: "text-inherit",
};
export const fgClass = (
color: Color | "inherit" = "primary",
inverted = false,
) => {
if (color === "inherit") {
return "text-inherit";
}
return inverted ? invertedColorMap[color] : colorMap[color];
};

View File

@@ -102,3 +102,13 @@ html {
user-select: none;
/* Standard */
}
@layer utilities {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
}

View File

@@ -1,4 +1,3 @@
import typography from "@tailwindcss/typography";
import kobalte from "@kobalte/tailwindcss";
import core from "./tailwind/core-plugin";
@@ -6,7 +5,7 @@ import core from "./tailwind/core-plugin";
const config = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {},
plugins: [typography, core, kobalte],
plugins: [core, kobalte],
};
export default config;

View File

@@ -1,5 +1,4 @@
import plugin from "tailwindcss/plugin";
import { typography } from "./typography";
// @ts-expect-error: lib of tailwind has no types
import { parseColor } from "tailwindcss/lib/util/color";
@@ -154,7 +153,7 @@ export default plugin.withOptions(
backgroundColor: theme("colors.secondary.700"),
},
".bg-inv-acc-4": {
backgroundColor: theme("colors.secondary.900"),
backgroundColor: theme("colors.primary.950"),
},
// bg inverse accent
@@ -252,7 +251,7 @@ export default plugin.withOptions(
500: toRGB("#526f6f"),
600: toRGB("#4b6767"),
700: toRGB("#345253"),
800: toRGB("#2b4647"),
800: toRGB("#2e4a4b"),
900: toRGB("#203637"),
950: toRGB("#162324"),
},
@@ -316,7 +315,6 @@ export default plugin.withOptions(
"0px 0px 0px 1px white, 0px 0px 0px 2px var(--clr-bg-inv-acc-4, #203637), 2px 2px 0px 0px var(--clr-bg-inv-acc-2, #4F747A) inset",
},
},
...typography,
},
}),
);

View File

@@ -1,22 +0,0 @@
import defaultTheme from "tailwindcss/defaultTheme";
import type { Config } from "tailwindcss";
export const typography: Partial<Config["theme"]> = {
fontFamily: {
sans: ["Archivo SemiCondensed", ...defaultTheme.fontFamily.sans],
},
fontSize: {
...defaultTheme.fontSize,
title: ["1.125rem", { lineHeight: "124%" }],
"title-m": ["1.25rem", { lineHeight: "124%" }],
"title-l": ["1.375rem", { lineHeight: "124%" }],
label: ["0.8125rem", { lineHeight: "100%" }],
"label-s": ["0.75rem", { lineHeight: "100%" }],
"label-xs": ["0.6875rem", { lineHeight: "124%" }],
},
// textColor: {
// ...defaultTheme.textColor,
// primary: "#0D1416",
// secondary: "#2C4347",
// },
} as const;