diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19c0a71fe1..352b733183 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -405,6 +405,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/addon-docs': specifier: ^9.1.2 version: 9.1.2(@types/react@18.3.23)(storybook@9.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))) @@ -417,6 +420,9 @@ importers: '@storybook/react-vite': specifier: ^9.1.2 version: 9.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.45.1)(storybook@9.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)))(typescript@5.8.3)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/test': + specifier: ^8.6.14 + version: 8.6.14(storybook@9.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))) '@tailwindcss/typography': specifier: ^0.5.16 version: 0.5.16(tailwindcss@3.4.17) @@ -444,6 +450,9 @@ importers: '@vitest/browser': specifier: ^3.2.4 version: 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))(vitest@3.2.4) + '@xyflow/react': + specifier: ^12.8.4 + version: 12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -465,6 +474,9 @@ importers: globals: specifier: ^16.3.0 version: 16.3.0 + lucide-react: + specifier: ^0.542.0 + version: 0.542.0(react@18.3.1) playwright: specifier: ^1.54.1 version: 1.54.1 @@ -486,6 +498,9 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.3.1 + tailwind-scrollbar: + specifier: ^4.0.2 + version: 4.0.2(react@18.3.1)(tailwindcss@3.4.17) tailwindcss: specifier: ^3.4.17 version: 3.4.17 @@ -1284,6 +1299,9 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -1363,6 +1381,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.2': resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} peerDependencies: @@ -1420,6 +1451,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.9': resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: @@ -1446,6 +1490,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -1494,6 +1551,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -2035,6 +2105,11 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + '@storybook/instrumenter@8.6.14': + resolution: {integrity: sha512-iG4MlWCcz1L7Yu8AwgsnfVAmMbvyRSk700Mfy2g4c8y5O+Cv1ejshE1LBBsCwHgkuqU0H4R0qu4g23+6UnUemQ==} + peerDependencies: + storybook: ^8.6.14 + '@storybook/react-dom-shim@9.0.18': resolution: {integrity: sha512-qGR/d9x9qWRRxITaBVQkMnb73kwOm+N8fkbZRxc7U4lxupXRvkMIDh247nn71SYVBnvbh6//AL7P6ghiPWZYjA==} peerDependencies: @@ -2091,6 +2166,11 @@ packages: typescript: optional: true + '@storybook/test@8.6.14': + resolution: {integrity: sha512-GkPNBbbZmz+XRdrhMtkxPotCLOQ1BaGNp/gFZYdGDk2KmUWBKmvc5JxxOhtoXM2703IzNFlQHSSNnhrDZYuLlw==} + peerDependencies: + storybook: ^8.6.14 + '@swc/core-darwin-arm64@1.13.2': resolution: {integrity: sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==} engines: {node: '>=10'} @@ -2377,10 +2457,18 @@ packages: resolution: {integrity: sha512-a+MxoAXG+Sq94Jp67OtveKOp2vQq75AWdVI8DRt6w19B0NEqpfm784FTLbVp/qdR1wmxCOmKAvElGSIiBOx5OQ==} engines: {node: '>=12'} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} + '@testing-library/jest-dom@6.5.0': + resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@6.6.3': resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} @@ -2400,6 +2488,12 @@ packages: '@types/react-dom': optional: true + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@testing-library/user-event@14.6.1': resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} engines: {node: '>=12', npm: '>=6'} @@ -2610,6 +2704,9 @@ packages: '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -2787,6 +2884,9 @@ packages: '@vitest/browser': optional: true + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -2812,6 +2912,12 @@ packages: vite: optional: true + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.2.3': resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} @@ -2824,6 +2930,9 @@ packages: '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + '@vitest/spy@3.2.3': resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} @@ -2835,6 +2944,12 @@ packages: peerDependencies: vitest: 3.2.4 + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.2.3': resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} @@ -2987,6 +3102,15 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@xyflow/react@12.8.4': + resolution: {integrity: sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.68': + resolution: {integrity: sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -4817,6 +4941,11 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lucide-react@0.542.0: + resolution: {integrity: sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} @@ -5408,6 +5537,11 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prism-react-renderer@2.4.1: + resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} + peerDependencies: + react: '>=16.0.0' + proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -6050,6 +6184,12 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-scrollbar@4.0.2: + resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==} + engines: {node: '>=12.13.0'} + peerDependencies: + tailwindcss: 4.x + tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -6151,10 +6291,18 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.3: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} @@ -7751,6 +7899,8 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7817,6 +7967,19 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 @@ -7885,6 +8048,24 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7905,6 +8086,16 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) @@ -7967,6 +8158,26 @@ snapshots: optionalDependencies: '@types/react': 18.3.23 + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 @@ -8649,6 +8860,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@storybook/instrumenter@8.6.14(storybook@9.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/global': 5.0.0 + '@vitest/utils': 2.1.9 + storybook: 9.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@storybook/react-dom-shim@9.0.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.0.18(@testing-library/dom@10.4.1)(prettier@3.6.2))': dependencies: react: 18.3.1 @@ -8721,6 +8938,17 @@ snapshots: optionalDependencies: typescript: 5.8.3 + '@storybook/test@8.6.14(storybook@9.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)))': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/instrumenter': 8.6.14(storybook@9.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))) + '@testing-library/dom': 10.4.0 + '@testing-library/jest-dom': 6.5.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/expect': 2.0.5 + '@vitest/spy': 2.0.5 + storybook: 9.1.2(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@6.3.5(@types/node@20.11.25)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@swc/core-darwin-arm64@1.13.2': optional: true @@ -8995,6 +9223,17 @@ snapshots: '@tanstack/virtual-file-routes@1.129.7': {} + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -9006,6 +9245,16 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/jest-dom@6.5.0': + dependencies: + '@adobe/css-tools': 4.4.3 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + '@testing-library/jest-dom@6.6.3': dependencies: '@adobe/css-tools': 4.4.3 @@ -9026,6 +9275,10 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -9283,6 +9536,8 @@ snapshots: '@types/pluralize@0.0.33': {} + '@types/prismjs@1.26.5': {} + '@types/prop-types@15.7.15': {} '@types/react-dom@18.3.7(@types/react@18.3.23)': @@ -9554,6 +9809,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.2.1 + tinyrainbow: 1.2.0 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -9586,6 +9848,14 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@24.1.0)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.2.3': dependencies: tinyrainbow: 2.0.0 @@ -9606,6 +9876,10 @@ snapshots: magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.2.3': dependencies: tinyspy: 4.0.3 @@ -9625,6 +9899,19 @@ snapshots: tinyrainbow: 2.0.0 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.4 + tinyrainbow: 1.2.0 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.4 + tinyrainbow: 1.2.0 + '@vitest/utils@3.2.3': dependencies: '@vitest/pretty-format': 3.2.3 @@ -9861,6 +10148,29 @@ snapshots: '@xtuc/long@4.2.2': {} + '@xyflow/react@12.8.4(@types/react@18.3.23)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@xyflow/system': 0.0.68 + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(immer@9.0.21)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.68': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -11758,6 +12068,10 @@ snapshots: dependencies: yallist: 4.0.0 + lucide-react@0.542.0(react@18.3.1): + dependencies: + react: 18.3.1 + lunr@2.3.9: {} lz-string@1.5.0: {} @@ -12540,6 +12854,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + prism-react-renderer@2.4.1(react@18.3.1): + dependencies: + '@types/prismjs': 1.26.5 + clsx: 2.1.1 + react: 18.3.1 + proc-log@5.0.0: {} process-nextick-args@2.0.1: {} @@ -13358,6 +13678,13 @@ snapshots: tailwind-merge@3.3.1: {} + tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@3.4.17): + dependencies: + prism-react-renderer: 2.4.1(react@18.3.1) + tailwindcss: 3.4.17 + transitivePeerDependencies: + - react + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 @@ -13487,8 +13814,12 @@ snapshots: tinypool@1.1.1: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} + tinyspy@3.0.2: {} + tinyspy@4.0.3: {} tldts-core@6.1.86: {} diff --git a/web/common/.gitignore b/web/common/.gitignore index 392d3f0ae6..93662554cd 100644 --- a/web/common/.gitignore +++ b/web/common/.gitignore @@ -4,3 +4,5 @@ tsconfig.tsbuildinfo *storybook.log storybook-static +**/__snapshots__/** +**/__screenshots__/** \ No newline at end of file diff --git a/web/common/package.json b/web/common/package.json index 5ad2c29389..2501b9f81a 100644 --- a/web/common/package.json +++ b/web/common/package.json @@ -4,10 +4,12 @@ "devDependencies": { "@eslint/js": "^9.31.0", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.8", "@storybook/addon-docs": "^9.1.2", "@storybook/addon-essentials": "^9.0.0-alpha.12", "@storybook/addon-onboarding": "^9.1.2", "@storybook/react-vite": "^9.1.2", + "@storybook/test": "^8.6.14", "@tailwindcss/typography": "^0.5.16", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", @@ -17,6 +19,7 @@ "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.7.0", "@vitest/browser": "^3.2.4", + "@xyflow/react": "^12.8.4", "autoprefixer": "^10.4.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -24,6 +27,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-storybook": "^9.1.2", "globals": "^16.3.0", + "lucide-react": "^0.542.0", "playwright": "^1.54.1", "postcss": "^8.5.6", "react": "^18.3.1", @@ -31,6 +35,7 @@ "storybook": "^9.1.2", "syncpack": "^13.0.4", "tailwind-merge": "^3.3.1", + "tailwind-scrollbar": "^4.0.2", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "typescript-eslint": "^8.38.0", @@ -62,9 +67,12 @@ "module": "dist/sqlmesh-common.es.js", "peerDependencies": { "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/typography": "^0.5.16", + "@xyflow/react": "^12.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1", "tailwind-merge": "^3.3.1", diff --git a/web/common/src/components/Badge/Badge.css b/web/common/src/components/Badge/Badge.css index 029ba541f1..582a1264fb 100644 --- a/web/common/src/components/Badge/Badge.css +++ b/web/common/src/components/Badge/Badge.css @@ -1,4 +1,4 @@ :root { - --color-badge-background: var(--color-gray-100); + --color-badge-background: var(--color-neutral-100); --color-badge-foreground: var(--color-prose); } diff --git a/web/common/src/components/Badge/Badge.stories.tsx b/web/common/src/components/Badge/Badge.stories.tsx index aec5bd0bca..09754d29a8 100644 --- a/web/common/src/components/Badge/Badge.stories.tsx +++ b/web/common/src/components/Badge/Badge.stories.tsx @@ -1,23 +1,11 @@ import type { Meta, StoryObj } from '@storybook/react-vite' -import { EnumShape, EnumSize } from '@/types/enums' +import type { Shape, Size } from '@/types' import { Badge } from './Badge' const meta: Meta = { title: 'Components/Badge', component: Badge, - tags: ['autodocs'], - argTypes: { - size: { - control: { type: 'select' }, - options: Object.values(EnumSize), - }, - shape: { - control: { type: 'select' }, - options: Object.values(EnumShape), - }, - children: { control: 'text' }, - }, } export default meta @@ -29,10 +17,13 @@ export const Default: Story = { }, } +const sizes: Size[] = ['2xs', 'xs', 's', 'm', 'l', 'xl', '2xl'] +const shapes: Shape[] = ['square', 'round', 'pill'] + export const Sizes: Story = { render: args => (
- {Object.values(EnumSize).map(size => ( + {sizes.map(size => ( (
- {Object.values(EnumShape).map(shape => ( + {shapes.map(shape => ( Primary Badge Secondary Badge Failed Badge diff --git a/web/common/src/components/Badge/Badge.test.tsx b/web/common/src/components/Badge/Badge.test.tsx deleted file mode 100644 index 9ee5f2c58c..0000000000 --- a/web/common/src/components/Badge/Badge.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { render, screen } from '@testing-library/react' - -import { Badge } from './Badge' -import { badgeVariants } from './help' -import { EnumShape, EnumSize } from '@/types/enums' -import { cn } from '@/utils' - -describe('Badge', () => { - it('renders with default props and children', () => { - render(Test Badge) - expect(screen.getByText('Test Badge')).toBeInTheDocument() - }) - - it('applies the size class for each size', () => { - Object.values(EnumSize).forEach(size => { - const variants = cn(badgeVariants({ size })) - render(Size {size}) - expect(screen.getByText(`Size ${size}`)).toHaveClass(variants) - }) - }) - - it('applies the shape class for each shape', () => { - Object.values(EnumShape).forEach(shape => { - const variants = cn(badgeVariants({ shape })) - render(Shape {shape}) - expect(screen.getByText(`Shape ${shape}`)).toHaveClass(variants) - }) - }) - - it('supports custom size and shape', () => { - render( - - Custom Size and Shape - , - ) - expect(screen.getByText('Custom Size and Shape')).toHaveClass( - cn(badgeVariants({ size: EnumSize.XXL, shape: EnumShape.Square })), - ) - }) - - it('applies custom className', () => { - render(Custom Class) - expect(screen.getByText('Custom Class')).toHaveClass('custom-class') - }) - - it('renders as a child element when asChild is true', () => { - render( - - Link Badge - , - ) - expect(screen.getByText('Link Badge').tagName).toBe('A') - }) -}) diff --git a/web/common/src/components/Badge/Badge.tsx b/web/common/src/components/Badge/Badge.tsx index 93f380bddd..2cf561ebc1 100644 --- a/web/common/src/components/Badge/Badge.tsx +++ b/web/common/src/components/Badge/Badge.tsx @@ -1,16 +1,13 @@ import { Slot } from '@radix-ui/react-slot' -import { type VariantProps } from 'class-variance-authority' import React from 'react' -import { type Size, type Shape } from '@/types/enums' +import type { Shape, Size } from '@/types' import { cn } from '@/utils' -import { badgeVariants } from './help' +import { cva } from 'class-variance-authority' import './Badge.css' -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps { +export interface BadgeProps extends React.HTMLAttributes { asChild?: boolean size?: Size shape?: Shape @@ -30,3 +27,33 @@ export const Badge = React.forwardRef( }, ) Badge.displayName = 'Badge' + +const size: Record = { + '2xs': 'h-5 px-2 text-2xs leading-none rounded-2xs', + xs: 'h-6 px-2 text-2xs rounded-xs', + s: 'h-7 px-3 text-xs rounded-sm', + m: 'h-8 px-4 rounded-md', + l: 'h-9 px-4 rounded-lg', + xl: 'h-10 px-4 rounded-xl', + '2xl': 'h-11 px-6 rounded-2xl', +} + +const shape: Record = { + square: 'rounded-none', + round: 'rounded-inherit', + pill: 'rounded-full', +} + +const badgeVariants = cva( + 'bg-badge-background text-badge-foreground font-mono inline-flex align-middle items-center justify-center gap-2 leading-none whitespace-nowrap font-semibold', + { + variants: { + size, + shape, + }, + defaultVariants: { + size: 's', + shape: 'round', + }, + }, +) diff --git a/web/common/src/components/Badge/help.ts b/web/common/src/components/Badge/help.ts deleted file mode 100644 index df489eb8d2..0000000000 --- a/web/common/src/components/Badge/help.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { cva } from 'class-variance-authority' - -import { EnumShape, EnumSize } from '@/types/enums' - -export const badgeVariants = cva( - 'bg-badge-background text-badge-foreground font-mono inline-flex align-middle items-center justify-center gap-2 leading-none whitespace-nowrap font-semibold', - { - variants: { - size: { - [EnumSize.XXS]: 'h-5 px-2 text-2xs leading-none rounded-2xs', - [EnumSize.XS]: 'h-6 px-2 text-2xs rounded-xs', - [EnumSize.S]: 'h-7 px-3 text-xs rounded-sm', - [EnumSize.M]: 'h-8 px-4 rounded-md', - [EnumSize.L]: 'h-9 px-4 rounded-lg', - [EnumSize.XL]: 'h-10 px-4 rounded-xl', - [EnumSize.XXL]: 'h-11 px-6 rounded-2xl', - }, - shape: { - [EnumShape.Square]: 'rounded-none', - [EnumShape.Round]: 'rounded-inherit', - [EnumShape.Pill]: 'rounded-full', - }, - }, - defaultVariants: { - size: EnumSize.S, - shape: EnumShape.Round, - }, - }, -) diff --git a/web/common/src/components/Button/Button.css b/web/common/src/components/Button/Button.css new file mode 100644 index 0000000000..339c14675b --- /dev/null +++ b/web/common/src/components/Button/Button.css @@ -0,0 +1,31 @@ +:root { + --color-button-primary-background: var(--color-action); + --color-button-primary-foreground: var(--color-light); + --color-button-primary-hover: var(--color-action-hover); + --color-button-primary-active: var(--color-action-active); + + --color-button-secondary-background: var(--color-neutral-100); + --color-button-secondary-foreground: var(--color-prose); + --color-button-secondary-hover: var(--color-neutral-125); + --color-button-secondary-active: var(--color-neutral-150); + + --color-button-alternative-background: var(--color-light); + --color-button-alternative-foreground: var(--color-prose); + --color-button-alternative-hover: var(--color-neutral-125); + --color-button-alternative-active: var(--color-neutral-150); + + --color-button-destructive-background: var(--color-neutral-100); + --color-button-destructive-foreground: var(--color-destructive-foreground); + --color-button-destructive-hover: var(--color-neutral-125); + --color-button-destructive-active: var(--color-neutral-150); + + --color-button-danger-background: var(--color-destructive); + --color-button-danger-foreground: var(--color-light); + --color-button-danger-hover: var(--color-destructive-hover); + --color-button-danger-active: var(--color-destructive-active); + + --color-button-transparent-background: transparent; + --color-button-transparent-foreground: var(--color-prose); + --color-button-secondary-hover: var(--color-neutral-125); + --color-button-secondary-active: var(--color-neutral-150); +} diff --git a/web/common/src/components/Button/Button.stories.tsx b/web/common/src/components/Button/Button.stories.tsx new file mode 100644 index 0000000000..57fb9f26e2 --- /dev/null +++ b/web/common/src/components/Button/Button.stories.tsx @@ -0,0 +1,120 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import type { Size } from '@/types' +import { Button, type ButtonVariant } from './Button' +import { fn, expect, userEvent, within } from 'storybook/test' + +const buttonVariants: ButtonVariant[] = [ + 'primary', + 'secondary', + 'alternative', + 'destructive', + 'danger', + 'transparent', +] + +const meta: Meta = { + title: 'Components/Button', + component: Button, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + children: 'Default Button', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + await expect(canvas.getByText('Default Button')).toBeInTheDocument() + }, +} + +export const Variants: Story = { + render: args => ( +
+ {Object.values(buttonVariants).map(variant => ( + + ))} +
+ ), +} + +const sizes: Size[] = ['2xs', 'xs', 's', 'm', 'l', 'xl', '2xl'] + +export const Sizes: Story = { + render: args => ( +
+ {sizes.map(size => ( + + ))} +
+ ), +} + +export const Disabled: Story = { + args: { + children: 'Disabled Button', + disabled: true, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + await expect(button).toBeDisabled() + await expect(button).toHaveTextContent('Disabled Button') + }, +} + +export const AsChild: Story = { + render: args => ( + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const linkElement = canvas.getByText('Link as Button') + await expect(linkElement.tagName).toBe('A') + await expect(linkElement).toHaveAttribute('href', '#') + }, +} + +export const InteractiveClick: Story = { + args: { + children: 'Click Me', + onClick: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement) + const user = userEvent.setup() + const button = canvas.getByRole('button') + await expect(button).toBeInTheDocument() + await user.click(button) + await expect(args.onClick).toHaveBeenCalledTimes(1) + await user.click(button) + await expect(args.onClick).toHaveBeenCalledTimes(2) + }, +} diff --git a/web/common/src/components/Button/Button.tsx b/web/common/src/components/Button/Button.tsx new file mode 100644 index 0000000000..46f9c8cf1b --- /dev/null +++ b/web/common/src/components/Button/Button.tsx @@ -0,0 +1,89 @@ +import React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva } from 'class-variance-authority' + +import { cn } from '@/utils' +import type { Shape, Size } from '@/types' + +import './Button.css' + +export type ButtonVariant = + | 'primary' + | 'secondary' + | 'alternative' + | 'destructive' + | 'danger' + | 'transparent' + +export interface ButtonProps + extends React.ButtonHTMLAttributes { + variant?: ButtonVariant + size?: Size + shape?: Shape + asChild?: boolean +} + +export const Button = React.forwardRef( + ({ className, variant, disabled, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + }, +) +Button.displayName = 'Button' + +const size: Record = { + '2xs': 'h-5 px-2 text-2xs leading-none rounded-2xs', + xs: 'h-6 px-2 text-2xs rounded-xs', + s: 'h-7 px-3 text-xs rounded-sm', + m: 'h-8 px-4 rounded-md', + l: 'h-9 px-4 rounded-lg', + xl: 'h-10 px-4 rounded-xl', + '2xl': 'h-11 px-6 rounded-2xl', +} + +const variant: Record = { + primary: + 'bg-button-primary-background text-button-primary-foreground hover:bg-button-primary-hover active:bg-button-primary-active', + secondary: + 'bg-button-secondary-background text-button-secondary-foreground hover:bg-button-secondary-hover active:bg-button-secondary-active', + alternative: + 'bg-button-alternative-background text-button-alternative-foreground border-neutral-200 hover:bg-button-alternative-hover active:bg-button-alternative-active', + destructive: + 'bg-button-destructive-background text-button-destructive-foreground hover:bg-button-destructive-hover active:bg-button-destructive-active', + danger: + 'bg-button-danger-background text-button-danger-foreground hover:bg-button-danger-hover active:bg-button-danger-active', + transparent: + 'bg-button-transparent-background text-button-transparent-foreground hover:bg-button-transparent-hover active:bg-button-transparent-active', +} + +const shape: Record = { + square: 'rounded-none', + round: 'rounded-inherit', + pill: 'rounded-full', +} + +const buttonVariants = cva( + 'inline-flex items-center w-fit justify-center gap-1 whitespace-nowrap leading-none font-semibold ring-offset-light transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focused focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 border border-[transparent]', + { + variants: { + variant, + size, + shape, + }, + defaultVariants: { + variant: 'primary', + size: 's', + shape: 'round', + }, + }, +) diff --git a/web/common/src/components/CopyButton/CopyButton.stories.tsx b/web/common/src/components/CopyButton/CopyButton.stories.tsx new file mode 100644 index 0000000000..191171f495 --- /dev/null +++ b/web/common/src/components/CopyButton/CopyButton.stories.tsx @@ -0,0 +1,126 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { expect, userEvent, waitFor, within, fn } from 'storybook/test' + +import { CopyButton } from './CopyButton' +import { Check, Copy } from 'lucide-react' + +const meta: Meta = { + title: 'Components/CopyButton', + component: CopyButton, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + text: 'Hello, World!', + children: copied => (copied ? 'Copied!' : 'Copy'), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + await expect(button).toHaveTextContent('Copy') + await expect(button).toBeEnabled() + const writeTextSpy = fn().mockResolvedValue(undefined) + if (navigator.clipboard) { + navigator.clipboard.writeText = writeTextSpy + } else { + Object.defineProperty(navigator, 'clipboard', { + writable: true, + value: { + writeText: writeTextSpy, + }, + }) + } + const user = userEvent.setup() + await user.click(button) + await expect(writeTextSpy).toHaveBeenCalledWith('Hello, World!') + await waitFor(() => { + expect(button).toHaveTextContent('Copied!') + }) + await expect(button).toBeDisabled() + }, +} + +export const WithIcons: Story = { + args: { + text: 'Copy this text with icon feedback', + children: copied => ( + <> + {copied ? ( + <> + + Copied! + + ) : ( + <> + + Copy + + )} + + ), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + + // Initial state with Copy icon + await expect(button).toHaveTextContent('Copy') + + // Mock clipboard API + Object.defineProperty(navigator, 'clipboard', { + writable: true, + value: { + writeText: fn().mockResolvedValue(undefined), + }, + }) + + const user = userEvent.setup() + await user.click(button) + + // Should switch to Check icon and Copied text + await waitFor(() => { + expect(button).toHaveTextContent('Copied!') + }) + }, +} + +export const IconOnly: Story = { + args: { + text: 'This is the text to copy', + children: copied => (copied ? : ), + }, +} + +export const CustomDelay: Story = { + args: { + text: 'This stays copied for 5 seconds', + delay: 5000, + children: copied => (copied ? 'Copied! (5s delay)' : 'Copy (5s feedback)'), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const button = canvas.getByRole('button') + + // Mock clipboard API + Object.defineProperty(navigator, 'clipboard', { + writable: true, + value: { + writeText: fn().mockResolvedValue(undefined), + }, + }) + + const user = userEvent.setup() + await user.click(button) + + // Should show copied state + await waitFor(() => { + expect(button).toHaveTextContent('Copied! (5s delay)') + }) + + // Button should remain disabled for custom delay + await expect(button).toBeDisabled() + }, +} diff --git a/web/common/src/components/CopyButton/CopyButton.test.tsx b/web/common/src/components/CopyButton/CopyButton.test.tsx new file mode 100644 index 0000000000..b2f8b38407 --- /dev/null +++ b/web/common/src/components/CopyButton/CopyButton.test.tsx @@ -0,0 +1,112 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { + vi, + describe, + it, + expect, + afterEach, + beforeEach, + type MockInstance, +} from 'vitest' + +import { CopyButton } from './CopyButton' + +describe('CopyButton', () => { + let writeTextSpy: MockInstance + + beforeEach(() => { + writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText') + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('copies text to clipboard on click', async () => { + const user = userEvent.setup() + const writeTextSpy = vi.spyOn(navigator.clipboard, 'writeText') + render( + + {copied => (copied ? 'Copied!' : 'Copy')} + , + ) + const button = screen.getByRole('button') + await user.click(button) + expect(writeTextSpy).toHaveBeenCalledWith('Hello, World!') + expect(writeTextSpy).toHaveBeenCalledTimes(1) + }) + + it('shows copied state after clicking', async () => { + const user = userEvent.setup() + render( + + {copied => (copied ? 'Copied!' : 'Copy')} + , + ) + const button = screen.getByRole('button') + expect(button).toHaveTextContent('Copy') + await user.click(button) + await waitFor(() => { + expect(button).toHaveTextContent('Copied!') + }) + }) + + it('disables button while in copied state', async () => { + const user = userEvent.setup() + render( + + {copied => (copied ? 'Copied!' : 'Copy')} + , + ) + const button = screen.getByRole('button') + expect(button).toBeEnabled() + await user.click(button) + await waitFor(() => { + expect(button).toBeDisabled() + }) + }) + + it('resets to initial state after delay', async () => { + const user = userEvent.setup() + render( + + {copied => (copied ? 'Copied!' : 'Copy')} + , + ) + const button = screen.getByRole('button') + await user.click(button) + await waitFor(() => { + expect(button).toHaveTextContent('Copied!') + }) + await waitFor( + () => { + expect(button).toHaveTextContent('Copy') + expect(button).toBeEnabled() + }, + { timeout: 200 }, + ) + }) + + it('calls onClick handler if provided', async () => { + const onClickSpy = vi.fn() + const user = userEvent.setup() + render( + + {() => 'Copy'} + , + ) + await user.click(screen.getByRole('button')) + expect(onClickSpy).toHaveBeenCalled() + expect(writeTextSpy).toHaveBeenCalled() + }) +}) diff --git a/web/common/src/components/CopyButton/CopyButton.tsx b/web/common/src/components/CopyButton/CopyButton.tsx new file mode 100644 index 0000000000..5eba483010 --- /dev/null +++ b/web/common/src/components/CopyButton/CopyButton.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react' + +import { Button, type ButtonProps } from '@/components/Button/Button' +import { cn } from '@/utils' + +type TimerID = ReturnType + +export interface CopyButtonProps extends Omit { + text: string + delay?: number + children: (copied: boolean) => React.ReactNode +} + +export const CopyButton = React.forwardRef( + ( + { + text, + title = 'Copy to clipboard', + variant = 'secondary', + size = 'xs', + delay = 2000, + disabled = false, + className, + children, + onClick, + ...props + }, + ref, + ) => { + const [copied, setCopied] = useState(null) + + const copy = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + if (copied) { + clearTimeout(copied) + } + + navigator.clipboard.writeText(text).then(() => { + setCopied(setTimeout(() => setCopied(null), delay)) + }) + + onClick?.(e) + } + + return ( + + ) + }, +) +CopyButton.displayName = 'CopyButton' diff --git a/web/common/src/components/HorizontalContainer/HorizontalContainer.stories.tsx b/web/common/src/components/HorizontalContainer/HorizontalContainer.stories.tsx new file mode 100644 index 0000000000..0857900abc --- /dev/null +++ b/web/common/src/components/HorizontalContainer/HorizontalContainer.stories.tsx @@ -0,0 +1,78 @@ +import { HorizontalContainer } from './HorizontalContainer' + +export default { + title: 'Components/Containers/HorizontalContainer', + component: HorizontalContainer, +} + +const content = Array.from({ length: 20 }, (_, i) => ( +
+ Col {i + 1} +
+)) + +export const Default = (args: any) => ( +
+ + {content} + +
+) +Default.storyName = 'Default (No Scroll)' + +export const WithScroll = (args: any) => ( +
+ +
{content}
+
+
+) + +export const CustomClassName = (args: any) => ( +
+ + {content} + +
+) +CustomClassName.storyName = 'With Custom ClassName' + +export const NestedHorizontalContainer = (args: any) => ( +
+ +
Left
+
+ +
{content}
+
+
+
+ Right +
+
+
+) +NestedHorizontalContainer.storyName = 'Nested HorizontalContainer' diff --git a/web/common/src/components/HorizontalContainer/HorizontalContainer.test.tsx b/web/common/src/components/HorizontalContainer/HorizontalContainer.test.tsx new file mode 100644 index 0000000000..52c62c7029 --- /dev/null +++ b/web/common/src/components/HorizontalContainer/HorizontalContainer.test.tsx @@ -0,0 +1,61 @@ +import { createRef } from 'react' +import { describe, expect, it } from 'vitest' + +import { render, screen } from '@testing-library/react' +import { HorizontalContainer } from './HorizontalContainer' + +describe('HorizontalContainer', () => { + it('renders children correctly', () => { + render( + +
Test Child
+
, + ) + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('should force layout to be horizontal (still having flex-row)', () => { + render( + +
Child
+
, + ) + const container = screen.getByText('Child').parentElement + expect(container).toHaveClass('flex-row') + }) + + it('renders ScrollContainer when scroll is true', () => { + render( + +
Scroll Child
+
, + ) + expect( + screen.getByText('Scroll Child').parentElement?.parentElement, + ).toHaveClass('overflow-x-scroll scrollbar-h-[6px]') + }) + + it('renders a div when scroll is false', () => { + render( + +
Div Child
+
, + ) + const container = screen.getByText('Div Child').parentElement + expect(container).toHaveClass('overflow-hidden') + }) + + it('forwards ref to the div element when scroll is false', () => { + const ref = createRef() + render( + +
Ref Child
+
, + ) + expect(ref.current).toBeInstanceOf(HTMLElement) + expect(ref.current?.tagName).toBe('DIV') + }) +}) diff --git a/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx b/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx new file mode 100644 index 0000000000..c1e2c66ed0 --- /dev/null +++ b/web/common/src/components/HorizontalContainer/HorizontalContainer.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +import { cn } from '@/utils' +import { ScrollContainer } from '../ScrollContainer/ScrollContainer' + +export interface HorizontalContainerProps + extends React.HTMLAttributes { + scroll?: boolean +} + +export const HorizontalContainer = React.forwardRef< + HTMLDivElement, + HorizontalContainerProps +>(({ children, className, scroll = false, ...props }, ref) => { + return scroll ? ( + + + {children} + + + ) : ( +
+ {children} +
+ ) +}) + +HorizontalContainer.displayName = 'HorizontalContainer' diff --git a/web/common/src/components/ModelName/ModelName.css b/web/common/src/components/ModelName/ModelName.css new file mode 100644 index 0000000000..42e11a061b --- /dev/null +++ b/web/common/src/components/ModelName/ModelName.css @@ -0,0 +1,17 @@ +:root { + --color-model-name-grayscale-link-underline: var(--color-neutral-125); + --color-model-name-grayscale-link-underline-hover: var(--color-neutral-500); + --color-model-name-link-underline: var(--color-link-underline); + --color-model-name-link-underline-hover: var(--color-link-hover); + + --color-model-name-grayscale-catalog: var(--color-neutral-400); + --color-model-name-grayscale-schema: var(--color-neutral-600); + --color-model-name-grayscale-model: var(--color-neutral-800); + + --color-model-name-catalog: var(--color-catalog); + --color-model-name-schema: var(--color-schema); + --color-model-name-model: var(--color-model); + + --color-model-name-copy-icon: var(--color-neutral-600); + --color-model-name-copy-icon-hover: var(--color-neutral-100); +} diff --git a/web/common/src/components/ModelName/ModelName.stories.tsx b/web/common/src/components/ModelName/ModelName.stories.tsx new file mode 100644 index 0000000000..fe54681f10 --- /dev/null +++ b/web/common/src/components/ModelName/ModelName.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { ModelName } from './ModelName' + +const meta: Meta = { + title: 'Components/ModelName', + component: ModelName, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + name: 'catalog.schema.model', + }, +} + +export const WithoutCatalog: Story = { + args: { + name: 'catalog.schema.model', + hideCatalog: true, + }, +} + +export const WithoutSchema: Story = { + args: { + name: 'catalog.schema.model', + hideSchema: true, + }, +} + +export const WithoutIcon: Story = { + args: { + name: 'catalog.schema.model', + hideIcon: true, + }, +} + +export const WithTooltip: Story = { + args: { + name: 'catalog.schema.model', + hideCatalog: true, + hideSchema: true, + showTooltip: true, + }, +} + +export const WithoutTooltip: Story = { + args: { + name: 'catalog.model', + showTooltip: false, + }, +} + +export const CustomClassName: Story = { + args: { + name: 'catalog.schema.model', + className: 'text-xl font-bold', + }, +} + +export const LongName: Story = { + args: { + name: 'veryveryverylongcatalogname.veryveryverylongschamename.veryveryverylongmodelnameveryveryverylongmodelname', + }, +} + +export const Grayscale: Story = { + args: { + name: 'catalog.schema.model', + grayscale: true, + }, +} + +export const Link: Story = { + args: { + name: 'catalog.schema.model', + link: 'https://www.google.com', + grayscale: false, + showCopy: true, + }, +} + +export const LinkGrayscale: Story = { + args: { + name: 'catalog.schema.model', + link: 'https://www.google.com', + grayscale: true, + showCopy: true, + }, +} diff --git a/web/common/src/components/ModelName/ModelName.test.tsx b/web/common/src/components/ModelName/ModelName.test.tsx new file mode 100644 index 0000000000..37e78650dd --- /dev/null +++ b/web/common/src/components/ModelName/ModelName.test.tsx @@ -0,0 +1,72 @@ +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import { render, screen, within } from '@testing-library/react' +import { ModelName } from './ModelName' + +describe('ModelName', () => { + it('renders full model name with catalog, schema, and model', () => { + render() + expect(screen.getByText('cat')).toBeInTheDocument() + expect(screen.getByText('sch')).toBeInTheDocument() + expect(screen.getByText('model')).toBeInTheDocument() + }) + + it('hides catalog when hideCatalog is true', () => { + render( + , + ) + expect(screen.queryByText('cat')).not.toBeInTheDocument() + expect(screen.getByText('sch')).toBeInTheDocument() + expect(screen.getByText('model')).toBeInTheDocument() + }) + + it('hides schema when hideSchema is true', () => { + render( + , + ) + expect(screen.getByText('cat')).toBeInTheDocument() + expect(screen.queryByText('sch')).not.toBeInTheDocument() + expect(screen.getByText('model')).toBeInTheDocument() + }) + + it('hides icon when hideIcon is true', () => { + const { container } = render( + , + ) + // Should not render the Box icon SVG + expect(container.querySelector('svg')).toBeNull() + }) + + it('shows tooltip when showTooltip is true and catalog or schema is hidden', async () => { + render( + , + ) + // Tooltip trigger is present (icon) + const modelName = screen.getByTestId('model-name') + expect(modelName).toBeInTheDocument() + await userEvent.hover(modelName) + const tooltip = await screen.findByRole('tooltip') + expect(tooltip).toBeInTheDocument() + within(tooltip).getByText('cat.sch.model') + }) + + it('throws error if name is empty', () => { + // Suppress error output for this test + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) + expect(() => render()).toThrow() + spy.mockRestore() + }) +}) diff --git a/web/common/src/components/ModelName/ModelName.tsx b/web/common/src/components/ModelName/ModelName.tsx new file mode 100644 index 0000000000..58ab33e1cb --- /dev/null +++ b/web/common/src/components/ModelName/ModelName.tsx @@ -0,0 +1,218 @@ +import { Box, Check, Copy } from 'lucide-react' +import { useMemo } from 'react' + +import { cn, truncate } from '@/utils' +import { Tooltip } from '@/components/Tooltip/Tooltip' +import React from 'react' + +import './ModelName.css' +import { CopyButton } from '../CopyButton/CopyButton' + +export interface ModelNameProps extends React.HTMLAttributes { + name: string + hideCatalog?: boolean + hideSchema?: boolean + hideIcon?: boolean + showTooltip?: boolean + showCopy?: boolean + truncateMaxChars?: number + truncateLimitBefore?: number + truncateLimitAfter?: number + grayscale?: boolean + link?: string + className?: string +} + +const MODEL_NAME_TOOLTIP_SIDE_OFFSET = 6 +const MODEL_NAME_ICON_SIZE = 16 + +export const ModelName = React.forwardRef( + ( + { + name, + hideCatalog = false, + hideSchema = false, + hideIcon = false, + showTooltip = true, + showCopy = false, + truncateMaxChars = 25, + truncateLimitBefore = 5, + truncateLimitAfter = 7, + grayscale = false, + link, + className, + ...props + }, + ref, + ) => { + if (!name) throw new Error('Model name should not be empty') + + const truncateMaxCharsModel = truncateMaxChars * 2 + + const { catalog, schema, model, withTooltip } = useMemo(() => { + const [model, schema, catalog] = name.split('.').reverse() + + return { + catalog: hideCatalog ? undefined : catalog, + schema: hideSchema ? undefined : schema, + model, + withTooltip: + ((hideCatalog && catalog) || + (hideSchema && schema) || + [catalog, schema].some(v => v && v.length > truncateMaxChars) || + model.length > truncateMaxCharsModel) && + showTooltip, + } + }, [ + name, + hideCatalog, + hideSchema, + truncateMaxCharsModel, + showTooltip, + truncateMaxChars, + ]) + + function renderTooltip() { + return ( + + {name} + + ) + } + + function renderIcon() { + return ( + + ) + } + + console.assert(name.length > 0, 'Model name should not be empty') + + function renderName() { + return ( + + {catalog && ( + <> + + {_truncate(catalog)} + + . + + )} + {schema && ( + <> + + {_truncate(schema)} + + . + + )} + + {truncate(model, truncateMaxCharsModel, 15)} + + + ) + } + + function renderNameWithTooltip() { + return withTooltip ? renderTooltip() : renderName() + } + + function _truncate(name: string, maxChars: number = truncateMaxChars) { + return truncate( + name, + maxChars, + truncateLimitBefore, + '...', + truncateLimitAfter, + ) + } + + return ( + + {!hideIcon && renderIcon()} + {link ? ( + + {renderNameWithTooltip()} + + ) : ( + renderNameWithTooltip() + )} + {showCopy && ( + + {copied => + copied ? ( + + ) : ( + + ) + } + + )} + + ) + }, +) + +ModelName.displayName = 'ModelName' diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.css b/web/common/src/components/ScrollContainer/ScrollContainer.css new file mode 100644 index 0000000000..98ba5055a2 --- /dev/null +++ b/web/common/src/components/ScrollContainer/ScrollContainer.css @@ -0,0 +1,4 @@ +:root { + --scrollbar-thumb: var(--color-neutral-300); + --scrollbar-track: var(--color-neutral-100); +} diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx b/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx new file mode 100644 index 0000000000..46b972d8f8 --- /dev/null +++ b/web/common/src/components/ScrollContainer/ScrollContainer.stories.tsx @@ -0,0 +1,109 @@ +import { ScrollContainer } from './ScrollContainer' + +export default { + title: 'Components/Containers/ScrollContainer', + component: ScrollContainer, +} + +const content = Array.from({ length: 30 }, (_, i) => ( +
+ Row {i + 1} +
+)) + +export const VerticalScroll = (args: any) => ( +
+ + {content} + +
+) + +export const HorizontalScroll = (args: any) => ( +
+ +
+ {Array.from({ length: 10 }, (_, i) => ( + + Column {i + 1} + + ))} +
+
+
+) + +export const BothDirectionsScroll = (args: any) => ( +
+ +
+ {Array.from({ length: 30 }, (_, i) => ( +
+ Row {i + 1} - This is a long line of text that should cause + horizontal scrolling when combined with the vertical scroll +
+ ))} +
+
+
+) + +export const CustomClassName = (args: any) => ( +
+ + {content} + +
+) +CustomClassName.storyName = 'With Custom ClassName' + +export const PageContentLayout = (args: any) => ( +
+ +
+
+ Actions +
+
+ Content +
+
+ End +
+
+
+
+) diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.test.tsx b/web/common/src/components/ScrollContainer/ScrollContainer.test.tsx new file mode 100644 index 0000000000..b557ecfd38 --- /dev/null +++ b/web/common/src/components/ScrollContainer/ScrollContainer.test.tsx @@ -0,0 +1,67 @@ +import { createRef } from 'react' +import { describe, expect, it } from 'vitest' +import { render, screen } from '@testing-library/react' + +import { ScrollContainer } from './ScrollContainer' + +describe('ScrollContainer', () => { + it('renders children correctly', () => { + render( + +
Test Child
+
, + ) + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('applies custom className', () => { + render( + +
Child
+
, + ) + const container = screen.getByText('Child').parentElement + expect(container).toHaveClass('custom-class') + }) + + it('applies vertical and horizontal scroll classes based on direction', () => { + const { rerender } = render( + +
Child
+
, + ) + let container = screen.getByText('Child').parentElement + expect(container).toHaveClass('overflow-y-scroll scrollbar-w-[6px]') + expect(container).toHaveClass('overflow-x-hidden') + + rerender( + +
Child
+
, + ) + container = screen.getByText('Child').parentElement + expect(container).toHaveClass('overflow-y-hidden') + expect(container).toHaveClass('overflow-x-scroll scrollbar-h-[6px]') + + rerender( + +
Child
+
, + ) + container = screen.getByText('Child').parentElement + expect(container).toHaveClass('overflow-y-scroll scrollbar-w-[6px]') + expect(container).toHaveClass('overflow-x-scroll scrollbar-h-[6px]') + }) + + it('forwards ref to the span element', () => { + const ref = createRef() + render( + // @ts-expect-error: ScrollContainer's ref type is HTMLDivElement, but it renders a span + +
Child
+
, + ) + expect(ref.current).toBeInstanceOf(HTMLElement) + expect(ref.current?.tagName).toBe('DIV') + }) +}) diff --git a/web/common/src/components/ScrollContainer/ScrollContainer.tsx b/web/common/src/components/ScrollContainer/ScrollContainer.tsx new file mode 100644 index 0000000000..19ce969e2f --- /dev/null +++ b/web/common/src/components/ScrollContainer/ScrollContainer.tsx @@ -0,0 +1,39 @@ +import React from 'react' + +import { cn } from '@/utils' +import type { LayoutDirection } from '@/types' + +import './ScrollContainer.css' + +export interface ScrollContainerProps + extends React.HTMLAttributes { + direction?: LayoutDirection +} + +export const ScrollContainer = React.forwardRef< + HTMLDivElement, + ScrollContainerProps +>(({ children, className, direction = 'vertical', ...props }, ref) => { + const vertical = direction === 'vertical' || direction === 'both' + const horizontal = direction === 'horizontal' || direction === 'both' + return ( +
+ {children} +
+ ) +}) + +ScrollContainer.displayName = 'ScrollContainer' diff --git a/web/common/src/components/Tooltip/Tooltip.css b/web/common/src/components/Tooltip/Tooltip.css new file mode 100644 index 0000000000..ba080f6974 --- /dev/null +++ b/web/common/src/components/Tooltip/Tooltip.css @@ -0,0 +1,4 @@ +:root { + --color-tooltip-background: var(--color-dark); + --color-tooltip-foreground: var(--color-light); +} diff --git a/web/common/src/components/Tooltip/Tooltip.stories.tsx b/web/common/src/components/Tooltip/Tooltip.stories.tsx new file mode 100644 index 0000000000..37f76f7e27 --- /dev/null +++ b/web/common/src/components/Tooltip/Tooltip.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { Tooltip } from '@/components/Tooltip/Tooltip' +import { Button } from '@/components/Button/Button' + +const meta: Meta = { + title: 'Components/Tooltip', + component: Tooltip, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + trigger: , + children: 'This is a tooltip', + }, +} diff --git a/web/common/src/components/Tooltip/Tooltip.tsx b/web/common/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 0000000000..8096e8742e --- /dev/null +++ b/web/common/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,58 @@ +import { + TooltipProvider, + Tooltip as TooltipRoot, + TooltipTrigger, + TooltipPortal, + TooltipContent, +} from '@radix-ui/react-tooltip' +import React from 'react' + +import { cn } from '@/utils' +import type { Position } from '@/types' + +import './Tooltip.css' + +export type TooltipSide = Extract +export type TooltipAlign = Extract + +export function Tooltip({ + delayDuration = 200, + sideOffset = 0, + alignOffset = 0, + side = 'right', + align = 'center', + trigger, + children, + className, +}: { + trigger: React.ReactNode + side?: TooltipSide + align?: TooltipAlign + delayDuration?: number + sideOffset?: number + alignOffset?: number + children: React.ReactNode + className?: string +}) { + return ( + + + {trigger} + + + {children} + + + + + ) +} diff --git a/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx b/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx new file mode 100644 index 0000000000..8283b50a8d --- /dev/null +++ b/web/common/src/components/VerticalContainer/VerticalContainer.stories.tsx @@ -0,0 +1,78 @@ +import { VerticalContainer } from './VerticalContainer' + +export default { + title: 'Components/Containers/VerticalContainer', + component: VerticalContainer, +} + +const content = Array.from({ length: 20 }, (_, i) => ( +
+ Row {i + 1} +
+)) + +export const Default = (args: any) => ( +
+ + {content} + +
+) +Default.storyName = 'Default (No Scroll)' + +export const WithScroll = (args: any) => ( +
+ + {content} + +
+) + +export const CustomClassName = (args: any) => ( +
+ + {content} + +
+) +CustomClassName.storyName = 'With Custom ClassName' + +export const NestedVerticalContainer = (args: any) => ( +
+ +
Header
+
+ {content} +
+
+ Footer +
+
+
+) +NestedVerticalContainer.storyName = 'Nested VerticalContainer' diff --git a/web/common/src/components/VerticalContainer/VerticalContainer.test.tsx b/web/common/src/components/VerticalContainer/VerticalContainer.test.tsx new file mode 100644 index 0000000000..75a5c50c4e --- /dev/null +++ b/web/common/src/components/VerticalContainer/VerticalContainer.test.tsx @@ -0,0 +1,61 @@ +import { createRef } from 'react' +import { describe, expect, it } from 'vitest' + +import { render, screen } from '@testing-library/react' +import { VerticalContainer } from './VerticalContainer' + +describe('VerticalContainer', () => { + it('renders children correctly', () => { + render( + +
Test Child
+
, + ) + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('should force layout to be vertical (still having flex-col)', () => { + render( + +
Child
+
, + ) + const container = screen.getByText('Child').parentElement + expect(container).toHaveClass('flex-col') + }) + + it('renders ScrollContainer when scroll is true', () => { + render( + +
Scroll Child
+
, + ) + expect( + screen.getByText('Scroll Child').parentElement?.parentElement, + ).toHaveClass('overflow-y-scroll scrollbar-w-[6px]') + }) + + it('renders a div when scroll is false', () => { + render( + +
Div Child
+
, + ) + const container = screen.getByText('Div Child').parentElement + expect(container).toHaveClass('overflow-hidden') + }) + + it('forwards ref to the div element when scroll is false', () => { + const ref = createRef() + render( + +
Ref Child
+
, + ) + expect(ref.current).toBeInstanceOf(HTMLElement) + expect(ref.current?.tagName).toBe('DIV') + }) +}) diff --git a/web/common/src/components/VerticalContainer/VerticalContainer.tsx b/web/common/src/components/VerticalContainer/VerticalContainer.tsx new file mode 100644 index 0000000000..e592265dca --- /dev/null +++ b/web/common/src/components/VerticalContainer/VerticalContainer.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +import { cn } from '@/utils' +import { ScrollContainer } from '../ScrollContainer/ScrollContainer' + +export interface VerticalContainerProps + extends React.HTMLAttributes { + scroll?: boolean +} + +export const VerticalContainer = React.forwardRef< + HTMLDivElement, + VerticalContainerProps +>(({ children, className, scroll = false, ...props }, ref) => { + return scroll ? ( + + + {children} + + + ) : ( +
+ {children} +
+ ) +}) + +VerticalContainer.displayName = 'VerticalContainer' diff --git a/web/common/src/index.ts b/web/common/src/index.ts index 309e993504..6cd52abead 100644 --- a/web/common/src/index.ts +++ b/web/common/src/index.ts @@ -1,37 +1,39 @@ // Components export { Badge, type BadgeProps } from '@/components/Badge/Badge' +export { Button, type ButtonProps } from '@/components/Button/Button' +export { + CopyButton, + type CopyButtonProps, +} from '@/components/CopyButton/CopyButton' +export { + HorizontalContainer, + type HorizontalContainerProps, +} from '@/components/HorizontalContainer/HorizontalContainer' +export { + ScrollContainer, + type ScrollContainerProps, +} from '@/components/ScrollContainer/ScrollContainer' +export { + VerticalContainer, + type VerticalContainerProps, +} from '@/components/VerticalContainer/VerticalContainer' +export { + ModelName, + type ModelNameProps, +} from '@/components/ModelName/ModelName' +export { Tooltip } from '@/components/Tooltip/Tooltip' // Utils -export { cn, isNil, notNil } from '@/utils' +export { cn, truncate } from '@/utils' // Types -export type { Nil, Optional, Maybe } from '@/types' -export { - EnumSize, - EnumHeadlineLevel, - EnumSide, - EnumLayoutDirection, - EnumShape, - type Size, - type HeadlineLevel, - type Side, - type LayoutDirection, - type Shape, -} from '@/types/enums' - -// Design Tokens -export { - colorToken, - spacingToken, - textSizeToken, - type ColorTokens, - type SpacingTokens, - type TypographyTokens, - type DesignTokens, - type ColorScale, - type ColorVariant, - type StepScale, - type TextSize, - type TextRole, - type CSSCustomProperty, -} from '@/styles/tokens' +export type { + Brand, + Branded, + Size, + HeadlineLevel, + Side, + LayoutDirection, + Shape, + Position, +} from '@/types' diff --git a/web/common/src/styles/design/palette.css b/web/common/src/styles/design/palette.css index 7fa379437f..4370038c97 100644 --- a/web/common/src/styles/design/palette.css +++ b/web/common/src/styles/design/palette.css @@ -194,6 +194,8 @@ --color-gray-550: hsl(190, 8%, 42%); --color-gray-600: hsl(190, 8%, 38%); --color-gray-700: hsl(202, 8%, 26%); + --color-gray-725: hsl(202, 8%, 22%); + --color-gray-750: hsl(202, 8%, 20%); --color-gray-800: hsl(214, 8%, 14%); --color-gray-900: hsl(226, 8%, 4%); } diff --git a/web/common/src/styles/design/semantic-colors.css b/web/common/src/styles/design/semantic-colors.css index 4a58ba52ff..f2a45e5eef 100644 --- a/web/common/src/styles/design/semantic-colors.css +++ b/web/common/src/styles/design/semantic-colors.css @@ -5,4 +5,61 @@ --color-dark: hsl(226, 24%, 8%); --color-brand: var(--color-tobiko); --color-prose: var(--color-gray-800); + --color-focused: var(--color-brand); + + /* Neutral */ + --color-neutral-3: var(--color-gray-5); + --color-neutral-5: var(--color-gray-5); + --color-neutral-10: var(--color-gray-10); + --color-neutral-15: var(--color-gray-15); + --color-neutral-20: var(--color-gray-20); + --color-neutral-25: var(--color-gray-25); + --color-neutral-100: var(--color-gray-100); + --color-neutral-125: var(--color-gray-125); + --color-neutral-150: var(--color-gray-150); + --color-neutral-200: var(--color-gray-200); + --color-neutral-300: var(--color-gray-300); + --color-neutral-400: var(--color-gray-400); + --color-neutral-500: var(--color-gray-500); + --color-neutral: var(--color-gray-500); + --color-neutral-525: var(--color-gray-525); + --color-neutral-550: var(--color-gray-550); + --color-neutral-600: var(--color-gray-600); + --color-neutral-700: var(--color-gray-700); + --color-neutral-725: var(--color-gray-725); + --color-neutral-750: var(--color-gray-750); + --color-neutral-800: var(--color-gray-800); + --color-neutral-900: var(--color-gray-900); + + /* Model */ + --color-catalog: var(--color-deep-blue-800); + --color-schema: var(--color-deep-blue-600); + --color-model: var(--color-deep-blue-500); + + /* Destructive */ + --color-destructive: var(--color-scarlet-500); + --color-destructive-foreground: var(--color-scarlet-600); + --color-destructive-hover: var(--color-scarlet-525); + --color-destructive-active: var(--color-scarlet-550); + + /* Success */ + --color-success: var(--color-emerald-500); + + /* Warning */ + --color-warning: var(--color-mandarin-500); + + /* Action */ + --color-action: var(--color-deep-blue-500); + --color-action-hover: var(--color-deep-blue-525); + --color-action-active: var(--color-deep-blue-550); + + /* Accent */ + --color-accent: var(--color-purple-500); + + /* Link */ + --color-link: var(--color-action); + --color-link-hover: var(--color-action-hover); + --color-link-active: var(--color-action-active); + --color-link-visited: var(--color-purple-600); + --color-link-underline: var(--color-deep-blue-125); } diff --git a/web/common/src/styles/tokens.ts b/web/common/src/styles/tokens.ts deleted file mode 100644 index 75d233fdb4..0000000000 --- a/web/common/src/styles/tokens.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Design Token TypeScript Definitions - * Type-safe access to CSS custom properties defined in the design system - */ - -// Color Tokens -export interface ColorTokens { - // Brand Colors - '--color-tobiko': string - '--color-sqlmesh': string - '--color-sqlglot': string - '--color-pacific': string - '--color-wasabi': string - '--color-yuzu': string - '--color-uni': string - '--color-salmon': string - - // Base Colors - '--color-white': string - '--color-black': string - '--color-cyan': string - '--color-deep-blue': string - '--color-purple': string - '--color-emerald': string - '--color-mandarin': string - '--color-scarlet': string - '--color-sunflower': string - '--color-peach': string - '--color-turquoise': string - '--color-fuchsia': string - '--color-gray': string - - // Semantic Colors - '--color-light': string - '--color-dark': string - '--color-brand': string - '--color-prose': string - '--color-badge-background': string - '--color-badge-foreground': string -} - -// Spacing Tokens -export interface SpacingTokens { - '--one': string - '--base': string - '--half': string - '--step': string - '--step-2': string - '--step-3': string - '--step-4': string - '--step-5': string - '--step-6': string - '--step-7': string - '--step-8': string - '--step-9': string - '--step-10': string - '--step-11': string - '--step-12': string - '--step-15': string - '--step-16': string - '--step-20': string - '--step-24': string - '--step-30': string - '--step-32': string -} - -// Typography Tokens -export interface TypographyTokens { - // Font Families - '--font-sans': string - '--font-accent': string - '--font-serif': string - '--font-mono': string - - // Font Sizes - '--font-size': string - '--text-2xs': string - '--text-xs': string - '--text-s': string - '--text-m': string - '--text-l': string - '--text-xl': string - '--text-2xl': string - '--text-3xl': string - '--text-4xl': string - '--text-headline': string - '--text-display': string - '--text-header': string - '--text-tagline': string - '--text-title': string - '--text-subtitle': string - - // Line Heights - '--leading': string - '--text-leading-xs': string - '--text-leading-s': string - '--text-leading-m': string - '--text-leading-l': string - '--text-leading-xl': string - - // Font Weights - '--font-weight': string - '--text-thin': string - '--text-extra-light': string - '--text-light': string - '--text-normal': string - '--text-medium': string - '--text-semibold': string - '--text-bold': string - '--text-extra-bold': string - '--text-black': string -} - -// Combined Design Tokens -export interface DesignTokens - extends ColorTokens, - SpacingTokens, - TypographyTokens {} - -// Utility type for accessing CSS custom properties -export type CSSCustomProperty = T - -// Type-safe color scale definitions -export type ColorScale = - | '5' - | '10' - | '15' - | '20' - | '25' - | '50' - | '60' - | '75' - | '100' - | '125' - | '150' - | '200' - | '300' - | '400' - | '500' - | '525' - | '550' - | '600' - | '700' - | '725' - | '750' - | '800' - | '900' - -export type ColorVariant = - | 'cyan' - | 'deep-blue' - | 'pacific' - | 'purple' - | 'emerald' - | 'mandarin' - | 'scarlet' - | 'gray' - | 'uni' - | 'salmon' - | 'turquoise' - | 'fuchsia' - -// Helper function to build color custom property strings -export function colorToken( - variant: ColorVariant, - scale?: ColorScale, -): CSSCustomProperty { - return scale ? `--color-${variant}-${scale}` : `--color-${variant}` -} - -// Step scale for spacing -export type StepScale = - | 2 - | 3 - | 4 - | 5 - | 6 - | 7 - | 8 - | 9 - | 10 - | 11 - | 12 - | 15 - | 16 - | 20 - | 24 - | 30 - | 32 - -// Helper function to build spacing custom property strings -export function spacingToken( - step?: StepScale | 'half', -): CSSCustomProperty { - if (step === 'half') return '--half' - return step ? `--step-${step}` : '--step' -} - -// Text size variants -export type TextSize = - | '2xs' - | 'xs' - | 's' - | 'm' - | 'l' - | 'xl' - | '2xl' - | '3xl' - | '4xl' -export type TextRole = - | 'headline' - | 'display' - | 'header' - | 'tagline' - | 'title' - | 'subtitle' - -// Helper function to build text size custom property strings -export function textSizeToken( - size: TextSize | TextRole, -): CSSCustomProperty { - return `--text-${size}` -} diff --git a/web/common/src/types.ts b/web/common/src/types.ts new file mode 100644 index 0000000000..d23ea8b86b --- /dev/null +++ b/web/common/src/types.ts @@ -0,0 +1,18 @@ +export declare const __brand: unique symbol + +export type Brand = { [__brand]: B } +export type Branded = T & Brand + +export type Size = '2xs' | 'xs' | 's' | 'm' | 'l' | 'xl' | '2xl' +export type HeadlineLevel = 1 | 2 | 3 | 4 | 5 | 6 +export type Side = 'left' | 'right' | 'both' +export type LayoutDirection = 'vertical' | 'horizontal' | 'both' +export type Shape = 'square' | 'round' | 'pill' +export type Position = + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'center' + | 'start' + | 'end' diff --git a/web/common/src/types/enums.ts b/web/common/src/types/enums.ts deleted file mode 100644 index 9985bc7e5b..0000000000 --- a/web/common/src/types/enums.ts +++ /dev/null @@ -1,43 +0,0 @@ -export const EnumSize = { - XXS: '2xs', - XS: 'xs', - S: 's', - M: 'm', - L: 'l', - XL: 'xl', - XXL: '2xl', -} as const -export type Size = (typeof EnumSize)[keyof typeof EnumSize] - -export const EnumHeadlineLevel = { - H1: 1, - H2: 2, - H3: 3, - H4: 4, - H5: 5, - H6: 6, -} as const -export type HeadlineLevel = - (typeof EnumHeadlineLevel)[keyof typeof EnumHeadlineLevel] - -export const EnumSide = { - LEFT: 'left', - RIGHT: 'right', - BOTH: 'both', -} as const -export type Side = (typeof EnumSide)[keyof typeof EnumSide] - -export const EnumLayoutDirection = { - VERTICAL: 'vertical', - HORIZONTAL: 'horizontal', - BOTH: 'both', -} as const -export type LayoutDirection = - (typeof EnumLayoutDirection)[keyof typeof EnumLayoutDirection] - -export const EnumShape = { - Square: 'square', - Round: 'round', - Pill: 'pill', -} as const -export type Shape = (typeof EnumShape)[keyof typeof EnumShape] diff --git a/web/common/src/types/index.ts b/web/common/src/types/index.ts deleted file mode 100644 index a2d8ca7e51..0000000000 --- a/web/common/src/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Nil = undefined | null -export type Optional = T | undefined -export type Maybe = T | Nil diff --git a/web/common/src/utils.ts b/web/common/src/utils.ts new file mode 100644 index 0000000000..56557b61cf --- /dev/null +++ b/web/common/src/utils.ts @@ -0,0 +1,32 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} +export function truncate( + text: string, + maxChars = 0, + limitBefore = 5, + delimiter = '...', + limitAfter?: number, +): string { + const textLength = text.length + + limitBefore = Math.abs(limitBefore) + limitAfter = limitAfter == null ? limitBefore : Math.abs(limitAfter) + + if (maxChars > textLength || limitBefore + limitAfter >= textLength) { + return text + } + + if (limitAfter === 0) { + return text.substring(0, limitBefore) + delimiter + } + + return ( + text.substring(0, limitBefore) + + delimiter + + text.substring(textLength - limitAfter) + ) +} diff --git a/web/common/src/utils/index.ts b/web/common/src/utils/index.ts deleted file mode 100644 index 95e869c212..0000000000 --- a/web/common/src/utils/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Nil } from '@/types' -import { clsx, type ClassValue } from 'clsx' -import { twMerge } from 'tailwind-merge' - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} - -export function isNil(value: unknown): value is Nil { - return value == null -} -export function notNil(value: unknown): value is NonNullable { - return value != null -} diff --git a/web/common/tailwind.base.config.js b/web/common/tailwind.base.config.js index e1b44d12d9..005c353379 100644 --- a/web/common/tailwind.base.config.js +++ b/web/common/tailwind.base.config.js @@ -11,10 +11,100 @@ module.exports = { light: 'var(--color-light)', brand: 'var(--color-brand)', prose: 'var(--color-prose)', + focused: 'var(--color-focused)', + neutral: { + DEFAULT: 'var(--color-neutral)', + 3: 'var(--color-neutral-3)', + 5: 'var(--color-neutral-5)', + 10: 'var(--color-neutral-10)', + 15: 'var(--color-neutral-15)', + 20: 'var(--color-neutral-20)', + 25: 'var(--color-neutral-25)', + 100: 'var(--color-neutral-100)', + 125: 'var(--color-neutral-125)', + 150: 'var(--color-neutral-150)', + 200: 'var(--color-neutral-200)', + 300: 'var(--color-neutral-300)', + 400: 'var(--color-neutral-400)', + 500: 'var(--color-neutral-500)', + 525: 'var(--color-neutral-525)', + 550: 'var(--color-neutral-550)', + 600: 'var(--color-neutral-600)', + 700: 'var(--color-neutral-700)', + 725: 'var(--color-neutral-725)', + 750: 'var(--color-neutral-750)', + 800: 'var(--color-neutral-800)', + 900: 'var(--color-neutral-900)', + }, + link: { + underline: 'var(--color-link-underline)', + hover: 'var(--color-link-hover)', + active: 'var(--color-link-active)', + visited: 'var(--color-link-visited)', + }, + 'model-name': { + 'grayscale-link-underline': + 'var(--color-model-name-grayscale-link-underline)', + 'grayscale-link-underline-hover': + 'var(--color-model-name-grayscale-link-hover)', + 'grayscale-catalog': 'var(--color-model-name-grayscale-catalog)', + 'grayscale-schema': 'var(--color-model-name-grayscale-schema)', + 'grayscale-model': 'var(--color-model-name-grayscale-model)', + 'link-underline': 'var(--color-model-name-link-underline)', + 'link-underline-hover': + 'var(--color-model-name-link-underline-hover)', + catalog: 'var(--color-model-name-catalog)', + schema: 'var(--color-model-name-schema)', + model: 'var(--color-model-name-model)', + 'copy-icon': 'var(--color-model-name-copy-icon)', + 'copy-icon-hover': 'var(--color-model-name-copy-icon-hover)', + }, badge: { background: 'var(--color-badge-background)', foreground: 'var(--color-badge-foreground)', }, + button: { + primary: { + background: 'var(--color-button-primary-background)', + foreground: 'var(--color-button-primary-foreground)', + hover: 'var(--color-button-primary-hover)', + active: 'var(--color-button-primary-active)', + }, + secondary: { + background: 'var(--color-button-secondary-background)', + foreground: 'var(--color-button-secondary-foreground)', + hover: 'var(--color-button-secondary-hover)', + active: 'var(--color-button-secondary-active)', + }, + alternative: { + background: 'var(--color-button-alternative-background)', + foreground: 'var(--color-button-alternative-foreground)', + hover: 'var(--color-button-alternative-hover)', + active: 'var(--color-button-alternative-active)', + }, + destructive: { + background: 'var(--color-button-destructive-background)', + foreground: 'var(--color-button-destructive-foreground)', + hover: 'var(--color-button-destructive-hover)', + active: 'var(--color-button-destructive-active)', + }, + danger: { + background: 'var(--color-button-danger-background)', + foreground: 'var(--color-button-danger-foreground)', + hover: 'var(--color-button-danger-hover)', + active: 'var(--color-button-danger-active)', + }, + transparent: { + background: 'var(--color-button-transparent-background)', + foreground: 'var(--color-button-transparent-foreground)', + hover: 'var(--color-button-transparent-hover)', + active: 'var(--color-button-transparent-active)', + }, + }, + tooltip: { + background: 'var(--color-tooltip-background)', + foreground: 'var(--color-tooltip-foreground)', + }, }, borderRadius: { '2xs': 'var(--radius-xs)', @@ -41,5 +131,11 @@ module.exports = { }, }, }, - plugins: [require('@tailwindcss/typography')], + plugins: [ + require('@tailwindcss/typography'), + require('tailwind-scrollbar')({ + nocompatible: true, + preferredStrategy: 'pseudoelements', + }), + ], }