diff --git a/apps/webui/components.json b/apps/webui/components.json new file mode 100644 index 0000000..db30f5b --- /dev/null +++ b/apps/webui/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/app.css", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/styles/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/apps/webui/index.html b/apps/webui/index.html index 596c6c0..1847814 100644 --- a/apps/webui/index.html +++ b/apps/webui/index.html @@ -4,6 +4,20 @@ + diff --git a/apps/webui/package.json b/apps/webui/package.json index 95d3884..b95942b 100644 --- a/apps/webui/package.json +++ b/apps/webui/package.json @@ -10,49 +10,79 @@ }, "dependencies": { "@abraham/reflection": "^0.12.0", - "@ark-ui/solid": "^4.10.2", "@codemirror/language": "6.0.0", "@corvu/drawer": "^0.2.3", "@corvu/otp-field": "^0.1.4", "@corvu/resizable": "^0.2.4", "@graphiql/toolkit": "^0.11.1", - "@kobalte/core": "^0.13.9", - "@kobalte/tailwindcss": "^0.9.0", + "@hookform/resolvers": "^5.0.1", "@outposts/injection-js": "^2.5.1", - "@solid-primitives/graphql": "^2.2.0", - "@solid-primitives/refs": "^1.1.0", - "@tailwindcss/postcss": "^4.0.9", + "@radix-ui/react-accordion": "^1.2.7", + "@radix-ui/react-alert-dialog": "^1.1.10", + "@radix-ui/react-aspect-ratio": "^1.1.4", + "@radix-ui/react-avatar": "^1.1.6", + "@radix-ui/react-checkbox": "^1.2.2", + "@radix-ui/react-collapsible": "^1.1.7", + "@radix-ui/react-context-menu": "^2.2.11", + "@radix-ui/react-dialog": "^1.1.10", + "@radix-ui/react-dropdown-menu": "^2.1.11", + "@radix-ui/react-hover-card": "^1.1.10", + "@radix-ui/react-label": "^2.1.4", + "@radix-ui/react-menubar": "^1.1.11", + "@radix-ui/react-navigation-menu": "^1.2.9", + "@radix-ui/react-popover": "^1.1.10", + "@radix-ui/react-progress": "^1.1.4", + "@radix-ui/react-radio-group": "^1.3.3", + "@radix-ui/react-scroll-area": "^1.2.5", + "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-separator": "^1.1.4", + "@radix-ui/react-slider": "^1.3.2", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-switch": "^1.2.2", + "@radix-ui/react-tabs": "^1.1.8", + "@radix-ui/react-toggle": "^1.1.6", + "@radix-ui/react-toggle-group": "^1.1.7", + "@radix-ui/react-tooltip": "^1.2.3", + "@rsbuild/plugin-react": "^1.2.0", "@tanstack/react-router": "^1.112.13", "@tanstack/router-devtools": "^1.112.13", - "@tanstack/solid-router": "^1.112.12", "arktype": "^2.1.6", "chart.js": "^4.4.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cmdk-solid": "^1.1.2", - "embla-carousel-solid": "^8.5.2", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", "graphiql": "^3.8.3", - "lucide-solid": "^0.477.0", + "input-otp": "^1.4.2", + "jotai": "^2.12.3", + "jotai-signal": "^0.9.0", + "lucide-react": "^0.503.0", + "next-themes": "^0.4.6", "oidc-client-rx": "0.1.0-alpha.9", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.1.0", + "react-day-picker": "8.10.1", + "react-dom": "^19.1.0", + "react-hook-form": "^7.56.0", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.15.3", "rxjs": "^7.8.2", - "solid-js": "^1.9.5", - "solid-sonner": "^0.2.8", - "tailwind-merge": "^3.0.2", - "tailwindcss": "^3.4.17", - "tailwindcss-animate": "^1.0.7" + "sonner": "^2.0.3", + "tailwind-merge": "^3.2.0", + "tailwindcss": "^4.0.6", + "tw-animate-css": "^1.2.7", + "type-fest": "^4.40.0", + "vaul": "^1.1.2", + "zod": "^3.24.3" }, "devDependencies": { "@rsbuild/core": "^1.2.15", - "@rsbuild/plugin-babel": "^1.0.4", - "@rsbuild/plugin-solid": "^1.0.5", "@tailwindcss/postcss": "^4.0.9", "@tanstack/react-router": "^1.112.0", "@tanstack/router-devtools": "^1.112.6", "@tanstack/router-plugin": "^1.112.13", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", "chalk": "^5.4.1", "commander": "^13.1.0", "postcss": "^8.5.3" diff --git a/apps/webui/postcss.config.mjs b/apps/webui/postcss.config.mjs index 684db48..7595f17 100644 --- a/apps/webui/postcss.config.mjs +++ b/apps/webui/postcss.config.mjs @@ -1,5 +1,5 @@ export default { plugins: { - 'tailwindcss': {}, + '@tailwindcss/postcss': {}, }, -}; +}; \ No newline at end of file diff --git a/apps/webui/rsbuild.config.ts b/apps/webui/rsbuild.config.ts index 78d34b8..8252ac2 100644 --- a/apps/webui/rsbuild.config.ts +++ b/apps/webui/rsbuild.config.ts @@ -1,6 +1,5 @@ import { defineConfig } from '@rsbuild/core'; -import { pluginBabel } from '@rsbuild/plugin-babel'; -import { pluginSolid } from '@rsbuild/plugin-solid'; +import { pluginReact } from '@rsbuild/plugin-react'; import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack'; export default defineConfig({ @@ -8,16 +7,11 @@ export default defineConfig({ title: 'Konobangu', favicon: './public/assets/favicon.ico', }, - plugins: [ - pluginBabel({ - include: /\.(?:jsx|tsx)$/, - }), - pluginSolid(), - ], + plugins: [pluginReact()], tools: { rspack: { plugins: [ - TanStackRouterRspack({ target: 'solid', autoCodeSplitting: true }), + TanStackRouterRspack({ target: 'react', autoCodeSplitting: true }), ], }, }, diff --git a/apps/webui/src/app.css b/apps/webui/src/app.css index 3d968b8..11ec438 100644 --- a/apps/webui/src/app.css +++ b/apps/webui/src/app.css @@ -1,149 +1,141 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@import "tw-animate-css"; -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; +@custom-variant dark (&:is(.dark *)); - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - - --info: 204 94% 94%; - --info-foreground: 199 89% 48%; - - --success: 149 80% 90%; - --success-foreground: 160 84% 39%; - - --warning: 48 96% 89%; - --warning-foreground: 25 95% 53%; - - --error: 0 93% 94%; - --error-foreground: 0 84% 60%; - - --ring: 240 5.9% 10%; - - --radius: 0.5rem; - - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } } - .dark, - [data-kb-theme="dark"] { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - - --info: 204 94% 94%; - --info-foreground: 199 89% 48%; - - --success: 149 80% 90%; - --success-foreground: 160 84% 39%; - - --warning: 48 96% 89%; - --warning-foreground: 25 95% 53%; - - --error: 0 93% 94%; - --error-foreground: 0 84% 60%; - - --ring: 240 4.9% 83.9%; - - --radius: 0.5rem; - - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } } - - /* custom start */ - ::-webkit-scrollbar { - width: 16px; - } - - ::-webkit-scrollbar-thumb { - border-radius: 9999px; - border: 4px solid transparent; - background-clip: content-box; - @apply bg-accent; - } - - ::-webkit-scrollbar-corner { - display: none; - } - /* custom end */ } @layer base { * { - @apply border-border; + @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; - font-feature-settings: - "rlig" 1, - "calt" 1; } -} - -@media (max-width: 640px) { - .container { - @apply px-4; - } -} +} \ No newline at end of file diff --git a/apps/webui/src/auth/config.ts b/apps/webui/src/auth/config.ts index 1e5c00f..df98564 100644 --- a/apps/webui/src/auth/config.ts +++ b/apps/webui/src/auth/config.ts @@ -1,7 +1,14 @@ import { LogLevel, type OpenIdConfiguration } from 'oidc-client-rx'; +import type { ValueOf } from 'type-fest'; -export const isBasicAuth = process.env.AUTH_TYPE === 'basic'; -export const isOidcAuth = process.env.AUTH_TYPE === 'oidc'; +export const AuthMethodEnum = { + BASIC: 'basic', + OIDC: 'oidc', +} as const; + +export type AuthMethodType = ValueOf; + +export const AppAuthMethod = process.env.AUTH_TYPE as AuthMethodType; export function buildOidcConfig(): OpenIdConfiguration { const origin = window.location.origin; diff --git a/apps/webui/src/auth/context.ts b/apps/webui/src/auth/context.ts index e683480..eba92e7 100644 --- a/apps/webui/src/auth/context.ts +++ b/apps/webui/src/auth/context.ts @@ -1,4 +1,85 @@ -import { createSignal } from 'solid-js'; -import { isBasicAuth } from './config'; +import { UnreachableError } from '@/errors/common'; +import type { Injector, Provider } from '@outposts/injection-js'; +import type { AnyRouter } from '@tanstack/react-router'; +import { atomSignal } from 'jotai-signal'; +import type { Atom } from 'jotai/vanilla'; +import { + CHECK_AUTH_RESULT_EVENT, + type CheckAuthResultEventType, + OidcSecurityService, + provideAuth as provideOidcAuth, + withCheckAuthResultEvent, + withDefaultFeatures, +} from 'oidc-client-rx'; +import { withTanstackRouter } from 'oidc-client-rx/adapters/@tanstack/react-router'; +import { NEVER, type Observable, map, of } from 'rxjs'; +import { AppAuthMethod, AuthMethodEnum, buildOidcConfig } from './config'; -export const [isAuthenticated, setIsAuthenticated] = createSignal(isBasicAuth); +export function provideAuth(router: AnyRouter): Provider[] { + if (AppAuthMethod === AuthMethodEnum.OIDC) { + return provideOidcAuth( + { + config: buildOidcConfig(), + }, + withDefaultFeatures({ + router: { enabled: false }, + securityStorage: { type: 'local-storage' }, + }), + withTanstackRouter(router), + withCheckAuthResultEvent() + ); + } + return []; +} + +export function setupAuthContext(injector: Injector) { + if (AppAuthMethod === AuthMethodEnum.OIDC) { + const oidcSecurityService = injector.get(OidcSecurityService); + oidcSecurityService.checkAuth().subscribe(); + } +} +export interface OidcAuthContext { + type: typeof AuthMethodEnum.OIDC; + oidcSecurityService: OidcSecurityService; + isAuthenticated$: Observable; + userData$: Observable; + checkAuthResultEvent$: Observable; +} + +export interface BasicAuthContext { + type: typeof AuthMethodEnum.BASIC; + isAuthenticated$: Observable; + userData$: Observable<{}>; + checkAuthResultEvent$: Observable; +} + +export type AuthContext = OidcAuthContext | BasicAuthContext; + +const BASIC_AUTH_IS_AUTHENTICATED$ = of(true) as Observable; + +const BASIC_AUTH_USER_DATA$ = of({}); + +export function authContextFromInjector(injector: Injector): AuthContext { + if (AppAuthMethod === AuthMethodEnum.OIDC) { + const oidcSecurityService = injector.get(OidcSecurityService)!; + + return { + type: AuthMethodEnum.OIDC, + oidcSecurityService: injector.get(OidcSecurityService), + isAuthenticated$: oidcSecurityService.isAuthenticated$.pipe( + map((s) => s.isAuthenticated) + ), + userData$: oidcSecurityService.userData$.pipe(map((s) => s.userData)), + checkAuthResultEvent$: injector.get(CHECK_AUTH_RESULT_EVENT), + }; + } + if (AppAuthMethod === AuthMethodEnum.BASIC) { + return { + type: AuthMethodEnum.BASIC, + isAuthenticated$: BASIC_AUTH_IS_AUTHENTICATED$, + userData$: BASIC_AUTH_USER_DATA$, + checkAuthResultEvent$: NEVER, + }; + } + throw new UnreachableError('Invalid auth method'); +} diff --git a/apps/webui/src/auth/guard.ts b/apps/webui/src/auth/guard.ts index f5a37bf..efafafd 100644 --- a/apps/webui/src/auth/guard.ts +++ b/apps/webui/src/auth/guard.ts @@ -1,12 +1,14 @@ +import type { RouterContext } from '@/traits/router'; import { runInInjectionContext } from '@outposts/injection-js'; import { autoLoginPartialRoutesGuard } from 'oidc-client-rx'; import { firstValueFrom } from 'rxjs'; -import type { RouterContext } from '~/traits/router'; +import { authContextFromInjector } from './context'; export const beforeLoadGuard = async ({ context, }: { context: RouterContext }) => { - if (!context.isAuthenticated()) { + const { isAuthenticated$ } = authContextFromInjector(context.injector); + if (!(await firstValueFrom(isAuthenticated$))) { const guard$ = runInInjectionContext(context.injector, () => autoLoginPartialRoutesGuard() ); diff --git a/apps/webui/src/auth/hooks.ts b/apps/webui/src/auth/hooks.ts index 6fc7a0f..394a5fa 100644 --- a/apps/webui/src/auth/hooks.ts +++ b/apps/webui/src/auth/hooks.ts @@ -1,55 +1,34 @@ -import { CHECK_AUTH_RESULT_EVENT } from 'oidc-client-rx'; -import { - InjectorContextVoidInjector, - useOidcClient, -} from 'oidc-client-rx/adapters/solid-js'; -import { NEVER, map, of } from 'rxjs'; -import { from } from 'solid-js'; -import { isBasicAuth, isOidcAuth } from './config'; - -const BASIC_AUTH_IS_AUTHENTICATED$ = of({ - isAuthenticated: true, - allConfigsAuthenticated: [], -}); - -const BASIC_AUTH_USER_DATA$ = of({ - userData: {}, - allUserData: [], -}); - -const useOidcClientExt = isOidcAuth - ? useOidcClient - : () => ({ - oidcSecurityService: undefined, - injector: InjectorContextVoidInjector, - }); +import { atomWithObservable } from 'jotai/utils'; +import { useInjector } from 'oidc-client-rx/adapters/react'; +import { useMemo } from 'react'; +import type { Observable } from 'rxjs'; +import { authContextFromInjector } from './context'; export function useAuth() { - const { oidcSecurityService, injector } = useOidcClientExt(); + const injector = useInjector(); - const isAuthenticated$ = ( - oidcSecurityService?.isAuthenticated$ ?? BASIC_AUTH_IS_AUTHENTICATED$ - ).pipe(map((s) => s.isAuthenticated)); + const authContext = useMemo( + () => authContextFromInjector(injector), + [injector] + ); - const userData$ = ( - oidcSecurityService?.userData$ ?? BASIC_AUTH_USER_DATA$ - ).pipe(map((s) => s.userData)); + const isAuthenticated = useMemo( + () => + atomWithObservable( + () => authContext.isAuthenticated$ as Observable + ), + [authContext.isAuthenticated$] + ); - const isAuthenticated = from(isAuthenticated$); - - const userData = from(userData$); - - const checkAuthResultEvent$ = isBasicAuth - ? NEVER - : injector.get(CHECK_AUTH_RESULT_EVENT); + const userData = useMemo( + () => atomWithObservable(() => authContext.userData$ as Observable), + [authContext] + ); return { - oidcSecurityService, - isAuthenticated$, - isAuthenticated, - userData$, userData, injector, - checkAuthResultEvent$, + isAuthenticated, + authContext, }; } diff --git a/apps/webui/src/components/layout/app-icon.tsx b/apps/webui/src/components/layout/app-icon.tsx index 12fb759..b34be10 100644 --- a/apps/webui/src/components/layout/app-icon.tsx +++ b/apps/webui/src/components/layout/app-icon.tsx @@ -1,9 +1,10 @@ -import {Image} from '@kobalte/core/image'; import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, -} from '~/components/ui/sidebar'; +} from '@/components/ui/sidebar'; + +import { Image } from '@/components/ui/image'; export function AppIcon() { return ( @@ -11,21 +12,20 @@ export function AppIcon() { -
- - +
+ App Logo - KO - +
-
- Konobangu - @dumtruck +
+ Konobangu + @dumtruck
diff --git a/apps/webui/src/components/layout/app-layout.tsx b/apps/webui/src/components/layout/app-layout.tsx index ab54346..2ba4f02 100644 --- a/apps/webui/src/components/layout/app-layout.tsx +++ b/apps/webui/src/components/layout/app-layout.tsx @@ -1,54 +1,53 @@ -import { useMatches } from '@tanstack/solid-router'; -import { - type ComponentProps, - type FlowProps, - For, - Show, - createMemo, - splitProps, -} from 'solid-js'; -import { Dynamic } from 'solid-js/web'; -import { AppSidebar } from '~/components/layout/app-sidebar'; +import { AppSidebar } from '@/components/layout/app-sidebar'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, + BreadcrumbPage, BreadcrumbSeparator, -} from '~/components/ui/breadcrumb'; -import { Separator } from '~/components/ui/separator'; +} from '@/components/ui/breadcrumb'; +import { Separator } from '@/components/ui/separator'; import { SidebarInset, SidebarProvider, SidebarTrigger, -} from '~/components/ui/sidebar'; -import type { RouteStateDataOption } from '~/traits/router'; -import type { RouteBreadcrumbItem } from '~/traits/router'; -import { cn } from '~/utils/styles'; +} from '@/components/ui/sidebar'; +import { cn } from '@/styles/utils'; +import type { RouteStateDataOption } from '@/traits/router'; +import type { RouteBreadcrumbItem } from '@/traits/router'; +import { useMatches } from '@tanstack/react-router'; +import { + type DetailedHTMLProps, + Fragment, + type HTMLAttributes, + useMemo, +} from 'react'; import { ProLink } from '../ui/pro-link'; export type AppAsideBreadcrumbItem = RouteBreadcrumbItem; -export interface AppAsideProps extends FlowProps> { + +export interface AppAsideProps + extends DetailedHTMLProps, HTMLDivElement> { breadcrumb?: AppAsideBreadcrumbItem[]; extractBreadcrumbFromRoutes?: boolean; } -export function AppAside(props: AppAsideProps) { - const [local, other] = splitProps(props, [ - 'children', - 'class', - 'breadcrumb', - 'extractBreadcrumbFromRoutes', - ]); - +export function AppAside({ + children, + className, + breadcrumb: propBreadcrumb, + extractBreadcrumbFromRoutes, + ...other +}: AppAsideProps) { const matches = useMatches(); - const breadcrumb = createMemo(() => { - if (local.breadcrumb) { - return local.breadcrumb; + const breadcrumb = useMemo(() => { + if (propBreadcrumb) { + return propBreadcrumb; } - if (local.extractBreadcrumbFromRoutes) { - return matches() + if (extractBreadcrumbFromRoutes) { + return matches .map((m, i, arr) => { const staticData = m.staticData as RouteStateDataOption; if (staticData.breadcrumb) { @@ -67,61 +66,69 @@ export function AppAside(props: AppAsideProps) { .filter((b): b is AppAsideBreadcrumbItem => !!b); } return []; - }); + }, [matches, propBreadcrumb, extractBreadcrumbFromRoutes]); - const breadcrumbLength = breadcrumb().length; + const breadcrumbLength = breadcrumb.length; return ( -
-
- - - - - - - {(item, index) => { - const iconEl = ( - - - - ); +
+
+ + {breadcrumbLength > 0 && ( + <> + + + + {breadcrumb.map((item, index) => { + const iconEl = item.icon ? ( + + ) : null; - const isCurrent = index() + 1 === breadcrumbLength; + const isCurrent = index + 1 === breadcrumbLength; + const LinkChild = ( + item.link ? ProLink : Fragment + ) as typeof ProLink; return ( - <> - 0}> - - - - + })} + + + + )}
- {local.children} + {children}
diff --git a/apps/webui/src/components/layout/app-not-found.tsx b/apps/webui/src/components/layout/app-not-found.tsx index d756f7f..49cbf49 100644 --- a/apps/webui/src/components/layout/app-not-found.tsx +++ b/apps/webui/src/components/layout/app-not-found.tsx @@ -2,7 +2,7 @@ import { type AnyRoute, type ParsedLocation, redirect, -} from '@tanstack/solid-router'; +} from '@tanstack/react-router'; import { ProLink } from '../ui/pro-link'; export function guardRouteIndexAsNotFound( @@ -21,19 +21,19 @@ export function guardRouteIndexAsNotFound( export function AppNotFoundComponent() { return ( -
-
-
-

+
+
+
+

404 Page Not Found

-

+

Sorry, we couldn't find the page you're looking for.

Return to website diff --git a/apps/webui/src/components/layout/app-sidebar.tsx b/apps/webui/src/components/layout/app-sidebar.tsx index 4d9d26a..a80eb7c 100644 --- a/apps/webui/src/components/layout/app-sidebar.tsx +++ b/apps/webui/src/components/layout/app-sidebar.tsx @@ -1,12 +1,12 @@ -import type { ComponentProps } from 'solid-js'; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail, -} from '~/components/ui/sidebar'; -import { AppNavMainData } from '~/config/app-layout'; +} from '@/components/ui/sidebar'; +import { AppNavMainData } from '@/config/app-layout'; +import type { ComponentPropsWithoutRef } from 'react'; import { AppIcon } from './app-icon'; import { NavMain } from './nav-main'; import { NavUser } from './nav-user'; @@ -19,7 +19,10 @@ const data = { }, }; -type AppSidebarRootProps = Omit, 'collapsible'>; +type AppSidebarRootProps = Omit< + ComponentPropsWithoutRef, + 'collapsible' +>; export const AppSidebar = (props: AppSidebarRootProps) => { return ( diff --git a/apps/webui/src/components/layout/app-skeleton.tsx b/apps/webui/src/components/layout/app-skeleton.tsx index 4fcb80b..7d6e6e2 100644 --- a/apps/webui/src/components/layout/app-skeleton.tsx +++ b/apps/webui/src/components/layout/app-skeleton.tsx @@ -1,12 +1,12 @@ export function AppSkeleton() { return ( -
-
-
-
-
+
+
+
+
+
-
+
); } diff --git a/apps/webui/src/components/layout/nav-main.tsx b/apps/webui/src/components/layout/nav-main.tsx index 1cf0aa2..45209b0 100644 --- a/apps/webui/src/components/layout/nav-main.tsx +++ b/apps/webui/src/components/layout/nav-main.tsx @@ -1,12 +1,12 @@ -import { ChevronRight, type LucideIcon } from 'lucide-solid'; -import { For, Show, createSignal } from 'solid-js'; +'use client'; + +import { ChevronRight, type LucideIcon } from 'lucide-react'; -import { useMatch, useMatches } from '@tanstack/solid-router'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, -} from '~/components/ui/collapsible'; +} from '@/components/ui/collapsible'; import { SidebarGroup, SidebarGroupLabel, @@ -16,7 +16,8 @@ import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, -} from '~/components/ui/sidebar'; +} from '@/components/ui/sidebar'; +import { useMatches } from '@tanstack/react-router'; import { ProLink, type ProLinkProps } from '../ui/pro-link'; export interface NavMainItem { @@ -43,7 +44,7 @@ export function NavMain({ if (!linkTo) { return false; } - return matches().some((match) => match.pathname.startsWith(linkTo)); + return matches.some((match) => match.pathname.startsWith(linkTo)); }; const renderSidebarMenuItemButton = (item: NavMainItem) => { @@ -51,71 +52,68 @@ export function NavMain({ <> {item.icon && } {item.title} - + ); }; - return ( - - {(group) => ( - - {group.group} - - - {(item) => { - return ( - - - {renderSidebarMenuItemButton(item)} - - - } - > - - - {renderSidebarMenuItemButton(item)} - - - - - {(subItem) => ( - - - {subItem.title} - - - )} - - - - - - ); - }} - - - - )} - - ); + return groups.map((group, groupIndex) => { + return ( + + {group.group} + + {group.items.map((item, itemIndex) => { + if (!item.children?.length) { + return ( + + + + {renderSidebarMenuItemButton(item)} + + + + ); + } + return ( + + + + + {renderSidebarMenuItemButton(item)} + + + + + {(item.children || []).map((subItem, subItemIndex) => { + return ( + + + + {subItem.title} + + + + ); + })} + + + + + ); + })} + + + ); + }); } diff --git a/apps/webui/src/components/layout/nav-projects.tsx b/apps/webui/src/components/layout/nav-projects.tsx index 7b91ecf..0d7d745 100644 --- a/apps/webui/src/components/layout/nav-projects.tsx +++ b/apps/webui/src/components/layout/nav-projects.tsx @@ -1,12 +1,14 @@ -import { Link } from '@tanstack/solid-router'; +'use client'; + +import { Link } from '@tanstack/react-router'; + import { Folder, Forward, type LucideIcon, MoreHorizontal, Trash2, -} from 'lucide-solid'; -import { type ComponentProps, For } from 'solid-js'; +} from 'lucide-react'; import { DropdownMenu, @@ -14,7 +16,7 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, -} from '~/components/ui/dropdown-menu'; +} from '@/components/ui/dropdown-menu'; import { SidebarGroup, SidebarGroupLabel, @@ -22,7 +24,9 @@ import { SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, -} from '~/components/ui/sidebar'; + useSidebar, +} from '@/components/ui/sidebar'; +import type { ComponentProps } from 'react'; export function NavProjects({ projects, @@ -33,44 +37,52 @@ export function NavProjects({ link: ComponentProps; }[]; }) { + const { isMobile } = useSidebar(); + return ( - + Projects - - {(item) => ( - - + {projects.map((item) => ( + + + {item.name} - - - + + + + + - More - - - - - View Project - - - - Share Project - - - - - Delete Project - - - - - )} - + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} - - + + More diff --git a/apps/webui/src/components/layout/nav-user.tsx b/apps/webui/src/components/layout/nav-user.tsx index 97ec47a..6afed55 100644 --- a/apps/webui/src/components/layout/nav-user.tsx +++ b/apps/webui/src/components/layout/nav-user.tsx @@ -1,3 +1,5 @@ +'use client'; + import { BadgeCheck, Bell, @@ -5,9 +7,9 @@ import { CreditCard, LogOut, Sparkles, -} from 'lucide-solid'; +} from 'lucide-react'; -import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, DropdownMenuContent, @@ -16,12 +18,13 @@ import { DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from '~/components/ui/dropdown-menu'; +} from '@/components/ui/dropdown-menu'; import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, -} from '~/components/ui/sidebar'; + useSidebar, +} from '@/components/ui/sidebar'; export function NavUser({ user, @@ -32,35 +35,43 @@ export function NavUser({ avatar: string; }; }) { + const { isMobile } = useSidebar(); + return ( - - - - CN - -
- {user.name} - {user.email} -
- + + + + + CN + +
+ {user.name} + {user.email} +
+ +
- - -
- + + +
+ - CN + CN -
- {user.name} - {user.email} +
+ {user.name} + {user.email}
diff --git a/apps/webui/src/components/ui/accordion.tsx b/apps/webui/src/components/ui/accordion.tsx index 9222210..90ad605 100644 --- a/apps/webui/src/components/ui/accordion.tsx +++ b/apps/webui/src/components/ui/accordion.tsx @@ -1,91 +1,64 @@ -import type { JSX, ValidComponent } from 'solid-js'; -import { splitProps } from 'solid-js'; +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" -import * as AccordionPrimitive from '@kobalte/core/accordion'; -import type { PolymorphicProps } from '@kobalte/core/polymorphic'; +import { cn } from "@/styles/utils" -import { cn } from '~/utils/styles'; +function Accordion({ + ...props +}: React.ComponentProps) { + return +} -const Accordion = AccordionPrimitive.Root; - -type AccordionItemProps = - AccordionPrimitive.AccordionItemProps & { - class?: string | undefined; - }; - -const AccordionItem = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AccordionItemProps, ['class']); +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { return ( - - ); -}; + + ) +} -type AccordionTriggerProps = - AccordionPrimitive.AccordionTriggerProps & { - class?: string | undefined; - children?: JSX.Element; - }; - -const AccordionTrigger = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AccordionTriggerProps, [ - 'class', - 'children', - ]); +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { return ( - + svg]:rotate-180', - local.class + data-slot="accordion-trigger" + className={cn( + "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", + className )} - {...others} + {...props} > - {local.children} - - - + {children} + - ); -}; + ) +} -type AccordionContentProps = - AccordionPrimitive.AccordionContentProps & { - class?: string | undefined; - children?: JSX.Element; - }; - -const AccordionContent = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AccordionContentProps, [ - 'class', - 'children', - ]); +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { return ( -
{local.children}
+
{children}
- ); -}; + ) +} -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/apps/webui/src/components/ui/alert-dialog.tsx b/apps/webui/src/components/ui/alert-dialog.tsx index 1d8f93d..dc90446 100644 --- a/apps/webui/src/components/ui/alert-dialog.tsx +++ b/apps/webui/src/components/ui/alert-dialog.tsx @@ -1,117 +1,146 @@ -import type { JSX, ValidComponent } from 'solid-js'; -import { splitProps } from 'solid-js'; +"use client" -import * as AlertDialogPrimitive from '@kobalte/core/alert-dialog'; -import type { PolymorphicProps } from '@kobalte/core/polymorphic'; +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" -import { cn } from '~/utils/styles'; +import { cn } from "@/styles/utils" +import { buttonVariants } from "@/components/ui/button" -const AlertDialog = AlertDialogPrimitive.Root; -const AlertDialogTrigger = AlertDialogPrimitive.Trigger; -const AlertDialogPortal = AlertDialogPrimitive.Portal; +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} -type AlertDialogOverlayProps = - AlertDialogPrimitive.AlertDialogOverlayProps & { - class?: string | undefined; - }; +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogOverlay = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AlertDialogOverlayProps, [ - 'class', - ]); +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { return ( - ); -}; + ) +} -type AlertDialogContentProps = - AlertDialogPrimitive.AlertDialogContentProps & { - class?: string | undefined; - children?: JSX.Element; - }; - -const AlertDialogContent = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AlertDialogContentProps, [ - 'class', - 'children', - ]); +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { return ( - {local.children} - - - - - - Close - - + {...props} + /> - ); -}; + ) +} -type AlertDialogTitleProps = - AlertDialogPrimitive.AlertDialogTitleProps & { - class?: string | undefined; - }; +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogTitle = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AlertDialogTitleProps, ['class']); +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { return ( - ); -}; + ) +} -type AlertDialogDescriptionProps = - AlertDialogPrimitive.AlertDialogDescriptionProps & { - class?: string | undefined; - }; - -const AlertDialogDescription = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AlertDialogDescriptionProps, [ - 'class', - ]); +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { return ( - ); -}; + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} export { AlertDialog, @@ -119,6 +148,10 @@ export { AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, -}; + AlertDialogAction, + AlertDialogCancel, +} diff --git a/apps/webui/src/components/ui/alert.tsx b/apps/webui/src/components/ui/alert.tsx index b9fcb0c..c7715ae 100644 --- a/apps/webui/src/components/ui/alert.tsx +++ b/apps/webui/src/components/ui/alert.tsx @@ -1,63 +1,66 @@ -import type { Component, ComponentProps, ValidComponent } from 'solid-js'; -import { splitProps } from 'solid-js'; +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" -import * as AlertPrimitive from '@kobalte/core/alert'; -import type { PolymorphicProps } from '@kobalte/core/polymorphic'; -import type { VariantProps } from 'class-variance-authority'; -import { cva } from 'class-variance-authority'; - -import { cn } from '~/utils/styles'; +import { cn } from "@/styles/utils" const alertVariants = cva( - 'relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:top-4 [&>svg]:left-4 [&>svg]:text-foreground [&>svg~*]:pl-7', + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { - default: 'bg-background text-foreground', + default: "bg-card text-card-foreground", destructive: - 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, } -); +) -type AlertRootProps = - AlertPrimitive.AlertRootProps & - VariantProps & { class?: string | undefined }; - -const Alert = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AlertRootProps, [ - 'class', - 'variant', - ]); +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { return ( - - ); -}; + ) +} -const AlertTitle: Component> = (props) => { - const [local, others] = splitProps(props, ['class']); +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { return ( -
- ); -}; + ) +} -const AlertDescription: Component> = (props) => { - const [local, others] = splitProps(props, ['class']); +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { return ( -
- ); -}; +
+ ) +} -export { Alert, AlertTitle, AlertDescription }; +export { Alert, AlertTitle, AlertDescription } diff --git a/apps/webui/src/components/ui/aspect-ratio.tsx b/apps/webui/src/components/ui/aspect-ratio.tsx index f5a8a31..9b491fb 100644 --- a/apps/webui/src/components/ui/aspect-ratio.tsx +++ b/apps/webui/src/components/ui/aspect-ratio.tsx @@ -1,31 +1,9 @@ -import type { Component, ComponentProps } from "solid-js" -import { mergeProps, splitProps } from "solid-js" +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" -type AspectRatioProps = ComponentProps<"div"> & { ratio?: number } - -const AspectRatio: Component = (rawProps) => { - const props = mergeProps({ ratio: 1 / 1 }, rawProps) - const [local, others] = splitProps(props, ["ratio"]) - return ( -
-
-
- ) +function AspectRatio({ + ...props +}: React.ComponentProps) { + return } export { AspectRatio } diff --git a/apps/webui/src/components/ui/avatar.tsx b/apps/webui/src/components/ui/avatar.tsx index f33e7a9..0bde4b8 100644 --- a/apps/webui/src/components/ui/avatar.tsx +++ b/apps/webui/src/components/ui/avatar.tsx @@ -1,64 +1,53 @@ -import type { ValidComponent } from 'solid-js'; -import { splitProps } from 'solid-js'; +"use client" -import * as ImagePrimitive from '@kobalte/core/image'; -import type { PolymorphicProps } from '@kobalte/core/polymorphic'; +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" -import { cn } from '~/utils/styles'; +import { cn } from "@/styles/utils" -type AvatarRootProps = - ImagePrimitive.ImageRootProps & { - class?: string | undefined; - }; - -const Avatar = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AvatarRootProps, ['class']); +function Avatar({ + className, + ...props +}: React.ComponentProps) { return ( - - ); -}; + ) +} -type AvatarImageProps = - ImagePrimitive.ImageImgProps & { - class?: string | undefined; - }; - -const AvatarImage = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AvatarImageProps, ['class']); +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { return ( - - ); -}; + ) +} -type AvatarFallbackProps = - ImagePrimitive.ImageFallbackProps & { class?: string | undefined }; - -const AvatarFallback = ( - props: PolymorphicProps> -) => { - const [local, others] = splitProps(props as AvatarFallbackProps, ['class']); +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { return ( - - ); -}; + ) +} -export { Avatar, AvatarImage, AvatarFallback }; +export { Avatar, AvatarImage, AvatarFallback } diff --git a/apps/webui/src/components/ui/badge-delta.tsx b/apps/webui/src/components/ui/badge-delta.tsx deleted file mode 100644 index 5a5221a..0000000 --- a/apps/webui/src/components/ui/badge-delta.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import type { Component, JSXElement } from 'solid-js'; -import { createEffect, on, splitProps } from 'solid-js'; - -import type { VariantProps } from 'class-variance-authority'; -import { cva } from 'class-variance-authority'; - -import type { BadgeProps } from '~/components/ui/badge'; -import { Badge } from '~/components/ui/badge'; -import { cn } from '~/utils/styles'; - -type DeltaType = - | 'increase' - | 'moderateIncrease' - | 'unchanged' - | 'moderateDecrease' - | 'decrease'; - -const badgeDeltaVariants = cva('', { - variants: { - variant: { - success: 'bg-success text-success-foreground hover:bg-success', - warning: 'bg-warning text-warning-foreground hover:bg-warning', - error: 'bg-error text-error-foreground hover:bg-error', - }, - }, -}); -type DeltaVariant = NonNullable< - VariantProps['variant'] ->; - -const iconMap: { - [key in DeltaType]: (props: { class?: string }) => JSXElement; -} = { - increase: (props) => ( - - - - - - ), - moderateIncrease: (props) => ( - - - - - ), - unchanged: (props) => ( - - - - - - ), - moderateDecrease: (props) => ( - - - - - ), - decrease: (props) => ( - - - - - - ), -}; - -const variantMap: { [key in DeltaType]: DeltaVariant } = { - increase: 'success', - moderateIncrease: 'success', - unchanged: 'warning', - moderateDecrease: 'error', - decrease: 'error', -}; - -type BadgeDeltaProps = Omit & { - deltaType: DeltaType; -}; - -const BadgeDelta: Component = (props) => { - const [local, others] = splitProps(props, ['class', 'children', 'deltaType']); - - // eslint-disable-next-line solid/reactivity - let Icon = iconMap[local.deltaType]; - createEffect( - on( - () => local.deltaType, - () => { - Icon = iconMap[local.deltaType]; - } - ) - ); - - return ( - - - - {local.children} - - - ); -}; - -export { BadgeDelta }; diff --git a/apps/webui/src/components/ui/badge.tsx b/apps/webui/src/components/ui/badge.tsx index cbf766c..cc8324d 100644 --- a/apps/webui/src/components/ui/badge.tsx +++ b/apps/webui/src/components/ui/badge.tsx @@ -1,48 +1,46 @@ -import type { Component, ComponentProps } from 'solid-js'; -import { splitProps } from 'solid-js'; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import type { VariantProps } from 'class-variance-authority'; -import { cva } from 'class-variance-authority'; - -import { cn } from '~/utils/styles'; +import { cn } from "@/styles/utils" const badgeVariants = cva( - 'inline-flex items-center rounded-md border px-2.5 py-0.5 font-semibold text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", { variants: { variant: { - default: 'border-transparent bg-primary text-primary-foreground', - secondary: 'border-transparent bg-secondary text-secondary-foreground', - outline: 'text-foreground', - success: 'border-success-foreground bg-success text-success-foreground', - warning: 'border-warning-foreground bg-warning text-warning-foreground', - error: 'border-error-foreground bg-error text-error-foreground', + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, } -); +) -type BadgeProps = ComponentProps<'div'> & - VariantProps & { - round?: boolean; - }; +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" -const Badge: Component = (props) => { - const [local, others] = splitProps(props, ['class', 'variant', 'round']); return ( -
- ); -}; + ) +} -export type { BadgeProps }; -export { Badge, badgeVariants }; +export { Badge, badgeVariants } diff --git a/apps/webui/src/components/ui/bar-list.tsx b/apps/webui/src/components/ui/bar-list.tsx deleted file mode 100644 index e8ea05c..0000000 --- a/apps/webui/src/components/ui/bar-list.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import type { ComponentProps, JSX } from 'solid-js'; -import { For, Show, mergeProps, splitProps } from 'solid-js'; -import { Dynamic } from 'solid-js/web'; - -import { cn } from '~/utils/styles'; - -type Bar = T & { - value: number; - name: JSX.Element; - icon?: (props: ComponentProps<'svg'>) => JSX.Element; - href?: string; - target?: string; -}; - -type SortOrder = 'ascending' | 'descending' | 'none'; - -type ValueFormatter = (value: number) => string; - -const defaultValueFormatter: ValueFormatter = (value: number) => - value.toString(); - -type BarListProps = ComponentProps<'div'> & { - data: Bar[]; - valueFormatter?: ValueFormatter; - sortOrder?: SortOrder; -}; - -const BarList = (rawProps: BarListProps) => { - const props = mergeProps( - { - valueFormatter: defaultValueFormatter, - sortOrder: 'descending' as SortOrder, - }, - rawProps - ); - const [local, others] = splitProps(props, [ - 'class', - 'data', - 'valueFormatter', - 'sortOrder', - ]); - - const sortedData = () => { - if (local.sortOrder === 'none') { - return local.data; - } - return local.data.sort((a, b) => - local.sortOrder === 'ascending' ? a.value - b.value : b.value - a.value - ); - }; - - const widths = () => { - const maxValue = Math.max(...sortedData().map((item) => item.value), 0); - return sortedData().map((item) => - item.value === 0 ? 0 : Math.max((item.value / maxValue) * 100, 2) - ); - }; - - return ( -
- - {(bar, idx) => { - return ( -
-
-
- - {(icon) => ( - - )} - - {bar.name}

}> - {(href) => ( - - {bar.name} - - )} -
-
-
-
- {local.valueFormatter(bar.value)} -
-
- ); - }} -
-
- ); -}; - -export { BarList }; diff --git a/apps/webui/src/components/ui/breadcrumb.tsx b/apps/webui/src/components/ui/breadcrumb.tsx index c647f27..aebbf90 100644 --- a/apps/webui/src/components/ui/breadcrumb.tsx +++ b/apps/webui/src/components/ui/breadcrumb.tsx @@ -1,125 +1,109 @@ -import type { Component, ComponentProps, JSX, ValidComponent } from 'solid-js'; -import { Show, splitProps } from 'solid-js'; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" -import type { PolymorphicProps } from '@kobalte/core'; -import * as BreadcrumbPrimitive from '@kobalte/core/breadcrumbs'; +import { cn } from "@/styles/utils" -import { cn } from '~/utils/styles'; +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return