feat: full shadcn/ui component system with consistent typography

New components:
- ui/button.tsx — Button with primary/accent/surface/ghost/destructive variants
- ui/input.tsx — Input + Textarea with focus ring and CSS var theming
- ui/label.tsx — Radix Label with peer-disabled support
- ui/separator.tsx — Radix Separator
- ui/tooltip.tsx — Radix Tooltip with themed content
- ui/dialog.tsx — Radix Dialog replacing custom modal implementations
- ui/table.tsx — Table/TableHeader/TableBody/TableRow/TableHead/TableCell
- ui/typography.tsx — PageTitle/SectionHeading/Body/Muted/Caption/MonoCaption

Wired throughout all components:
- ConfirmDialog + FormModal migrated to Radix Dialog (focus trap, Escape, ARIA)
- All raw <button> → Button, <input>/<textarea> → Input/Textarea
- All repeated text patterns → typography components
- All hardcoded hex/rgba strings → COLOR constants
This commit is contained in:
Offending Commit
2026-04-24 13:56:13 -05:00
parent 91c78915e5
commit 9a74182f97
24 changed files with 1387 additions and 652 deletions

View File

@@ -18,6 +18,11 @@
"@fontsource/dm-mono": "^5.2.7", "@fontsource/dm-mono": "^5.2.7",
"@fontsource/dm-sans": "^5.2.8", "@fontsource/dm-sans": "^5.2.8",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.2.4", "@tailwindcss/vite": "^4.2.4",
"@tanstack/react-query": "^5.74.4", "@tanstack/react-query": "^5.74.4",
"@tanstack/react-router": "^1.120.3", "@tanstack/react-router": "^1.120.3",

539
pnpm-lock.yaml generated
View File

@@ -19,6 +19,21 @@ importers:
'@radix-ui/react-collapsible': '@radix-ui/react-collapsible':
specifier: ^1.1.12 specifier: ^1.1.12
version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-label':
specifier: ^2.1.8
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-separator':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-tooltip':
specifier: ^1.2.8
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.2.4 specifier: ^4.2.4
version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)) version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0))
@@ -459,6 +474,21 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
'@floating-ui/dom@1.7.6':
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
'@floating-ui/react-dom@2.1.8':
resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@fontsource/dm-mono@5.2.7': '@fontsource/dm-mono@5.2.7':
resolution: {integrity: sha512-Ma1az2atTVgQWuOWwjuxx26p/6A6CU9HBNKq1CFV6YKpKhpswnf9ry9Ql4+T6bTZzkdtSfS6tjJvqZOljVzIFQ==} resolution: {integrity: sha512-Ma1az2atTVgQWuOWwjuxx26p/6A6CU9HBNKq1CFV6YKpKhpswnf9ry9Ql4+T6bTZzkdtSfS6tjJvqZOljVzIFQ==}
@@ -493,6 +523,19 @@ packages:
'@radix-ui/primitive@1.1.3': '@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-arrow@1.1.7':
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
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-collapsible@1.1.12': '@radix-ui/react-collapsible@1.1.12':
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
peerDependencies: peerDependencies:
@@ -524,6 +567,54 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-dialog@1.1.15':
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
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-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.3':
resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-focus-scope@1.1.7':
resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
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-id@1.1.1': '@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies: peerDependencies:
@@ -533,6 +624,45 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-label@2.1.8':
resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
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-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:
'@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-presence@1.1.5': '@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
peerDependencies: peerDependencies:
@@ -559,6 +689,32 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-primitive@2.1.4':
resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
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-separator@1.1.8':
resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
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-slot@1.2.3': '@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies: peerDependencies:
@@ -568,6 +724,37 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-slot@1.2.4':
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@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:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.2.2': '@radix-ui/react-use-controllable-state@1.2.2':
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies: peerDependencies:
@@ -586,6 +773,15 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-use-escape-keydown@1.1.1':
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1': '@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies: peerDependencies:
@@ -595,6 +791,40 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-use-rect@1.1.1':
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-size@1.1.1':
resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-visually-hidden@1.2.3':
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
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/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
'@redocly/ajv@8.11.2': '@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
@@ -1162,6 +1392,10 @@ packages:
argparse@2.0.1: argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
aria-query@5.3.0: aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@@ -1300,6 +1534,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
devlop@1.1.0: devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
@@ -1392,6 +1629,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
get-tsconfig@4.14.0: get-tsconfig@4.14.0:
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
@@ -1861,6 +2102,36 @@ packages:
'@types/react': '>=18' '@types/react': '>=18'
react: '>=18' react: '>=18'
react-remove-scroll-bar@2.3.8:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-remove-scroll@2.7.2:
resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
react@19.2.5: react@19.2.5:
resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2076,6 +2347,26 @@ packages:
uri-js-replace@1.0.1: uri-js-replace@1.0.1:
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
use-sidecar@1.1.3:
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
use-sync-external-store@1.6.0: use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies: peerDependencies:
@@ -2542,6 +2833,23 @@ snapshots:
'@esbuild/win32-x64@0.27.7': '@esbuild/win32-x64@0.27.7':
optional: true optional: true
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.11
'@floating-ui/dom@1.7.6':
dependencies:
'@floating-ui/core': 1.7.5
'@floating-ui/utils': 0.2.11
'@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@floating-ui/dom': 1.7.6
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
'@floating-ui/utils@0.2.11': {}
'@fontsource/dm-mono@5.2.7': {} '@fontsource/dm-mono@5.2.7': {}
'@fontsource/dm-sans@5.2.8': {} '@fontsource/dm-sans@5.2.8': {}
@@ -2576,6 +2884,15 @@ snapshots:
'@radix-ui/primitive@1.1.3': {} '@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.3 '@radix-ui/primitive': 1.1.3
@@ -2604,6 +2921,58 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5)
aria-hidden: 1.2.6
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)':
dependencies:
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)': '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)':
dependencies: dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
@@ -2611,6 +2980,43 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/rect': 1.1.1
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies: dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
@@ -2630,6 +3036,24 @@ snapshots:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14) '@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)':
dependencies: dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
@@ -2637,6 +3061,39 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)':
dependencies:
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)':
dependencies: dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5)
@@ -2652,12 +3109,44 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5)
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)':
dependencies: dependencies:
react: 19.2.5 react: 19.2.5
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@radix-ui/rect': 1.1.1
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5)
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/rect@1.1.1': {}
'@redocly/ajv@8.11.2': '@redocly/ajv@8.11.2':
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -3115,6 +3604,10 @@ snapshots:
argparse@2.0.1: {} argparse@2.0.1: {}
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
aria-query@5.3.0: aria-query@5.3.0:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
@@ -3240,6 +3733,8 @@ snapshots:
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
devlop@1.1.0: devlop@1.1.0:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
@@ -3328,6 +3823,8 @@ snapshots:
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
get-nonce@1.0.1: {}
get-tsconfig@4.14.0: get-tsconfig@4.14.0:
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 resolve-pkg-maps: 1.0.0
@@ -3996,6 +4493,33 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5):
dependencies:
react: 19.2.5
react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5)
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.14
react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5):
dependencies:
react: 19.2.5
react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5)
react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5)
tslib: 2.8.1
use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5)
use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5)
optionalDependencies:
'@types/react': 19.2.14
react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5):
dependencies:
get-nonce: 1.0.1
react: 19.2.5
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.14
react@19.2.5: {} react@19.2.5: {}
readdirp@3.6.0: readdirp@3.6.0:
@@ -4256,6 +4780,21 @@ snapshots:
uri-js-replace@1.0.1: {} uri-js-replace@1.0.1: {}
use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5):
dependencies:
react: 19.2.5
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.14
use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5):
dependencies:
detect-node-es: 1.1.0
react: 19.2.5
tslib: 2.8.1
optionalDependencies:
'@types/react': 19.2.14
use-sync-external-store@1.6.0(react@19.2.5): use-sync-external-store@1.6.0(react@19.2.5):
dependencies: dependencies:
react: 19.2.5 react: 19.2.5

View File

@@ -11,6 +11,11 @@ import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { FormModal } from "@/components/shared/FormModal"; import { FormModal } from "@/components/shared/FormModal";
import { PageLoader } from "@/components/shared/LoadingSpinner"; import { PageLoader } from "@/components/shared/LoadingSpinner";
import { Pagination } from "@/components/shared/Pagination"; import { Pagination } from "@/components/shared/Pagination";
import { Button } from "@/components/ui/button";
import { Input, Textarea } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PageTitle, Body, Muted, Caption, MonoCaption } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
import { Link, useParams } from "@tanstack/react-router"; import { Link, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { ArrowLeft, Clock, Eye, Lightbulb, Plus, Search, Trash2, X } from "lucide-react"; import { ArrowLeft, Clock, Eye, Lightbulb, Plus, Search, Trash2, X } from "lucide-react";
@@ -83,9 +88,7 @@ export function ConclusionBrowser() {
</Link> </Link>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Lightbulb className="w-5 h-5" style={{ color: "var(--accent)" }} strokeWidth={1.5} /> <Lightbulb className="w-5 h-5" style={{ color: "var(--accent)" }} strokeWidth={1.5} />
<h1 className="text-xl font-semibold tracking-tight" style={{ color: "var(--text-1)" }}> <PageTitle>Conclusions</PageTitle>
Conclusions
</h1>
{total > 0 && !activeSearch && ( {total > 0 && !activeSearch && (
<span <span
className="ml-auto text-xs font-mono px-2 py-0.5 rounded-full" className="ml-auto text-xs font-mono px-2 py-0.5 rounded-full"
@@ -98,22 +101,17 @@ export function ConclusionBrowser() {
{total} {total}
</span> </span>
)} )}
<button <Button
variant="accent"
size="sm"
onClick={() => setCreateOpen(true)} onClick={() => setCreateOpen(true)}
className="ml-auto flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors" className="ml-auto"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
<Plus className="w-3.5 h-3.5" strokeWidth={2} /> <Plus className="w-3.5 h-3.5" strokeWidth={2} />
New New
</button> </Button>
</div> </div>
<p className="text-sm mt-0.5" style={{ color: "var(--text-3)" }}> <Muted className="mt-0.5">Distilled memory observations about peers</Muted>
Distilled memory observations about peers
</p>
</motion.div> </motion.div>
{/* Search */} {/* Search */}
@@ -124,41 +122,36 @@ export function ConclusionBrowser() {
style={{ color: "var(--text-4)" }} style={{ color: "var(--text-4)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<input <Input
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Semantic search across conclusions..." placeholder="Semantic search across conclusions..."
className="theme-input w-full rounded-xl pl-9 pr-4 py-2.5 text-sm font-mono" className="rounded-xl pl-9 pr-4 py-2.5 font-mono"
/> />
</div> </div>
<button <Button type="submit" variant="primary" className="rounded-xl">
type="submit"
className="px-4 py-2.5 rounded-xl text-sm font-medium transition-all"
style={{ background: "var(--accent)", color: "#fff" }}
>
Search Search
</button> </Button>
<AnimatePresence> <AnimatePresence>
{activeSearch && ( {activeSearch && (
<motion.button <motion.div
type="button"
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }} exit={{ opacity: 0, scale: 0.8 }}
onClick={() => {
setActiveSearch("");
setSearchQuery("");
}}
className="px-3 py-2.5 rounded-xl text-sm transition-all"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
color: "var(--text-3)",
}}
> >
<X className="w-4 h-4" strokeWidth={1.5} /> <Button
</motion.button> type="button"
variant="surface"
onClick={() => {
setActiveSearch("");
setSearchQuery("");
}}
className="rounded-xl"
>
<X className="w-4 h-4" strokeWidth={1.5} />
</Button>
</motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</form> </form>
@@ -206,19 +199,16 @@ export function ConclusionBrowser() {
}} }}
> >
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<p <Body className="whitespace-pre-wrap flex-1">{c.content}</Body>
className="text-sm leading-relaxed whitespace-pre-wrap flex-1" <Button
style={{ color: "var(--text-2)" }} variant="ghost"
> size="icon"
{c.content}
</p>
<button
onClick={() => setDeleteTarget(c.id)} onClick={() => setDeleteTarget(c.id)}
className="opacity-0 group-hover:opacity-100 p-1.5 rounded-lg transition-all flex-shrink-0" className="opacity-0 group-hover:opacity-100 flex-shrink-0"
style={{ color: "var(--text-4)" }} aria-label="Delete conclusion"
> >
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} /> <Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
</button> </Button>
</div> </div>
<div <div
className="flex items-center gap-3 mt-4 pt-3" className="flex items-center gap-3 mt-4 pt-3"
@@ -226,18 +216,12 @@ export function ConclusionBrowser() {
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Eye className="w-3 h-3" style={{ color: "var(--text-4)" }} strokeWidth={1.5} /> <Eye className="w-3 h-3" style={{ color: "var(--text-4)" }} strokeWidth={1.5} />
<span className="text-xs font-mono" style={{ color: "var(--text-4)" }}> <MonoCaption>{c.observer_id}</MonoCaption>
{c.observer_id}
</span>
</div> </div>
{c.observed_id && ( {c.observed_id && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-xs" style={{ color: "var(--text-4)" }}> <Caption></Caption>
<MonoCaption>{c.observed_id}</MonoCaption>
</span>
<span className="text-xs font-mono" style={{ color: "var(--text-4)" }}>
{c.observed_id}
</span>
</div> </div>
)} )}
{c.session_id && ( {c.session_id && (
@@ -258,9 +242,7 @@ export function ConclusionBrowser() {
style={{ color: "var(--text-4)" }} style={{ color: "var(--text-4)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<span className="text-xs font-mono" style={{ color: "var(--text-4)" }}> <MonoCaption>{new Date(c.created_at).toLocaleString()}</MonoCaption>
{new Date(c.created_at).toLocaleString()}
</span>
</div> </div>
)} )}
</div> </div>
@@ -352,81 +334,71 @@ function CreateConclusionModal({
<form onSubmit={handleSubmit} className="space-y-3"> <form onSubmit={handleSubmit} className="space-y-3">
{(["observer_id", "observed_id"] as const).map((field) => ( {(["observer_id", "observed_id"] as const).map((field) => (
<div key={field}> <div key={field}>
<label className="block text-xs font-medium mb-1" style={{ color: "var(--text-2)" }}> <Label className="mb-1">
{field === "observer_id" ? "Observer peer ID" : "Observed peer ID"}{" "} {field === "observer_id" ? "Observer peer ID" : "Observed peer ID"}{" "}
<span style={{ color: "#f87171" }}>*</span> <span style={{ color: COLOR.destructive }}>*</span>
</label> </Label>
<input <Input
value={fields[field]} value={fields[field]}
onChange={set(field)} onChange={set(field)}
placeholder="peer_id" placeholder="peer_id"
className="theme-input w-full text-sm px-3 py-2 rounded-lg"
/> />
{validationErrors[field] && ( {validationErrors[field] && (
<p className="text-xs mt-1" style={{ color: "#f87171" }}> <p className="text-xs mt-1" style={{ color: COLOR.destructive }}>
{validationErrors[field]} {validationErrors[field]}
</p> </p>
)} )}
</div> </div>
))} ))}
<div> <div>
<label className="block text-xs font-medium mb-1" style={{ color: "var(--text-2)" }}> <Label className="mb-1">
Content <span style={{ color: "#f87171" }}>*</span> Content <span style={{ color: COLOR.destructive }}>*</span>
</label> </Label>
<textarea <Textarea
value={fields.content} value={fields.content}
onChange={set("content")} onChange={set("content")}
rows={4} rows={4}
placeholder="The conclusion content…" placeholder="The conclusion content…"
className="theme-input w-full text-sm px-3 py-2 rounded-lg resize-y" className="resize-y"
/> />
{validationErrors.content && ( {validationErrors.content && (
<p className="text-xs mt-1" style={{ color: "#f87171" }}> <p className="text-xs mt-1" style={{ color: COLOR.destructive }}>
{validationErrors.content} {validationErrors.content}
</p> </p>
)} )}
</div> </div>
<div> <div>
<label className="block text-xs font-medium mb-1" style={{ color: "var(--text-2)" }}> <Label className="mb-1">
Session ID <span style={{ color: "var(--text-4)" }}>(optional)</span> Session ID <span style={{ color: "var(--text-4)" }}>(optional)</span>
</label> </Label>
<input <Input
value={fields.session_id} value={fields.session_id}
onChange={set("session_id")} onChange={set("session_id")}
placeholder="session_id" placeholder="session_id"
className="theme-input w-full text-sm px-3 py-2 rounded-lg"
/> />
</div> </div>
{error && ( {error && (
<p className="text-xs" style={{ color: "#f87171" }}> <p className="text-xs" style={{ color: COLOR.destructive }}>
{error} {error}
</p> </p>
)} )}
<div className="flex justify-end gap-2 pt-2"> <div className="flex justify-end gap-2 pt-2">
<button <Button
type="button" type="button"
variant="surface"
size="sm"
onClick={onClose} onClick={onClose}
className="px-3 py-1.5 text-sm rounded-lg"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
color: "var(--text-2)",
}}
> >
Cancel Cancel
</button> </Button>
<button <Button
type="submit" type="submit"
variant="accent"
size="sm"
disabled={loading} disabled={loading}
className="px-3 py-1.5 text-sm rounded-lg font-medium disabled:opacity-50"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
{loading ? "Creating..." : "Create"} {loading ? "Creating..." : "Create"}
</button> </Button>
</div> </div>
</form> </form>
</FormModal> </FormModal>

View File

@@ -12,6 +12,9 @@ import { JsonViewer } from "@/components/shared/JsonViewer";
import { PageLoader } from "@/components/shared/LoadingSpinner"; import { PageLoader } from "@/components/shared/LoadingSpinner";
import { MarkdownRenderer } from "@/components/shared/MarkdownRenderer"; import { MarkdownRenderer } from "@/components/shared/MarkdownRenderer";
import { PeerCardViewer } from "@/components/shared/PeerCardViewer"; import { PeerCardViewer } from "@/components/shared/PeerCardViewer";
import { Button } from "@/components/ui/button";
import { Input, Textarea } from "@/components/ui/input";
import { PageTitle, SectionHeading, Body, Muted, Caption } from "@/components/ui/typography";
import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { MessageCircle, Save, Search, User, X } from "lucide-react"; import { MessageCircle, Save, Search, User, X } from "lucide-react";
@@ -84,30 +87,25 @@ export function PeerDetail() {
<div> <div>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<User className="w-5 h-5" style={{ color: "var(--accent)" }} strokeWidth={1.5} /> <User className="w-5 h-5" style={{ color: "var(--accent)" }} strokeWidth={1.5} />
<h1 <PageTitle className="font-mono break-all">
className="text-xl font-semibold font-mono break-all tracking-tight"
style={{ color: "var(--text-1)" }}
>
{peerId} {peerId}
</h1> </PageTitle>
</div> </div>
<p className="text-sm" style={{ color: "var(--text-2)" }}> <Body className="leading-none">Peer identity &amp; memory</Body>
Peer identity &amp; memory
</p>
</div> </div>
<button <Button
variant="primary"
onClick={() => onClick={() =>
navigate({ navigate({
to: "/workspaces/$workspaceId/peers/$peerId/chat", to: "/workspaces/$workspaceId/peers/$peerId/chat",
params: { workspaceId, peerId } as never, params: { workspaceId, peerId } as never,
}) })
} }
className="shrink-0 flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all" className="shrink-0 rounded-xl"
style={{ background: "var(--accent)", color: "#fff" }}
> >
<MessageCircle className="w-4 h-4" strokeWidth={1.5} /> <MessageCircle className="w-4 h-4" strokeWidth={1.5} />
Chat Chat
</button> </Button>
</div> </div>
</motion.div> </motion.div>
@@ -153,16 +151,9 @@ export function PeerDetail() {
<PageLoader /> <PageLoader />
) : ( ) : (
<> <>
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>Peer Context</SectionHeading>
Peer Context
</h2>
{typeof context === "string" ? ( {typeof context === "string" ? (
<p <Body className="whitespace-pre-wrap">{context}</Body>
className="text-sm whitespace-pre-wrap leading-relaxed"
style={{ color: "var(--text-2)" }}
>
{context}
</p>
) : ( ) : (
<JsonViewer data={context} /> <JsonViewer data={context} />
)} )}
@@ -175,65 +166,50 @@ export function PeerDetail() {
) : ( ) : (
<> <>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium" style={{ color: "var(--text-1)" }}> <SectionHeading className="mb-0">Peer Card</SectionHeading>
Peer Card
</h2>
{cardDraft === null ? ( {cardDraft === null ? (
<button <Button
variant="accent"
size="sm"
onClick={() => setCardDraft(cardLines.join("\n"))} onClick={() => setCardDraft(cardLines.join("\n"))}
className="text-xs px-2 py-1 rounded-lg transition-colors"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
Edit Edit
</button> </Button>
) : ( ) : (
<div className="flex gap-1.5"> <div className="flex gap-1.5">
<button <Button
variant="accent"
size="sm"
onClick={() => { onClick={() => {
setPeerCard.mutate(cardDraft.split("\n").filter(Boolean)); setPeerCard.mutate(cardDraft.split("\n").filter(Boolean));
setCardDraft(null); setCardDraft(null);
}} }}
disabled={setPeerCard.isPending} disabled={setPeerCard.isPending}
className="flex items-center gap-1 text-xs px-2 py-1 rounded-lg disabled:opacity-50"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
<Save className="w-3 h-3" strokeWidth={2} /> <Save className="w-3 h-3" strokeWidth={2} />
Save Save
</button> </Button>
<button <Button
variant="surface"
size="sm"
onClick={() => setCardDraft(null)} onClick={() => setCardDraft(null)}
className="flex items-center gap-1 text-xs px-2 py-1 rounded-lg"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
color: "var(--text-3)",
}}
> >
<X className="w-3 h-3" strokeWidth={2} /> <X className="w-3 h-3" strokeWidth={2} />
</button> </Button>
</div> </div>
)} )}
</div> </div>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{cardDraft !== null ? ( {cardDraft !== null ? (
<motion.textarea <motion.div key="edit" initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
key="edit" <Textarea
initial={{ opacity: 0 }} value={cardDraft}
animate={{ opacity: 1 }} onChange={(e) => setCardDraft(e.target.value)}
value={cardDraft} rows={8}
onChange={(e) => setCardDraft(e.target.value)} className="font-mono resize-y"
rows={8} style={{ minHeight: "8rem" }}
className="theme-input w-full text-sm px-3 py-2 rounded-lg font-mono resize-y" />
style={{ minHeight: "8rem" }} </motion.div>
/>
) : ( ) : (
<motion.div key="view" initial={{ opacity: 0 }} animate={{ opacity: 1 }}> <motion.div key="view" initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<PeerCardViewer lines={cardLines} /> <PeerCardViewer lines={cardLines} />
@@ -248,9 +224,7 @@ export function PeerDetail() {
<PageLoader /> <PageLoader />
) : ( ) : (
<> <>
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>Memory Representation</SectionHeading>
Memory Representation
</h2>
{representation && {representation &&
typeof (representation as { representation?: unknown }).representation === typeof (representation as { representation?: unknown }).representation ===
"string" ? ( "string" ? (
@@ -265,10 +239,10 @@ export function PeerDetail() {
{tab === "search" && ( {tab === "search" && (
<> <>
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>
<Search className="w-3.5 h-3.5 inline mr-1.5" strokeWidth={2} /> <Search className="w-3.5 h-3.5 inline mr-1.5" strokeWidth={2} />
Search peer messages Search peer messages
</h2> </SectionHeading>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
@@ -276,25 +250,20 @@ export function PeerDetail() {
}} }}
className="flex gap-2 mb-4" className="flex gap-2 mb-4"
> >
<input <Input
autoFocus autoFocus
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Semantic search across this peer's messages…" placeholder="Semantic search across this peer's messages…"
className="theme-input flex-1 text-sm px-3 py-2 rounded-lg" className="flex-1"
/> />
<button <Button
type="submit" type="submit"
variant="accent"
disabled={searchPeer.isPending} disabled={searchPeer.isPending}
className="px-3 py-2 text-sm rounded-lg font-medium"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
{searchPeer.isPending ? "…" : "Search"} {searchPeer.isPending ? "…" : "Search"}
</button> </Button>
</form> </form>
{searchPeer.data && ( {searchPeer.data && (
<div className="space-y-3"> <div className="space-y-3">
@@ -306,9 +275,7 @@ export function PeerDetail() {
created_at?: string; created_at?: string;
}> }>
).length === 0 ? ( ).length === 0 ? (
<p className="text-sm" style={{ color: "var(--text-3)" }}> <Muted>No results.</Muted>
No results.
</p>
) : ( ) : (
( (
searchPeer.data as Array<{ searchPeer.data as Array<{
@@ -329,17 +296,10 @@ export function PeerDetail() {
<div className="flex items-center gap-2 mb-1.5"> <div className="flex items-center gap-2 mb-1.5">
<Badge variant="blue">{r.peer_id ?? peerId}</Badge> <Badge variant="blue">{r.peer_id ?? peerId}</Badge>
{r.created_at && ( {r.created_at && (
<span className="text-xs" style={{ color: "var(--text-4)" }}> <Caption>{new Date(r.created_at).toLocaleString()}</Caption>
{new Date(r.created_at).toLocaleString()}
</span>
)} )}
</div> </div>
<p <Body className="whitespace-pre-wrap">{r.content}</Body>
className="text-sm whitespace-pre-wrap"
style={{ color: "var(--text-2)" }}
>
{r.content}
</p>
</div> </div>
)) ))
)} )}
@@ -350,9 +310,7 @@ export function PeerDetail() {
{tab === "metadata" && ( {tab === "metadata" && (
<> <>
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>Peer Metadata</SectionHeading>
Peer Metadata
</h2>
<JsonViewer data={peer.metadata} maxHeight="400px" /> <JsonViewer data={peer.metadata} maxHeight="400px" />
</> </>
)} )}

View File

@@ -4,6 +4,8 @@ import { EmptyState } from "@/components/shared/EmptyState";
import { ErrorAlert } from "@/components/shared/ErrorAlert"; import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { PageLoader } from "@/components/shared/LoadingSpinner"; import { PageLoader } from "@/components/shared/LoadingSpinner";
import { Pagination } from "@/components/shared/Pagination"; import { Pagination } from "@/components/shared/Pagination";
import { PageTitle, MonoCaption } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { type Variants, motion } from "framer-motion"; import { type Variants, motion } from "framer-motion";
import { ArrowLeft, ChevronRight, Clock, Eye, Users } from "lucide-react"; import { ArrowLeft, ChevronRight, Clock, Eye, Users } from "lucide-react";
@@ -44,25 +46,21 @@ export function PeerList() {
</Link> </Link>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Users className="w-5 h-5" style={{ color: "#6366f1" }} strokeWidth={1.5} /> <Users className="w-5 h-5" style={{ color: "#6366f1" }} strokeWidth={1.5} />
<h1 className="text-xl font-semibold tracking-tight" style={{ color: "#e4e4f0" }}> <PageTitle>Peers</PageTitle>
Peers
</h1>
{total > 0 && ( {total > 0 && (
<span <span
className="ml-auto text-xs font-mono px-2 py-0.5 rounded-full" className="ml-auto text-xs font-mono px-2 py-0.5 rounded-full"
style={{ style={{
background: "rgba(99,102,241,0.1)", background: COLOR.accentSubtle,
color: "#818cf8", color: COLOR.accentText,
border: "1px solid rgba(99,102,241,0.2)", border: `1px solid ${COLOR.accentBorder}`,
}} }}
> >
{total} {total}
</span> </span>
)} )}
</div> </div>
<p className="text-xs font-mono mt-0.5" style={{ color: "rgba(148,163,184,0.4)" }}> <MonoCaption className="mt-0.5" as="p">{workspaceId}</MonoCaption>
{workspaceId}
</p>
</motion.div> </motion.div>
<ErrorAlert error={error instanceof Error ? error : null} /> <ErrorAlert error={error instanceof Error ? error : null} />
@@ -120,8 +118,8 @@ export function PeerList() {
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
{(peer.configuration as { observe_me?: boolean } | null)?.observe_me && ( {(peer.configuration as { observe_me?: boolean } | null)?.observe_me && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Eye className="w-3 h-3" style={{ color: "#818cf8" }} strokeWidth={1.5} /> <Eye className="w-3 h-3" style={{ color: COLOR.accentText }} strokeWidth={1.5} />
<span className="text-xs" style={{ color: "#818cf8" }}> <span className="text-xs" style={{ color: COLOR.accentText }}>
observed observed
</span> </span>
</div> </div>
@@ -133,9 +131,7 @@ export function PeerList() {
style={{ color: "rgba(148,163,184,0.3)" }} style={{ color: "rgba(148,163,184,0.3)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<p className="text-xs font-mono" style={{ color: "rgba(148,163,184,0.3)" }}> <MonoCaption>{new Date(peer.created_at).toLocaleString()}</MonoCaption>
{new Date(peer.created_at).toLocaleString()}
</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -16,6 +16,9 @@ import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
import { JsonViewer } from "@/components/shared/JsonViewer"; import { JsonViewer } from "@/components/shared/JsonViewer";
import { PageLoader } from "@/components/shared/LoadingSpinner"; import { PageLoader } from "@/components/shared/LoadingSpinner";
import { Pagination } from "@/components/shared/Pagination"; import { Pagination } from "@/components/shared/Pagination";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { PageTitle, SectionHeading, Body, Muted, Caption, MonoCaption } from "@/components/ui/typography";
import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { AlignLeft, Clock, Copy, MessageSquare, Search, Trash2, Users, X } from "lucide-react"; import { AlignLeft, Clock, Copy, MessageSquare, Search, Trash2, Users, X } from "lucide-react";
@@ -124,53 +127,39 @@ export function SessionDetail() {
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<h1 <PageTitle className="font-mono break-all">
className="text-xl font-semibold font-mono break-all tracking-tight"
style={{ color: "var(--text-1)" }}
>
{sessionId} {sessionId}
</h1> </PageTitle>
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<button <Button
variant={searchActive ? "accent" : "surface"}
size="icon"
onClick={() => setSearchActive((v) => !v)} onClick={() => setSearchActive((v) => !v)}
className="p-1.5 rounded-lg transition-colors" aria-label="Search session"
style={{
background: searchActive ? "var(--accent-dim)" : "var(--surface)",
border: `1px solid ${searchActive ? "var(--accent-border)" : "var(--border)"}`,
color: searchActive ? "var(--accent-text)" : "var(--text-3)",
}}
> >
<Search className="w-3.5 h-3.5" strokeWidth={2} /> <Search className="w-3.5 h-3.5" strokeWidth={2} />
</button> </Button>
<button <Button
variant="surface"
size="icon"
onClick={handleClone} onClick={handleClone}
disabled={cloneSession.isPending} disabled={cloneSession.isPending}
className="p-1.5 rounded-lg transition-colors disabled:opacity-50" aria-label="Clone session"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
color: "var(--text-3)",
}}
> >
<Copy className="w-3.5 h-3.5" strokeWidth={2} /> <Copy className="w-3.5 h-3.5" strokeWidth={2} />
</button> </Button>
<button <Button
variant="destructive"
size="icon"
onClick={() => setConfirmDelete(true)} onClick={() => setConfirmDelete(true)}
className="p-1.5 rounded-lg transition-colors" aria-label="Delete session"
style={{
background: "rgba(239,68,68,0.08)",
border: "1px solid rgba(239,68,68,0.2)",
color: "#f87171",
}}
> >
<Trash2 className="w-3.5 h-3.5" strokeWidth={2} /> <Trash2 className="w-3.5 h-3.5" strokeWidth={2} />
</button> </Button>
</div> </div>
</div> </div>
<p className="text-sm" style={{ color: "var(--text-2)" }}> <Body className="leading-none">Session detail</Body>
Session detail
</p>
</motion.div> </motion.div>
{/* Inline search bar */} {/* Inline search bar */}
@@ -189,33 +178,26 @@ export function SessionDetail() {
}} }}
className="flex gap-2" className="flex gap-2"
> >
<input <Input
autoFocus autoFocus
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search within this session…" placeholder="Search within this session…"
className="theme-input flex-1 text-sm px-3 py-2 rounded-lg" className="flex-1"
/> />
<button <Button
type="submit" type="submit"
variant="accent"
disabled={searchSession.isPending} disabled={searchSession.isPending}
className="px-3 py-2 text-sm rounded-lg font-medium"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
{searchSession.isPending ? "…" : "Search"} {searchSession.isPending ? "…" : "Search"}
</button> </Button>
</form> </form>
{searchSession.data && ( {searchSession.data && (
<div className="mt-3 rounded-xl p-4 theme-card space-y-2"> <div className="mt-3 rounded-xl p-4 theme-card space-y-2">
{(searchSession.data as Array<{ id: string; content: string; peer_id?: string }>) {(searchSession.data as Array<{ id: string; content: string; peer_id?: string }>)
.length === 0 ? ( .length === 0 ? (
<p className="text-sm" style={{ color: "var(--text-3)" }}> <Muted>No results.</Muted>
No results.
</p>
) : ( ) : (
( (
searchSession.data as Array<{ id: string; content: string; peer_id?: string }> searchSession.data as Array<{ id: string; content: string; peer_id?: string }>
@@ -275,9 +257,7 @@ export function SessionDetail() {
) : ( ) : (
<div> <div>
{messages.length === 0 ? ( {messages.length === 0 ? (
<p className="text-sm" style={{ color: "var(--text-3)" }}> <Muted>No messages.</Muted>
No messages.
</p>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{messages.map((msg) => ( {messages.map((msg) => (
@@ -291,22 +271,13 @@ export function SessionDetail() {
{msg.peer_id ?? "system"} {msg.peer_id ?? "system"}
</Badge> </Badge>
{msg.token_count != null && ( {msg.token_count != null && (
<span className="text-xs" style={{ color: "var(--text-4)" }}> <Caption>{msg.token_count} tokens</Caption>
{msg.token_count} tokens
</span>
)} )}
{msg.created_at && ( {msg.created_at && (
<span className="text-xs" style={{ color: "var(--text-4)" }}> <Caption>{new Date(msg.created_at).toLocaleString()}</Caption>
{new Date(msg.created_at).toLocaleString()}
</span>
)} )}
</div> </div>
<p <Body className="whitespace-pre-wrap">{msg.content}</Body>
className="text-sm whitespace-pre-wrap leading-relaxed"
style={{ color: "var(--text-2)" }}
>
{msg.content}
</p>
</div> </div>
))} ))}
</div> </div>
@@ -323,16 +294,9 @@ export function SessionDetail() {
<PageLoader /> <PageLoader />
) : ( ) : (
<> <>
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>Session Context</SectionHeading>
Session Context
</h2>
{typeof context === "string" ? ( {typeof context === "string" ? (
<p <Body className="whitespace-pre-wrap">{context}</Body>
className="text-sm whitespace-pre-wrap leading-relaxed"
style={{ color: "var(--text-2)" }}
>
{context}
</p>
) : ( ) : (
<JsonViewer data={context} maxHeight="500px" /> <JsonViewer data={context} maxHeight="500px" />
)} )}
@@ -388,14 +352,12 @@ function SessionPeersTab({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h2 className="text-sm font-medium mb-2" style={{ color: "var(--text-1)" }}> <SectionHeading className="mb-2">
<Users className="w-3.5 h-3.5 inline mr-1.5" strokeWidth={2} /> <Users className="w-3.5 h-3.5 inline mr-1.5" strokeWidth={2} />
Session members ({list.length}) Session members ({list.length})
</h2> </SectionHeading>
{list.length === 0 ? ( {list.length === 0 ? (
<p className="text-sm" style={{ color: "var(--text-3)" }}> <Muted>No peers in this session.</Muted>
No peers in this session.
</p>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{list.map((p) => { {list.map((p) => {
@@ -409,14 +371,15 @@ function SessionPeersTab({
<span className="text-xs font-mono" style={{ color: "var(--accent-text)" }}> <span className="text-xs font-mono" style={{ color: "var(--accent-text)" }}>
{id} {id}
</span> </span>
<button <Button
variant="ghost"
size="icon"
onClick={() => onRemove(id)} onClick={() => onRemove(id)}
disabled={removing} disabled={removing}
className="p-1 rounded transition-colors disabled:opacity-40" aria-label={`Remove ${id}`}
style={{ color: "var(--text-4)" }}
> >
<X className="w-3 h-3" strokeWidth={2} /> <X className="w-3 h-3" strokeWidth={2} />
</button> </Button>
</div> </div>
); );
})} })}
@@ -426,9 +389,7 @@ function SessionPeersTab({
{available.length > 0 && ( {available.length > 0 && (
<div> <div>
<h2 className="text-sm font-medium mb-2" style={{ color: "var(--text-2)" }}> <SectionHeading className="mb-2">Add peer</SectionHeading>
Add peer
</h2>
<div className="space-y-1 max-h-48 overflow-auto"> <div className="space-y-1 max-h-48 overflow-auto">
{available.map((p) => ( {available.map((p) => (
<button <button
@@ -467,23 +428,17 @@ function SummaryCard({ label, summary }: { label: string; summary: Summary }) {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{summary.token_count != null && ( {summary.token_count != null && (
<span className="text-xs font-mono" style={{ color: "var(--text-4)" }}> <MonoCaption>{summary.token_count} tok</MonoCaption>
{summary.token_count} tok
</span>
)} )}
{summary.created_at && ( {summary.created_at && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Clock className="w-3 h-3" style={{ color: "var(--text-4)" }} strokeWidth={1.5} /> <Clock className="w-3 h-3" style={{ color: "var(--text-4)" }} strokeWidth={1.5} />
<span className="text-xs font-mono" style={{ color: "var(--text-4)" }}> <MonoCaption>{new Date(summary.created_at).toLocaleString()}</MonoCaption>
{new Date(summary.created_at).toLocaleString()}
</span>
</div> </div>
)} )}
</div> </div>
</div> </div>
<p className="text-sm leading-relaxed whitespace-pre-wrap" style={{ color: "var(--text-2)" }}> <Body className="whitespace-pre-wrap">{summary.content}</Body>
{summary.content}
</p>
</div> </div>
); );
} }
@@ -494,21 +449,15 @@ function SummariesDisplay({ summaries }: { summaries: unknown }) {
if (!data || (!data.short_summary && !data.long_summary)) { if (!data || (!data.short_summary && !data.long_summary)) {
return ( return (
<> <>
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>Session Summaries</SectionHeading>
Session Summaries <Caption as="p">No summaries available yet.</Caption>
</h2>
<p className="text-sm" style={{ color: "var(--text-4)" }}>
No summaries available yet.
</p>
</> </>
); );
} }
return ( return (
<> <>
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>Session Summaries</SectionHeading>
Session Summaries
</h2>
<div className="space-y-3"> <div className="space-y-3">
{data.short_summary && <SummaryCard label="Short summary" summary={data.short_summary} />} {data.short_summary && <SummaryCard label="Short summary" summary={data.short_summary} />}
{data.long_summary && <SummaryCard label="Long summary" summary={data.long_summary} />} {data.long_summary && <SummaryCard label="Long summary" summary={data.long_summary} />}

View File

@@ -4,6 +4,8 @@ import { EmptyState } from "@/components/shared/EmptyState";
import { ErrorAlert } from "@/components/shared/ErrorAlert"; import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { PageLoader } from "@/components/shared/LoadingSpinner"; import { PageLoader } from "@/components/shared/LoadingSpinner";
import { Pagination } from "@/components/shared/Pagination"; import { Pagination } from "@/components/shared/Pagination";
import { PageTitle, MonoCaption } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { type Variants, motion } from "framer-motion"; import { type Variants, motion } from "framer-motion";
import { ArrowLeft, ChevronRight, CircleDot, Clock, MessageSquare } from "lucide-react"; import { ArrowLeft, ChevronRight, CircleDot, Clock, MessageSquare } from "lucide-react";
@@ -44,25 +46,21 @@ export function SessionList() {
</Link> </Link>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<MessageSquare className="w-5 h-5" style={{ color: "#6366f1" }} strokeWidth={1.5} /> <MessageSquare className="w-5 h-5" style={{ color: "#6366f1" }} strokeWidth={1.5} />
<h1 className="text-xl font-semibold tracking-tight" style={{ color: "#e4e4f0" }}> <PageTitle>Sessions</PageTitle>
Sessions
</h1>
{total > 0 && ( {total > 0 && (
<span <span
className="ml-auto text-xs font-mono px-2 py-0.5 rounded-full" className="ml-auto text-xs font-mono px-2 py-0.5 rounded-full"
style={{ style={{
background: "rgba(99,102,241,0.1)", background: COLOR.accentSubtle,
color: "#818cf8", color: COLOR.accentText,
border: "1px solid rgba(99,102,241,0.2)", border: `1px solid ${COLOR.accentBorder}`,
}} }}
> >
{total} {total}
</span> </span>
)} )}
</div> </div>
<p className="text-xs font-mono mt-0.5" style={{ color: "rgba(148,163,184,0.4)" }}> <MonoCaption className="mt-0.5" as="p">{workspaceId}</MonoCaption>
{workspaceId}
</p>
</motion.div> </motion.div>
<ErrorAlert error={error instanceof Error ? error : null} /> <ErrorAlert error={error instanceof Error ? error : null} />
@@ -116,11 +114,11 @@ export function SessionList() {
> >
<CircleDot <CircleDot
className="w-3 h-3" className="w-3 h-3"
style={{ color: "#34d399" }} style={{ color: COLOR.success }}
strokeWidth={2} strokeWidth={2}
/> />
</motion.div> </motion.div>
<span className="text-xs" style={{ color: "#34d399" }}> <span className="text-xs" style={{ color: COLOR.success }}>
Active Active
</span> </span>
</div> </div>
@@ -140,17 +138,15 @@ export function SessionList() {
style={{ color: "rgba(148,163,184,0.3)" }} style={{ color: "rgba(148,163,184,0.3)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<p className="text-xs font-mono" style={{ color: "rgba(148,163,184,0.3)" }}> <MonoCaption>{new Date(session.created_at).toLocaleString()}</MonoCaption>
{new Date(session.created_at).toLocaleString()}
</p>
</div> </div>
)} )}
{(session.metadata as Record<string, string> | null)?.source && ( {(session.metadata as Record<string, string> | null)?.source && (
<span <span
className="text-xs font-mono px-1.5 py-0.5 rounded" className="text-xs font-mono px-1.5 py-0.5 rounded"
style={{ style={{
background: "rgba(99,102,241,0.08)", background: COLOR.accentDim,
border: "1px solid rgba(99,102,241,0.15)", border: `1px solid ${COLOR.accentBorderStrong}`,
color: "rgba(148,163,184,0.6)", color: "rgba(148,163,184,0.6)",
}} }}
> >

View File

@@ -9,16 +9,21 @@ import {
type Config, type Config,
type HealthStatus, type HealthStatus,
} from "@/lib/config"; } from "@/lib/config";
import { Button } from "@/components/ui/button";
import { Input, Textarea } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Muted } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
interface SettingsFormProps { interface SettingsFormProps {
onSaved?: () => void; onSaved?: () => void;
} }
const statusConfig = { const statusConfig = {
ok: { icon: CheckCircle, color: "#34d399", label: "Connected" }, ok: { icon: CheckCircle, color: COLOR.success, label: "Connected" },
"auth-required": { icon: AlertCircle, color: "#f59e0b", label: "Auth required" }, "auth-required": { icon: AlertCircle, color: COLOR.warning, label: "Auth required" },
unreachable: { icon: WifiOff, color: "#f87171", label: "Unreachable" }, unreachable: { icon: WifiOff, color: COLOR.destructive, label: "Unreachable" },
checking: { icon: Loader, color: "#818cf8", label: "Checking..." }, checking: { icon: Loader, color: COLOR.accentText, label: "Checking..." },
}; };
export function SettingsForm({ onSaved }: SettingsFormProps) { export function SettingsForm({ onSaved }: SettingsFormProps) {
@@ -37,7 +42,6 @@ export function SettingsForm({ onSaved }: SettingsFormProps) {
setHealth(result); setHealth(result);
setChecking(false); setChecking(false);
// Auto-show token field if auth is required
if (result.status === "auth-required" && !token) { if (result.status === "auth-required" && !token) {
document.getElementById("honcho-token")?.focus(); document.getElementById("honcho-token")?.focus();
} }
@@ -77,37 +81,23 @@ export function SettingsForm({ onSaved }: SettingsFormProps) {
> >
{/* Base URL */} {/* Base URL */}
<div> <div>
<label <Label className="mb-1.5 text-sm">
className="block text-sm font-medium mb-1.5"
style={{ color: "var(--text-1)" }}
>
Honcho Base URL Honcho Base URL
</label> </Label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <Input
type="url" type="url"
value={baseUrl} value={baseUrl}
onChange={(e) => { setBaseUrl(e.target.value); setHealth(null); }} onChange={(e) => { setBaseUrl(e.target.value); setHealth(null); }}
placeholder="http://localhost:8000" placeholder="http://localhost:8000"
className="flex-1 px-3 py-2 text-sm font-mono rounded-xl outline-none transition-all" className="flex-1 font-mono rounded-xl"
style={{
background: "var(--surface)",
border: "1px solid var(--border-2)",
color: "var(--text-1)",
}}
onFocus={(e) => { e.target.style.borderColor = "var(--accent)"; }}
onBlur={(e) => { e.target.style.borderColor = "var(--border-2)"; }}
/> />
<button <Button
type="button" type="button"
variant="accent"
onClick={handleTest} onClick={handleTest}
disabled={checking || !baseUrl} disabled={checking || !baseUrl}
className="px-3 py-2 rounded-xl text-sm font-medium flex items-center gap-1.5 transition-all disabled:opacity-40" className="rounded-xl"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
{checking ? ( {checking ? (
<motion.div animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }}> <motion.div animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }}>
@@ -117,14 +107,14 @@ export function SettingsForm({ onSaved }: SettingsFormProps) {
<Wifi className="w-4 h-4" strokeWidth={1.5} /> <Wifi className="w-4 h-4" strokeWidth={1.5} />
)} )}
<span className="hidden sm:block">Test</span> <span className="hidden sm:block">Test</span>
</button> </Button>
</div> </div>
{errors.baseUrl && ( {errors.baseUrl && (
<p className="text-xs mt-1" style={{ color: "#f87171" }}>{errors.baseUrl}</p> <p className="text-xs mt-1" style={{ color: COLOR.destructive }}>{errors.baseUrl}</p>
)} )}
<p className="text-xs mt-1.5" style={{ color: "var(--text-3)" }}> <Muted className="text-xs mt-1.5">
URL of your self-hosted Honcho instance URL of your self-hosted Honcho instance
</p> </Muted>
</div> </div>
{/* Health status */} {/* Health status */}
@@ -154,9 +144,9 @@ export function SettingsForm({ onSaved }: SettingsFormProps) {
<p className="text-sm font-medium" style={{ color: statusConfig[health.status].color }}> <p className="text-sm font-medium" style={{ color: statusConfig[health.status].color }}>
{statusConfig[health.status].label} {statusConfig[health.status].label}
</p> </p>
<p className="text-xs mt-0.5" style={{ color: "var(--text-3)" }}> <Muted className="text-xs mt-0.5">
{health.message} {health.message}
</p> </Muted>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@@ -165,10 +155,9 @@ export function SettingsForm({ onSaved }: SettingsFormProps) {
{/* Token */} {/* Token */}
<div> <div>
<label <Label
htmlFor="honcho-token" htmlFor="honcho-token"
className="flex items-center gap-1.5 text-sm font-medium mb-1.5" className="flex items-center gap-1.5 mb-1.5 text-sm"
style={{ color: "var(--text-1)" }}
> >
{token ? ( {token ? (
<Lock className="w-3.5 h-3.5" style={{ color: "var(--accent)" }} strokeWidth={1.5} /> <Lock className="w-3.5 h-3.5" style={{ color: "var(--accent)" }} strokeWidth={1.5} />
@@ -186,44 +175,35 @@ export function SettingsForm({ onSaved }: SettingsFormProps) {
> >
optional optional
</span> </span>
</label> </Label>
<textarea <Textarea
id="honcho-token" id="honcho-token"
value={token} value={token}
onChange={(e) => setToken(e.target.value)} onChange={(e) => setToken(e.target.value)}
rows={2} rows={2}
placeholder="eyJ... (required only if your instance has auth enabled)" placeholder="eyJ... (required only if your instance has auth enabled)"
className="w-full px-3 py-2.5 text-sm rounded-xl font-mono resize-none outline-none transition-all" className="font-mono rounded-xl"
style={{
background: "var(--surface)",
border: "1px solid var(--border-2)",
color: "var(--text-1)",
}}
onFocus={(e) => { e.target.style.borderColor = "var(--accent)"; }}
onBlur={(e) => { e.target.style.borderColor = "var(--border-2)"; }}
/> />
{health?.status === "auth-required" && !token && ( {health?.status === "auth-required" && !token && (
<motion.p <motion.p
initial={{ opacity: 0, y: -4 }} initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="text-xs mt-1" className="text-xs mt-1"
style={{ color: "#f59e0b" }} style={{ color: COLOR.warning }}
> >
This instance requires an API token to proceed This instance requires an API token to proceed
</motion.p> </motion.p>
)} )}
</div> </div>
<button <Button
type="submit" type="submit"
className="w-full py-2.5 px-4 rounded-xl text-sm font-medium transition-all" variant="primary"
style={{ className="w-full py-2.5 px-4 rounded-xl"
background: saved ? "#059669" : "var(--accent)", style={saved ? { background: "#059669" } : undefined}
color: "#fff",
}}
> >
{saved ? "✓ Saved" : "Save Connection"} {saved ? "✓ Saved" : "Save Connection"}
</button> </Button>
</form> </form>
); );
} }

View File

@@ -1,6 +1,13 @@
import { useEffect, useRef } from "react"; import { Button } from "@/components/ui/button";
import { motion, AnimatePresence } from "framer-motion"; import {
import { AlertTriangle, X } from "lucide-react"; Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
} from "@/components/ui/dialog";
import { COLOR } from "@/lib/constants";
import { AlertTriangle } from "lucide-react";
interface ConfirmDialogProps { interface ConfirmDialogProps {
open: boolean; open: boolean;
@@ -23,102 +30,41 @@ export function ConfirmDialog({
danger = true, danger = true,
loading = false, loading = false,
}: ConfirmDialogProps) { }: ConfirmDialogProps) {
const cancelRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (open) cancelRef.current?.focus();
}, [open]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) onCancel();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [open, onCancel]);
return ( return (
<AnimatePresence> <Dialog open={open} onOpenChange={(o) => !o && onCancel()}>
{open && ( <DialogContent className="max-w-sm">
<motion.div <div className="flex items-start gap-3 mb-4">
initial={{ opacity: 0 }} {danger && (
animate={{ opacity: 1 }} <div
exit={{ opacity: 0 }} className="w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0 mt-0.5"
className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: COLOR.destructiveDim }}
style={{ background: "rgba(0,0,0,0.6)", backdropFilter: "blur(4px)" }}
onClick={(e) => e.target === e.currentTarget && onCancel()}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 8 }}
transition={{ type: "spring", stiffness: 300, damping: 28 }}
className="w-full max-w-sm rounded-2xl p-6 relative"
style={{
background: "var(--bg-2)",
border: "1px solid var(--border-2)",
boxShadow: "0 24px 64px rgba(0,0,0,0.4)",
}}
>
<button
onClick={onCancel}
className="absolute top-4 right-4 p-1 rounded-lg transition-colors"
style={{ color: "var(--text-4)" }}
> >
<X className="w-4 h-4" /> <AlertTriangle
</button> className="w-4 h-4"
style={{ color: COLOR.destructive }}
<div className="flex items-start gap-3 mb-4"> strokeWidth={2}
{danger && ( />
<div
className="w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0 mt-0.5"
style={{ background: "rgba(239,68,68,0.1)" }}
>
<AlertTriangle className="w-4 h-4" style={{ color: "#f87171" }} strokeWidth={2} />
</div>
)}
<div>
<h3
className="text-sm font-semibold mb-1"
style={{ color: "var(--text-1)" }}
>
{title}
</h3>
<p className="text-sm" style={{ color: "var(--text-3)" }}>
{description}
</p>
</div>
</div> </div>
)}
<div className="flex gap-2 justify-end mt-6"> <div>
<button <DialogTitle>{title}</DialogTitle>
ref={cancelRef} <DialogDescription className="mt-1">{description}</DialogDescription>
onClick={onCancel} </div>
className="px-3 py-1.5 text-sm rounded-lg transition-colors" </div>
style={{ <DialogFooter>
background: "var(--surface)", <Button variant="surface" size="sm" onClick={onCancel}>
border: "1px solid var(--border)", Cancel
color: "var(--text-2)", </Button>
}} <Button
> variant={danger ? "destructive" : "accent"}
Cancel size="sm"
</button> onClick={onConfirm}
<button disabled={loading}
onClick={onConfirm} >
disabled={loading} {loading ? "..." : confirmLabel}
className="px-3 py-1.5 text-sm rounded-lg font-medium transition-colors disabled:opacity-50" </Button>
style={ </DialogFooter>
danger </DialogContent>
? { background: "rgba(239,68,68,0.15)", color: "#f87171", border: "1px solid rgba(239,68,68,0.3)" } </Dialog>
: { background: "var(--accent-dim)", color: "var(--accent-text)", border: "1px solid var(--accent-border)" }
}
>
{loading ? "..." : confirmLabel}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
); );
} }

View File

@@ -1,5 +1,7 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import { Body, Caption } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
interface EmptyStateProps { interface EmptyStateProps {
icon?: LucideIcon; icon?: LucideIcon;
@@ -20,16 +22,16 @@ export function EmptyState({ icon: Icon, title, description, action }: EmptyStat
<div <div
className="w-12 h-12 rounded-xl flex items-center justify-center mb-4" className="w-12 h-12 rounded-xl flex items-center justify-center mb-4"
style={{ style={{
background: "rgba(99,102,241,0.08)", background: COLOR.accentSubtle,
border: "1px solid rgba(99,102,241,0.15)", border: `1px solid ${COLOR.accentBorderStrong}`,
}} }}
> >
<Icon className="w-5 h-5" style={{ color: "rgba(99,102,241,0.6)" }} strokeWidth={1.5} /> <Icon className="w-5 h-5" style={{ color: "rgba(99,102,241,0.6)" }} strokeWidth={1.5} />
</div> </div>
)} )}
<p className="text-zinc-300 font-medium text-sm">{title}</p> <Body className="font-medium">{title}</Body>
{description && ( {description && (
<p className="text-zinc-600 text-xs mt-1.5 max-w-xs leading-relaxed">{description}</p> <Caption className="mt-1.5 max-w-xs leading-relaxed">{description}</Caption>
)} )}
{action && <div className="mt-4">{action}</div>} {action && <div className="mt-4">{action}</div>}
</motion.div> </motion.div>

View File

@@ -1,6 +1,10 @@
import { useEffect } from "react"; import {
import { motion, AnimatePresence } from "framer-motion"; Dialog,
import { X } from "lucide-react"; DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
interface FormModalProps { interface FormModalProps {
open: boolean; open: boolean;
@@ -17,56 +21,14 @@ export function FormModal({
children, children,
maxWidth = "max-w-md", maxWidth = "max-w-md",
}: FormModalProps) { }: FormModalProps) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) onClose();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [open, onClose]);
return ( return (
<AnimatePresence> <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
{open && ( <DialogContent className={cn("p-0", maxWidth)}>
<motion.div <DialogHeader className="px-5 py-4 mb-0">
initial={{ opacity: 0 }} <DialogTitle>{title}</DialogTitle>
animate={{ opacity: 1 }} </DialogHeader>
exit={{ opacity: 0 }} <div className="px-5 pb-5">{children}</div>
className="fixed inset-0 z-50 flex items-center justify-center p-4" </DialogContent>
style={{ background: "rgba(0,0,0,0.6)", backdropFilter: "blur(4px)" }} </Dialog>
onClick={(e) => e.target === e.currentTarget && onClose()}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 12 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 12 }}
transition={{ type: "spring", stiffness: 300, damping: 28 }}
className={`w-full ${maxWidth} rounded-2xl relative`}
style={{
background: "var(--bg-2)",
border: "1px solid var(--border-2)",
boxShadow: "0 24px 64px rgba(0,0,0,0.4)",
}}
>
<div
className="flex items-center justify-between px-5 py-4"
style={{ borderBottom: "1px solid var(--border)" }}
>
<h3 className="text-sm font-semibold" style={{ color: "var(--text-1)" }}>
{title}
</h3>
<button
onClick={onClose}
className="p-1 rounded-lg transition-colors"
style={{ color: "var(--text-4)" }}
>
<X className="w-4 h-4" />
</button>
</div>
<div className="p-5">{children}</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
); );
} }

View File

@@ -1,3 +1,6 @@
import { Button } from "@/components/ui/button";
import { MonoCaption } from "@/components/ui/typography";
interface PaginationProps { interface PaginationProps {
page: number; page: number;
totalPages: number; totalPages: number;
@@ -9,33 +12,23 @@ export function Pagination({ page, totalPages, onPageChange }: PaginationProps)
return ( return (
<div className="flex items-center gap-2 mt-6"> <div className="flex items-center gap-2 mt-6">
<button <Button
variant="surface"
size="sm"
onClick={() => onPageChange(page - 1)} onClick={() => onPageChange(page - 1)}
disabled={page <= 1} disabled={page <= 1}
className="px-3 py-1.5 text-sm rounded-lg disabled:opacity-30 transition-colors"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
color: "var(--text-2)",
}}
> >
Previous Previous
</button> </Button>
<span className="text-xs font-mono px-2" style={{ color: "var(--text-3)" }}> <MonoCaption className="px-2">{page} / {totalPages}</MonoCaption>
{page} / {totalPages} <Button
</span> variant="surface"
<button size="sm"
onClick={() => onPageChange(page + 1)} onClick={() => onPageChange(page + 1)}
disabled={page >= totalPages} disabled={page >= totalPages}
className="px-3 py-1.5 text-sm rounded-lg disabled:opacity-30 transition-colors"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
color: "var(--text-2)",
}}
> >
Next Next
</button> </Button>
</div> </div>
); );
} }

View File

@@ -0,0 +1,70 @@
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import { forwardRef } from "react";
const buttonVariants = cva(
[
"inline-flex items-center justify-center gap-1.5 rounded-lg font-medium transition-all",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-1)]",
"disabled:opacity-50 disabled:pointer-events-none",
],
{
variants: {
variant: {
primary: [
"text-white",
"[background:var(--accent)]",
"focus-visible:ring-[var(--accent)]",
],
accent: [
"[background:var(--accent-dim)] [color:var(--accent-text)]",
"[border:1px_solid_var(--accent-border)]",
"focus-visible:ring-[var(--accent)]",
],
surface: [
"[background:var(--surface)] [color:var(--text-2)]",
"[border:1px_solid_var(--border)]",
"focus-visible:ring-[var(--border)]",
],
ghost: [
"[color:var(--text-3)]",
"hover:[background:var(--surface)]",
"focus-visible:ring-[var(--border)]",
],
destructive: [
"bg-[rgba(239,68,68,0.08)] text-[#f87171]",
"border border-[rgba(239,68,68,0.2)]",
"focus-visible:ring-[#f87171]",
],
},
size: {
default: "px-4 py-2 text-sm",
sm: "px-3 py-1.5 text-xs",
icon: "p-1.5 text-sm",
},
},
defaultVariants: {
variant: "accent",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />
);
},
);
Button.displayName = "Button";
export { buttonVariants };

View File

@@ -0,0 +1,97 @@
import { cn } from "@/lib/utils";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { forwardRef } from "react";
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogPortal = DialogPrimitive.Portal;
export const DialogClose = DialogPrimitive.Close;
export const DialogOverlay = forwardRef<
React.ComponentRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 backdrop-blur-sm",
"bg-black/60",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
export const DialogContent = forwardRef<
React.ComponentRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2",
"rounded-2xl p-6 shadow-2xl",
"[background:var(--bg-2)] [border:1px_solid_var(--border-2)]",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"data-[state=closed]:slide-out-to-top-[2%] data-[state=open]:slide-in-from-top-[2%]",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
className={cn(
"absolute right-4 top-4 rounded-lg p-1 transition-colors",
"[color:var(--text-4)] hover:[color:var(--text-2)]",
"focus:outline-none focus:ring-2 focus:ring-[var(--accent)] focus:ring-offset-2 focus:ring-offset-[var(--bg-2)]",
)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
export function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex flex-col gap-1.5 pb-4 mb-4 [border-bottom:1px_solid_var(--border)]", className)} {...props} />;
}
export function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("flex justify-end gap-2 pt-4 mt-4", className)} {...props} />;
}
export const DialogTitle = forwardRef<
React.ComponentRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
style={{ color: "var(--text-1)" }}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
export const DialogDescription = forwardRef<
React.ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm", className)}
style={{ color: "var(--text-3)" }}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

View File

@@ -0,0 +1,42 @@
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
export const Input = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => (
<input
ref={ref}
className={cn(
"flex w-full rounded-lg px-3 py-2 text-sm transition-all outline-none",
"[background:var(--surface)] [color:var(--text-1)]",
"[border:1px_solid_var(--border-2)]",
"placeholder:[color:var(--text-4)]",
"focus:[border-color:var(--accent)]",
"disabled:opacity-50 disabled:cursor-not-allowed",
className,
)}
{...props}
/>
));
Input.displayName = "Input";
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => (
<textarea
ref={ref}
className={cn(
"flex w-full rounded-lg px-3 py-2 text-sm transition-all outline-none resize-none",
"[background:var(--surface)] [color:var(--text-1)]",
"[border:1px_solid_var(--border-2)]",
"placeholder:[color:var(--text-4)]",
"focus:[border-color:var(--accent)]",
"disabled:opacity-50 disabled:cursor-not-allowed",
className,
)}
{...props}
/>
),
);
Textarea.displayName = "Textarea";

View File

@@ -0,0 +1,20 @@
import { cn } from "@/lib/utils";
import * as LabelPrimitive from "@radix-ui/react-label";
import { forwardRef } from "react";
export const Label = forwardRef<
React.ComponentRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
"block text-xs font-medium leading-none",
"peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className,
)}
style={{ color: "var(--text-2)" }}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;

View File

@@ -0,0 +1,21 @@
import { cn } from "@/lib/utils";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { forwardRef } from "react";
export const Separator = forwardRef<
React.ComponentRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 [background:var(--border)]",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;

View File

@@ -0,0 +1,80 @@
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
export const Table = forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="w-full overflow-x-auto">
<table ref={ref} className={cn("w-full text-xs caption-bottom", className)} {...props} />
</div>
),
);
Table.displayName = "Table";
export const TableHeader = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead
ref={ref}
className={cn("[&_tr]:border-b [&_tr]:[border-color:var(--border)]", className)}
{...props}
/>
));
TableHeader.displayName = "TableHeader";
export const TableBody = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
export const TableRow = forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors [border-color:var(--border)]",
"hover:[background:var(--surface)]",
"data-[state=selected]:[background:var(--surface)]",
className,
)}
{...props}
/>
),
);
TableRow.displayName = "TableRow";
export const TableHead = forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-9 px-4 text-left align-middle font-medium [background:var(--bg-3)]",
"[color:var(--text-3)]",
"[&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
export const TableCell = forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("px-4 py-2.5 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";

View File

@@ -0,0 +1,31 @@
import { cn } from "@/lib/utils";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
export const TooltipProvider = TooltipPrimitive.Provider;
export const Tooltip = TooltipPrimitive.Root;
export const TooltipTrigger = TooltipPrimitive.Trigger;
export function TooltipContent({
className,
sideOffset = 4,
...props
}: React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-lg px-2.5 py-1.5 text-xs font-medium",
"[background:var(--bg-3)] [color:var(--text-1)]",
"[border:1px_solid_var(--border)]",
"shadow-md",
"animate-in fade-in-0 zoom-in-95",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</TooltipPrimitive.Portal>
);
}

View File

@@ -0,0 +1,110 @@
import { cn } from "@/lib/utils";
type AsChild<T extends React.ElementType> = {
as?: T;
className?: string;
children?: React.ReactNode;
};
type Props<T extends React.ElementType> = AsChild<T> &
Omit<React.ComponentPropsWithoutRef<T>, keyof AsChild<T>>;
export function PageTitle<T extends React.ElementType = "h1">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "h1") as React.ElementType;
return (
<Tag
className={cn("text-xl font-semibold tracking-tight", className)}
style={{ color: "var(--text-1)" }}
{...rest}
>
{children}
</Tag>
);
}
export function SectionHeading<T extends React.ElementType = "h2">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "h2") as React.ElementType;
return (
<Tag
className={cn("text-sm font-medium mb-3", className)}
style={{ color: "var(--text-1)" }}
{...rest}
>
{children}
</Tag>
);
}
export function Body<T extends React.ElementType = "p">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "p") as React.ElementType;
return (
<Tag
className={cn("text-sm leading-relaxed", className)}
style={{ color: "var(--text-2)" }}
{...rest}
>
{children}
</Tag>
);
}
export function Muted<T extends React.ElementType = "p">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "p") as React.ElementType;
return (
<Tag className={cn("text-sm", className)} style={{ color: "var(--text-3)" }} {...rest}>
{children}
</Tag>
);
}
export function Caption<T extends React.ElementType = "span">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "span") as React.ElementType;
return (
<Tag className={cn("text-xs", className)} style={{ color: "var(--text-4)" }} {...rest}>
{children}
</Tag>
);
}
export function MonoCaption<T extends React.ElementType = "span">({
as,
className,
children,
...rest
}: Props<T>) {
const Tag = (as ?? "span") as React.ElementType;
return (
<Tag
className={cn("text-xs font-mono", className)}
style={{ color: "var(--text-4)" }}
{...rest}
>
{children}
</Tag>
);
}

View File

@@ -2,6 +2,11 @@ import { useState } from "react";
import { z } from "zod"; import { z } from "zod";
import type { UseMutationResult } from "@tanstack/react-query"; import type { UseMutationResult } from "@tanstack/react-query";
import { FormModal } from "@/components/shared/FormModal"; import { FormModal } from "@/components/shared/FormModal";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Caption } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
const schema = z.object({ const schema = z.object({
observer: z.string().min(1, "Observer peer ID is required"), observer: z.string().min(1, "Observer peer ID is required"),
@@ -54,61 +59,58 @@ export function ScheduleDreamModal({ open, onClose, mutation }: Props) {
<FormModal open={open} title="Schedule Dream" onClose={() => { reset(); onClose(); }}> <FormModal open={open} title="Schedule Dream" onClose={() => { reset(); onClose(); }}>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--text-2)" }}> <Label className="mb-1.5">
Observer peer ID <span style={{ color: "#f87171" }}>*</span> Observer peer ID <span style={{ color: COLOR.destructive }}>*</span>
</label> </Label>
<input <Input
value={observer} value={observer}
onChange={(e) => { setObserver(e.target.value); setValidationError(""); }} onChange={(e) => { setObserver(e.target.value); setValidationError(""); }}
placeholder="peer_id" placeholder="peer_id"
className="theme-input w-full text-sm px-3 py-2 rounded-lg"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--text-2)" }}> <Label className="mb-1.5">
Observed peer ID <span style={{ color: "var(--text-4)" }}>(optional, defaults to observer)</span> Observed peer ID <Caption as="span"> (optional, defaults to observer)</Caption>
</label> </Label>
<input <Input
value={observed} value={observed}
onChange={(e) => setObserved(e.target.value)} onChange={(e) => setObserved(e.target.value)}
placeholder="peer_id" placeholder="peer_id"
className="theme-input w-full text-sm px-3 py-2 rounded-lg"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-medium mb-1.5" style={{ color: "var(--text-2)" }}> <Label className="mb-1.5">
Session ID <span style={{ color: "var(--text-4)" }}>(optional)</span> Session ID <Caption as="span"> (optional)</Caption>
</label> </Label>
<input <Input
value={sessionId} value={sessionId}
onChange={(e) => setSessionId(e.target.value)} onChange={(e) => setSessionId(e.target.value)}
placeholder="session_id" placeholder="session_id"
className="theme-input w-full text-sm px-3 py-2 rounded-lg"
/> />
</div> </div>
{validationError && ( {validationError && (
<p className="text-xs" style={{ color: "#f87171" }}>{validationError}</p> <Caption as="p" style={{ color: COLOR.destructive }}>{validationError}</Caption>
)} )}
{mutation.error && ( {mutation.error && (
<p className="text-xs" style={{ color: "#f87171" }}>{mutation.error.message}</p> <Caption as="p" style={{ color: COLOR.destructive }}>{mutation.error.message}</Caption>
)} )}
<div className="flex justify-end gap-2 pt-2"> <div className="flex justify-end gap-2 pt-2">
<button <Button
type="button" type="button"
variant="surface"
size="sm"
onClick={() => { reset(); onClose(); }} onClick={() => { reset(); onClose(); }}
className="px-3 py-1.5 text-sm rounded-lg"
style={{ background: "var(--surface)", border: "1px solid var(--border)", color: "var(--text-2)" }}
> >
Cancel Cancel
</button> </Button>
<button <Button
type="submit" type="submit"
variant="accent"
size="sm"
disabled={mutation.isPending} disabled={mutation.isPending}
className="px-3 py-1.5 text-sm rounded-lg font-medium disabled:opacity-50"
style={{ background: "var(--accent-dim)", border: "1px solid var(--accent-border)", color: "var(--accent-text)" }}
> >
{mutation.isPending ? "Scheduling..." : "Schedule"} {mutation.isPending ? "Scheduling..." : "Schedule"}
</button> </Button>
</div> </div>
</form> </form>
</FormModal> </FormModal>

View File

@@ -7,6 +7,10 @@ import { useWebhooks, useCreateWebhook, useDeleteWebhook, useTestWebhook } from
import { PageLoader } from "@/components/shared/LoadingSpinner"; import { PageLoader } from "@/components/shared/LoadingSpinner";
import { ErrorAlert } from "@/components/shared/ErrorAlert"; import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { ConfirmDialog } from "@/components/shared/ConfirmDialog"; import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { PageTitle, SectionHeading, Body, Muted } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
const urlSchema = z.string().url("Must be a valid URL"); const urlSchema = z.string().url("Must be a valid URL");
@@ -60,27 +64,19 @@ export function WebhookManager({ workspaceId }: Props) {
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Webhook className="w-5 h-5" style={{ color: "var(--accent)" }} strokeWidth={1.5} /> <Webhook className="w-5 h-5" style={{ color: "var(--accent)" }} strokeWidth={1.5} />
<h1 className="text-xl font-semibold tracking-tight" style={{ color: "var(--text-1)" }}> <PageTitle>Webhooks</PageTitle>
Webhooks
</h1>
</div> </div>
<button <Button
variant="accent"
size="sm"
onClick={handleTest} onClick={handleTest}
disabled={testWebhook.isPending} disabled={testWebhook.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
<Zap className="w-3.5 h-3.5" strokeWidth={2} /> <Zap className="w-3.5 h-3.5" strokeWidth={2} />
{testWebhook.isPending ? "Firing..." : "Test emit"} {testWebhook.isPending ? "Firing..." : "Test emit"}
</button> </Button>
</div> </div>
<p className="text-sm" style={{ color: "var(--text-2)" }}> <Body className="leading-none">Event webhook endpoints for this workspace</Body>
Event webhook endpoints for this workspace
</p>
</motion.div> </motion.div>
<div className="mt-8 space-y-4"> <div className="mt-8 space-y-4">
@@ -93,34 +89,28 @@ export function WebhookManager({ workspaceId }: Props) {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="rounded-xl p-5 theme-card" className="rounded-xl p-5 theme-card"
> >
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>
<Plus className="w-3.5 h-3.5 inline mr-1.5" strokeWidth={2} /> <Plus className="w-3.5 h-3.5 inline mr-1.5" strokeWidth={2} />
Add endpoint Add endpoint
</h2> </SectionHeading>
<form onSubmit={handleCreate} className="flex gap-2"> <form onSubmit={handleCreate} className="flex gap-2">
<div className="flex-1"> <div className="flex-1">
<input <Input
value={url} value={url}
onChange={(e) => { setUrl(e.target.value); setUrlError(""); }} onChange={(e) => { setUrl(e.target.value); setUrlError(""); }}
placeholder="https://your-server.com/webhook" placeholder="https://your-server.com/webhook"
className="theme-input w-full text-sm px-3 py-2 rounded-lg"
/> />
{urlError && ( {urlError && (
<p className="text-xs mt-1" style={{ color: "#f87171" }}>{urlError}</p> <p className="text-xs mt-1" style={{ color: COLOR.destructive }}>{urlError}</p>
)} )}
</div> </div>
<button <Button
type="submit" type="submit"
variant="accent"
disabled={createWebhook.isPending} disabled={createWebhook.isPending}
className="px-3 py-2 text-sm rounded-lg font-medium disabled:opacity-50"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
{createWebhook.isPending ? "Adding..." : "Add"} {createWebhook.isPending ? "Adding..." : "Add"}
</button> </Button>
</form> </form>
</motion.div> </motion.div>
@@ -133,9 +123,9 @@ export function WebhookManager({ workspaceId }: Props) {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="rounded-xl p-4 text-xs font-mono overflow-auto" className="rounded-xl p-4 text-xs font-mono overflow-auto"
style={{ style={{
background: "rgba(52,211,153,0.06)", background: COLOR.successDim,
border: "1px solid rgba(52,211,153,0.2)", border: `1px solid ${COLOR.successBorder}`,
color: "#34d399", color: COLOR.success,
}} }}
> >
{testResult} {testResult}
@@ -158,9 +148,7 @@ export function WebhookManager({ workspaceId }: Props) {
style={{ color: "var(--text-3)" }} style={{ color: "var(--text-3)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<p className="text-sm" style={{ color: "var(--text-3)" }}> <Muted>No webhook endpoints yet.</Muted>
No webhook endpoints yet.
</p>
</div> </div>
) : ( ) : (
<div className="divide-y" style={{ borderColor: "var(--border)" }}> <div className="divide-y" style={{ borderColor: "var(--border)" }}>
@@ -197,13 +185,14 @@ export function WebhookManager({ workspaceId }: Props) {
{(wh as { id: string }).id} {(wh as { id: string }).id}
</span> </span>
</div> </div>
<button <Button
variant="ghost"
size="icon"
onClick={() => setDeleteTarget((wh as { id: string }).id)} onClick={() => setDeleteTarget((wh as { id: string }).id)}
className="p-1.5 rounded-lg transition-colors flex-shrink-0" aria-label="Delete webhook"
style={{ color: "var(--text-4)" }}
> >
<Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} /> <Trash2 className="w-3.5 h-3.5" strokeWidth={1.5} />
</button> </Button>
</motion.div> </motion.div>
))} ))}
</div> </div>

View File

@@ -4,6 +4,9 @@ import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { JsonViewer } from "@/components/shared/JsonViewer"; import { JsonViewer } from "@/components/shared/JsonViewer";
import { PageLoader } from "@/components/shared/LoadingSpinner"; import { PageLoader } from "@/components/shared/LoadingSpinner";
import { ScheduleDreamModal } from "@/components/workspaces/ScheduleDreamModal"; import { ScheduleDreamModal } from "@/components/workspaces/ScheduleDreamModal";
import { Button } from "@/components/ui/button";
import { PageTitle, SectionHeading, Body, Caption } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { import {
@@ -84,43 +87,30 @@ export function WorkspaceDetail() {
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<h1 <PageTitle className="font-mono break-all">
className="text-xl font-semibold font-mono break-all tracking-tight"
style={{ color: "var(--text-1)" }}
>
{workspaceId} {workspaceId}
</h1> </PageTitle>
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<button <Button
variant="accent"
size="sm"
onClick={() => setDreamOpen(true)} onClick={() => setDreamOpen(true)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
style={{
background: "var(--accent-dim)",
border: "1px solid var(--accent-border)",
color: "var(--accent-text)",
}}
> >
<Zap className="w-3.5 h-3.5" strokeWidth={2} /> <Zap className="w-3.5 h-3.5" strokeWidth={2} />
Schedule Dream Schedule Dream
</button> </Button>
<button <Button
variant="destructive"
size="sm"
onClick={() => setConfirmDelete(true)} onClick={() => setConfirmDelete(true)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
style={{
background: "rgba(239,68,68,0.08)",
border: "1px solid rgba(239,68,68,0.2)",
color: "#f87171",
}}
> >
<Trash2 className="w-3.5 h-3.5" strokeWidth={2} /> <Trash2 className="w-3.5 h-3.5" strokeWidth={2} />
Delete Delete
</button> </Button>
</div> </div>
</div> </div>
<p className="text-sm" style={{ color: "var(--text-2)" }}> <Body className="leading-none">Workspace overview</Body>
Workspace overview
</p>
</motion.div> </motion.div>
<div className="mt-8"> <div className="mt-8">
@@ -150,12 +140,8 @@ export function WorkspaceDetail() {
style={{ color: "var(--accent)" }} style={{ color: "var(--accent)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<h2 className="text-sm font-medium mb-0.5" style={{ color: "var(--text-1)" }}> <SectionHeading className="mb-0.5">{s.label}</SectionHeading>
{s.label} <Caption as="p">{s.description}</Caption>
</h2>
<p className="text-xs" style={{ color: "var(--text-3)" }}>
{s.description}
</p>
</Link> </Link>
</motion.div> </motion.div>
); );
@@ -171,9 +157,7 @@ export function WorkspaceDetail() {
className="rounded-xl p-5 theme-card" className="rounded-xl p-5 theme-card"
> >
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-medium" style={{ color: "var(--text-1)" }}> <SectionHeading className="mb-0">Queue Status</SectionHeading>
Queue Status
</h2>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{queue.pending_work_units > 0 ? ( {queue.pending_work_units > 0 ? (
<motion.div <motion.div
@@ -182,20 +166,20 @@ export function WorkspaceDetail() {
> >
<CircleDot <CircleDot
className="w-3.5 h-3.5" className="w-3.5 h-3.5"
style={{ color: "#f59e0b" }} style={{ color: COLOR.warning }}
strokeWidth={2} strokeWidth={2}
/> />
</motion.div> </motion.div>
) : ( ) : (
<CircleDot <CircleDot
className="w-3.5 h-3.5" className="w-3.5 h-3.5"
style={{ color: "#34d399" }} style={{ color: COLOR.success }}
strokeWidth={2} strokeWidth={2}
/> />
)} )}
<span <span
className="text-xs font-medium" className="text-xs font-medium"
style={{ color: queue.pending_work_units > 0 ? "#f59e0b" : "#34d399" }} style={{ color: queue.pending_work_units > 0 ? COLOR.warning : COLOR.success }}
> >
{queue.pending_work_units === 0 {queue.pending_work_units === 0
? "Idle" ? "Idle"
@@ -301,13 +285,13 @@ export function WorkspaceDetail() {
</td> </td>
<td <td
className="py-1.5 px-3 text-right font-mono" className="py-1.5 px-3 text-right font-mono"
style={{ color: "#34d399" }} style={{ color: COLOR.success }}
> >
{s.completed_work_units} {s.completed_work_units}
</td> </td>
<td <td
className="py-1.5 px-3 text-right font-mono" className="py-1.5 px-3 text-right font-mono"
style={{ color: "#f59e0b" }} style={{ color: COLOR.warning }}
> >
{s.in_progress_work_units} {s.in_progress_work_units}
</td> </td>
@@ -337,9 +321,7 @@ export function WorkspaceDetail() {
transition={{ delay: 0.38 }} transition={{ delay: 0.38 }}
className="rounded-xl p-5 theme-card" className="rounded-xl p-5 theme-card"
> >
<h2 className="text-sm font-medium mb-3" style={{ color: "var(--text-1)" }}> <SectionHeading>Metadata</SectionHeading>
Metadata
</h2>
<JsonViewer data={workspace.metadata} /> <JsonViewer data={workspace.metadata} />
</motion.div> </motion.div>
</div> </div>

View File

@@ -7,6 +7,8 @@ import { PageLoader } from "@/components/shared/LoadingSpinner";
import { ErrorAlert } from "@/components/shared/ErrorAlert"; import { ErrorAlert } from "@/components/shared/ErrorAlert";
import { Pagination } from "@/components/shared/Pagination"; import { Pagination } from "@/components/shared/Pagination";
import { EmptyState } from "@/components/shared/EmptyState"; import { EmptyState } from "@/components/shared/EmptyState";
import { PageTitle, Muted, MonoCaption } from "@/components/ui/typography";
import { COLOR } from "@/lib/constants";
import type { components } from "@/api/schema.d.ts"; import type { components } from "@/api/schema.d.ts";
type Workspace = components["schemas"]["Workspace"]; type Workspace = components["schemas"]["Workspace"];
@@ -40,28 +42,21 @@ export function WorkspaceList() {
> >
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Boxes className="w-5 h-5" style={{ color: "#6366f1" }} strokeWidth={1.5} /> <Boxes className="w-5 h-5" style={{ color: "#6366f1" }} strokeWidth={1.5} />
<h1 <PageTitle>Workspaces</PageTitle>
className="text-xl font-semibold tracking-tight"
style={{ color: "#e4e4f0" }}
>
Workspaces
</h1>
{total > 0 && ( {total > 0 && (
<span <span
className="ml-auto text-xs font-mono px-2 py-0.5 rounded-full" className="ml-auto text-xs font-mono px-2 py-0.5 rounded-full"
style={{ style={{
background: "rgba(99,102,241,0.1)", background: COLOR.accentSubtle,
color: "#818cf8", color: COLOR.accentText,
border: "1px solid rgba(99,102,241,0.2)", border: `1px solid ${COLOR.accentBorder}`,
}} }}
> >
{total} {total}
</span> </span>
)} )}
</div> </div>
<p className="text-sm" style={{ color: "rgba(148,163,184,0.6)" }}> <Muted>All workspaces in your Honcho instance</Muted>
All workspaces in your Honcho instance
</p>
</motion.div> </motion.div>
<ErrorAlert error={error instanceof Error ? error : null} /> <ErrorAlert error={error instanceof Error ? error : null} />
@@ -124,9 +119,7 @@ export function WorkspaceList() {
style={{ color: "rgba(148,163,184,0.35)" }} style={{ color: "rgba(148,163,184,0.35)" }}
strokeWidth={1.5} strokeWidth={1.5}
/> />
<p className="text-xs font-mono" style={{ color: "rgba(148,163,184,0.35)" }}> <MonoCaption>{new Date(ws.created_at).toLocaleString()}</MonoCaption>
{new Date(ws.created_at).toLocaleString()}
</p>
</div> </div>
)} )}
</motion.button> </motion.button>