Compare commits

...
Author SHA1 Message Date
Val Alexander 028d18d2f0 Add missing closing brace in plugins route
Add a missing closing brace and semicolon to the object returned in src/routes/plugins/index.tsx. This properly terminates the returned route configuration (including the apiError field) and fixes the syntax error so the file compiles.
2026-04-13 01:33:02 -05:00
Val Alexander e2217fd230 Merge branch 'staging' into modern-utility-store 2026-04-12 23:22:11 -05:00
Val Alexanderandgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> 1e1f5a6e2c Update src/components/layout/Container.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-12 22:32:01 -05:00
Val Alexanderandgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> f9e4b95002 Update src/routes/plugins/index.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-12 22:29:25 -05:00
Val Alexander 2af0b416a8 Merge pull request #2 from BunsDev/v0/bunsdev-586a3c32
Redesign ClawHub marketplace with new OpenClaw branding
2026-04-10 07:19:34 -05:00
v0andVal Alexander 8dff99de19 feat: optimize layout and create DESIGN.md
Fix orphan cards, enforce equal card heights, add branding accents, improve visual hierarchy, add responsive breakpoints, create design document

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 12:16:47 +00:00
v0andVal Alexander bbbdad73c5 feat: add user preferences customization section
Add 'usePreferences' hook and new Switch component; enhance settings page with Customization section.

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 09:05:22 +00:00
v0andVal Alexander 18aea044fc feat: highlight parent tabs with activePathPrefixes
Add activePathPrefixes to NavItem and update navigation to highlight parent tabs.

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 09:00:59 +00:00
v0andVal Alexander 2a4295c3ac fix: add global overflow protection to detail pages
Add overflow prevention for text elements and links.

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 08:59:15 +00:00
v0andVal Alexander d761eb768d refactor: optimize skill detail page layout
Redesign skill detail page to maximize space, remove sidebar, create metadata bar, and add responsive breakpoints.

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 08:57:08 +00:00
v0andVal Alexander f274b337bd fix: add defensive checks to PluginsIndex for SSR errors
Handle undefined loader data in PluginsIndex component.

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 08:53:37 +00:00
v0andVal Alexander cbdfc327be fix: add defensive checks in fetchPluginCatalog
Ensure proper handling of undefined and unexpected API responses.

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 08:52:13 +00:00
v0andVal Alexander df531f4742 feat: use Vite's native tsconfig paths
Replace deprecated plugin with native option and remove unused import.

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 08:50:29 +00:00
v0andVal Alexander 21d6d01f13 feat: update branding to OpenClaw's black, white, and red color scheme
Implement new color scheme across dark, light themes and interactive elements

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 07:21:43 +00:00
v0andVal Alexander 98eb954145 feat: handle errors in Package API calls to prevent SSR and HMR errors
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 07:19:17 +00:00
v0andVal Alexander adc017aa4b fix: resolve SSR error bubbling in TanStack Router
Ensure loader errors don't escape SSR and hydrate correctly.

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 07:16:53 +00:00
v0andVal Alexander d6996379c1 fix: resolve 500 errors in TanStack Router loaders
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 06:50:38 +00:00
v0andVal Alexander c1eb58e51f style: polish marketplace UI with modern design updates
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 05:44:10 +00:00
v0andVal Alexander 21e0bb16f0 feat: redesign ClawHub marketplace with modern utility store theme
Update styles.css and index.tsx for new modern design

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
2026-04-10 05:22:00 +00:00
16 changed files with 2420 additions and 762 deletions
+356
View File
@@ -0,0 +1,356 @@
# ClawHub Design System
This document outlines the design rules, patterns, and guidelines for the ClawHub platform to ensure consistency, accessibility, and maintainability across all components.
---
## Color System
### Brand Palette (OpenClaw)
ClawHub uses a strict **3-5 color palette** based on the OpenClaw brand:
| Token | Light Mode | Dark Mode | Usage |
|-------|------------|-----------|-------|
| `--accent` | `#dc2626` | `#dc2626` | Primary actions, interactive elements, emphasis |
| `--accent-deep` | `#b91c1c` | `#ef4444` | Hover states, secondary emphasis |
| `--ink` | `#0a0a0a` | `#fafafa` | Primary text |
| `--ink-soft` | `#525252` | `#a1a1a1` | Secondary text, descriptions |
| `--surface` | `#ffffff` | `#121212` | Card backgrounds, elevated surfaces |
| `--bg` | `#fafafa` | `#0a0a0a` | Page background |
### Rules
1. **Never exceed 5 colors** without explicit design approval
2. **Never use purple/violet prominently** unless explicitly requested
3. **Always override text color** when changing background color to ensure contrast
4. **Use semantic tokens** (`--accent`, `--ink`, `--surface`) instead of raw colors
---
## Typography
### Font Stack
```css
--font-sans: 'Geist', system-ui, sans-serif;
--font-mono: 'Geist Mono', monospace;
--font-display: 'Geist', system-ui, sans-serif;
```
### Scale
| Token | Size | Usage |
|-------|------|-------|
| `--fs-xs` | 0.75rem (12px) | Labels, badges, metadata |
| `--fs-sm` | 0.875rem (14px) | Body text, descriptions |
| `--fs-base` | 1rem (16px) | Default body text |
| `--fs-md` | 1.125rem (18px) | Subheadings |
| `--fs-lg` | 1.25rem (20px) | Section titles |
| `--fs-xl` | 1.5rem (24px) | Page headings |
### Rules
1. **Maximum 2 font families** per page
2. **Line height 1.4-1.6** for body text (use `leading-relaxed`)
3. **Never use decorative fonts** for body text
4. **Minimum font size: 14px** for readability
5. Use `text-balance` or `text-pretty` for titles
---
## Layout
### Method Priority
Use this hierarchy for layout decisions:
1. **Flexbox** - Default for most layouts
2. **CSS Grid** - Only for complex 2D layouts (cards, galleries)
3. **Never use floats** or absolute positioning unless absolutely necessary
### Spacing Scale
```css
--space-1: 0.25rem /* 4px */
--space-2: 0.5rem /* 8px */
--space-3: 0.75rem /* 12px */
--space-4: 1rem /* 16px */
--space-5: 1.5rem /* 24px */
--space-6: 2rem /* 32px */
```
### Grid Patterns
#### Auto-fit Grid (Recommended for Cards)
```css
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
```
- Automatically adjusts columns based on container width
- Prevents orphan items on partial rows
- Maintains consistent card widths
#### Fixed Grid (When exact columns needed)
```css
/* 3-column at desktop, 2 at tablet, 1 at mobile */
grid-template-columns: repeat(3, minmax(0, 1fr));
@media (max-width: 860px) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 520px) {
grid-template-columns: 1fr;
}
```
### Container Widths
| Size | Max Width | Usage |
|------|-----------|-------|
| Default | `--page-max` (1200px) | Standard pages |
| Narrow | `--page-narrow` (720px) | Reading content, forms |
| Wide | Full width | Dashboards, data tables |
---
## Components
### Cards
```css
.card {
padding: var(--space-4);
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
}
```
**Rules:**
- Always use `display: flex; flex-direction: column;` for consistent height
- Add `flex: 1` to content area for equal-height cards in grids
- Include hover state with `border-color` and subtle `box-shadow`
### Buttons
| Variant | Usage |
|---------|-------|
| `primary` | Main actions (Submit, Save, Download) |
| `secondary` | Alternative actions |
| `ghost` | Tertiary actions, navigation |
| `destructive` | Delete, remove, dangerous actions |
**Rules:**
- Always include visible focus state
- Minimum touch target: 44x44px on mobile
- Include `aria-label` when icon-only
### Form Controls
- Labels above inputs (not inline)
- Error states use `--status-error-fg`
- Focus rings use `--accent` with 0.2 opacity
- Minimum input height: 40px
---
## Responsive Breakpoints
```css
/* Mobile first - base styles for mobile */
@media (min-width: 520px) {
/* Small tablets, large phones */
}
@media (min-width: 640px) {
/* Tablets */
}
@media (min-width: 860px) {
/* Small desktops, landscape tablets */
}
@media (min-width: 1024px) {
/* Desktops */
}
@media (min-width: 1280px) {
/* Large desktops */
}
```
### Rules
1. **Mobile-first approach** - Base styles target mobile
2. **Progressive enhancement** - Add complexity as viewport increases
3. **Test intermediate breakpoints** - Avoid jarring layout jumps
4. **Never hide critical content** on mobile
---
## Accessibility
### Color Contrast
- Normal text: Minimum 4.5:1 ratio
- Large text (18px+): Minimum 3:1 ratio
- Interactive elements: Minimum 3:1 ratio
### Focus States
```css
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 2px;
}
```
### Screen Readers
- Use `sr-only` class for visually hidden but accessible text
- Always include `alt` text for images (empty `alt=""` for decorative)
- Use semantic HTML elements (`main`, `nav`, `article`, `section`)
- Proper heading hierarchy (h1 > h2 > h3, no skipping)
### Motion
```css
/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
---
## Animation
### Timing
```css
--transition-fast: 150ms;
--transition-base: 200ms;
--transition-slow: 300ms;
```
### Easing
- Use `ease` or `ease-out` for most transitions
- Use `ease-in-out` for enter/exit animations
- Never use `linear` except for continuous animations
### Rules
1. **Subtle by default** - Avoid flashy animations
2. **Purpose-driven** - Animation should provide feedback
3. **Respect preferences** - Support `prefers-reduced-motion`
4. **Performance** - Use `transform` and `opacity` only
---
## Icons
### Usage
- Use Lucide icons consistently
- Standard sizes: 14px, 16px, 20px, 24px
- Include `aria-hidden="true"` for decorative icons
- Never use emojis as icons
### Placement
- Left of labels in buttons and navigation
- Right of labels for external links or dropdowns
- Centered when used alone with `aria-label`
---
## Dark Mode
### Implementation
```css
[data-theme="dark"] {
/* Dark mode overrides */
}
```
### Rules
1. Never use pure white (`#ffffff`) on dark backgrounds
2. Reduce shadow intensity in dark mode
3. Adjust image brightness if needed
4. Test contrast ratios in both modes
---
## Performance
### CSS
1. Use CSS custom properties for theming
2. Avoid deeply nested selectors (max 3 levels)
3. Use `will-change` sparingly
4. Prefer `transform` over `top/left` for animations
### Images
1. Always specify `width` and `height` attributes
2. Use `loading="lazy"` for below-fold images
3. Use appropriate formats (WebP with fallbacks)
4. Include placeholder or skeleton states
---
## Code Style
### CSS Class Naming
```css
/* Component */
.component-name { }
/* Component modifier */
.component-name.variant { }
/* Component child */
.component-name-child { }
/* State */
.component-name.is-active { }
.component-name[data-state="open"] { }
```
### File Organization
```
src/
components/
ui/ # Primitive components (Button, Input, Card)
layout/ # Layout components (Container, Header)
styles.css # Global styles and design tokens
lib/
theme.ts # Theme utilities
preferences.ts # User preference management
```
---
## Checklist
Before shipping any UI changes, verify:
- [ ] Color contrast meets WCAG AA standards
- [ ] Focus states are visible
- [ ] Layout works at all breakpoints
- [ ] Animations respect `prefers-reduced-motion`
- [ ] Text is readable at default browser zoom
- [ ] Interactive elements have 44px minimum touch target
- [ ] Semantic HTML is used appropriately
- [ ] Dark mode has been tested
+136 -3
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "clawhub",
@@ -19,6 +20,8 @@
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
@@ -31,6 +34,7 @@
"@tanstack/react-table": "^8.21.3",
"@tanstack/router-plugin": "1.167.2",
"@vercel/analytics": "^2.0.1",
"class-variance-authority": "^0.7.1",
"clawhub-schema": "workspace:*",
"clsx": "^2.1.1",
"convex": "^1.34.1",
@@ -40,6 +44,8 @@
"ignore": "^7.0.5",
"lucide-react": "^0.577.0",
"monaco-editor": "^0.55.1",
"next": "^16.2.3",
"next-themes": "^0.4.6",
"nitro": "3.0.260311-beta",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@@ -50,6 +56,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^6.1.1",
"yaml": "^2.8.3",
"zod": "^4.3.6",
@@ -265,6 +272,56 @@
"@fontsource/manrope": ["@fontsource/manrope@5.2.8", "", {}, "sha512-gJHJmcuUk7qWcNCfcAri/DJQtXtBYqi9yKratr4jXhSo0I3xUtNNKI+igQIcw5c+m95g0vounk8ZnX/kb8o0TA=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -281,6 +338,24 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw=="],
"@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="],
"@oozcitak/infra": ["@oozcitak/infra@2.0.2", "", { "dependencies": { "@oozcitak/util": "^10.0.0" } }, "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA=="],
@@ -439,7 +514,9 @@
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
@@ -535,6 +612,8 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
@@ -743,6 +822,8 @@
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clawhub": ["clawhub@workspace:packages/clawhub"],
"clawhub-schema": ["clawhub-schema@workspace:packages/schema"],
@@ -751,6 +832,8 @@
"cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
@@ -1095,6 +1178,10 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"next": ["next@16.2.3", "", { "dependencies": { "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.3", "@next/swc-darwin-x64": "16.2.3", "@next/swc-linux-arm64-gnu": "16.2.3", "@next/swc-linux-arm64-musl": "16.2.3", "@next/swc-linux-x64-gnu": "16.2.3", "@next/swc-linux-x64-musl": "16.2.3", "@next/swc-win32-arm64-msvc": "16.2.3", "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"nf3": ["nf3@0.3.13", "", {}, "sha512-drDt0yl4d/yUhlpD0GzzqahSpA5eUNeIfFq0/aoZb0UlPY0ZwP4u1EfREVvZrYdEnJ3OU9Le9TrzbvWgEkkeKw=="],
"nitro": ["nitro@3.0.260311-beta", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.4", "db0": "^0.3.4", "env-runner": "^0.1.6", "h3": "^2.0.1-rc.16", "hookable": "^6.0.1", "nf3": "^0.3.11", "ocache": "^0.1.2", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "rolldown": "^1.0.0-rc.8", "srvx": "^0.11.9", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.6" }, "peerDependencies": { "dotenv": "*", "giget": "*", "jiti": "^2.6.1", "rollup": "^4.59.0", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2", "zephyr-agent": "^0.1.15" }, "optionalPeers": ["dotenv", "giget", "jiti", "rollup", "vite", "xml2js", "zephyr-agent"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-0o0fJ9LUh4WKUqJNX012jyieUOtMCnadkNDWr0mHzdraoHpJP/1CGNefjRyZyMXSpoJfwoWdNEZu2iGf35TUvQ=="],
@@ -1223,6 +1310,8 @@
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="],
@@ -1263,6 +1352,8 @@
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
@@ -1307,6 +1398,8 @@
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
@@ -1399,10 +1492,14 @@
"@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
@@ -1415,14 +1512,14 @@
"@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -1435,6 +1532,12 @@
"@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-switch/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -1449,6 +1552,8 @@
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
@@ -1491,6 +1596,8 @@
"jsdom/undici": ["undici@7.24.5", "", {}, "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
@@ -1506,5 +1613,31 @@
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="],
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
}
}
+6
View File
@@ -45,6 +45,8 @@
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
@@ -57,6 +59,7 @@
"@tanstack/react-table": "^8.21.3",
"@tanstack/router-plugin": "1.167.2",
"@vercel/analytics": "^2.0.1",
"class-variance-authority": "^0.7.1",
"clawhub-schema": "workspace:*",
"clsx": "^2.1.1",
"convex": "^1.34.1",
@@ -66,6 +69,8 @@
"ignore": "^7.0.5",
"lucide-react": "^0.577.0",
"monaco-editor": "^0.55.1",
"next": "^16.2.3",
"next-themes": "^0.4.6",
"nitro": "3.0.260311-beta",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@@ -76,6 +81,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^6.1.1",
"yaml": "^2.8.3",
"zod": "^4.3.6"
+22 -11
View File
@@ -1,5 +1,5 @@
import { useAuthActions } from "@convex-dev/auth/react";
import { Link, useNavigate } from "@tanstack/react-router";
import { Link, useLocation, useNavigate } from "@tanstack/react-router";
import { Ghost, Github, Menu, Monitor, Moon, Plug, Search, Sun, Wrench } from "lucide-react";
import { useMemo, useRef, useState } from "react";
import { getUserFacingAuthError } from "../lib/authErrorMessage";
@@ -42,6 +42,7 @@ export default function Header() {
const isSoulMode = siteMode === "souls";
const clawHubUrl = getClawHubSiteUrl();
const navigate = useNavigate();
const location = useLocation();
const avatar = me?.image ?? (me?.email ? gravatarUrl(me.email) : undefined);
const handle = me?.handle ?? me?.displayName ?? "user";
@@ -277,12 +278,16 @@ export default function Header() {
) : null}
{primaryItems.map((item) => {
const Icon = item.icon ? NAV_ICONS[item.icon] : null;
const isActiveByPrefix = item.activePathPrefixes?.some((prefix) =>
location.pathname.startsWith(prefix)
);
return (
<Link
key={item.to + item.label}
to={item.to}
className="navbar-tab"
search={item.search ?? {}}
data-status={isActiveByPrefix ? "active" : undefined}
>
{Icon ? <Icon size={14} className="opacity-50" aria-hidden="true" /> : null}
{item.label}
@@ -291,16 +296,22 @@ export default function Header() {
})}
</div>
<div className="navbar-tabs-secondary">
{secondaryItems.map((item) => (
<Link
key={item.to + item.label}
to={item.to}
search={item.search ?? {}}
className="navbar-tab navbar-tab-secondary"
>
{item.label === "Management" ? "Manage" : item.label}
</Link>
))}
{secondaryItems.map((item) => {
const isActiveByPrefix = item.activePathPrefixes?.some((prefix) =>
location.pathname.startsWith(prefix)
);
return (
<Link
key={item.to + item.label}
to={item.to}
search={item.search ?? {}}
className="navbar-tab navbar-tab-secondary"
data-status={isActiveByPrefix ? "active" : undefined}
>
{item.label === "Management" ? "Manage" : item.label}
</Link>
);
})}
</div>
</nav>
</div>
+66 -68
View File
@@ -401,78 +401,76 @@ export function SkillDetailPage({
/>
) : null}
<div className="detail-layout">
<div className="detail-main">
{nixSnippet ? (
<Card>
<h3 className="m-0 text-[length:var(--text-base)] font-semibold">
Install via Nix
</h3>
<pre className="hero-install-code mt-2">
{nixSnippet}
</pre>
</Card>
) : null}
<SkillMetadataSidebar
skill={skill}
latestVersion={latestVersion}
owner={owner}
ownerHandle={ownerHandle}
clawdis={clawdis}
osLabels={osLabels}
tagEntries={tagEntries}
isMalwareBlocked={modInfo?.isMalwareBlocked}
isRemoved={modInfo?.isRemoved}
nixPlugin={nixPlugin}
/>
{configExample ? (
<Card>
<h3 className="m-0 text-[length:var(--text-base)] font-semibold">
Config example
</h3>
<pre className="hero-install-code mt-2">
{configExample}
</pre>
</Card>
) : null}
<div className="detail-content-full">
{nixSnippet ? (
<Card>
<h3 className="m-0 text-[length:var(--text-base)] font-semibold">
Install via Nix
</h3>
<pre className="hero-install-code mt-2">
{nixSnippet}
</pre>
</Card>
) : null}
<SkillDetailTabs
activeTab={activeTab}
setActiveTab={setActiveTab}
onCompareIntent={() => setShouldPrefetchCompare(true)}
readmeContent={readmeContent}
readmeError={readmeError}
latestFiles={latestFiles}
latestVersionId={latestVersion?._id ?? null}
skill={skill as Doc<"skills">}
diffVersions={diffVersions}
versions={versions}
nixPlugin={Boolean(nixPlugin)}
suppressVersionScanResults={suppressVersionScanResults}
scanResultsSuppressedMessage={scanResultsSuppressedMessage}
/>
{configExample ? (
<Card>
<h3 className="m-0 text-[length:var(--text-base)] font-semibold">
Config example
</h3>
<pre className="hero-install-code mt-2">
{configExample}
</pre>
</Card>
) : null}
<ClientOnly
fallback={
<Card>
<h2 className="section-title text-[1.2rem] m-0">
Comments
</h2>
<p className="section-subtitle mt-3 mb-0">
Loading comments...
</p>
</Card>
}
>
<SkillCommentsPanel
skillId={skill._id}
isAuthenticated={isAuthenticated}
me={me ?? null}
/>
</ClientOnly>
</div>
<SkillMetadataSidebar
skill={skill}
latestVersion={latestVersion}
owner={owner}
ownerHandle={ownerHandle}
clawdis={clawdis}
osLabels={osLabels}
tagEntries={tagEntries}
isMalwareBlocked={modInfo?.isMalwareBlocked}
isRemoved={modInfo?.isRemoved}
nixPlugin={nixPlugin}
<SkillDetailTabs
activeTab={activeTab}
setActiveTab={setActiveTab}
onCompareIntent={() => setShouldPrefetchCompare(true)}
readmeContent={readmeContent}
readmeError={readmeError}
latestFiles={latestFiles}
latestVersionId={latestVersion?._id ?? null}
skill={skill as Doc<"skills">}
diffVersions={diffVersions}
versions={versions}
nixPlugin={Boolean(nixPlugin)}
suppressVersionScanResults={suppressVersionScanResults}
scanResultsSuppressedMessage={scanResultsSuppressedMessage}
/>
<ClientOnly
fallback={
<Card>
<h2 className="section-title text-[1.2rem] m-0">
Comments
</h2>
<p className="section-subtitle mt-3 mb-0">
Loading comments...
</p>
</Card>
}
>
<SkillCommentsPanel
skillId={skill._id}
isAuthenticated={isAuthenticated}
me={me ?? null}
/>
</ClientOnly>
</div>
</div>
+61 -82
View File
@@ -3,7 +3,7 @@ import {
PLATFORM_SKILL_LICENSE,
PLATFORM_SKILL_LICENSE_SUMMARY,
} from "clawhub-schema/licenseConstants";
import { Package, Star } from "lucide-react";
import { Calendar, Download, Package, Scale, Star, Tag } from "lucide-react";
import type { Id } from "../../convex/_generated/dataModel";
import { formatCompactStat } from "../lib/numberFormat";
import type { PublicPublisher, PublicSkill } from "../lib/publicUser";
@@ -39,104 +39,83 @@ export function SkillMetadataSidebar({
nixPlugin,
}: SkillMetadataSidebarProps) {
const convexSiteUrl = getRuntimeEnv("VITE_CONVEX_SITE_URL") ?? "https://clawhub.ai";
const showDownload = !nixPlugin && !isMalwareBlocked && !isRemoved;
return (
<aside className="detail-sidebar">
{/* Download / Install */}
{!nixPlugin && !isMalwareBlocked && !isRemoved ? (
<div className="sidebar-card">
<h3 className="sidebar-card-title">Download</h3>
<Button asChild variant="primary" className="w-full justify-center">
<a href={`${convexSiteUrl}/api/v1/download?slug=${skill.slug}`}>
Download zip
</a>
</Button>
<div className="detail-meta-bar">
{/* Stats row */}
<div className="meta-bar-stats">
<div className="meta-stat">
<Download size={14} aria-hidden="true" />
<span className="meta-stat-value">{formatCompactStat(skill.stats.downloads)}</span>
<span className="meta-stat-label">downloads</span>
</div>
) : null}
{/* Stats */}
<div className="sidebar-card">
<h3 className="sidebar-card-title">Stats</h3>
<div className="sidebar-stat-grid">
<div className="sidebar-stat">
<span className="sidebar-stat-value">
<Package size={14} aria-hidden="true" />
{formatCompactStat(skill.stats.downloads)}
</span>
<span className="sidebar-stat-label">Downloads</span>
</div>
<div className="sidebar-stat">
<span className="sidebar-stat-value">
<Star size={14} aria-hidden="true" />
{formatCompactStat(skill.stats.stars)}
</span>
<span className="sidebar-stat-label">Stars</span>
</div>
<div className="sidebar-stat">
<span className="sidebar-stat-value">
{formatCompactStat(skill.stats.versions ?? 0)}
</span>
<span className="sidebar-stat-label">Versions</span>
</div>
<div className="meta-stat">
<Star size={14} aria-hidden="true" />
<span className="meta-stat-value">{formatCompactStat(skill.stats.stars)}</span>
<span className="meta-stat-label">stars</span>
</div>
<div className="meta-stat">
<Package size={14} aria-hidden="true" />
<span className="meta-stat-value">{formatCompactStat(skill.stats.versions ?? 0)}</span>
<span className="meta-stat-label">versions</span>
</div>
</div>
{/* Metadata */}
<div className="sidebar-card">
<h3 className="sidebar-card-title">Details</h3>
<dl className="sidebar-metadata">
<div className="sidebar-metadata-row">
<dt>Updated</dt>
<dd>{timeAgo(skill.updatedAt)}</dd>
{/* Details row */}
<div className="meta-bar-details">
<div className="meta-detail">
<Calendar size={12} aria-hidden="true" />
<span>Updated {timeAgo(skill.updatedAt)}</span>
</div>
{latestVersion?.version ? (
<div className="meta-detail">
<Tag size={12} aria-hidden="true" />
<span>v{latestVersion.version}</span>
</div>
<div className="sidebar-metadata-row">
<dt>Created</dt>
<dd>{timeAgo(skill.createdAt)}</dd>
) : null}
<div className="meta-detail">
<Scale size={12} aria-hidden="true" />
<span>{PLATFORM_SKILL_LICENSE}</span>
</div>
{osLabels.length > 0 ? (
<div className="meta-detail">
<span>{osLabels.join(", ")}</span>
</div>
{latestVersion?.version ? (
<div className="sidebar-metadata-row">
<dt>Version</dt>
<dd>v{latestVersion.version}</dd>
</div>
) : null}
<div className="sidebar-metadata-row">
<dt>License</dt>
<dd>{PLATFORM_SKILL_LICENSE} ({PLATFORM_SKILL_LICENSE_SUMMARY})</dd>
</div>
{osLabels.length ? (
<div className="sidebar-metadata-row">
<dt>Platforms</dt>
<dd>{osLabels.join(", ")}</dd>
</div>
) : null}
</dl>
) : null}
</div>
{/* Tags */}
{tagEntries.length > 0 ? (
<div className="sidebar-card">
<h3 className="sidebar-card-title">Tags</h3>
<div className="sidebar-tags">
{/* Tags and Publisher row */}
<div className="meta-bar-footer">
<div className="meta-bar-publisher">
<UserBadge
user={owner}
fallbackHandle={ownerHandle}
prefix=""
size="sm"
showName
/>
</div>
{tagEntries.length > 0 ? (
<div className="meta-bar-tags">
{tagEntries.map(([tag]) => (
<Badge key={tag} variant="compact">
{tag}
</Badge>
))}
</div>
</div>
) : null}
) : null}
{/* Owner */}
<div className="sidebar-card">
<h3 className="sidebar-card-title">Publisher</h3>
<UserBadge
user={owner}
fallbackHandle={ownerHandle}
prefix=""
size="md"
showName
/>
{showDownload ? (
<Button asChild variant="primary" size="sm">
<a href={`${convexSiteUrl}/api/v1/download?slug=${skill.slug}`}>
<Download size={14} aria-hidden="true" />
Download
</a>
</Button>
) : null}
</div>
</aside>
</div>
);
}
+32
View File
@@ -0,0 +1,32 @@
import * as SwitchPrimitive from "@radix-ui/react-switch";
import * as React from "react";
import { cn } from "../../lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitive.Root
ref={ref}
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent",
"transition-colors duration-200 ease-out",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bg)]",
"disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=checked]:bg-[color:var(--accent)] data-[state=unchecked]:bg-[color:var(--surface-muted)]",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm ring-0",
"transition-transform duration-200 ease-out",
"data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
));
Switch.displayName = SwitchPrimitive.Root.displayName;
export { Switch };
+5
View File
@@ -23,6 +23,8 @@ export interface NavItem {
soulModeOnly: boolean;
/** Link hidden when siteMode === "souls" */
soulModeHide: boolean;
/** Additional path prefixes that should also highlight this nav item (e.g. /skill for /skills) */
activePathPrefixes?: string[];
}
// ---------------------------------------------------------------------------
@@ -68,6 +70,7 @@ export const PRIMARY_NAV_ITEMS: NavItem[] = [
staffOnly: false,
soulModeOnly: false,
soulModeHide: true,
activePathPrefixes: ["/skill/"],
},
{
label: "Plugins",
@@ -77,6 +80,7 @@ export const PRIMARY_NAV_ITEMS: NavItem[] = [
staffOnly: false,
soulModeOnly: false,
soulModeHide: true,
activePathPrefixes: ["/plugin/"],
},
{
label: "Souls",
@@ -88,6 +92,7 @@ export const PRIMARY_NAV_ITEMS: NavItem[] = [
soulModeOnly: false,
// In soul-mode this is the primary tab; in skills-mode it is also shown.
soulModeHide: false,
activePathPrefixes: ["/soul/"],
},
];
+79 -60
View File
@@ -299,32 +299,51 @@ export async function fetchPluginCatalog(params: {
executesCode?: boolean;
limit?: number;
}): Promise<PluginCatalogResult> {
if (params.family) {
const response = await fetchPackages({
q: params.q,
cursor: params.cursor,
family: params.family,
isOfficial: params.isOfficial,
executesCode: params.executesCode,
limit: params.limit,
});
if (hasOwnProperty(response, "results") && Array.isArray(response.results)) {
try {
if (params.family) {
const response = await fetchPackages({
q: params.q,
cursor: params.cursor,
family: params.family,
isOfficial: params.isOfficial,
executesCode: params.executesCode,
limit: params.limit,
});
if (hasOwnProperty(response, "results") && Array.isArray(response.results)) {
return {
items: response.results.map((entry) => entry?.package).filter(Boolean) as PackageListItem[],
nextCursor: null,
};
}
const browseResponse = response as PackageCatalogBrowseResponse;
return {
items: response.results.map((entry) => entry.package),
items: browseResponse?.items ?? [],
nextCursor: browseResponse?.nextCursor ?? null,
};
}
if (params.q?.trim()) {
const url = await packageApiUrl(`${ApiRoutes.plugins}/search`);
url.searchParams.set("q", params.q.trim());
if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
if (typeof params.isOfficial === "boolean") {
url.searchParams.set("isOfficial", String(params.isOfficial));
}
if (typeof params.executesCode === "boolean") {
url.searchParams.set("executesCode", String(params.executesCode));
}
const response = await fetchJson<{
results?: Array<{ score: number; package: PackageListItem }>;
}>(url);
return {
items: (response?.results ?? []).map((entry) => entry?.package).filter(Boolean) as PackageListItem[],
nextCursor: null,
};
}
const browseResponse = response as PackageCatalogBrowseResponse;
return {
items: browseResponse.items,
nextCursor: browseResponse.nextCursor,
};
}
if (params.q?.trim()) {
const url = await packageApiUrl(`${ApiRoutes.plugins}/search`);
url.searchParams.set("q", params.q.trim());
const url = await packageApiUrl(ApiRoutes.plugins);
if (params.cursor) url.searchParams.set("cursor", params.cursor);
if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
if (typeof params.isOfficial === "boolean") {
url.searchParams.set("isOfficial", String(params.isOfficial));
@@ -332,53 +351,53 @@ export async function fetchPluginCatalog(params: {
if (typeof params.executesCode === "boolean") {
url.searchParams.set("executesCode", String(params.executesCode));
}
const response = await fetchJson<{
results: Array<{ score: number; package: PackageListItem }>;
}>(url);
const result = await fetchJson<PluginCatalogResult>(url);
return {
items: response.results.map((entry) => entry.package),
nextCursor: null,
items: result?.items ?? [],
nextCursor: result?.nextCursor ?? null,
};
} catch {
// Return empty result on API error to prevent SSR crashes
return { items: [], nextCursor: null };
}
const url = await packageApiUrl(ApiRoutes.plugins);
if (params.cursor) url.searchParams.set("cursor", params.cursor);
if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
if (typeof params.isOfficial === "boolean") {
url.searchParams.set("isOfficial", String(params.isOfficial));
}
if (typeof params.executesCode === "boolean") {
url.searchParams.set("executesCode", String(params.executesCode));
}
return await fetchJson<PluginCatalogResult>(url);
}
export async function fetchPackageDetail(name: string) {
const url = await packageApiUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}`);
const response = await packageFetch(url, "application/json");
if (response.status === 404) {
return {
package: null,
owner: null,
} satisfies PackageDetailResponse;
export async function fetchPackageDetail(name: string): Promise<PackageDetailResponse> {
try {
const url = await packageApiUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}`);
const response = await packageFetch(url, "application/json");
if (response.status === 404 || !response.ok) {
return { package: null, owner: null };
}
return (await response.json()) as PackageDetailResponse;
} catch {
// Return empty result on API error to prevent SSR crashes
return { package: null, owner: null };
}
if (!response.ok) throw await createPackageApiError(response);
return (await response.json()) as PackageDetailResponse;
}
export async function fetchPackageVersion(name: string, version: string) {
const url = await packageApiUrl(
`${ApiRoutes.packages}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`,
);
return await fetchJson<PackageVersionDetail>(url);
export async function fetchPackageVersion(name: string, version: string): Promise<PackageVersionDetail | null> {
try {
const url = await packageApiUrl(
`${ApiRoutes.packages}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`,
);
return await fetchJson<PackageVersionDetail>(url);
} catch {
// Return null on API error to prevent SSR crashes
return null;
}
}
export async function fetchPackageReadme(name: string, version?: string | null) {
const url = await packageApiUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}/file`);
url.searchParams.set("path", "README.md");
if (version) url.searchParams.set("version", version);
const response = await packageFetch(url, "text/plain");
if (response.ok) return await response.text();
if (response.status === 403 || response.status === 423 || response.status === 404) return null;
throw await createPackageApiError(response);
export async function fetchPackageReadme(name: string, version?: string | null): Promise<string | null> {
try {
const url = await packageApiUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}/file`);
url.searchParams.set("path", "README.md");
if (version) url.searchParams.set("version", version);
const response = await packageFetch(url, "text/plain");
if (response.ok) return await response.text();
return null;
} catch {
// Return null on API error to prevent SSR crashes
return null;
}
}
+165
View File
@@ -0,0 +1,165 @@
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
const PREFERENCES_KEY = "clawhub-preferences";
export type LayoutDensity = "comfortable" | "compact";
export type ListViewMode = "grid" | "list";
export type SidebarPosition = "left" | "right";
export type CodeFontSize = "small" | "medium" | "large";
export type AnimationLevel = "full" | "reduced" | "none";
export interface UserPreferences {
// Display preferences
layoutDensity: LayoutDensity;
listViewMode: ListViewMode;
showDescriptions: boolean;
showStats: boolean;
showTags: boolean;
// Advanced layout options
advancedMode: boolean;
sidebarPosition: SidebarPosition;
stickyHeader: boolean;
// Code & content preferences
codeFontSize: CodeFontSize;
lineNumbers: boolean;
wordWrap: boolean;
// Accessibility & motion
animationLevel: AnimationLevel;
reducedMotion: boolean;
highContrast: boolean;
// Notification preferences
emailNotifications: boolean;
browserNotifications: boolean;
// Experimental features
experimentalFeatures: boolean;
}
const defaultPreferences: UserPreferences = {
layoutDensity: "comfortable",
listViewMode: "grid",
showDescriptions: true,
showStats: true,
showTags: true,
advancedMode: false,
sidebarPosition: "right",
stickyHeader: true,
codeFontSize: "medium",
lineNumbers: true,
wordWrap: true,
animationLevel: "full",
reducedMotion: false,
highContrast: false,
emailNotifications: true,
browserNotifications: false,
experimentalFeatures: false,
};
// Simple event emitter for cross-tab sync
const listeners = new Set<() => void>();
function subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function notifyListeners() {
listeners.forEach((listener) => listener());
}
function getStoredPreferences(): UserPreferences {
if (typeof window === "undefined") return defaultPreferences;
try {
const stored = window.localStorage.getItem(PREFERENCES_KEY);
if (!stored) return defaultPreferences;
const parsed = JSON.parse(stored) as Partial<UserPreferences>;
return { ...defaultPreferences, ...parsed };
} catch {
return defaultPreferences;
}
}
function savePreferences(prefs: UserPreferences) {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(PREFERENCES_KEY, JSON.stringify(prefs));
notifyListeners();
} catch {
// Storage might be full or disabled
}
}
// Server snapshot for SSR
function getServerSnapshot(): UserPreferences {
return defaultPreferences;
}
export function usePreferences() {
const preferences = useSyncExternalStore(
subscribe,
getStoredPreferences,
getServerSnapshot
);
const updatePreference = useCallback(<K extends keyof UserPreferences>(
key: K,
value: UserPreferences[K]
) => {
const current = getStoredPreferences();
const updated = { ...current, [key]: value };
savePreferences(updated);
}, []);
const updatePreferences = useCallback((updates: Partial<UserPreferences>) => {
const current = getStoredPreferences();
const updated = { ...current, ...updates };
savePreferences(updated);
}, []);
const resetPreferences = useCallback(() => {
savePreferences(defaultPreferences);
}, []);
// Apply preferences as CSS variables/classes
useEffect(() => {
if (typeof document === "undefined") return;
const root = document.documentElement;
// Layout density
root.dataset.density = preferences.layoutDensity;
// Animation level
root.dataset.animation = preferences.animationLevel;
// High contrast mode
root.classList.toggle("high-contrast", preferences.highContrast);
// Reduced motion
root.classList.toggle("reduce-motion", preferences.reducedMotion || preferences.animationLevel === "none");
// Code font size
root.style.setProperty("--code-font-size",
preferences.codeFontSize === "small" ? "12px" :
preferences.codeFontSize === "large" ? "16px" : "14px"
);
}, [preferences]);
return {
preferences,
updatePreference,
updatePreferences,
resetPreferences,
isAdvancedMode: preferences.advancedMode,
};
}
export { defaultPreferences };
+256 -82
View File
@@ -1,5 +1,19 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useAction, useQuery } from "convex/react";
import {
ArrowRight,
Code2,
Download,
Flame,
Ghost,
Package,
Search,
Sparkles,
Star,
TrendingUp,
Users,
Zap,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { api } from "../../convex/_generated/api";
import { SkillCard } from "../components/SkillCard";
@@ -25,6 +39,19 @@ function Home() {
return mode === "souls" ? <OnlyCrabsHome /> : <SkillsHome />;
}
const popularSearches = ["AI Writing", "Screenshot", "Productivity", "Analytics", "Automation"];
const categories = [
{ name: "Productivity", icon: "⚡", count: 324, className: "productivity" },
{ name: "AI & ML", icon: "🧠", count: 218, className: "ai" },
{ name: "Developer Tools", icon: "💻", count: 456, className: "developer" },
{ name: "Design", icon: "🎨", count: 189, className: "design" },
{ name: "Analytics", icon: "📊", count: 142, className: "analytics" },
{ name: "Security", icon: "🔐", count: 98, className: "security" },
{ name: "Automation", icon: "⚙️", count: 276, className: "automation" },
{ name: "Media", icon: "🖼️", count: 167, className: "media" },
];
function SkillsHome() {
type SkillPageEntry = {
skill: PublicSkill;
@@ -37,6 +64,8 @@ function SkillsHome() {
const [trending, setTrending] = useState<SkillPageEntry[]>([]);
const [recent, setRecent] = useState<SkillPageEntry[]>([]);
const [skillCount, setSkillCount] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const navigate = Route.useNavigate();
useEffect(() => {
let cancelled = false;
@@ -44,13 +73,13 @@ function SkillsHome() {
Promise.all([
convexHttp.query(api.skills.listHighlightedPublic, { limit: 6 }),
convexHttp.query(api.skills.listPublicPageV4, {
numItems: 8,
numItems: 6,
sort: "downloads",
dir: "desc",
nonSuspiciousOnly: true,
}),
convexHttp.query(api.skills.listPublicPageV4, {
numItems: 8,
numItems: 6,
sort: "updated",
dir: "desc",
nonSuspiciousOnly: true,
@@ -71,51 +100,80 @@ function SkillsHome() {
};
}, []);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
const q = searchQuery.trim();
if (!q) return;
void navigate({
to: "/search",
search: { q, type: undefined },
});
};
return (
<main>
{/* Hero Section */}
<section className="home-hero">
<div className="home-hero-inner">
<div className="home-hero-grid">
<div className="home-hero-copy">
<div className="home-hero-kicker">Discovery hub</div>
<h1 className="home-hero-title">The collaborative hub for agent skills</h1>
<p className="home-hero-subtitle">
{skillCount != null
? `${formatCompactStat(skillCount)} public skill bundles, plugin packages, and builder profiles in one shared index. Browse fast, fork the good stuff, ship your own.`
: "Public skill bundles, plugin packages, and builder profiles in one shared index. Browse fast, fork the good stuff, ship your own."}
</p>
<div className="home-hero-actions">
<Button asChild variant="primary">
<Link
to="/skills"
search={{
q: undefined,
sort: undefined,
dir: undefined,
highlighted: undefined,
nonSuspicious: true,
view: undefined,
focus: undefined,
}}
>
Browse All Skills & Plugins
</Link>
</Button>
<Button asChild className="home-hero-publish-btn">
<Link
to="/publish-skill"
search={{ updateSlug: undefined }}
>
+ Publish Yours
</Link>
</Button>
{/* Badge */}
<div className="home-hero-kicker">
<Sparkles size={14} className="home-hero-kicker-icon" />
<span>
{skillCount != null
? `${formatCompactStat(skillCount)} curated tools`
: "Thousands of curated tools"}
</span>
</div>
<p className="home-hero-explainer">
Sharp filters. Clean listings. Discovery that feels more like a real index and less
like a sad spreadsheet.
{/* Headline */}
<h1 className="home-hero-title">
Discover tools that{" "}
<span className="home-hero-title-accent">power your work</span>
</h1>
{/* Subheadline */}
<p className="home-hero-subtitle">
The modern marketplace for internet tools. Find, compare, and install the best
software to supercharge your productivity.
</p>
{/* Search */}
<form className="home-hero-search" onSubmit={handleSearch}>
<div className="home-hero-search-wrapper">
<Search size={20} className="home-hero-search-icon" />
<input
type="text"
className="home-hero-search-input"
placeholder="Search for tools, categories, or features..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button type="submit" className="home-hero-search-btn">
<span>Search</span>
<ArrowRight size={16} />
</button>
</div>
</form>
{/* Popular searches */}
<div className="home-hero-popular">
<span className="home-hero-popular-label">Popular:</span>
{popularSearches.map((search) => (
<button
key={search}
type="button"
className="home-hero-popular-tag"
onClick={() => setSearchQuery(search)}
>
{search}
</button>
))}
</div>
</div>
{/* Discovery Panels */}
<div className="home-hero-panels" id="home-discovery">
<Link
to="/skills"
@@ -130,19 +188,25 @@ function SkillsHome() {
}}
className="home-hero-panel"
>
<span className="home-hero-panel-label">Skills</span>
<strong>Browse ranked skill bundles</strong>
<span>Popular installs, fresh updates, staff picks.</span>
<div className="home-hero-panel-icon">
<Zap size={20} />
</div>
<strong>Skills</strong>
<span>Browse ranked skill bundles</span>
</Link>
<Link to="/plugins" className="home-hero-panel">
<span className="home-hero-panel-label">Plugins</span>
<strong>Find agent-ready packages</strong>
<span>Code plugins, bundles, and verified publishers.</span>
<div className="home-hero-panel-icon">
<Code2 size={20} />
</div>
<strong>Plugins</strong>
<span>Agent-ready packages</span>
</Link>
<Link to="/users" search={{ q: undefined }} className="home-hero-panel">
<span className="home-hero-panel-label">Users</span>
<strong>Meet the builders</strong>
<span>Profiles, bios, and the people shipping useful stuff.</span>
<div className="home-hero-panel-icon">
<Users size={20} />
</div>
<strong>Builders</strong>
<span>Meet the creators</span>
</Link>
<Link
to="/souls"
@@ -155,20 +219,51 @@ function SkillsHome() {
}}
className="home-hero-panel"
>
<span className="home-hero-panel-label">Souls</span>
<strong>SOUL.md discovery is coming</strong>
<span>Holding page for the next catalog surface.</span>
<div className="home-hero-panel-icon">
<Ghost size={20} />
</div>
<strong>Souls</strong>
<span>SOUL.md discovery</span>
</Link>
</div>
</div>
</div>
</section>
{/* Stats Bar */}
<section className="home-section">
<div className="home-stats">
<div className="home-stat">
<div className="home-stat-value">
{skillCount != null ? formatCompactStat(skillCount) : "2.4K"}+
</div>
<div className="home-stat-label">Curated Tools</div>
</div>
<div className="home-stat">
<div className="home-stat-value">180K+</div>
<div className="home-stat-label">Active Users</div>
</div>
<div className="home-stat">
<div className="home-stat-value">12M+</div>
<div className="home-stat-label">Total Downloads</div>
</div>
<div className="home-stat">
<div className="home-stat-value">4.8</div>
<div className="home-stat-label">Avg. Rating</div>
</div>
</div>
</section>
{/* Trending */}
{trending.length > 0 ? (
<section className="home-section">
<div className="home-section-header">
<h2 className="home-section-title">Trending</h2>
<h2 className="home-section-title">
<span className="home-section-title-icon trending">
<TrendingUp size={16} />
</span>
Trending Now
</h2>
<Link
to="/skills"
search={{
@@ -182,7 +277,8 @@ function SkillsHome() {
}}
className="home-section-link"
>
See all
View all
<ArrowRight size={14} />
</Link>
</div>
<div className="results-list">
@@ -202,7 +298,12 @@ function SkillsHome() {
{recent.length > 0 ? (
<section className="home-section">
<div className="home-section-header">
<h2 className="home-section-title">Recently updated</h2>
<h2 className="home-section-title">
<span className="home-section-title-icon recent">
<Sparkles size={16} />
</span>
Recently Updated
</h2>
<Link
to="/skills"
search={{
@@ -216,7 +317,8 @@ function SkillsHome() {
}}
className="home-section-link"
>
See all
View all
<ArrowRight size={14} />
</Link>
</div>
<div className="results-list">
@@ -236,7 +338,12 @@ function SkillsHome() {
{highlighted.length > 0 ? (
<section className="home-section">
<div className="home-section-header">
<h2 className="home-section-title">Staff picks</h2>
<h2 className="home-section-title">
<span className="home-section-title-icon featured">
<Star size={16} />
</span>
Staff Picks
</h2>
<Link
to="/skills"
search={{
@@ -250,12 +357,12 @@ function SkillsHome() {
}}
className="home-section-link"
>
See all
View all
<ArrowRight size={14} />
</Link>
</div>
<div className="grid">
{
highlighted.map((entry) => (
{highlighted.map((entry) => (
<SkillCard
key={entry.skill._id}
skill={entry.skill}
@@ -280,41 +387,94 @@ function SkillsHome() {
</section>
) : null}
{/* Categories */}
<section className="home-section">
<div className="home-section-header">
<h2 className="home-section-title">Browse by Category</h2>
</div>
<div className="home-categories">
{categories.map((category) => (
<Link
key={category.name}
to="/skills"
search={{
q: category.name,
sort: undefined,
dir: undefined,
highlighted: undefined,
nonSuspicious: true,
view: undefined,
focus: undefined,
}}
className="home-category-card"
>
<div className={`home-category-icon ${category.className}`}>{category.icon}</div>
<div className="home-category-content">
<div className="home-category-name">{category.name}</div>
<div className="home-category-count">{category.count} tools</div>
</div>
</Link>
))}
</div>
</section>
{/* Quick links */}
<section className="home-section">
<div className="home-quick-links">
<Link
to="/skills"
search={{ q: undefined, sort: "stars" as const, dir: "desc" as const, highlighted: undefined, nonSuspicious: true, view: undefined, focus: undefined }}
search={{
q: undefined,
sort: "stars" as const,
dir: "desc" as const,
highlighted: undefined,
nonSuspicious: true,
view: undefined,
focus: undefined,
}}
className="home-quick-link"
>
<Star size={14} className="home-quick-link-icon" />
Most starred
</Link>
<Link
to="/skills"
search={{ q: undefined, sort: "newest" as const, dir: undefined, highlighted: undefined, nonSuspicious: true, view: undefined, focus: undefined }}
search={{
q: undefined,
sort: "newest" as const,
dir: undefined,
highlighted: undefined,
nonSuspicious: true,
view: undefined,
focus: undefined,
}}
className="home-quick-link"
>
<Sparkles size={14} className="home-quick-link-icon" />
New this week
</Link>
<Link to="/plugins" className="home-quick-link">
<Code2 size={14} className="home-quick-link-icon" />
Browse plugins
</Link>
<Link to="/users" search={{ q: undefined }} className="home-quick-link">
<Users size={14} className="home-quick-link-icon" />
Browse users
</Link>
<Link
to="/souls"
search={{ q: undefined, sort: undefined, dir: undefined, view: undefined, focus: undefined }}
className="home-quick-link"
>
Souls coming soon
</Link>
<Link
to="/skills"
search={{ q: undefined, sort: undefined, dir: undefined, highlighted: true, nonSuspicious: undefined, view: undefined, focus: undefined }}
search={{
q: undefined,
sort: undefined,
dir: undefined,
highlighted: true,
nonSuspicious: undefined,
view: undefined,
focus: undefined,
}}
className="home-quick-link"
>
<Star size={14} className="home-quick-link-icon" />
Staff picks
</Link>
</div>
@@ -323,7 +483,6 @@ function SkillsHome() {
);
}
function OnlyCrabsHome() {
const navigate = Route.useNavigate();
const ensureSoulSeeds = useAction(api.seed.ensureSoulSeeds);
@@ -344,8 +503,13 @@ function OnlyCrabsHome() {
<div className="home-hero-inner">
<div className="home-hero-grid">
<div className="home-hero-copy">
<div className="home-hero-kicker">OnlyCrabs</div>
<h1 className="home-hero-title">SoulHub, where system lore lives.</h1>
<div className="home-hero-kicker">
<Ghost size={14} className="home-hero-kicker-icon" />
<span>OnlyCrabs</span>
</div>
<h1 className="home-hero-title">
<span className="home-hero-title-accent">SoulHub</span>, where system lore lives.
</h1>
<p className="home-hero-subtitle">
Share SOUL.md bundles, version them like docs, and keep personal system lore in one
public place.
@@ -366,16 +530,20 @@ function OnlyCrabsHome() {
});
}}
>
<input
className="home-hero-search-input"
type="text"
placeholder="Search souls, prompts, or lore"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
<Button variant="primary" type="submit">
Search
</Button>
<div className="home-hero-search-wrapper">
<Search size={20} className="home-hero-search-icon" />
<input
className="home-hero-search-input"
type="text"
placeholder="Search souls, prompts, or lore"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
<button type="submit" className="home-hero-search-btn">
<span>Search</span>
<ArrowRight size={16} />
</button>
</div>
</form>
</div>
</div>
@@ -384,7 +552,12 @@ function OnlyCrabsHome() {
<section className="home-section">
<div className="home-section-header">
<h2 className="home-section-title">Latest souls</h2>
<h2 className="home-section-title">
<span className="home-section-title-icon recent">
<Sparkles size={16} />
</span>
Latest Souls
</h2>
<Link
to="/souls"
search={{
@@ -396,7 +569,8 @@ function OnlyCrabsHome() {
}}
className="home-section-link"
>
See all
View all
<ArrowRight size={14} />
</Link>
</div>
<div className="grid">
+13 -45
View File
@@ -14,7 +14,6 @@ import {
fetchPackageReadme,
fetchPackageVersion,
getPackageDownloadPath,
isRateLimitedPackageApiError,
type PackageDetailResponse,
type PackageVersionDetail,
} from "../../lib/packageApi";
@@ -36,6 +35,7 @@ type PluginDetailLoaderData = {
export const Route = createFileRoute("/plugins/$name")({
loader: async ({ params }): Promise<PluginDetailLoaderData> => {
// All fetch functions now handle errors internally and return null/empty on failure
const requestedName = params.name;
const candidateNames = requestedName.includes("/")
? [requestedName]
@@ -43,24 +43,9 @@ export const Route = createFileRoute("/plugins/$name")({
let resolvedName = requestedName;
let detail: PackageDetailResponse = { package: null, owner: null };
for (const candidateName of candidateNames) {
let candidateDetail: PackageDetailResponse;
try {
candidateDetail = await fetchPackageDetail(candidateName);
} catch (error) {
if (isRateLimitedPackageApiError(error)) {
return {
detail: { package: null, owner: null },
version: null,
readme: null,
rateLimited: {
scope: "detail",
retryAfterSeconds: error.retryAfterSeconds,
},
};
}
throw error;
}
const candidateDetail = await fetchPackageDetail(candidateName);
if (candidateDetail.package) {
detail = candidateDetail;
resolvedName = candidateName;
@@ -70,35 +55,18 @@ export const Route = createFileRoute("/plugins/$name")({
}
if (!detail.package) {
return {
detail,
version: null,
readme: null,
rateLimited: null,
};
return { detail, version: null, readme: null, rateLimited: null };
}
let metadataRateLimited: PluginDetailRateLimitState = null;
const readmePromise = fetchPackageReadme(resolvedName).catch((error: unknown) => {
if (!isRateLimitedPackageApiError(error)) throw error;
metadataRateLimited ??= {
scope: "metadata",
retryAfterSeconds: error.retryAfterSeconds,
};
return null;
});
const versionPromise = detail.package?.latestVersion
? fetchPackageVersion(resolvedName, detail.package.latestVersion).catch((error: unknown) => {
if (!isRateLimitedPackageApiError(error)) throw error;
metadataRateLimited ??= {
scope: "metadata",
retryAfterSeconds: error.retryAfterSeconds,
};
return null;
})
: Promise.resolve(null);
const [version, readme] = await Promise.all([versionPromise, readmePromise]);
return { detail, version, readme, rateLimited: metadataRateLimited };
// Fetch readme and version in parallel - functions handle errors internally
const [version, readme] = await Promise.all([
detail.package.latestVersion
? fetchPackageVersion(resolvedName, detail.package.latestVersion)
: Promise.resolve(null),
fetchPackageReadme(resolvedName),
]);
return { detail, version, readme, rateLimited: null };
},
head: ({ params, loaderData }) => ({
meta: [
+39 -32
View File
@@ -6,7 +6,6 @@ import { PluginListItem } from "../../components/PluginListItem";
import { Button } from "../../components/ui/button";
import {
fetchPluginCatalog,
isRateLimitedPackageApiError,
type PackageListItem,
} from "../../lib/packageApi";
@@ -23,6 +22,7 @@ type PluginsLoaderData = {
nextCursor: string | null;
rateLimited: boolean;
retryAfterSeconds: number | null;
apiError?: boolean;
};
function formatRetryDelay(retryAfterSeconds: number | null) {
@@ -54,42 +54,41 @@ export const Route = createFileRoute("/plugins/")({
: undefined,
}),
loaderDeps: ({ search }) => search,
loader: async ({ deps }) => {
try {
const data = await fetchPluginCatalog({
q: deps.q,
cursor: deps.q ? undefined : deps.cursor,
family: deps.family,
isOfficial: deps.verified,
executesCode: deps.executesCode,
limit: 50,
});
return {
items: data.items ?? [],
nextCursor: data.nextCursor ?? null,
rateLimited: false,
retryAfterSeconds: null,
} satisfies PluginsLoaderData;
} catch (error) {
if (isRateLimitedPackageApiError(error)) {
return {
items: [],
nextCursor: null,
rateLimited: true,
retryAfterSeconds: (error as { retryAfterSeconds?: number }).retryAfterSeconds ?? null,
} satisfies PluginsLoaderData;
}
throw error;
}
loader: async ({ deps }): Promise<PluginsLoaderData> => {
// fetchPluginCatalog now handles errors internally and returns empty results
const data = await fetchPluginCatalog({
q: deps.q,
cursor: deps.q ? undefined : deps.cursor,
family: deps.family,
isOfficial: deps.verified,
executesCode: deps.executesCode,
limit: 50,
});
const items = data?.items ?? [];
return {
items,
nextCursor: data?.nextCursor ?? null,
rateLimited: false,
retryAfterSeconds: null,
apiError: items.length === 0 && !deps.q && !deps.family && !deps.verified && !deps.executesCode,
};
},
component: PluginsIndex,
});
export function PluginsIndex() {
function PluginsIndex() {
const search = Route.useSearch();
const navigate = Route.useNavigate();
const { items, nextCursor, rateLimited, retryAfterSeconds } =
Route.useLoaderData() as PluginsLoaderData;
const loaderData = Route.useLoaderData() as PluginsLoaderData | undefined;
// Defensive handling for when loader data is unavailable (SSR errors, etc.)
const items = loaderData?.items ?? [];
const nextCursor = loaderData?.nextCursor ?? null;
const rateLimited = loaderData?.rateLimited ?? false;
const retryAfterSeconds = loaderData?.retryAfterSeconds ?? null;
const apiError = loaderData?.apiError ?? !loaderData;
const [query, setQuery] = useState(search.q ?? "");
const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -201,7 +200,15 @@ export function PluginsIndex() {
</span>
</div>
{rateLimited ? (
{apiError ? (
<div className="empty-state">
<AlertTriangle size={20} aria-hidden="true" />
<p className="empty-state-title">Unable to load plugins</p>
<p className="empty-state-body">
The plugin catalog is temporarily unavailable. Please try again later.
</p>
</div>
) : rateLimited ? (
<div className="empty-state">
<AlertTriangle size={20} aria-hidden="true" />
<p className="empty-state-title">Plugin catalog is temporarily unavailable</p>
+351 -1
View File
@@ -1,15 +1,28 @@
import { createFileRoute } from "@tanstack/react-router";
import { useMutation, useQuery } from "convex/react";
import {
Eye,
Grid3X3,
LayoutGrid,
List,
Monitor,
Moon,
RotateCcw,
Settings2,
Sparkles,
Sun,
Zap,
} from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { api } from "../../convex/_generated/api";
import type { Id } from "../../convex/_generated/dataModel";
import { Container } from "../components/layout/Container";
import { SignInButton } from "../components/SignInButton";
import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar";
import { Badge } from "../components/ui/badge";
import { Button } from "../components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
import { SignInButton } from "../components/SignInButton";
import {
Dialog,
DialogContent,
@@ -21,9 +34,25 @@ import {
} from "../components/ui/dialog";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../components/ui/select";
import { Separator } from "../components/ui/separator";
import { Switch } from "../components/ui/switch";
import { Textarea } from "../components/ui/textarea";
import { gravatarUrl } from "../lib/gravatar";
import {
type AnimationLevel,
type CodeFontSize,
type LayoutDensity,
type ListViewMode,
usePreferences,
} from "../lib/preferences";
import { useThemeMode } from "../lib/theme";
export const Route = createFileRoute("/settings")({
component: Settings,
@@ -33,6 +62,8 @@ export function Settings() {
const me = useQuery(api.users.me);
const updateProfile = useMutation(api.users.updateProfile);
const deleteAccount = useMutation(api.users.deleteAccount);
const { mode: themeMode, setMode: setThemeMode } = useThemeMode();
const { preferences, updatePreference, resetPreferences, isAdvancedMode } = usePreferences();
const tokens = useQuery(api.tokens.listMine, me ? {} : "skip") as
| Array<{
_id: Id<"apiTokens">;
@@ -208,6 +239,325 @@ export function Settings() {
</form>
</Card>
{/* Customization */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Settings2 size={18} />
Customization
</CardTitle>
<CardDescription>
Personalize your ClawHub experience
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="advanced-mode" className="text-sm text-[color:var(--ink-soft)]">
Advanced
</Label>
<Switch
id="advanced-mode"
checked={isAdvancedMode}
onCheckedChange={(checked) => updatePreference("advancedMode", checked)}
/>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Theme Section */}
<div className="space-y-3">
<Label className="text-sm font-semibold text-[color:var(--ink)]">Theme</Label>
<div className="flex gap-2">
<Button
variant={themeMode === "light" ? "primary" : "ghost"}
size="sm"
onClick={() => setThemeMode("light")}
className="flex items-center gap-2"
>
<Sun size={14} />
Light
</Button>
<Button
variant={themeMode === "dark" ? "primary" : "ghost"}
size="sm"
onClick={() => setThemeMode("dark")}
className="flex items-center gap-2"
>
<Moon size={14} />
Dark
</Button>
<Button
variant={themeMode === "system" ? "primary" : "ghost"}
size="sm"
onClick={() => setThemeMode("system")}
className="flex items-center gap-2"
>
<Monitor size={14} />
System
</Button>
</div>
</div>
<Separator />
{/* Layout Section */}
<div className="space-y-4">
<Label className="text-sm font-semibold text-[color:var(--ink)]">Layout</Label>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="layout-density" className="text-xs text-[color:var(--ink-soft)]">
Density
</Label>
<Select
value={preferences.layoutDensity}
onValueChange={(value) => updatePreference("layoutDensity", value as LayoutDensity)}
>
<SelectTrigger id="layout-density">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="comfortable">
<span className="flex items-center gap-2">
<LayoutGrid size={14} />
Comfortable
</span>
</SelectItem>
<SelectItem value="compact">
<span className="flex items-center gap-2">
<Grid3X3 size={14} />
Compact
</span>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="list-view" className="text-xs text-[color:var(--ink-soft)]">
Default view
</Label>
<Select
value={preferences.listViewMode}
onValueChange={(value) => updatePreference("listViewMode", value as ListViewMode)}
>
<SelectTrigger id="list-view">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="grid">
<span className="flex items-center gap-2">
<LayoutGrid size={14} />
Grid
</span>
</SelectItem>
<SelectItem value="list">
<span className="flex items-center gap-2">
<List size={14} />
List
</span>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="show-descriptions" className="text-sm">Show descriptions</Label>
<Switch
id="show-descriptions"
checked={preferences.showDescriptions}
onCheckedChange={(checked) => updatePreference("showDescriptions", checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="show-stats" className="text-sm">Show statistics</Label>
<Switch
id="show-stats"
checked={preferences.showStats}
onCheckedChange={(checked) => updatePreference("showStats", checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="show-tags" className="text-sm">Show tags</Label>
<Switch
id="show-tags"
checked={preferences.showTags}
onCheckedChange={(checked) => updatePreference("showTags", checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="sticky-header" className="text-sm">Sticky header</Label>
<Switch
id="sticky-header"
checked={preferences.stickyHeader}
onCheckedChange={(checked) => updatePreference("stickyHeader", checked)}
/>
</div>
</div>
</div>
{isAdvancedMode ? (
<>
<Separator />
{/* Code & Content Section - Advanced */}
<div className="space-y-4">
<Label className="text-sm font-semibold text-[color:var(--ink)] flex items-center gap-2">
<Sparkles size={14} className="text-[color:var(--accent)]" />
Code &amp; Content
</Label>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="code-font-size" className="text-xs text-[color:var(--ink-soft)]">
Code font size
</Label>
<Select
value={preferences.codeFontSize}
onValueChange={(value) => updatePreference("codeFontSize", value as CodeFontSize)}
>
<SelectTrigger id="code-font-size">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="small">Small (12px)</SelectItem>
<SelectItem value="medium">Medium (14px)</SelectItem>
<SelectItem value="large">Large (16px)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="animation-level" className="text-xs text-[color:var(--ink-soft)]">
Animation level
</Label>
<Select
value={preferences.animationLevel}
onValueChange={(value) => updatePreference("animationLevel", value as AnimationLevel)}
>
<SelectTrigger id="animation-level">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="full">
<span className="flex items-center gap-2">
<Zap size={14} />
Full
</span>
</SelectItem>
<SelectItem value="reduced">Reduced</SelectItem>
<SelectItem value="none">None</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="line-numbers" className="text-sm">Line numbers in code</Label>
<Switch
id="line-numbers"
checked={preferences.lineNumbers}
onCheckedChange={(checked) => updatePreference("lineNumbers", checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="word-wrap" className="text-sm">Word wrap in code</Label>
<Switch
id="word-wrap"
checked={preferences.wordWrap}
onCheckedChange={(checked) => updatePreference("wordWrap", checked)}
/>
</div>
</div>
</div>
<Separator />
{/* Accessibility Section - Advanced */}
<div className="space-y-4">
<Label className="text-sm font-semibold text-[color:var(--ink)] flex items-center gap-2">
<Eye size={14} className="text-[color:var(--accent)]" />
Accessibility
</Label>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<Label htmlFor="reduced-motion" className="text-sm">Reduced motion</Label>
<p className="text-xs text-[color:var(--ink-soft)]">Minimize animations</p>
</div>
<Switch
id="reduced-motion"
checked={preferences.reducedMotion}
onCheckedChange={(checked) => updatePreference("reducedMotion", checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="high-contrast" className="text-sm">High contrast</Label>
<p className="text-xs text-[color:var(--ink-soft)]">Increase color contrast</p>
</div>
<Switch
id="high-contrast"
checked={preferences.highContrast}
onCheckedChange={(checked) => updatePreference("highContrast", checked)}
/>
</div>
</div>
</div>
<Separator />
{/* Experimental Features - Advanced */}
<div className="space-y-4">
<Label className="text-sm font-semibold text-[color:var(--ink)] flex items-center gap-2">
<Sparkles size={14} className="text-[color:var(--gold)]" />
Experimental
</Label>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="experimental-features" className="text-sm">Enable experimental features</Label>
<p className="text-xs text-[color:var(--ink-soft)]">Try new features before they&apos;re released</p>
</div>
<Switch
id="experimental-features"
checked={preferences.experimentalFeatures}
onCheckedChange={(checked) => updatePreference("experimentalFeatures", checked)}
/>
</div>
</div>
</>
) : null}
<Separator />
{/* Reset Section */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[color:var(--ink)]">Reset preferences</p>
<p className="text-xs text-[color:var(--ink-soft)]">Restore all settings to defaults</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
resetPreferences();
toast.success("Preferences reset to defaults");
}}
className="flex items-center gap-2"
>
<RotateCcw size={14} />
Reset
</Button>
</div>
</CardContent>
</Card>
{/* Organizations */}
<Card>
<CardHeader>
+831 -373
View File
File diff suppressed because it is too large Load Diff
+2 -5
View File
@@ -6,7 +6,6 @@ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { nitro } from "nitro/vite";
import { defineConfig, type Plugin } from "vite";
import viteTsConfigPaths from "vite-tsconfig-paths";
const require = createRequire(import.meta.url);
@@ -166,6 +165,8 @@ const config = defineConfig({
"convex/values": convexValuesPath,
"@convex-dev/auth/react": convexAuthReactPath,
},
// Use native Vite tsconfig paths resolution instead of the plugin
tsconfigPaths: true,
},
optimizeDeps: {
include: ["convex/react", "convex/browser"],
@@ -179,10 +180,6 @@ const config = defineConfig({
onwarn: handleRollupWarning,
},
}),
// this is the plugin that enables path aliases
viteTsConfigPaths({
projects: ["./tsconfig.json"],
}),
tailwindcss(),
tanstackStart(),
viteReact(),