feat: implement Premium features page using shadcn/ui and Tailwind (#15094)

resolves coder/internal#176

This is a proof of concept to showcase the use of Tailwind + shadcn/ui.
The goal here was to implement a lower complexity page to reduce the
initial risk and highlight the specific global changes needed.

Changes:

- Create a separate version of Premium features content for Enterprise
and OSS users
- Use shadcn/ui button component and setup styling and variants to match
the buttons defined in the Coder Kit Figma.
https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=3-1756
- Setup theming using CSS variables and custom Tailwind colors by using
the dark class name on the root element.
- CSS variable design token naming and colors defined in Figma.
https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=1-2


<img width="1199" alt="Screenshot 2024-11-01 at 12 46 03 PM"
src="https://github.com/user-attachments/assets/a251450f-8447-405a-9cc4-9f6aad9cd19f">
<img width="1197" alt="Screenshot 2024-11-01 at 12 45 36 PM"
src="https://github.com/user-attachments/assets/e78a5a40-cbdf-4d85-9ffe-873505800fcc">
This commit is contained in:
Jaayden Halko
2024-11-06 12:53:00 -05:00
committed by GitHub
parent cafb963bf4
commit d4131badb6
16 changed files with 860 additions and 74 deletions
+5 -4
View File
@@ -47,10 +47,11 @@
"playwright.reuseBrowser": true,
"[javascript][javascriptreact][json][jsonc][typescript][typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
// "editor.codeActionsOnSave": {
// "source.organizeImports.biome": "explicit"
// }
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit"
// "source.organizeImports.biome": "explicit"
}
},
"[css][html][markdown][yaml]": {
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "/components",
"utils": "/utils",
"ui": "/components/ui",
"lib": "/lib",
"hooks": "/hooks"
}
}
+11 -5
View File
@@ -36,6 +36,7 @@
"@alwaysmeticulous/recorder-loader": "2.137.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@emotion/cache": "11.13.1",
"@emotion/css": "11.13.4",
"@emotion/react": "11.13.3",
"@emotion/styled": "11.13.0",
@@ -49,6 +50,7 @@
"@mui/system": "5.16.7",
"@mui/utils": "5.16.6",
"@mui/x-tree-view": "7.18.0",
"@radix-ui/react-slot": "1.1.0",
"@tanstack/react-query-devtools": "4.35.3",
"@xterm/addon-canvas": "0.7.0",
"@xterm/addon-fit": "0.10.0",
@@ -63,6 +65,8 @@
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-plugin-annotation": "3.0.1",
"chroma-js": "2.4.2",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"color-convert": "2.0.1",
"cron-parser": "4.9.0",
"cronstrue": "2.50.0",
@@ -74,6 +78,7 @@
"front-matter": "4.0.2",
"jszip": "3.10.1",
"lodash": "4.17.21",
"lucide-react": "0.454.0",
"monaco-editor": "0.52.0",
"pretty-bytes": "6.1.1",
"react": "18.3.1",
@@ -93,6 +98,8 @@
"resize-observer-polyfill": "1.5.1",
"rollup-plugin-visualizer": "5.12.0",
"semver": "7.6.2",
"tailwind-merge": "2.5.4",
"tailwindcss-animate": "1.0.7",
"tzdata": "1.0.40",
"ua-parser-js": "1.0.33",
"ufuzzy": "npm:@leeoniya/ufuzzy@1.0.10",
@@ -141,6 +148,7 @@
"@types/ua-parser-js": "0.7.36",
"@types/uuid": "9.0.2",
"@vitejs/plugin-react": "4.3.3",
"autoprefixer": "10.4.20",
"chromatic": "11.16.3",
"eventsourcemock": "2.0.0",
"express": "4.21.0",
@@ -151,6 +159,7 @@
"jest-websocket-mock": "2.5.0",
"jest_workaround": "0.1.14",
"msw": "2.3.5",
"postcss": "8.4.47",
"prettier": "3.3.3",
"protobufjs": "7.4.0",
"rxjs": "7.8.1",
@@ -158,6 +167,7 @@
"storybook": "8.3.5",
"storybook-addon-remix-react-router": "3.0.1",
"storybook-react-context": "0.6.0",
"tailwindcss": "3.4.13",
"ts-node": "10.9.1",
"ts-proto": "1.164.0",
"ts-prune": "0.10.3",
@@ -166,11 +176,7 @@
"vite-plugin-checker": "0.8.0",
"vite-plugin-turbosnap": "1.0.3"
},
"browserslist": [
"chrome 110",
"firefox 111",
"safari 16.0"
],
"browserslist": ["chrome 110", "firefox 111", "safari 16.0"],
"resolutions": {
"optionator": "0.9.3",
"semver": "7.6.2"
+330 -37
View File
@@ -21,6 +21,9 @@ importers:
'@emoji-mart/react':
specifier: 1.1.1
version: 1.1.1(emoji-mart@5.6.0)(react@18.3.1)
'@emotion/cache':
specifier: 11.13.1
version: 11.13.1
'@emotion/css':
specifier: 11.13.4
version: 11.13.4
@@ -60,6 +63,9 @@ importers:
'@mui/x-tree-view':
specifier: 7.18.0
version: 7.18.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot':
specifier: 1.1.0
version: 1.1.0(@types/react@18.3.12)(react@18.3.1)
'@tanstack/react-query-devtools':
specifier: 4.35.3
version: 4.35.3(@tanstack/react-query@4.35.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -102,6 +108,12 @@ importers:
chroma-js:
specifier: 2.4.2
version: 2.4.2
class-variance-authority:
specifier: 0.7.0
version: 0.7.0
clsx:
specifier: 2.1.1
version: 2.1.1
color-convert:
specifier: 2.0.1
version: 2.0.1
@@ -135,6 +147,9 @@ importers:
lodash:
specifier: 4.17.21
version: 4.17.21
lucide-react:
specifier: 0.454.0
version: 0.454.0(react@18.3.1)
monaco-editor:
specifier: 0.52.0
version: 0.52.0
@@ -192,6 +207,12 @@ importers:
semver:
specifier: 7.6.2
version: 7.6.2
tailwind-merge:
specifier: 2.5.4
version: 2.5.4
tailwindcss-animate:
specifier: 1.0.7
version: 1.0.7(tailwindcss@3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)))
tzdata:
specifier: 1.0.40
version: 1.0.40
@@ -331,6 +352,9 @@ importers:
'@vitejs/plugin-react':
specifier: 4.3.3
version: 4.3.3(vite@5.4.10(@types/node@20.17.6))
autoprefixer:
specifier: 10.4.20
version: 10.4.20(postcss@8.4.47)
chromatic:
specifier: 11.16.3
version: 11.16.3
@@ -361,6 +385,9 @@ importers:
msw:
specifier: 2.3.5
version: 2.3.5(typescript@5.6.3)
postcss:
specifier: 8.4.47
version: 8.4.47
prettier:
specifier: 3.3.3
version: 3.3.3
@@ -382,6 +409,9 @@ importers:
storybook-react-context:
specifier: 0.6.0
version: 0.6.0(react-dom@18.3.1(react@18.3.1))
tailwindcss:
specifier: 3.4.13
version: 3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))
ts-node:
specifier: 10.9.1
version: 10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)
@@ -416,6 +446,10 @@ packages:
'@adobe/css-tools@4.4.0':
resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==}
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@alwaysmeticulous/recorder-loader@2.137.0':
resolution: {integrity: sha512-ux/xGYCNsOe8BzquEg7k7YSNJiw/0Sg2Pd/7fppYiVr5xEefpPeIhh3qwuupZgx6sB2t5KpKQdodNWVmGeyh/w==}
@@ -1666,15 +1700,6 @@ packages:
'@radix-ui/primitive@1.1.0':
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
'@radix-ui/react-compose-refs@1.0.1':
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-compose-refs@1.1.0':
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
@@ -1789,15 +1814,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.0.2':
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-slot@1.1.0':
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
peerDependencies:
@@ -2788,6 +2804,9 @@ packages:
engines: {node: '>=8.0.0'}
hasBin: true
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -2798,6 +2817,9 @@ packages:
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@@ -2836,6 +2858,13 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autoprefixer@10.4.20:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
postcss: ^8.1.0
available-typed-arrays@1.0.5:
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
engines: {node: '>= 0.4'}
@@ -2945,6 +2974,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
@@ -3046,6 +3079,9 @@ packages:
cjs-module-lexer@1.3.1:
resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==}
class-variance-authority@0.7.0:
resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==}
classnames@2.3.2:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
@@ -3061,6 +3097,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
clsx@2.0.0:
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
engines: {node: '>=6'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -3098,6 +3138,10 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@6.2.1:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
engines: {node: '>= 6'}
@@ -3185,6 +3229,11 @@ packages:
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
cssfontparser@1.2.1:
resolution: {integrity: sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==}
@@ -3324,6 +3373,9 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
diff-sequences@29.6.3:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -3332,6 +3384,9 @@ packages:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
@@ -3484,7 +3539,6 @@ packages:
eslint@8.52.0:
resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
espree@9.6.1:
@@ -3651,6 +3705,9 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@@ -4296,6 +4353,10 @@ packages:
'@swc/core': ^1.3.3
'@swc/jest': ^0.2.22
jiti@1.21.6:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -4373,6 +4434,14 @@ packages:
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
lilconfig@3.1.2:
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -4415,6 +4484,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.454.0:
resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
luxon@3.3.0:
resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==}
engines: {node: '>=12'}
@@ -4692,6 +4766,9 @@ packages:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nan@2.20.0:
resolution: {integrity: sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==}
@@ -4739,6 +4816,10 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
normalize-range@0.1.2:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
@@ -4750,6 +4831,10 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
@@ -4873,6 +4958,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
@@ -4899,6 +4988,43 @@ packages:
resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==}
engines: {node: '>=10'}
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
peerDependencies:
postcss: ^8.0.0
postcss-js@4.0.1:
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.4.21
postcss-load-config@4.0.2:
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
engines: {node: '>= 14'}
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.4.47:
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -5195,6 +5321,9 @@ packages:
peerDependencies:
react: '*'
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
@@ -5542,6 +5671,11 @@ packages:
stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
superjson@1.13.3:
resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==}
engines: {node: '>=10'}
@@ -5565,6 +5699,19 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tailwind-merge@2.5.4:
resolution: {integrity: sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==}
tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
tailwindcss@3.4.13:
resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==}
engines: {node: '>=14.0.0'}
hasBin: true
tar-fs@2.1.1:
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
@@ -5593,6 +5740,13 @@ packages:
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tiny-case@1.0.3:
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
@@ -5659,6 +5813,9 @@ packages:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
ts-morph@13.0.3:
resolution: {integrity: sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==}
@@ -6078,6 +6235,11 @@ packages:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
yaml@2.6.0:
resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==}
engines: {node: '>= 14'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@@ -6109,6 +6271,8 @@ snapshots:
'@adobe/css-tools@4.4.0': {}
'@alloc/quick-lru@5.2.0': {}
'@alwaysmeticulous/recorder-loader@2.137.0': {}
'@ampproject/remapping@2.3.0':
@@ -6528,7 +6692,7 @@ snapshots:
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/unitless': 0.10.0
'@emotion/utils': 1.4.0
'@emotion/utils': 1.4.1
csstype: 3.1.3
'@emotion/serialize@1.3.2':
@@ -7352,13 +7516,6 @@ snapshots:
'@radix-ui/primitive@1.1.0': {}
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.6
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
@@ -7459,14 +7616,6 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-slot@1.0.2(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.6
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
@@ -7848,7 +7997,7 @@ snapshots:
'@storybook/components@8.1.11(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-dialog': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1)
'@storybook/client-logger': 8.1.11
'@storybook/csf': 0.1.9
'@storybook/global': 5.0.0
@@ -8753,6 +8902,8 @@ snapshots:
dependencies:
entities: 2.2.0
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -8762,6 +8913,8 @@ snapshots:
arg@4.1.3: {}
arg@5.0.2: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
@@ -8808,6 +8961,16 @@ snapshots:
asynckit@0.4.0: {}
autoprefixer@10.4.20(postcss@8.4.47):
dependencies:
browserslist: 4.24.2
caniuse-lite: 1.0.30001677
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
postcss: 8.4.47
postcss-value-parser: 4.2.0
available-typed-arrays@1.0.5: {}
axios@1.7.4:
@@ -8972,6 +9135,8 @@ snapshots:
callsites@3.1.0: {}
camelcase-css@2.0.1: {}
camelcase@5.3.1: {}
camelcase@6.3.0: {}
@@ -9063,6 +9228,10 @@ snapshots:
cjs-module-lexer@1.3.1: {}
class-variance-authority@0.7.0:
dependencies:
clsx: 2.0.0
classnames@2.3.2: {}
cli-spinners@2.9.2: {}
@@ -9075,6 +9244,8 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clsx@2.0.0: {}
clsx@2.1.1: {}
co@4.6.0: {}
@@ -9103,6 +9274,8 @@ snapshots:
comma-separated-tokens@2.0.3: {}
commander@4.1.1: {}
commander@6.2.1: {}
commander@8.3.0: {}
@@ -9186,6 +9359,8 @@ snapshots:
css.escape@1.5.1: {}
cssesc@3.0.0: {}
cssfontparser@1.2.1: {}
cssom@0.3.8: {}
@@ -9310,10 +9485,14 @@ snapshots:
dependencies:
dequal: 2.0.3
didyoumean@1.2.2: {}
diff-sequences@29.6.3: {}
diff@4.0.2: {}
dlv@1.1.3: {}
doctrine@3.0.0:
dependencies:
esutils: 2.0.3
@@ -9776,6 +9955,8 @@ snapshots:
forwarded@0.2.0: {}
fraction.js@4.3.7: {}
fresh@0.5.2: {}
front-matter@4.0.2:
@@ -9846,7 +10027,6 @@ snapshots:
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
optional: true
glob-promise@4.2.2(glob@7.2.3):
dependencies:
@@ -10629,6 +10809,8 @@ snapshots:
'@swc/core': 1.3.38
'@swc/jest': 0.2.37(@swc/core@1.3.38)
jiti@1.21.6: {}
js-tokens@4.0.0: {}
js-yaml@3.14.1:
@@ -10733,6 +10915,10 @@ snapshots:
dependencies:
immediate: 3.0.6
lilconfig@2.1.0: {}
lilconfig@3.1.2: {}
lines-and-columns@1.2.4: {}
locate-path@5.0.0:
@@ -10773,6 +10959,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-react@0.454.0(react@18.3.1):
dependencies:
react: 18.3.1
luxon@3.3.0: {}
lz-string@1.5.0: {}
@@ -11205,6 +11395,12 @@ snapshots:
mute-stream@1.0.0: {}
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nan@2.20.0:
optional: true
@@ -11234,6 +11430,8 @@ snapshots:
normalize-path@3.0.0: {}
normalize-range@0.1.2: {}
npm-run-path@4.0.1:
dependencies:
path-key: 3.1.1
@@ -11242,6 +11440,8 @@ snapshots:
object-assign@4.1.1: {}
object-hash@3.0.0: {}
object-inspect@1.13.1: {}
object-is@1.1.5:
@@ -11363,6 +11563,8 @@ snapshots:
picomatch@2.3.1: {}
pify@2.3.0: {}
pirates@4.0.6: {}
pkg-dir@4.2.0:
@@ -11385,6 +11587,38 @@ snapshots:
dependencies:
'@babel/runtime': 7.25.6
postcss-import@15.1.0(postcss@8.4.47):
dependencies:
postcss: 8.4.47
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.8
postcss-js@4.0.1(postcss@8.4.47):
dependencies:
camelcase-css: 2.0.1
postcss: 8.4.47
postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)):
dependencies:
lilconfig: 3.1.2
yaml: 2.6.0
optionalDependencies:
postcss: 8.4.47
ts-node: 10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)
postcss-nested@6.2.0(postcss@8.4.47):
dependencies:
postcss: 8.4.47
postcss-selector-parser: 6.1.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.4.47:
dependencies:
nanoid: 3.3.7
@@ -11721,6 +11955,10 @@ snapshots:
lodash: 4.17.21
react: 18.3.1
read-cache@1.0.0:
dependencies:
pify: 2.3.0
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
@@ -12141,6 +12379,16 @@ snapshots:
stylis@4.2.0: {}
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.5
commander: 4.1.1
glob: 10.3.10
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.6
ts-interface-checker: 0.1.13
superjson@1.13.3:
dependencies:
copy-anything: 3.0.5
@@ -12161,6 +12409,39 @@ snapshots:
symbol-tree@3.2.4: {}
tailwind-merge@2.5.4: {}
tailwindcss-animate@1.0.7(tailwindcss@3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))):
dependencies:
tailwindcss: 3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))
tailwindcss@3.4.13(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3)):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.2
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.6
lilconfig: 2.1.0
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.4.47
postcss-import: 15.1.0(postcss@8.4.47)
postcss-js: 4.0.1(postcss@8.4.47)
postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.1(@swc/core@1.3.38)(@types/node@20.17.6)(typescript@5.6.3))
postcss-nested: 6.2.0(postcss@8.4.47)
postcss-selector-parser: 6.1.2
resolve: 1.22.8
sucrase: 3.35.0
transitivePeerDependencies:
- ts-node
tar-fs@2.1.1:
dependencies:
chownr: 1.1.4
@@ -12209,6 +12490,14 @@ snapshots:
text-table@0.2.0:
optional: true
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
tiny-case@1.0.3: {}
tiny-invariant@1.3.3: {}
@@ -12261,6 +12550,8 @@ snapshots:
ts-dedent@2.2.0: {}
ts-interface-checker@0.1.13: {}
ts-morph@13.0.3:
dependencies:
'@ts-morph/common': 0.12.3
@@ -12648,6 +12939,8 @@ snapshots:
yaml@1.10.2: {}
yaml@2.6.0: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+53
View File
@@ -0,0 +1,53 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "utils/cn";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 font-semibold border-solid",
{
variants: {
variant: {
default:
"bg-surface-invert-primary text-content-invert hover:bg-surface-invert-secondary",
outline:
"border border-border-default text-content-primary bg-transparent hover:bg-surface-secondary",
subtle:
"border-none bg-transparent text-content-secondary hover:text-content-primary",
warning:
"border border-border-error text-content-primary bg-surface-error hover:bg-transparent",
},
size: {
default: "h-10 px-3 py-2",
sm: "h-8 px-2 text-xs",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
+31 -6
View File
@@ -1,4 +1,6 @@
import createCache from "@emotion/cache";
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
import { CacheProvider } from "@emotion/react";
import CssBaseline from "@mui/material/CssBaseline";
import {
ThemeProvider as MuiThemeProvider,
@@ -55,6 +57,21 @@ export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
// The janky casting here is find because of the much more type safe fallback
// We need to support `themePreference` being wrong anyway because the database
// value could be anything, like an empty string.
useEffect(() => {
const root = document.documentElement;
if (themePreference === "auto") {
root.classList.add(preferredColorScheme);
} else {
root.classList.add(themePreference);
}
return () => {
root.classList.remove("light", "dark");
};
}, [themePreference, preferredColorScheme]);
const theme =
themes[themePreference as keyof typeof themes] ??
themes[preferredColorScheme];
@@ -66,6 +83,12 @@ export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
);
};
// This is being added to allow Tailwind classes to be used with MUI components. https://mui.com/material-ui/integrations/interoperability/#tailwind-css
const cache = createCache({
key: "css",
prepend: true,
});
interface ThemeOverrideProps {
theme: Theme;
children?: ReactNode;
@@ -73,11 +96,13 @@ interface ThemeOverrideProps {
export const ThemeOverride: FC<ThemeOverrideProps> = ({ theme, children }) => {
return (
<MuiThemeProvider theme={theme}>
<EmotionThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
{children}
</EmotionThemeProvider>
</MuiThemeProvider>
<CacheProvider value={cache}>
<MuiThemeProvider theme={theme}>
<EmotionThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
{children}
</EmotionThemeProvider>
</MuiThemeProvider>
</CacheProvider>
);
};
+41
View File
@@ -0,0 +1,41 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--content-primary: #27272a;
--content-secondary: #52525b;
--content-link: #2563eb;
--content-invert: #fafafa;
--content-disabled: #a1a1aa;
--content-success: #15803d;
--content-danger: #ef4444;
--surface-primary: #fafafa;
--surface-secondary: #f4f4f5;
--surface-tertiary: #e4e4e7;
--surface-invert-primary: #27272a;
--surface-invert-secondary: #3f3f46;
--surface-error: #450a0a;
--border-default: #e4e4e7;
--border-error: #ef4444;
--radius: 0.5rem;
}
.dark {
--content-primary: #fafafa;
--content-secondary: #a1a1aa;
--content-link: #60a5fa;
--content-invert: #09090b;
--content-disabled: #3f3f46;
--content-success: #16a34a;
--content-danger: #f87171;
--surface-primary: #09090b;
--surface-secondary: #18181b;
--surface-tertiary: #27272a;
--surface-invert-primary: #e4e4e7;
--surface-invert-secondary: #a1a1aa;
--surface-error: #450a0a;
--border-default: #27272a;
--border-error: #f87171;
}
}
+1
View File
@@ -1,4 +1,5 @@
import { createRoot } from "react-dom/client";
import "./index.css";
import { App } from "./App";
console.info(` ▄█▀ ▀█▄
@@ -11,6 +11,7 @@ import { UserAvatar } from "components/UserAvatar/UserAvatar";
import type { Permissions } from "contexts/auth/permissions";
import { type ClassName, useClassName } from "hooks/useClassName";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import type { FC, ReactNode } from "react";
import { Link, NavLink } from "react-router-dom";
@@ -39,6 +40,7 @@ export const SidebarView: FC<SidebarProps> = ({
permissions,
}) => {
const { showOrganizations } = useDashboard();
const { multiple_organizations: hasPremiumLicense } = useFeatureVisibility();
// TODO: Do something nice to scroll to the active org.
return (
@@ -52,6 +54,7 @@ export const SidebarView: FC<SidebarProps> = ({
<DeploymentSettingsNavigation
active={!activeOrganizationName && activeSettings}
permissions={permissions}
isPremium={hasPremiumLicense}
/>
{showOrganizations && (
<OrganizationsSettingsNavigation
@@ -69,6 +72,7 @@ interface DeploymentSettingsNavigationProps {
active: boolean;
/** Site-wide permissions. */
permissions: Permissions;
isPremium: boolean;
}
/**
@@ -81,6 +85,7 @@ interface DeploymentSettingsNavigationProps {
const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
active,
permissions,
isPremium,
}) => {
return (
<div css={{ paddingBottom: 12 }}>
@@ -150,6 +155,9 @@ const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
</Stack>
</SidebarNavSubItem>
)}
{!isPremium && (
<SidebarNavSubItem href="premium">Premium</SidebarNavSubItem>
)}
</Stack>
)}
</div>
@@ -0,0 +1,20 @@
import { useDashboard } from "modules/dashboard/useDashboard";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { PremiumPageView } from "./PremiumPageView";
const PremiumPage: FC = () => {
const { entitlements } = useDashboard();
return (
<>
<Helmet>
<title>{pageTitle("Premium Features")}</title>
</Helmet>
<PremiumPageView isEnterprise={entitlements.has_license} />
</>
);
};
export default PremiumPage;
@@ -0,0 +1,253 @@
import { Button } from "components/ui/button";
import { Activity, Coins, Expand, SquareArrowOutUpRight } from "lucide-react";
import type { FC } from "react";
import { docs } from "utils/docs";
export type PremiumPageViewProps = { isEnterprise: boolean };
export const PremiumPageView: FC<PremiumPageViewProps> = ({ isEnterprise }) => {
return isEnterprise ? <EnterpriseVersion /> : <OSSVersion />;
};
const EnterpriseVersion: FC = () => {
return (
<div className="max-w-4xl">
<header className="flex flex-row justify-between align-baseline pb-5">
<div>
<h1 className="text-3xl m-0 font-semibold">Premium</h1>
<p className="text-sm max-w-xl mt-2 text-content-secondary font-medium">
As an Enterprise license holder, you already benefit from Coder’s
features for secure, large-scale deployments. Upgrade to Coder
Premium for enhanced multi-tenant control and flexibility.
</p>
</div>
<Button asChild>
<a href="https://coder.com/contact/sales" className="no-underline">
<SquareArrowOutUpRight size={14} />
Contact sales
</a>
</Button>
</header>
<section className="pb-1">
<a
className="no-underline text-sm text-content-link"
href={docs("/admin/users/organizations")}
>
<span className="flex items-center">
<h2 className="text-sm font-semibold m-0">
Multi-Organization access controls&nbsp;
</h2>
<SquareArrowOutUpRight size={14} />
</span>
</a>
<p className="text-sm max-w-xl text-content-secondary mt-0 font-medium">
Manage multiple teams and projects within a single deployment, each
with isolated access.
</p>
</section>
<section className="pb-1">
<a
className="no-underline text-sm text-content-link"
href={docs("/admin/users/groups-roles")}
>
<span className="flex items-center">
<h2 className="text-sm font-semibold m-0">Custom role&nbsp;</h2>
<SquareArrowOutUpRight size={14} />
</span>
</a>
<p className="text-sm max-w-xl text-content-secondary mt-0 font-medium">
Configure specific permissions for teams or contractors with tailored
roles.
</p>
</section>
<section>
<a
className="no-underline text-sm text-content-link"
href={docs("/admin/users/quotas")}
>
<span className="flex items-center text-sm">
<h2 className="text-sm font-semibold m-0">
Org-Level quotas for chargeback&nbsp;
</h2>
<SquareArrowOutUpRight size={14} />
</span>
</a>
<p className="text-sm max-w-xl text-content-secondary mt-0 font-medium">
Set and monitor resource quotas at the organization level to support
internal cost tracking.
</p>
</section>
<section className="pt-10">
<p className="text-sm max-w-xl text-content-secondary mt-0 font-medium">
These Premium features enable you to manage team structure and budget
allocation across multiple platform teams.
</p>
</section>
</div>
);
};
const OSSVersion: FC = () => {
return (
<div className="max-w-4xl">
<div className="flex flex-row justify-between align-baseline pb-10">
<div>
<h1 className="text-3xl m-0 text-content-primary font-semibold">
Premium
</h1>
<p className="text-sm max-w-xl mt-2 text-content-secondary">
Coder Premium is designed for enterprises that need to scale their
Coder deployment efficiently, securely, and with full control. By
upgrading, your team gains access to advanced features enabling
governance across all environments.
</p>
</div>
<Button asChild>
<a href="https://coder.com/contact/sales" className="no-underline">
<SquareArrowOutUpRight size={14} />
Contact sales
</a>
</Button>
</div>
<section className="pb-10 max-w-xl text-sm text-content-secondary">
<h2 className="text-xl text-content-primary m-0">
<span className="flex flex-row items-center">
<Expand size={18} className="text-content-secondary" />
&nbsp; Deploy coder at scale
</span>
</h2>
<p>
Equip your enterprise to deploy and manage thousands of workspaces
with performance and reliability.
</p>
<ul className="pl-5">
<li>
<span className="text-content-primary font-semibold">
High availability
</span>
<br />
<span className="font-medium">
Scale with automatic failover and load balancing across multiple
Coder instances.
</span>
</li>
<li>
<span className="text-content-primary font-semibold">
Multi-Organization access control
</span>
<br />
<span className="font-medium">
Isolate teams, projects, and environments within a single Coder
deployment.
</span>
</li>
<li>
<span className="text-content-primary font-semibold">
Unlimited external authentication integrations
</span>
<br />
<span className="font-medium">
Integrate with external service providers like GitHub, JFrog, and
Vault.
</span>
</li>
</ul>
</section>
<section className="pb-10 max-w-xl text-sm text-content-secondary">
<h2 className="text-xl text-content-primary m-0">
<span className="flex flex-row items-center">
<Coins size={18} className="text-content-secondary" />
&nbsp; Control infrastructure costs
</span>
</h2>
<p>
Optimize cloud usage and maintain cost-effective resource management
for your teams.
</p>
<ul className="pl-5">
<li>
<span className="text-content-primary font-semibold">
Auto-Stop idle workspaces
</span>
<br />
<span className="font-medium">
Automatically shut down inactive workspaces to prevent unnecessary
costs.
</span>
</li>
<li>
<span className="text-content-primary font-semibold">
Resource quotas
</span>
<br />
<span className="font-medium">
Set user and team-specific limits to control spending and resource
allocation.
</span>
</li>
<li>
<span className="text-content-primary font-semibold">
Usage insights
</span>
<br />
<span className="font-medium">
Track workspace usage patterns to make data-driven decisions about
infrastructure needs.
</span>
</li>
</ul>
</section>
<section className="pb-5 max-w-xl text-sm text-content-secondary">
<h2 className="text-xl text-content-primary m-0">
<span className="flex flex-row items-center">
<Activity size={18} className="text-content-secondary" />
&nbsp; Govern workspace activity
</span>
</h2>
<p>
Maintain security and compliance across your organization with robust
governance features.
</p>
<ul className="pl-5">
<li>
<span className="text-content-primary font-semibold">
Audit logging
</span>
<br />
<span className="font-medium">
Capture detailed records of user actions and workspace activity
for compliance and security.
</span>
</li>
<li>
<span className="text-content-primary font-semibold">
Template permissions
</span>
<br />
<span className="font-medium">
Control who can create, modify, and access workspace templates
across teams.
</span>
</li>
<li>
<span className="text-content-primary font-semibold">
Workspace command logging
</span>
<br />
<span className="font-medium">
Monitor and log critical commands to ensure security and
compliance standards are met.
</span>
</li>
</ul>
</section>
</div>
);
};
+4
View File
@@ -270,6 +270,9 @@ const TemplateInsightsPage = lazy(
() =>
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
);
const PremiumPage = lazy(
() => import("./pages/DeploymentSettingsPage/PremiumPage/PremiumPage"),
);
const GroupsPage = lazy(() => import("./pages/GroupsPage/GroupsPage"));
const IconsPage = lazy(() => import("./pages/IconsPage/IconsPage"));
const AccessURLPage = lazy(() => import("./pages/HealthPage/AccessURLPage"));
@@ -450,6 +453,7 @@ export const router = createBrowserRouter(
path="notifications"
element={<DeploymentNotificationsPage />}
/>
<Route path="premium" element={<PremiumPage />} />
</Route>
<Route path="licenses">
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+49
View File
@@ -0,0 +1,49 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
corePlugins: {
preflight: false,
},
darkMode: ["selector"],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
important: "#root",
theme: {
fontSize: {
"2xs": ["0.626rem","0.875rem"],
sm: ["0.875rem", "1.5rem"],
"3xl": ["2rem", "2.5rem"],
},
extend: {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
colors: {
content: {
primary: "var(--content-primary)",
secondary: "var(--content-secondary)",
disabled: "var(--content-disabled)",
invert: "var(--content-invert)",
success: "var(--content-success)",
danger: "var(--content-danger)",
link: "var(--content-link)",
},
surface: {
primary: "var(--surface-primary)",
secondary: "var(--surface-secondary)",
tertiary: "var(--surface-tertiary)",
invert: {
primary: "var(--surface-invert-primary)",
secondary: "var(--surface-invert-secondary)",
},
error: "var(--surface-error)",
},
border: {
default: "var(--border-default)",
error: "var(--border-error)",
},
},
},
},
plugins: [require("tailwindcss-animate")],
};
+22 -22
View File
@@ -1,24 +1,24 @@
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"outDir": "build/",
"preserveWatchOutput": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "es2020",
"baseUrl": "src/"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules/", "_jest"],
"types": ["@emotion/react", "@testing-library/jest-dom", "jest", "node"]
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"outDir": "build/",
"preserveWatchOutput": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "es2020",
"baseUrl": "src/"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules/", "_jest"],
"types": ["@emotion/react", "@testing-library/jest-dom", "jest", "node"]
}