feat: FE solid-ui (shadcn port) — Tailwind + 14 components, all pages rewritten
CI / go test + web typecheck (push) Canceled after 0s
CI / build + deploy (Nix) — flowsight (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-09-15 17:40:39 +07:00
parent debc907188
commit 8392dbe724
34 changed files with 2390 additions and 542 deletions
+43 -27
View File
@@ -1,45 +1,61 @@
import { For, Show } from "solid-js";
import { fmtIDR } from "../lib/api";
import { Citations } from "./Citations";
import { Badge } from "./ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
export function AccumulationTable(props: { rows: { ticker: string; net_sum: number; brokers: number }[] }) {
return (
<div class="fs-card">
<h3>Top accumulation</h3>
<table class="fs-table">
<thead><tr><th>Ticker</th><th>Net</th><th>Brokers</th></tr></thead>
<tbody>
<For each={props.rows}>
{(r) => <tr><td><a href={`/report/${r.ticker}`}>{r.ticker}</a></td><td>{fmtIDR(r.net_sum)}</td><td>{r.brokers}</td></tr>}
</For>
</tbody>
</table>
<Show when={!props.rows?.length}><p class="fs-muted">No accumulation snapshots yet.</p></Show>
</div>
<Card>
<CardHeader>
<CardTitle>Top accumulation</CardTitle>
<CardDescription>Net broker accumulation per ticker.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader><TableRow><TableHead>Ticker</TableHead><TableHead class="text-right">Net</TableHead><TableHead class="text-right">Brokers</TableHead></TableRow></TableHeader>
<TableBody>
<For each={props.rows}>
{(r) => <TableRow><TableCell><a class="font-semibold text-primary hover:underline" href={`/report/${r.ticker}`}>{r.ticker}</a></TableCell><TableCell class="text-right font-mono">{fmtIDR(r.net_sum)}</TableCell><TableCell class="text-right">{r.brokers}</TableCell></TableRow>}
</For>
</TableBody>
</Table>
<Show when={!props.rows?.length}><p class="mt-3 text-sm text-muted-foreground">No accumulation snapshots yet.</p></Show>
</CardContent>
</Card>
);
}
export function RotationMap(props: { signal?: string; citations?: Parameters<typeof Citations>[0]["items"] }) {
return (
<div class="fs-card">
<h3>Sector rotation</h3>
<p>{props.signal || "No rotation flip detected."}</p>
<Citations items={props.citations} />
</div>
<Card>
<CardHeader>
<CardTitle>Sector rotation</CardTitle>
</CardHeader>
<CardContent>
<p class="text-sm">{props.signal || "No rotation flip detected."}</p>
<Citations items={props.citations} />
</CardContent>
</Card>
);
}
export function ActivityFeed(props: { events: { id?: number; ticker?: string; message?: string; date?: string }[] }) {
return (
<div class="fs-card">
<h3>Activity</h3>
<Show when={props.events?.length} fallback={<p class="fs-muted">No events yet.</p>}>
<ul>
<For each={props.events}>
{(e) => <li><strong>{e.ticker}</strong> — {e.message} <span class="fs-muted">{e.date}</span></li>}
</For>
</ul>
</Show>
</div>
<Card>
<CardHeader>
<CardTitle>Activity</CardTitle>
</CardHeader>
<CardContent>
<Show when={props.events?.length} fallback={<p class="text-sm text-muted-foreground">No events yet.</p>}>
<ul class="space-y-2 text-sm">
<For each={props.events}>
{(e) => <li><Badge variant="secondary">{e.ticker}</Badge> {e.message} <span class="text-muted-foreground">{e.date}</span></li>}
</For>
</ul>
</Show>
</CardContent>
</Card>
);
}
+11 -4
View File
@@ -1,5 +1,7 @@
import { For } from "solid-js";
import type { Citation } from "../lib/api";
import { Badge } from "./ui/badge";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export function Citations(props: { items?: Citation[] | string }) {
const list = (): Citation[] => {
@@ -10,12 +12,17 @@ export function Citations(props: { items?: Citation[] | string }) {
return props.items;
};
return (
<span>
<span class="inline-flex flex-wrap gap-1">
<For each={list()}>
{(c) => (
<span class={`fs-cite${c.stale ? " stale" : ""}`} title={`${c.endpoint} @ ${c.snapshot_at}${c.stale ? " (stale)" : ""}`}>
{c.endpoint} {c.ticker || ""} @ {(c.snapshot_at || "").slice(0, 10)}{c.stale ? " · stale" : ""}
</span>
<Tooltip>
<TooltipTrigger>
<Badge variant={c.stale ? "secondary" : "outline"} class={c.stale ? "opacity-70" : ""}>
{c.endpoint} {c.ticker || ""} @ {(c.snapshot_at || "").slice(0, 10)}{c.stale ? " · stale" : ""}
</Badge>
</TooltipTrigger>
<TooltipContent>{c.endpoint} @ {c.snapshot_at}{c.stale ? " (stale)" : ""}</TooltipContent>
</Tooltip>
)}
</For>
</span>
+31 -17
View File
@@ -1,7 +1,10 @@
import { createResource, createSignal, For, Show } from "solid-js";
import { api } from "../lib/api";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { TextField, TextFieldInput } from "./ui/text-field";
// WatchlistDrawer: global watchlist add/remove backed by /api/watchlist.
export function WatchlistDrawer() {
const [wl, { refetch }] = createResource(() => api.watchlist());
const [ticker, setTicker] = createSignal("");
@@ -19,17 +22,22 @@ export function WatchlistDrawer() {
refetch();
}
return (
<div class="fs-card">
<h3>Watchlist</h3>
<input class="fs-input" placeholder="BBCA" value={ticker()} onInput={(e) => setTicker(e.currentTarget.value)} style={{ width: "90px" }} />
<button class="fs-btn" onClick={add}>Add</button>
<Show when={err()}><p class="fs-muted">{err()}</p></Show>
<ul><For each={wl()?.watchlist || []}>{(t) => <li><a href={`/report/${t}`}>{t}</a> <button class="fs-btn ghost" onClick={() => del(t)}>×</button></li>}</For></ul>
</div>
<Card>
<CardHeader class="pb-2"><CardTitle class="text-base">Watchlist</CardTitle></CardHeader>
<CardContent class="space-y-2">
<div class="flex gap-2">
<TextField class="w-24"><TextFieldInput placeholder="BBCA" value={ticker()} onInput={(e) => setTicker(e.currentTarget.value)} /></TextField>
<Button size="sm" onClick={add}>Add</Button>
</div>
<Show when={err()}><p class="text-xs text-destructive">{err()}</p></Show>
<ul class="flex flex-wrap gap-1.5">
<For each={wl()?.watchlist || []}>{(t) => <li><a href={`/report/${t}`}><Badge variant="secondary">{t}</Badge></a> <button class="text-xs text-muted-foreground hover:text-foreground" onClick={() => del(t)}>×</button></li>}</For>
</ul>
</CardContent>
</Card>
);
}
// ChatSidebar: context-aware Q&A backed by POST /api/chat.
export function ChatSidebar() {
const [msg, setMsg] = createSignal("");
const [log, setLog] = createSignal<{ q: string; a: string }[]>([]);
@@ -45,13 +53,19 @@ export function ChatSidebar() {
}
}
return (
<div class="fs-card">
<h3>AI Chat</h3>
<For each={log()}>
{(m) => <div><p><strong>You:</strong> {m.q}</p><p class="fs-muted">{m.a.slice(0, 500)}</p></div>}
</For>
<input class="fs-input" placeholder="Ask about your watchlist…" value={msg()} onInput={(e) => setMsg(e.currentTarget.value)} style={{ width: "220px" }} />
<button class="fs-btn" onClick={send}>Send</button>
</div>
<Card>
<CardHeader class="pb-2"><CardTitle class="text-base">AI Chat</CardTitle></CardHeader>
<CardContent class="space-y-2">
<div class="max-h-64 space-y-2 overflow-y-auto">
<For each={log()}>
{(m) => <div class="text-sm"><p><strong>You:</strong> {m.q}</p><p class="text-muted-foreground">{m.a.slice(0, 500)}</p></div>}
</For>
</div>
<div class="flex gap-2">
<TextField class="flex-1"><TextFieldInput placeholder="Ask about your watchlist…" value={msg()} onInput={(e) => setMsg(e.currentTarget.value)} onKeyDown={(e: KeyboardEvent) => { if (e.key === "Enter") send(); }} /></TextField>
<Button size="sm" onClick={send}>Send</Button>
</div>
</CardContent>
</Card>
);
}
+15 -11
View File
@@ -1,14 +1,15 @@
import { createResource, createSignal, Show } from "solid-js";
import { api, type AuthUser } from "../lib/api";
import { Button } from "./ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
// AuthState: loads /api/auth/me once; exposes login/logout + theme toggle.
// Theme: .dark class on <html> (shadcn convention). Default dark.
function theme(): string {
return localStorage.getItem("fs-theme") ||
(matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
return localStorage.getItem("fs-theme") || "dark";
}
export function applyTheme(t: string) {
document.documentElement.dataset.theme = t;
document.documentElement.classList.toggle("dark", t === "dark");
localStorage.setItem("fs-theme", t);
}
applyTheme(theme());
@@ -16,12 +17,12 @@ applyTheme(theme());
export function ThemeToggle() {
const [t, setT] = createSignal(theme());
return (
<button class="fs-iconbtn" title="Toggle light/dark" onClick={() => {
<Button variant="ghost" size="icon" title="Toggle light/dark" onClick={() => {
const n = t() === "dark" ? "light" : "dark";
setT(n); applyTheme(n);
}}>
{t() === "dark" ? "☀️" : "🌙"}
</button>
</Button>
);
}
@@ -35,13 +36,16 @@ export function useAuth() {
export function AuthButton(props: { me: AuthUser | null | undefined; onLogout: () => void }) {
return (
<Show when={props.me} fallback={
<a class="fs-btn" href="/api/auth/start">Sign in with Google</a>
<a href="/api/auth/start"><Button>Sign in with Google</Button></a>
}>
{(u) => (
<span class="fs-user">
{u().avatar_url ? <img class="fs-avatar" src={u().avatar_url} alt="" /> : null}
{u().name || u().email}
<button class="fs-iconbtn" onClick={props.onLogout}>Logout</button>
<span class="flex items-center gap-2">
<Avatar class="size-8">
{u().avatar_url ? <AvatarImage src={u().avatar_url} alt="" /> : null}
<AvatarFallback>{(u().name || u().email || "?").slice(0, 1).toUpperCase()}</AvatarFallback>
</Avatar>
<span class="hidden text-sm font-medium md:inline">{u().name || u().email}</span>
<Button variant="ghost" size="sm" onClick={props.onLogout}>Logout</Button>
</span>
)}
</Show>
+72 -50
View File
@@ -1,66 +1,88 @@
import { For, Show, type JSX } from "solid-js";
import { For } from "solid-js";
import type { Citation } from "../lib/api";
import { Badge } from "./ui/badge";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
// Shared building blocks: StatCard, EmptyState, Badge, SignalBar, Skeleton.
export function StatCard(props: { title: string; value: string; sub?: string; children?: JSX.Element }) {
export function Citations(props: { items?: Citation[] | string }) {
const list = (): Citation[] => {
if (!props.items) return [];
if (typeof props.items === "string") {
try { return JSON.parse(props.items); } catch { return []; }
}
return props.items;
};
return (
<div class="fs-card">
<h3>{props.title}</h3>
<p class="fs-stat">{props.value}</p>
<Show when={props.sub}><p class="fs-muted" style={{ margin: 0 }}>{props.sub}</p></Show>
{props.children}
</div>
);
}
export function EmptyState(props: { icon: string; title: string; hint?: string; action?: JSX.Element }) {
return (
<div class="fs-card fs-empty">
<div class="big">{props.icon}</div>
<strong>{props.title}</strong>
<Show when={props.hint}><p class="fs-muted">{props.hint}</p></Show>
<Show when={props.action}><div style={{ "margin-top": "10px" }}>{props.action}</div></Show>
</div>
);
}
export function Skeleton(props: { rows?: number }) {
const n = props.rows ?? 3;
return (
<div class="fs-stack">
<For each={Array.from({ length: n })}>{() => <div class="fs-sk" style={{ height: "56px" }} />}</For>
</div>
<span class="inline-flex flex-wrap gap-1">
<For each={list()}>
{(c) => (
<Tooltip>
<TooltipTrigger>
<Badge variant={c.stale ? "secondary" : "outline"} class={c.stale ? "opacity-70" : ""}>
{c.endpoint} {c.ticker || ""} @ {(c.snapshot_at || "").slice(0, 10)}{c.stale ? " · stale" : ""}
</Badge>
</TooltipTrigger>
<TooltipContent>{c.endpoint} @ {c.snapshot_at}{c.stale ? " (stale)" : ""}</TooltipContent>
</Tooltip>
)}
</For>
</span>
);
}
export function RecBadge(props: { rec: string }) {
const cls = props.rec === "BUY" ? "buy" : props.rec === "HOLD" ? "hold" : "avoid";
return <span class={`fs-badge ${cls}`}>{props.rec}</span>;
const v = () =>
/buy/i.test(props.rec) ? { variant: "success" as const, icon: "▲" }
: /avoid|sell/i.test(props.rec) ? { variant: "error" as const, icon: "▼" }
: { variant: "warning" as const, icon: "●" };
return <Badge variant={v().variant}>{v().icon} {props.rec}</Badge>;
}
// SignalBar renders one -100..+100 signal as a labeled bar.
export function SignalBar(props: { label: string; value: number }) {
const v = Math.max(-100, Math.min(100, props.value));
const left = v < 0 ? `${50 + v / 2}%` : "50%";
const width = `${Math.abs(v) / 2}%`;
const cls = v < 0 ? "neg" : v > 0 ? "pos" : "";
export function BreakdownBars(props: { breakdown: Record<string, unknown> }) {
const entries = () => Object.entries(props.breakdown)
.filter(([, v]) => typeof v === "number")
.sort((a, b) => Math.abs(b[1] as number) - Math.abs(a[1] as number))
.slice(0, 6);
return (
<div class="fs-sig">
<span class="fs-muted">{props.label}</span>
<span class="bar"><i class={cls} style={{ left, width }} /></span>
<b>{v}</b>
<div class="space-y-1.5">
<For each={entries()}>
{([k, v]) => {
const n = v as number;
const pct = Math.min(100, Math.abs(n));
return (
<div class="flex items-center gap-2 text-xs">
<span class="w-24 shrink-0 truncate text-muted-foreground">{k}</span>
<div class="h-2 flex-1 overflow-hidden rounded-full bg-secondary">
<div
class="h-full rounded-full"
classList={{ "bg-success": n >= 0, "bg-error": n < 0 }}
style={{ width: `${pct}%`, "margin-left": n < 0 ? "auto" : undefined }}
/>
</div>
<span class="w-12 shrink-0 text-right font-mono">{n.toFixed(1)}</span>
</div>
);
}}
</For>
</div>
);
}
// BreakdownBars renders a screen/report signal breakdown map as bars.
// Unknown numeric fields are shown; booleans/strings are skipped except flags.
export function BreakdownBars(props: { breakdown: Record<string, unknown> }) {
const entries = () =>
Object.entries(props.breakdown || {}).filter(([, v]) => typeof v === "number") as [string, number][];
export function PageHead(props: { title: string; sub?: string }) {
return (
<Show when={entries().length} fallback={<span class="fs-muted">no signals</span>}>
<For each={entries()}>{([k, v]) => <SignalBar label={k.replace(/_/g, " ")} value={v} />}</For>
</Show>
<div>
<h1 class="text-2xl font-bold tracking-tight">{props.title}</h1>
{props.sub ? <p class="text-sm text-muted-foreground">{props.sub}</p> : null}
</div>
);
}
export function EmptyState(props: { icon: string; title: string; hint?: string; action?: import("solid-js").JSX.Element }) {
return (
<div class="flex flex-col items-center gap-1 rounded-lg border border-dashed px-6 py-10 text-center">
<div class="text-3xl">{props.icon}</div>
<p class="font-medium">{props.title}</p>
{props.hint ? <p class="text-sm text-muted-foreground">{props.hint}</p> : null}
{props.action}
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
import type { ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import * as ImagePrimitive from "@kobalte/core/image"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import { cn } from "../../lib/utils"
type AvatarRootProps<T extends ValidComponent = "span"> = ImagePrimitive.ImageRootProps<T> & {
class?: string | undefined
}
const Avatar = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, AvatarRootProps<T>>
) => {
const [local, others] = splitProps(props as AvatarRootProps, ["class"])
return (
<ImagePrimitive.Root
class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", local.class)}
{...others}
/>
)
}
type AvatarImageProps<T extends ValidComponent = "img"> = ImagePrimitive.ImageImgProps<T> & {
class?: string | undefined
}
const AvatarImage = <T extends ValidComponent = "img">(
props: PolymorphicProps<T, AvatarImageProps<T>>
) => {
const [local, others] = splitProps(props as AvatarImageProps, ["class"])
return <ImagePrimitive.Img class={cn("aspect-square size-full", local.class)} {...others} />
}
type AvatarFallbackProps<T extends ValidComponent = "span"> =
ImagePrimitive.ImageFallbackProps<T> & { class?: string | undefined }
const AvatarFallback = <T extends ValidComponent = "span">(
props: PolymorphicProps<T, AvatarFallbackProps<T>>
) => {
const [local, others] = splitProps(props as AvatarFallbackProps, ["class"])
return (
<ImagePrimitive.Fallback
class={cn("flex size-full items-center justify-center bg-muted", local.class)}
{...others}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }
+49
View File
@@ -0,0 +1,49 @@
import type { Component, ComponentProps } from "solid-js"
import { splitProps } from "solid-js"
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
import { cn } from "../../lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
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"
}
},
defaultVariants: {
variant: "default"
}
}
)
type BadgeProps = ComponentProps<"div"> &
VariantProps<typeof badgeVariants> & {
round?: boolean
}
const Badge: Component<BadgeProps> = (props) => {
const [local, others] = splitProps(props, ["class", "variant", "round"])
return (
<div
class={cn(
badgeVariants({ variant: local.variant }),
local.round && "rounded-full",
local.class
)}
{...others}
/>
)
}
export type { BadgeProps }
export { Badge, badgeVariants }
+54
View File
@@ -0,0 +1,54 @@
import type { JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import * as ButtonPrimitive from "@kobalte/core/button"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
import { cn } from "../../lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3 text-xs",
lg: "h-11 px-8",
icon: "size-10"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
)
type ButtonProps<T extends ValidComponent = "button"> = ButtonPrimitive.ButtonRootProps<T> &
VariantProps<typeof buttonVariants> & { class?: string | undefined; children?: JSX.Element }
const Button = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, ButtonProps<T>>
) => {
const [local, others] = splitProps(props as ButtonProps, ["variant", "size", "class"])
return (
<ButtonPrimitive.Root
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
{...others}
/>
)
}
export { Button, buttonVariants }
export type { ButtonProps }
+44
View File
@@ -0,0 +1,44 @@
import type { Component, ComponentProps } from "solid-js"
import { splitProps } from "solid-js"
import { cn } from "../../lib/utils"
const Card: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div
class={cn("rounded-lg border bg-card text-card-foreground shadow-sm", local.class)}
{...others}
/>
)
}
const CardHeader: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <div class={cn("flex flex-col space-y-1.5 p-6", local.class)} {...others} />
}
const CardTitle: Component<ComponentProps<"h3">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<h3 class={cn("text-lg font-semibold leading-none tracking-tight", local.class)} {...others} />
)
}
const CardDescription: Component<ComponentProps<"p">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <p class={cn("text-sm text-muted-foreground", local.class)} {...others} />
}
const CardContent: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <div class={cn("p-6 pt-0", local.class)} {...others} />
}
const CardFooter: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <div class={cn("flex items-center p-6 pt-0", local.class)} {...others} />
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
import type { Component, ComponentProps } from "solid-js"
import { splitProps } from "solid-js"
import { cn } from "../../lib/utils"
const Label: Component<ComponentProps<"label">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<label
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
local.class
)}
{...others}
/>
)
}
export { Label }
+35
View File
@@ -0,0 +1,35 @@
import type { Component, JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as ProgressPrimitive from "@kobalte/core/progress"
import { Label } from "./label"
type ProgressRootProps<T extends ValidComponent = "div"> =
ProgressPrimitive.ProgressRootProps<T> & { children?: JSX.Element }
const Progress = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, ProgressRootProps<T>>
) => {
const [local, others] = splitProps(props as ProgressRootProps, ["children"])
return (
<ProgressPrimitive.Root {...others}>
{local.children}
<ProgressPrimitive.Track class="relative h-2 w-full overflow-hidden rounded-full bg-secondary">
<ProgressPrimitive.Fill class="h-full w-[var(--kb-progress-fill-width)] flex-1 bg-primary transition-all" />
</ProgressPrimitive.Track>
</ProgressPrimitive.Root>
)
}
const ProgressLabel: Component<ProgressPrimitive.ProgressLabelProps> = (props) => {
return <ProgressPrimitive.Label {...props} />
}
const ProgressValueLabel: Component<ProgressPrimitive.ProgressValueLabelProps> = (props) => {
return <ProgressPrimitive.ValueLabel as={Label} {...props} />
}
export { Progress, ProgressLabel, ProgressValueLabel }
+30
View File
@@ -0,0 +1,30 @@
import type { ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as SeparatorPrimitive from "@kobalte/core/separator"
import { cn } from "../../lib/utils"
type SeparatorRootProps<T extends ValidComponent = "hr"> =
SeparatorPrimitive.SeparatorRootProps<T> & { class?: string | undefined }
const Separator = <T extends ValidComponent = "hr">(
props: PolymorphicProps<T, SeparatorRootProps<T>>
) => {
const [local, others] = splitProps(props as SeparatorRootProps, ["class", "orientation"])
return (
<SeparatorPrimitive.Root
orientation={local.orientation ?? "horizontal"}
class={cn(
"shrink-0 bg-border",
local.orientation === "vertical" ? "h-full w-px" : "h-px w-full",
local.class
)}
{...others}
/>
)
}
export { Separator }
+25
View File
@@ -0,0 +1,25 @@
import type { ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as SkeletonPrimitive from "@kobalte/core/skeleton"
import { cn } from "../../lib/utils"
type SkeletonRootProps<T extends ValidComponent = "div"> =
SkeletonPrimitive.SkeletonRootProps<T> & { class?: string | undefined }
const Skeleton = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SkeletonRootProps<T>>
) => {
const [local, others] = splitProps(props as SkeletonRootProps, ["class"])
return (
<SkeletonPrimitive.Root
class={cn("bg-primary/10 data-[animate='true']:animate-pulse", local.class)}
{...others}
/>
)
}
export { Skeleton }
+78
View File
@@ -0,0 +1,78 @@
import type { JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core"
import * as SwitchPrimitive from "@kobalte/core/switch"
import { cn } from "../../lib/utils"
const Switch = SwitchPrimitive.Root
const SwitchDescription = SwitchPrimitive.Description
const SwitchErrorMessage = SwitchPrimitive.ErrorMessage
type SwitchControlProps = SwitchPrimitive.SwitchControlProps & {
class?: string | undefined
children?: JSX.Element
}
const SwitchControl = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, SwitchControlProps>
) => {
const [local, others] = splitProps(props as SwitchControlProps, ["class", "children"])
return (
<>
<SwitchPrimitive.Input
class={cn(
"[&:focus-visible+div]:outline-none [&:focus-visible+div]:ring-2 [&:focus-visible+div]:ring-ring [&:focus-visible+div]:ring-offset-2 [&:focus-visible+div]:ring-offset-background",
local.class
)}
/>
<SwitchPrimitive.Control
class={cn(
"inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input transition-[color,background-color,box-shadow] data-[disabled]:cursor-not-allowed data-[checked]:bg-primary data-[disabled]:opacity-50",
local.class
)}
{...others}
>
{local.children}
</SwitchPrimitive.Control>
</>
)
}
type SwitchThumbProps = SwitchPrimitive.SwitchThumbProps & { class?: string | undefined }
const SwitchThumb = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SwitchThumbProps>
) => {
const [local, others] = splitProps(props as SwitchThumbProps, ["class"])
return (
<SwitchPrimitive.Thumb
class={cn(
"pointer-events-none block size-5 translate-x-0 rounded-full bg-background shadow-lg ring-0 transition-transform data-[checked]:translate-x-5",
local.class
)}
{...others}
/>
)
}
type SwitchLabelProps = SwitchPrimitive.SwitchLabelProps & { class?: string | undefined }
const SwitchLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, SwitchLabelProps>
) => {
const [local, others] = splitProps(props as SwitchLabelProps, ["class"])
return (
<SwitchPrimitive.Label
class={cn(
"text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70",
local.class
)}
{...others}
/>
)
}
export { Switch, SwitchControl, SwitchThumb, SwitchLabel, SwitchDescription, SwitchErrorMessage }
+71
View File
@@ -0,0 +1,71 @@
import type { Component, ComponentProps } from "solid-js"
import { splitProps } from "solid-js"
import { cn } from "../../lib/utils"
const Table: Component<ComponentProps<"table">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<div class="relative w-full overflow-auto">
<table class={cn("w-full caption-bottom text-sm", local.class)} {...others} />
</div>
)
}
const TableHeader: Component<ComponentProps<"thead">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <thead class={cn("[&_tr]:border-b", local.class)} {...others} />
}
const TableBody: Component<ComponentProps<"tbody">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <tbody class={cn("[&_tr:last-child]:border-0", local.class)} {...others} />
}
const TableFooter: Component<ComponentProps<"tfoot">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<tfoot class={cn("bg-primary font-medium text-primary-foreground", local.class)} {...others} />
)
}
const TableRow: Component<ComponentProps<"tr">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<tr
class={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
local.class
)}
{...others}
/>
)
}
const TableHead: Component<ComponentProps<"th">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<th
class={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
local.class
)}
{...others}
/>
)
}
const TableCell: Component<ComponentProps<"td">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<td class={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", local.class)} {...others} />
)
}
const TableCaption: Component<ComponentProps<"caption">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return <caption class={cn("mt-4 text-sm text-muted-foreground", local.class)} {...others} />
}
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
+88
View File
@@ -0,0 +1,88 @@
import type { ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as TabsPrimitive from "@kobalte/core/tabs"
import { cn } from "../../lib/utils"
const Tabs = TabsPrimitive.Root
type TabsListProps<T extends ValidComponent = "div"> = TabsPrimitive.TabsListProps<T> & {
class?: string | undefined
}
const TabsList = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TabsListProps<T>>
) => {
const [local, others] = splitProps(props as TabsListProps, ["class"])
return (
<TabsPrimitive.List
class={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
local.class
)}
{...others}
/>
)
}
type TabsTriggerProps<T extends ValidComponent = "button"> = TabsPrimitive.TabsTriggerProps<T> & {
class?: string | undefined
}
const TabsTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, TabsTriggerProps<T>>
) => {
const [local, others] = splitProps(props as TabsTriggerProps, ["class"])
return (
<TabsPrimitive.Trigger
class={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[selected]:bg-background data-[selected]:text-foreground data-[selected]:shadow-sm",
local.class
)}
{...others}
/>
)
}
type TabsContentProps<T extends ValidComponent = "div"> = TabsPrimitive.TabsContentProps<T> & {
class?: string | undefined
}
const TabsContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TabsContentProps<T>>
) => {
const [local, others] = splitProps(props as TabsContentProps, ["class"])
return (
<TabsPrimitive.Content
class={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
local.class
)}
{...others}
/>
)
}
type TabsIndicatorProps<T extends ValidComponent = "div"> = TabsPrimitive.TabsIndicatorProps<T> & {
class?: string | undefined
}
const TabsIndicator = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TabsIndicatorProps<T>>
) => {
const [local, others] = splitProps(props as TabsIndicatorProps, ["class"])
return (
<TabsPrimitive.Indicator
class={cn(
"duration-250ms absolute transition-all data-[orientation=horizontal]:-bottom-px data-[orientation=vertical]:-right-px data-[orientation=horizontal]:h-[2px] data-[orientation=vertical]:w-[2px]",
local.class
)}
{...others}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent, TabsIndicator }
+153
View File
@@ -0,0 +1,153 @@
import type { ValidComponent } from "solid-js"
import { mergeProps, splitProps } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core"
import * as TextFieldPrimitive from "@kobalte/core/text-field"
import { cva } from "class-variance-authority"
import { cn } from "../../lib/utils"
type TextFieldRootProps<T extends ValidComponent = "div"> =
TextFieldPrimitive.TextFieldRootProps<T> & {
class?: string | undefined
}
const TextField = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TextFieldRootProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldRootProps, ["class"])
return <TextFieldPrimitive.Root class={cn("flex flex-col gap-1", local.class)} {...others} />
}
type TextFieldInputProps<T extends ValidComponent = "input"> =
TextFieldPrimitive.TextFieldInputProps<T> & {
class?: string | undefined
type?:
| "button"
| "checkbox"
| "color"
| "date"
| "datetime-local"
| "email"
| "file"
| "hidden"
| "image"
| "month"
| "number"
| "password"
| "radio"
| "range"
| "reset"
| "search"
| "submit"
| "tel"
| "text"
| "time"
| "url"
| "week"
}
const TextFieldInput = <T extends ValidComponent = "input">(
rawProps: PolymorphicProps<T, TextFieldInputProps<T>>
) => {
const props = mergeProps<TextFieldInputProps<T>[]>({ type: "text" }, rawProps)
const [local, others] = splitProps(props as TextFieldInputProps, ["type", "class"])
return (
<TextFieldPrimitive.Input
type={local.type}
class={cn(
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[invalid]:border-error-foreground data-[invalid]:text-error-foreground",
local.class
)}
{...others}
/>
)
}
type TextFieldTextAreaProps<T extends ValidComponent = "textarea"> =
TextFieldPrimitive.TextFieldTextAreaProps<T> & { class?: string | undefined }
const TextFieldTextArea = <T extends ValidComponent = "textarea">(
props: PolymorphicProps<T, TextFieldTextAreaProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldTextAreaProps, ["class"])
return (
<TextFieldPrimitive.TextArea
class={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
local.class
)}
{...others}
/>
)
}
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
{
variants: {
variant: {
label: "data-[invalid]:text-destructive",
description: "font-normal text-muted-foreground",
error: "text-xs text-destructive"
}
},
defaultVariants: {
variant: "label"
}
}
)
type TextFieldLabelProps<T extends ValidComponent = "label"> =
TextFieldPrimitive.TextFieldLabelProps<T> & { class?: string | undefined }
const TextFieldLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, TextFieldLabelProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldLabelProps, ["class"])
return <TextFieldPrimitive.Label class={cn(labelVariants(), local.class)} {...others} />
}
type TextFieldDescriptionProps<T extends ValidComponent = "div"> =
TextFieldPrimitive.TextFieldDescriptionProps<T> & {
class?: string | undefined
}
const TextFieldDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TextFieldDescriptionProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldDescriptionProps, ["class"])
return (
<TextFieldPrimitive.Description
class={cn(labelVariants({ variant: "description" }), local.class)}
{...others}
/>
)
}
type TextFieldErrorMessageProps<T extends ValidComponent = "div"> =
TextFieldPrimitive.TextFieldErrorMessageProps<T> & {
class?: string | undefined
}
const TextFieldErrorMessage = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TextFieldErrorMessageProps<T>>
) => {
const [local, others] = splitProps(props as TextFieldErrorMessageProps, ["class"])
return (
<TextFieldPrimitive.ErrorMessage
class={cn(labelVariants({ variant: "error" }), local.class)}
{...others}
/>
)
}
export {
TextField,
TextFieldInput,
TextFieldTextArea,
TextFieldLabel,
TextFieldDescription,
TextFieldErrorMessage
}
+36
View File
@@ -0,0 +1,36 @@
import type { ValidComponent } from "solid-js"
import { splitProps, type Component } from "solid-js"
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as TooltipPrimitive from "@kobalte/core/tooltip"
import { cn } from "../../lib/utils"
const TooltipTrigger = TooltipPrimitive.Trigger
const Tooltip: Component<TooltipPrimitive.TooltipRootProps> = (props) => {
return <TooltipPrimitive.Root gutter={4} {...props} />
}
type TooltipContentProps<T extends ValidComponent = "div"> =
TooltipPrimitive.TooltipContentProps<T> & { class?: string | undefined }
const TooltipContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, TooltipContentProps<T>>
) => {
const [local, others] = splitProps(props as TooltipContentProps, ["class"])
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
class={cn(
"z-50 origin-[var(--kb-popover-content-transform-origin)] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
local.class
)}
{...others}
/>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent }