snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
920
apps/v4/registry/bases/radix/blocks/_registry.ts
Normal file
920
apps/v4/registry/bases/radix/blocks/_registry.ts
Normal file
@@ -0,0 +1,920 @@
|
||||
import { type Registry } from "shadcn/schema"
|
||||
|
||||
export const blocks: Registry["items"] = [
|
||||
{
|
||||
name: "preview",
|
||||
title: "Preview",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"alert-dialog",
|
||||
"avatar",
|
||||
"badge",
|
||||
"button",
|
||||
"button-group",
|
||||
"card",
|
||||
"checkbox",
|
||||
"combobox",
|
||||
"dropdown-menu",
|
||||
"empty",
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"item",
|
||||
"label",
|
||||
"popover",
|
||||
"radio-group",
|
||||
"select",
|
||||
"separator",
|
||||
"sheet",
|
||||
"slider",
|
||||
"spinner",
|
||||
"switch",
|
||||
"textarea",
|
||||
"tooltip",
|
||||
"example",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/preview/index.tsx",
|
||||
type: "registry:block",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "preview-02",
|
||||
title: "Preview 02",
|
||||
type: "registry:block",
|
||||
dependencies: ["react-qr-code"],
|
||||
registryDependencies: [
|
||||
"accordion",
|
||||
"badge",
|
||||
"breadcrumb",
|
||||
"button",
|
||||
"calendar",
|
||||
"card",
|
||||
"chart",
|
||||
"checkbox",
|
||||
"combobox",
|
||||
"dropdown-menu",
|
||||
"empty",
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"item",
|
||||
"label",
|
||||
"native-select",
|
||||
"progress",
|
||||
"radio-group",
|
||||
"select",
|
||||
"separator",
|
||||
"sidebar",
|
||||
"skeleton",
|
||||
"slider",
|
||||
"spinner",
|
||||
"switch",
|
||||
"table",
|
||||
"tabs",
|
||||
"textarea",
|
||||
"toggle-group",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/preview-02/index.tsx",
|
||||
type: "registry:block",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "preview-03",
|
||||
title: "Preview 03",
|
||||
type: "registry:block",
|
||||
registryDependencies: [],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/preview-03/index.tsx",
|
||||
type: "registry:block",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "login-01",
|
||||
title: "Login 01",
|
||||
description: "A simple login form.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-01/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-01/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "login-02",
|
||||
title: "Login 02",
|
||||
description: "A two column login page with a cover image.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-02/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-02/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "login-03",
|
||||
title: "Login 03",
|
||||
description: "A login page with a muted background color.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-03/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-03/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "login-04",
|
||||
title: "Login 04",
|
||||
description: "A login page with form and image.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-04/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-04/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "login-05",
|
||||
title: "Login 05",
|
||||
description: "A simple email-only login page.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/login-05/page.tsx",
|
||||
target: "app/login/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/login-05/components/login-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "login"],
|
||||
},
|
||||
{
|
||||
name: "signup-01",
|
||||
title: "Signup 01",
|
||||
description: "A simple signup form.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-01/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-01/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "signup-02",
|
||||
title: "Signup 02",
|
||||
description: "A two column signup page with a cover image.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-02/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-02/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "signup-03",
|
||||
title: "Signup 03",
|
||||
description: "A signup page with a muted background color.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-03/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-03/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "signup-04",
|
||||
title: "Signup 04",
|
||||
description: "A signup page with form and image.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "card", "input", "label", "field"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-04/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-04/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "signup-05",
|
||||
title: "Signup 05",
|
||||
description: "A simple signup form with social providers.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["button", "input", "label"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/signup-05/page.tsx",
|
||||
target: "app/signup/page.tsx",
|
||||
type: "registry:page",
|
||||
},
|
||||
{
|
||||
path: "blocks/signup-05/components/signup-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["authentication", "signup"],
|
||||
},
|
||||
{
|
||||
name: "dashboard-01",
|
||||
title: "Dashboard 01",
|
||||
type: "registry:block",
|
||||
description: "A dashboard with sidebar, charts and data table.",
|
||||
dependencies: [
|
||||
"@dnd-kit/core",
|
||||
"@dnd-kit/modifiers",
|
||||
"@dnd-kit/sortable",
|
||||
"@dnd-kit/utilities",
|
||||
"@tanstack/react-table",
|
||||
"zod",
|
||||
],
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"label",
|
||||
"chart",
|
||||
"card",
|
||||
"select",
|
||||
"tabs",
|
||||
"table",
|
||||
"toggle-group",
|
||||
"badge",
|
||||
"button",
|
||||
"checkbox",
|
||||
"dropdown-menu",
|
||||
"drawer",
|
||||
"input",
|
||||
"avatar",
|
||||
"sheet",
|
||||
"sonner",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/dashboard-01/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/data.json",
|
||||
type: "registry:file",
|
||||
target: "app/dashboard/data.json",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/chart-area-interactive.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/data-table.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/nav-documents.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/section-cards.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/dashboard-01/components/site-header.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["dashboard"],
|
||||
meta: {
|
||||
iframeHeight: "1000px",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sidebar-01",
|
||||
title: "Sidebar 01",
|
||||
type: "registry:block",
|
||||
description: "A simple sidebar with navigation grouped by section.",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"label",
|
||||
"dropdown-menu",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-01/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-01/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-01/components/search-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-01/components/version-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-02",
|
||||
title: "Sidebar 02",
|
||||
description: "A sidebar with collapsible sections.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"label",
|
||||
"dropdown-menu",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-02/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-02/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-02/components/search-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-02/components/version-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-03",
|
||||
title: "Sidebar 03",
|
||||
description: "A sidebar with submenus.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-03/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-03/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-04",
|
||||
title: "Sidebar 04",
|
||||
description: "A floating sidebar with submenus.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb", "separator"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-04/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-04/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-05",
|
||||
title: "Sidebar 05",
|
||||
description: "A sidebar with collapsible submenus.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"label",
|
||||
"collapsible",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-05/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-05/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-05/components/search-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-06",
|
||||
title: "Sidebar 06",
|
||||
description: "A sidebar with submenus as dropdowns.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"card",
|
||||
"dropdown-menu",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-06/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-06/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-06/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-06/components/sidebar-opt-in-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-07",
|
||||
title: "Sidebar 07",
|
||||
type: "registry:block",
|
||||
description: "A sidebar that collapses to icons.",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-07/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/nav-projects.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-07/components/team-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-08",
|
||||
title: "Sidebar 08",
|
||||
description: "An inset sidebar with secondary navigation.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-08/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/nav-projects.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-08/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-09",
|
||||
title: "Sidebar 09",
|
||||
description: "Collapsible nested sidebars.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
"switch",
|
||||
"label",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-09/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-09/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-09/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-10",
|
||||
title: "Sidebar 10",
|
||||
description: "A sidebar in a popover.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"popover",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-10/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-actions.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-favorites.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/nav-workspaces.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-10/components/team-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-11",
|
||||
title: "Sidebar 11",
|
||||
description: "A sidebar with a collapsible file tree.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb", "separator", "collapsible"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-11/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-11/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-12",
|
||||
title: "Sidebar 12",
|
||||
description: "A sidebar with a calendar.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"calendar",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-12/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-12/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-12/components/calendars.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-12/components/date-picker.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-12/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-13",
|
||||
title: "Sidebar 13",
|
||||
description: "A sidebar in a dialog.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb", "button", "dialog"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-13/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-13/components/settings-dialog.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-14",
|
||||
title: "Sidebar 14",
|
||||
description: "A sidebar on the right.",
|
||||
type: "registry:block",
|
||||
registryDependencies: ["sidebar", "breadcrumb"],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-14/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-14/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-15",
|
||||
title: "Sidebar 15",
|
||||
description: "A left and right sidebar.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"popover",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"calendar",
|
||||
"avatar",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-15/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/calendars.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/date-picker.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-favorites.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/nav-workspaces.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/sidebar-left.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/sidebar-right.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-15/components/team-switcher.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
{
|
||||
name: "sidebar-16",
|
||||
title: "Sidebar 16",
|
||||
description: "A sidebar with a sticky site header.",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"sidebar",
|
||||
"breadcrumb",
|
||||
"separator",
|
||||
"collapsible",
|
||||
"dropdown-menu",
|
||||
"avatar",
|
||||
"button",
|
||||
"label",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "blocks/sidebar-16/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/dashboard/page.tsx",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/app-sidebar.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/nav-main.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/nav-projects.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/nav-secondary.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/nav-user.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/search-form.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
{
|
||||
path: "blocks/sidebar-16/components/site-header.tsx",
|
||||
type: "registry:component",
|
||||
},
|
||||
],
|
||||
categories: ["sidebar", "dashboard"],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,284 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { NavDocuments } from "@/registry/bases/radix/blocks/dashboard-01/components/nav-documents"
|
||||
import { NavMain } from "@/registry/bases/radix/blocks/dashboard-01/components/nav-main"
|
||||
import { NavSecondary } from "@/registry/bases/radix/blocks/dashboard-01/components/nav-secondary"
|
||||
import { NavUser } from "@/registry/bases/radix/blocks/dashboard-01/components/nav-user"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/bases/radix/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="LayoutDashboardIcon"
|
||||
tabler="IconDashboard"
|
||||
hugeicons="DashboardSquare01Icon"
|
||||
phosphor="SquaresFourIcon"
|
||||
remixicon="RiDashboardLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Lifecycle",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="ListIcon"
|
||||
tabler="IconListDetails"
|
||||
hugeicons="Menu01Icon"
|
||||
phosphor="ListIcon"
|
||||
remixicon="RiListUnordered"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="ChartBarIcon"
|
||||
tabler="IconChartBar"
|
||||
hugeicons="ChartHistogramIcon"
|
||||
phosphor="ChartBarIcon"
|
||||
remixicon="RiBarChartLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FolderIcon"
|
||||
tabler="IconFolder"
|
||||
hugeicons="Folder01Icon"
|
||||
phosphor="FolderIcon"
|
||||
remixicon="RiFolderLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="UsersIcon"
|
||||
tabler="IconUsers"
|
||||
hugeicons="UserGroupIcon"
|
||||
phosphor="UsersIcon"
|
||||
remixicon="RiGroupLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
navClouds: [
|
||||
{
|
||||
title: "Capture",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="CameraIcon"
|
||||
tabler="IconCamera"
|
||||
hugeicons="Camera01Icon"
|
||||
phosphor="CameraIcon"
|
||||
remixicon="RiCameraLine"
|
||||
/>
|
||||
),
|
||||
isActive: true,
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Proposal",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FileTextIcon"
|
||||
tabler="IconFileDescription"
|
||||
hugeicons="File01Icon"
|
||||
phosphor="FileTextIcon"
|
||||
remixicon="RiFileTextLine"
|
||||
/>
|
||||
),
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Prompts",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FileTextIcon"
|
||||
tabler="IconFileAi"
|
||||
hugeicons="File01Icon"
|
||||
phosphor="FileTextIcon"
|
||||
remixicon="RiFileTextLine"
|
||||
/>
|
||||
),
|
||||
url: "#",
|
||||
items: [
|
||||
{
|
||||
title: "Active Proposals",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Archived",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="Settings2Icon"
|
||||
tabler="IconSettings"
|
||||
hugeicons="Settings05Icon"
|
||||
phosphor="GearIcon"
|
||||
remixicon="RiSettingsLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Get Help",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="CircleHelpIcon"
|
||||
tabler="IconHelp"
|
||||
hugeicons="HelpCircleIcon"
|
||||
phosphor="QuestionIcon"
|
||||
remixicon="RiQuestionLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Search",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="SearchIcon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
name: "Data Library",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="DatabaseIcon"
|
||||
tabler="IconDatabase"
|
||||
hugeicons="Database01Icon"
|
||||
phosphor="DatabaseIcon"
|
||||
remixicon="RiDatabase2Line"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FileChartColumnIcon"
|
||||
tabler="IconReport"
|
||||
hugeicons="Analytics01Icon"
|
||||
phosphor="ChartLineIcon"
|
||||
remixicon="RiFileChartLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Word Assistant",
|
||||
url: "#",
|
||||
icon: (
|
||||
<IconPlaceholder
|
||||
lucide="FileIcon"
|
||||
tabler="IconFileWord"
|
||||
hugeicons="File01Icon"
|
||||
phosphor="FileIcon"
|
||||
remixicon="RiFileLine"
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible="offcanvas" {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:p-1.5!"
|
||||
>
|
||||
<a href="#">
|
||||
<IconPlaceholder
|
||||
lucide="CommandIcon"
|
||||
tabler="IconInnerShadowTop"
|
||||
hugeicons="CommandIcon"
|
||||
phosphor="CommandIcon"
|
||||
remixicon="RiCommandLine"
|
||||
className="size-5!"
|
||||
/>
|
||||
<span className="text-base font-semibold">Acme Inc.</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavDocuments items={data.documents} />
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import { useIsMobile } from "@/registry/bases/radix/hooks/use-mobile"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/radix/ui/toggle-group"
|
||||
|
||||
export const description = "An interactive area chart"
|
||||
|
||||
const chartData = [
|
||||
{ date: "2024-04-01", desktop: 222, mobile: 150 },
|
||||
{ date: "2024-04-02", desktop: 97, mobile: 180 },
|
||||
{ date: "2024-04-03", desktop: 167, mobile: 120 },
|
||||
{ date: "2024-04-04", desktop: 242, mobile: 260 },
|
||||
{ date: "2024-04-05", desktop: 373, mobile: 290 },
|
||||
{ date: "2024-04-06", desktop: 301, mobile: 340 },
|
||||
{ date: "2024-04-07", desktop: 245, mobile: 180 },
|
||||
{ date: "2024-04-08", desktop: 409, mobile: 320 },
|
||||
{ date: "2024-04-09", desktop: 59, mobile: 110 },
|
||||
{ date: "2024-04-10", desktop: 261, mobile: 190 },
|
||||
{ date: "2024-04-11", desktop: 327, mobile: 350 },
|
||||
{ date: "2024-04-12", desktop: 292, mobile: 210 },
|
||||
{ date: "2024-04-13", desktop: 342, mobile: 380 },
|
||||
{ date: "2024-04-14", desktop: 137, mobile: 220 },
|
||||
{ date: "2024-04-15", desktop: 120, mobile: 170 },
|
||||
{ date: "2024-04-16", desktop: 138, mobile: 190 },
|
||||
{ date: "2024-04-17", desktop: 446, mobile: 360 },
|
||||
{ date: "2024-04-18", desktop: 364, mobile: 410 },
|
||||
{ date: "2024-04-19", desktop: 243, mobile: 180 },
|
||||
{ date: "2024-04-20", desktop: 89, mobile: 150 },
|
||||
{ date: "2024-04-21", desktop: 137, mobile: 200 },
|
||||
{ date: "2024-04-22", desktop: 224, mobile: 170 },
|
||||
{ date: "2024-04-23", desktop: 138, mobile: 230 },
|
||||
{ date: "2024-04-24", desktop: 387, mobile: 290 },
|
||||
{ date: "2024-04-25", desktop: 215, mobile: 250 },
|
||||
{ date: "2024-04-26", desktop: 75, mobile: 130 },
|
||||
{ date: "2024-04-27", desktop: 383, mobile: 420 },
|
||||
{ date: "2024-04-28", desktop: 122, mobile: 180 },
|
||||
{ date: "2024-04-29", desktop: 315, mobile: 240 },
|
||||
{ date: "2024-04-30", desktop: 454, mobile: 380 },
|
||||
{ date: "2024-05-01", desktop: 165, mobile: 220 },
|
||||
{ date: "2024-05-02", desktop: 293, mobile: 310 },
|
||||
{ date: "2024-05-03", desktop: 247, mobile: 190 },
|
||||
{ date: "2024-05-04", desktop: 385, mobile: 420 },
|
||||
{ date: "2024-05-05", desktop: 481, mobile: 390 },
|
||||
{ date: "2024-05-06", desktop: 498, mobile: 520 },
|
||||
{ date: "2024-05-07", desktop: 388, mobile: 300 },
|
||||
{ date: "2024-05-08", desktop: 149, mobile: 210 },
|
||||
{ date: "2024-05-09", desktop: 227, mobile: 180 },
|
||||
{ date: "2024-05-10", desktop: 293, mobile: 330 },
|
||||
{ date: "2024-05-11", desktop: 335, mobile: 270 },
|
||||
{ date: "2024-05-12", desktop: 197, mobile: 240 },
|
||||
{ date: "2024-05-13", desktop: 197, mobile: 160 },
|
||||
{ date: "2024-05-14", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-05-15", desktop: 473, mobile: 380 },
|
||||
{ date: "2024-05-16", desktop: 338, mobile: 400 },
|
||||
{ date: "2024-05-17", desktop: 499, mobile: 420 },
|
||||
{ date: "2024-05-18", desktop: 315, mobile: 350 },
|
||||
{ date: "2024-05-19", desktop: 235, mobile: 180 },
|
||||
{ date: "2024-05-20", desktop: 177, mobile: 230 },
|
||||
{ date: "2024-05-21", desktop: 82, mobile: 140 },
|
||||
{ date: "2024-05-22", desktop: 81, mobile: 120 },
|
||||
{ date: "2024-05-23", desktop: 252, mobile: 290 },
|
||||
{ date: "2024-05-24", desktop: 294, mobile: 220 },
|
||||
{ date: "2024-05-25", desktop: 201, mobile: 250 },
|
||||
{ date: "2024-05-26", desktop: 213, mobile: 170 },
|
||||
{ date: "2024-05-27", desktop: 420, mobile: 460 },
|
||||
{ date: "2024-05-28", desktop: 233, mobile: 190 },
|
||||
{ date: "2024-05-29", desktop: 78, mobile: 130 },
|
||||
{ date: "2024-05-30", desktop: 340, mobile: 280 },
|
||||
{ date: "2024-05-31", desktop: 178, mobile: 230 },
|
||||
{ date: "2024-06-01", desktop: 178, mobile: 200 },
|
||||
{ date: "2024-06-02", desktop: 470, mobile: 410 },
|
||||
{ date: "2024-06-03", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-04", desktop: 439, mobile: 380 },
|
||||
{ date: "2024-06-05", desktop: 88, mobile: 140 },
|
||||
{ date: "2024-06-06", desktop: 294, mobile: 250 },
|
||||
{ date: "2024-06-07", desktop: 323, mobile: 370 },
|
||||
{ date: "2024-06-08", desktop: 385, mobile: 320 },
|
||||
{ date: "2024-06-09", desktop: 438, mobile: 480 },
|
||||
{ date: "2024-06-10", desktop: 155, mobile: 200 },
|
||||
{ date: "2024-06-11", desktop: 92, mobile: 150 },
|
||||
{ date: "2024-06-12", desktop: 492, mobile: 420 },
|
||||
{ date: "2024-06-13", desktop: 81, mobile: 130 },
|
||||
{ date: "2024-06-14", desktop: 426, mobile: 380 },
|
||||
{ date: "2024-06-15", desktop: 307, mobile: 350 },
|
||||
{ date: "2024-06-16", desktop: 371, mobile: 310 },
|
||||
{ date: "2024-06-17", desktop: 475, mobile: 520 },
|
||||
{ date: "2024-06-18", desktop: 107, mobile: 170 },
|
||||
{ date: "2024-06-19", desktop: 341, mobile: 290 },
|
||||
{ date: "2024-06-20", desktop: 408, mobile: 450 },
|
||||
{ date: "2024-06-21", desktop: 169, mobile: 210 },
|
||||
{ date: "2024-06-22", desktop: 317, mobile: 270 },
|
||||
{ date: "2024-06-23", desktop: 480, mobile: 530 },
|
||||
{ date: "2024-06-24", desktop: 132, mobile: 180 },
|
||||
{ date: "2024-06-25", desktop: 141, mobile: 190 },
|
||||
{ date: "2024-06-26", desktop: 434, mobile: 380 },
|
||||
{ date: "2024-06-27", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-06-28", desktop: 149, mobile: 200 },
|
||||
{ date: "2024-06-29", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-30", desktop: 446, mobile: 400 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("90d")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d")
|
||||
}
|
||||
}, [isMobile])
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
const referenceDate = new Date("2024-06-30")
|
||||
let daysToSubtract = 90
|
||||
if (timeRange === "30d") {
|
||||
daysToSubtract = 30
|
||||
} else if (timeRange === "7d") {
|
||||
daysToSubtract = 7
|
||||
}
|
||||
const startDate = new Date(referenceDate)
|
||||
startDate.setDate(startDate.getDate() - daysToSubtract)
|
||||
return date >= startDate
|
||||
})
|
||||
|
||||
return (
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardTitle>Total Visitors</CardTitle>
|
||||
<CardDescription>
|
||||
<span className="hidden @[540px]/card:block">
|
||||
Total for the last 3 months
|
||||
</span>
|
||||
<span className="@[540px]/card:hidden">Last 3 months</span>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={timeRange}
|
||||
onValueChange={setTimeRange}
|
||||
variant="outline"
|
||||
className="hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"
|
||||
>
|
||||
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
|
||||
<ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger
|
||||
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
|
||||
size="sm"
|
||||
aria-label="Select a value"
|
||||
>
|
||||
<SelectValue placeholder="Last 3 months" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="90d" className="rounded-lg">
|
||||
Last 3 months
|
||||
</SelectItem>
|
||||
<SelectItem value="30d" className="rounded-lg">
|
||||
Last 30 days
|
||||
</SelectItem>
|
||||
<SelectItem value="7d" className="rounded-lg">
|
||||
Last 7 days
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-[250px] w-full"
|
||||
>
|
||||
<AreaChart data={filteredData}>
|
||||
<defs>
|
||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--color-desktop)"
|
||||
stopOpacity={1.0}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--color-desktop)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="var(--color-mobile)"
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="var(--color-mobile)"
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
}}
|
||||
indicator="dot"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="url(#fillMobile)"
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="url(#fillDesktop)"
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,892 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
type UniqueIdentifier,
|
||||
} from "@dnd-kit/core"
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import {
|
||||
columnFilteringFeature,
|
||||
columnVisibilityFeature,
|
||||
createColumnHelper,
|
||||
createFilteredRowModel,
|
||||
createPaginatedRowModel,
|
||||
createSortedRowModel,
|
||||
FlexRender,
|
||||
rowPaginationFeature,
|
||||
rowSelectionFeature,
|
||||
rowSortingFeature,
|
||||
tableFeatures,
|
||||
useTable,
|
||||
type ColumnFiltersState,
|
||||
type ColumnVisibilityState,
|
||||
type Row,
|
||||
type SortingState,
|
||||
} from "@tanstack/react-table"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { useIsMobile } from "@/registry/bases/radix/hooks/use-mobile"
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import { Checkbox } from "@/registry/bases/radix/ui/checkbox"
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/registry/bases/radix/ui/drawer"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import { Label } from "@/registry/bases/radix/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/bases/radix/ui/table"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/bases/radix/ui/tabs"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// New in v9: declare the features this table uses — anything you don't
|
||||
// register is tree-shaken out of the bundle.
|
||||
const features = tableFeatures({
|
||||
columnFilteringFeature,
|
||||
columnVisibilityFeature,
|
||||
rowPaginationFeature,
|
||||
rowSelectionFeature,
|
||||
rowSortingFeature,
|
||||
filteredRowModel: createFilteredRowModel(),
|
||||
paginatedRowModel: createPaginatedRowModel(),
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
})
|
||||
|
||||
const columnHelper = createColumnHelper<
|
||||
typeof features,
|
||||
z.infer<typeof schema>
|
||||
>()
|
||||
|
||||
export const schema = z.object({
|
||||
id: z.number(),
|
||||
header: z.string(),
|
||||
type: z.string(),
|
||||
status: z.string(),
|
||||
target: z.string(),
|
||||
limit: z.string(),
|
||||
reviewer: z.string(),
|
||||
})
|
||||
|
||||
// Create a separate component for the drag handle
|
||||
function DragHandle({ id }: { id: number }) {
|
||||
const { attributes, listeners } = useSortable({
|
||||
id,
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:bg-transparent"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="GripVerticalIcon"
|
||||
tabler="IconGripVertical"
|
||||
hugeicons="DragDropVerticalIcon"
|
||||
phosphor="DotsSixVerticalIcon"
|
||||
remixicon="RiDraggable"
|
||||
className="size-3 text-muted-foreground"
|
||||
/>
|
||||
<span className="sr-only">Drag to reorder</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const columns = columnHelper.columns([
|
||||
columnHelper.display({
|
||||
id: "drag",
|
||||
header: () => null,
|
||||
cell: ({ row }) => <DragHandle id={row.original.id} />,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}),
|
||||
columnHelper.accessor("header", {
|
||||
header: "Header",
|
||||
cell: ({ row }) => {
|
||||
return <TableCellViewer item={row.original} />
|
||||
},
|
||||
enableHiding: false,
|
||||
}),
|
||||
columnHelper.accessor("type", {
|
||||
header: "Section Type",
|
||||
cell: ({ row }) => (
|
||||
<div className="w-32">
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
{row.original.type}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
{row.original.status === "Done" ? (
|
||||
<IconPlaceholder
|
||||
lucide="CircleCheckIcon"
|
||||
tabler="IconCircleCheckFilled"
|
||||
hugeicons="CheckmarkCircle01Icon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiCheckboxCircleFill"
|
||||
className="fill-green-500 dark:fill-green-400"
|
||||
/>
|
||||
) : (
|
||||
<IconPlaceholder
|
||||
lucide="LoaderIcon"
|
||||
tabler="IconLoader"
|
||||
hugeicons="Loading03Icon"
|
||||
phosphor="SpinnerIcon"
|
||||
remixicon="RiLoader4Line"
|
||||
/>
|
||||
)}
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("target", {
|
||||
header: () => <div className="w-full text-right">Target</div>,
|
||||
cell: ({ row }) => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Label htmlFor={`${row.original.id}-target`} className="sr-only">
|
||||
Target
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
defaultValue={row.original.target}
|
||||
id={`${row.original.id}-target`}
|
||||
/>
|
||||
</form>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("limit", {
|
||||
header: () => <div className="w-full text-right">Limit</div>,
|
||||
cell: ({ row }) => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Label htmlFor={`${row.original.id}-limit`} className="sr-only">
|
||||
Limit
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
defaultValue={row.original.limit}
|
||||
id={`${row.original.id}-limit`}
|
||||
/>
|
||||
</form>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("reviewer", {
|
||||
header: "Reviewer",
|
||||
cell: ({ row }) => {
|
||||
const isAssigned = row.original.reviewer !== "Assign reviewer"
|
||||
|
||||
if (isAssigned) {
|
||||
return row.original.reviewer
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label htmlFor={`${row.original.id}-reviewer`} className="sr-only">
|
||||
Reviewer
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger
|
||||
className="w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate"
|
||||
size="sm"
|
||||
id={`${row.original.id}-reviewer`}
|
||||
>
|
||||
<SelectValue placeholder="Assign reviewer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectGroup>
|
||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||
<SelectItem value="Jamik Tashpulatov">
|
||||
Jamik Tashpulatov
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: () => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
|
||||
size="icon"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="EllipsisVerticalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeVerticalIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
||||
<DropdownMenuItem>Favorite</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
}),
|
||||
])
|
||||
|
||||
function DraggableRow({
|
||||
row,
|
||||
}: {
|
||||
row: Row<typeof features, z.infer<typeof schema>>
|
||||
}) {
|
||||
const { transform, transition, setNodeRef, isDragging } = useSortable({
|
||||
id: row.original.id,
|
||||
})
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
data-dragging={isDragging}
|
||||
ref={setNodeRef}
|
||||
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||
style={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: transition,
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
<FlexRender cell={cell} />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
export function DataTable({
|
||||
data: initialData,
|
||||
}: {
|
||||
data: z.infer<typeof schema>[]
|
||||
}) {
|
||||
const [data, setData] = React.useState(() => initialData)
|
||||
const [rowSelection, setRowSelection] = React.useState({})
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<ColumnVisibilityState>({})
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [pagination, setPagination] = React.useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
const sortableId = React.useId()
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
useSensor(TouchSensor, {}),
|
||||
useSensor(KeyboardSensor, {})
|
||||
)
|
||||
|
||||
const dataIds = React.useMemo<UniqueIdentifier[]>(
|
||||
() => data?.map(({ id }) => id) || [],
|
||||
[data]
|
||||
)
|
||||
|
||||
const table = useTable({
|
||||
features,
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
pagination,
|
||||
},
|
||||
getRowId: (row) => row.id.toString(),
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: setPagination,
|
||||
})
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event
|
||||
if (active && over && active.id !== over.id) {
|
||||
setData((data) => {
|
||||
const oldIndex = dataIds.indexOf(active.id)
|
||||
const newIndex = dataIds.indexOf(over.id)
|
||||
return arrayMove(data, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultValue="outline"
|
||||
className="w-full flex-col justify-start gap-6"
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 lg:px-6">
|
||||
<Label htmlFor="view-selector" className="sr-only">
|
||||
View
|
||||
</Label>
|
||||
<Select defaultValue="outline">
|
||||
<SelectTrigger
|
||||
className="flex w-fit @4xl/main:hidden"
|
||||
size="sm"
|
||||
id="view-selector"
|
||||
>
|
||||
<SelectValue placeholder="Select a view" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="outline">Outline</SelectItem>
|
||||
<SelectItem value="past-performance">Past Performance</SelectItem>
|
||||
<SelectItem value="key-personnel">Key Personnel</SelectItem>
|
||||
<SelectItem value="focus-documents">Focus Documents</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TabsList className="hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsTrigger value="outline">Outline</TabsTrigger>
|
||||
<TabsTrigger value="past-performance">
|
||||
Past Performance <Badge variant="secondary">3</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="key-personnel">
|
||||
Key Personnel <Badge variant="secondary">2</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconPlaceholder
|
||||
lucide="Columns3Icon"
|
||||
tabler="IconLayoutColumns"
|
||||
hugeicons="LeftToRightListBulletIcon"
|
||||
phosphor="ColumnsIcon"
|
||||
remixicon="RiLayoutColumnLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Columns
|
||||
<IconPlaceholder
|
||||
lucide="ChevronDownIcon"
|
||||
tabler="IconChevronDown"
|
||||
hugeicons="ArrowDown01Icon"
|
||||
phosphor="CaretDownIcon"
|
||||
remixicon="RiArrowDownSLine"
|
||||
data-icon="inline-end"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== "undefined" &&
|
||||
column.getCanHide()
|
||||
)
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="Add01Icon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
<span className="hidden lg:inline">Add Section</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent
|
||||
value="outline"
|
||||
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
|
||||
>
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
id={sortableId}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10 bg-muted">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<FlexRender header={header} />
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="**:data-[slot=table-cell]:first:w-8">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
<SortableContext
|
||||
items={dataIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<DraggableRow key={row.id} row={row} />
|
||||
))}
|
||||
</SortableContext>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-8 lg:w-fit">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
||||
Rows per page
|
||||
</Label>
|
||||
<Select
|
||||
value={`${table.state.pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value))
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
|
||||
<SelectValue placeholder={table.state.pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
<SelectGroup>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
||||
Page {table.state.pagination.pageIndex + 1} of{" "}
|
||||
{table.getPageCount()}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronsLeftIcon"
|
||||
tabler="IconChevronsLeft"
|
||||
hugeicons="ArrowLeftDoubleIcon"
|
||||
phosphor="CaretDoubleLeftIcon"
|
||||
remixicon="RiSkipLeftLine"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
size="icon"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronLeftIcon"
|
||||
tabler="IconChevronLeft"
|
||||
hugeicons="ArrowLeft01Icon"
|
||||
phosphor="CaretLeftIcon"
|
||||
remixicon="RiArrowLeftSLine"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
size="icon"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden size-8 lg:flex"
|
||||
size="icon"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronsRightIcon"
|
||||
tabler="IconChevronsRight"
|
||||
hugeicons="ArrowRightDoubleIcon"
|
||||
phosphor="CaretDoubleRightIcon"
|
||||
remixicon="RiSkipRightLine"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="past-performance"
|
||||
className="flex flex-col px-4 lg:px-6"
|
||||
>
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="focus-documents"
|
||||
className="flex flex-col px-4 lg:px-6"
|
||||
>
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
const chartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<Drawer direction={isMobile ? "bottom" : "right"}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="link" className="w-fit px-0 text-left text-foreground">
|
||||
{item.header}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader className="gap-1">
|
||||
<DrawerTitle>{item.header}</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Showing total visitors for the last 6 months
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
|
||||
{!isMobile && (
|
||||
<>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 10,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
hide
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dot" />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.6}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
<Separator />
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2 leading-none font-medium">
|
||||
Trending up by 5.2% this month{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Showing total visitors for the last 6 months. This is just
|
||||
some random text to test the layout. It spans multiple lines
|
||||
and should wrap around.
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<form className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="header">Header</Label>
|
||||
<Input id="header" defaultValue={item.header} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="type">Type</Label>
|
||||
<Select defaultValue={item.type}>
|
||||
<SelectTrigger id="type" className="w-full">
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Table of Contents">
|
||||
Table of Contents
|
||||
</SelectItem>
|
||||
<SelectItem value="Executive Summary">
|
||||
Executive Summary
|
||||
</SelectItem>
|
||||
<SelectItem value="Technical Approach">
|
||||
Technical Approach
|
||||
</SelectItem>
|
||||
<SelectItem value="Design">Design</SelectItem>
|
||||
<SelectItem value="Capabilities">Capabilities</SelectItem>
|
||||
<SelectItem value="Focus Documents">
|
||||
Focus Documents
|
||||
</SelectItem>
|
||||
<SelectItem value="Narrative">Narrative</SelectItem>
|
||||
<SelectItem value="Cover Page">Cover Page</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select defaultValue={item.status}>
|
||||
<SelectTrigger id="status" className="w-full">
|
||||
<SelectValue placeholder="Select a status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Done">Done</SelectItem>
|
||||
<SelectItem value="In Progress">In Progress</SelectItem>
|
||||
<SelectItem value="Not Started">Not Started</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="target">Target</Label>
|
||||
<Input id="target" defaultValue={item.target} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="limit">Limit</Label>
|
||||
<Input id="limit" defaultValue={item.limit} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="reviewer">Reviewer</Label>
|
||||
<Select defaultValue={item.reviewer}>
|
||||
<SelectTrigger id="reviewer" className="w-full">
|
||||
<SelectValue placeholder="Select a reviewer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||
<SelectItem value="Jamik Tashpulatov">
|
||||
Jamik Tashpulatov
|
||||
</SelectItem>
|
||||
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button>Submit</Button>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Done</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/registry/bases/radix/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NavDocuments({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
name: string
|
||||
url: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Documents</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
{item.icon}
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="rounded-sm data-[state=open]:bg-accent"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-24 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="FolderIcon"
|
||||
tabler="IconFolder"
|
||||
hugeicons="Folder01Icon"
|
||||
phosphor="FolderIcon"
|
||||
remixicon="RiFolderLine"
|
||||
/>
|
||||
<span>Open</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ShareIcon"
|
||||
tabler="IconShare3"
|
||||
hugeicons="Share01Icon"
|
||||
phosphor="ShareIcon"
|
||||
remixicon="RiShareLine"
|
||||
/>
|
||||
<span>Share</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<IconPlaceholder
|
||||
lucide="Trash2Icon"
|
||||
tabler="IconTrash"
|
||||
hugeicons="Delete02Icon"
|
||||
phosphor="TrashIcon"
|
||||
remixicon="RiDeleteBinLine"
|
||||
/>
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
className="text-sidebar-foreground/70"
|
||||
/>
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/bases/radix/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon?: React.ReactNode
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex items-center gap-2">
|
||||
<SidebarMenuButton
|
||||
tooltip="Quick Create"
|
||||
className="min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="CirclePlusIcon"
|
||||
tabler="IconCirclePlusFilled"
|
||||
hugeicons="PlusSignCircleIcon"
|
||||
phosphor="PlusCircleIcon"
|
||||
remixicon="RiAddCircleFill"
|
||||
/>
|
||||
<span>Quick Create</span>
|
||||
</SidebarMenuButton>
|
||||
<Button
|
||||
size="icon"
|
||||
className="size-8 group-data-[collapsible=icon]:opacity-0"
|
||||
variant="outline"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MailIcon"
|
||||
tabler="IconMail"
|
||||
hugeicons="Mail01Icon"
|
||||
phosphor="EnvelopeIcon"
|
||||
remixicon="RiMailLine"
|
||||
/>
|
||||
<span className="sr-only">Inbox</span>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/bases/radix/ui/sidebar"
|
||||
|
||||
export function NavSecondary({
|
||||
items,
|
||||
...props
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||
return (
|
||||
<SidebarGroup {...props}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/radix/ui/avatar"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/registry/bases/radix/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg grayscale">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
<IconPlaceholder
|
||||
lucide="EllipsisVerticalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeVerticalIcon"
|
||||
remixicon="RiMore2Line"
|
||||
className="ml-auto size-4"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="CircleUserRoundIcon"
|
||||
tabler="IconUserCircle"
|
||||
hugeicons="UserCircle02Icon"
|
||||
phosphor="UserCircleIcon"
|
||||
remixicon="RiUserLine"
|
||||
/>
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="BellIcon"
|
||||
tabler="IconNotification"
|
||||
hugeicons="Notification03Icon"
|
||||
phosphor="BellIcon"
|
||||
remixicon="RiNotification3Line"
|
||||
/>
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="LogOutIcon"
|
||||
tabler="IconLogout"
|
||||
hugeicons="Logout01Icon"
|
||||
phosphor="SignOutIcon"
|
||||
remixicon="RiLogoutBoxLine"
|
||||
/>
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function SectionCards() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4 dark:*:data-[slot=card]:bg-card">
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Total Revenue</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
$1,250.00
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
/>
|
||||
+12.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Trending up this month{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>New Customers</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
1,234
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="TrendingDownIcon"
|
||||
tabler="IconTrendingDown"
|
||||
hugeicons="ChartDownIcon"
|
||||
phosphor="TrendDownIcon"
|
||||
remixicon="RiArrowDownLine"
|
||||
/>
|
||||
-20%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Down 20% this period{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingDownIcon"
|
||||
tabler="IconTrendingDown"
|
||||
hugeicons="ChartDownIcon"
|
||||
phosphor="TrendDownIcon"
|
||||
remixicon="RiArrowDownLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Acquisition needs attention
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Active Accounts</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
45,678
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
/>
|
||||
+12.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Strong user retention{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">Engagement exceed targets</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Growth Rate</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
4.5%
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
/>
|
||||
+4.5%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
Steady performance increase{" "}
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="ChartUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">Meets growth projections</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import { SidebarTrigger } from "@/registry/bases/radix/ui/sidebar"
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<h1 className="text-base font-medium">Documents</h1>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
614
apps/v4/registry/bases/radix/blocks/dashboard-01/data.json
Normal file
614
apps/v4/registry/bases/radix/blocks/dashboard-01/data.json
Normal file
@@ -0,0 +1,614 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"header": "Cover page",
|
||||
"type": "Cover page",
|
||||
"status": "In Process",
|
||||
"target": "18",
|
||||
"limit": "5",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"header": "Table of contents",
|
||||
"type": "Table of contents",
|
||||
"status": "Done",
|
||||
"target": "29",
|
||||
"limit": "24",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"header": "Executive summary",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "10",
|
||||
"limit": "13",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"header": "Technical approach",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "27",
|
||||
"limit": "23",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"header": "Design",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "2",
|
||||
"limit": "16",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"header": "Capabilities",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "20",
|
||||
"limit": "8",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"header": "Integration with existing systems",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "21",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"header": "Innovation and Advantages",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "25",
|
||||
"limit": "26",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"header": "Overview of EMR's Innovative Solutions",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "7",
|
||||
"limit": "23",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"header": "Advanced Algorithms and Machine Learning",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "28",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"header": "Adaptive Communication Protocols",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "9",
|
||||
"limit": "31",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"header": "Advantages Over Current Technologies",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "12",
|
||||
"limit": "0",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"header": "Past Performance",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "33",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"header": "Customer Feedback and Satisfaction Levels",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "15",
|
||||
"limit": "34",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"header": "Implementation Challenges and Solutions",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "3",
|
||||
"limit": "35",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"header": "Security Measures and Data Protection Policies",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "6",
|
||||
"limit": "36",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"header": "Scalability and Future Proofing",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "4",
|
||||
"limit": "37",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"header": "Cost-Benefit Analysis",
|
||||
"type": "Plain language",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "38",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"header": "User Training and Onboarding Experience",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "17",
|
||||
"limit": "39",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"header": "Future Development Roadmap",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "11",
|
||||
"limit": "40",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"header": "System Architecture Overview",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "24",
|
||||
"limit": "18",
|
||||
"reviewer": "Maya Johnson"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"header": "Risk Management Plan",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "15",
|
||||
"limit": "22",
|
||||
"reviewer": "Carlos Rodriguez"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"header": "Compliance Documentation",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "31",
|
||||
"limit": "27",
|
||||
"reviewer": "Sarah Chen"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"header": "API Documentation",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "8",
|
||||
"limit": "12",
|
||||
"reviewer": "Raj Patel"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"header": "User Interface Mockups",
|
||||
"type": "Visual",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "25",
|
||||
"reviewer": "Leila Ahmadi"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"header": "Database Schema",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "20",
|
||||
"reviewer": "Thomas Wilson"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"header": "Testing Methodology",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "17",
|
||||
"limit": "14",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"header": "Deployment Strategy",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "30",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"header": "Budget Breakdown",
|
||||
"type": "Financial",
|
||||
"status": "In Process",
|
||||
"target": "13",
|
||||
"limit": "16",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"header": "Market Analysis",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "29",
|
||||
"limit": "32",
|
||||
"reviewer": "Sophia Martinez"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"header": "Competitor Comparison",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "19",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"header": "Maintenance Plan",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "16",
|
||||
"limit": "23",
|
||||
"reviewer": "Alex Thompson"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"header": "User Personas",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "24",
|
||||
"reviewer": "Nina Patel"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"header": "Accessibility Compliance",
|
||||
"type": "Legal",
|
||||
"status": "Done",
|
||||
"target": "18",
|
||||
"limit": "21",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"header": "Performance Metrics",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "23",
|
||||
"limit": "26",
|
||||
"reviewer": "David Kim"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"header": "Disaster Recovery Plan",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "17",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"header": "Third-party Integrations",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "25",
|
||||
"limit": "28",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"header": "User Feedback Summary",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "20",
|
||||
"limit": "15",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"header": "Localization Strategy",
|
||||
"type": "Narrative",
|
||||
"status": "In Process",
|
||||
"target": "12",
|
||||
"limit": "19",
|
||||
"reviewer": "Maria Garcia"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"header": "Mobile Compatibility",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "28",
|
||||
"limit": "31",
|
||||
"reviewer": "James Wilson"
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"header": "Data Migration Plan",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "22",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"header": "Quality Assurance Protocols",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "33",
|
||||
"reviewer": "Priya Singh"
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"header": "Stakeholder Analysis",
|
||||
"type": "Research",
|
||||
"status": "In Process",
|
||||
"target": "11",
|
||||
"limit": "14",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"header": "Environmental Impact Assessment",
|
||||
"type": "Research",
|
||||
"status": "Done",
|
||||
"target": "24",
|
||||
"limit": "27",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"header": "Intellectual Property Rights",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "17",
|
||||
"limit": "20",
|
||||
"reviewer": "Sarah Johnson"
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"header": "Customer Support Framework",
|
||||
"type": "Narrative",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "25",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"header": "Version Control Strategy",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "15",
|
||||
"limit": "18",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"header": "Continuous Integration Pipeline",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "29",
|
||||
"reviewer": "Michael Chen"
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"header": "Regulatory Compliance",
|
||||
"type": "Legal",
|
||||
"status": "In Process",
|
||||
"target": "13",
|
||||
"limit": "16",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"header": "User Authentication System",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "28",
|
||||
"limit": "31",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"header": "Data Analytics Framework",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "24",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"header": "Cloud Infrastructure",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "16",
|
||||
"limit": "19",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"header": "Network Security Measures",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "29",
|
||||
"limit": "32",
|
||||
"reviewer": "Lisa Wong"
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"header": "Project Timeline",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "14",
|
||||
"limit": "17",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"header": "Resource Allocation",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "30",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"header": "Team Structure and Roles",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "20",
|
||||
"limit": "23",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"header": "Communication Protocols",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "15",
|
||||
"limit": "18",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"header": "Success Metrics",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "30",
|
||||
"limit": "33",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"header": "Internationalization Support",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "23",
|
||||
"limit": "26",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"header": "Backup and Recovery Procedures",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "18",
|
||||
"limit": "21",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"header": "Monitoring and Alerting System",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "25",
|
||||
"limit": "28",
|
||||
"reviewer": "Daniel Park"
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"header": "Code Review Guidelines",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "12",
|
||||
"limit": "15",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"header": "Documentation Standards",
|
||||
"type": "Technical content",
|
||||
"status": "In Process",
|
||||
"target": "27",
|
||||
"limit": "30",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"header": "Release Management Process",
|
||||
"type": "Planning",
|
||||
"status": "Done",
|
||||
"target": "22",
|
||||
"limit": "25",
|
||||
"reviewer": "Assign reviewer"
|
||||
},
|
||||
{
|
||||
"id": 65,
|
||||
"header": "Feature Prioritization Matrix",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "19",
|
||||
"limit": "22",
|
||||
"reviewer": "Emma Davis"
|
||||
},
|
||||
{
|
||||
"id": 66,
|
||||
"header": "Technical Debt Assessment",
|
||||
"type": "Technical content",
|
||||
"status": "Done",
|
||||
"target": "24",
|
||||
"limit": "27",
|
||||
"reviewer": "Eddie Lake"
|
||||
},
|
||||
{
|
||||
"id": 67,
|
||||
"header": "Capacity Planning",
|
||||
"type": "Planning",
|
||||
"status": "In Process",
|
||||
"target": "21",
|
||||
"limit": "24",
|
||||
"reviewer": "Jamik Tashpulatov"
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
"header": "Service Level Agreements",
|
||||
"type": "Legal",
|
||||
"status": "Done",
|
||||
"target": "26",
|
||||
"limit": "29",
|
||||
"reviewer": "Assign reviewer"
|
||||
}
|
||||
]
|
||||
40
apps/v4/registry/bases/radix/blocks/dashboard-01/page.tsx
Normal file
40
apps/v4/registry/bases/radix/blocks/dashboard-01/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { AppSidebar } from "@/registry/bases/radix/blocks/dashboard-01/components/app-sidebar"
|
||||
import { ChartAreaInteractive } from "@/registry/bases/radix/blocks/dashboard-01/components/chart-area-interactive"
|
||||
import { DataTable } from "@/registry/bases/radix/blocks/dashboard-01/components/data-table"
|
||||
import { SectionCards } from "@/registry/bases/radix/blocks/dashboard-01/components/section-cards"
|
||||
import { SiteHeader } from "@/registry/bases/radix/blocks/dashboard-01/components/site-header"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
} from "@/registry/bases/radix/ui/sidebar"
|
||||
|
||||
import data from "./data.json"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar variant="inset" />
|
||||
<SidebarInset>
|
||||
<SiteHeader />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
||||
<SectionCards />
|
||||
<div className="px-4 lg:px-6">
|
||||
<ChartAreaInteractive />
|
||||
</div>
|
||||
<DataTable data={data} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Login to your account</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
<Button variant="outline" type="button">
|
||||
Login with Google
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
apps/v4/registry/bases/radix/blocks/login-01/page.tsx
Normal file
11
apps/v4/registry/bases/radix/blocks/login-01/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from "@/registry/bases/radix/blocks/login-01/components/login-form"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<FieldGroup>
|
||||
<div className="flex flex-col items-center gap-1 text-center">
|
||||
<h1 className="text-2xl font-bold">Login to your account</h1>
|
||||
<p className="text-sm text-balance text-muted-foreground">
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
className="bg-background"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
className="bg-background"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
</Field>
|
||||
<FieldSeparator>Or continue with</FieldSeparator>
|
||||
<Field>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with GitHub
|
||||
</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account?{" "}
|
||||
<a href="#" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
40
apps/v4/registry/bases/radix/blocks/login-02/page.tsx
Normal file
40
apps/v4/registry/bases/radix/blocks/login-02/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import { LoginForm } from "@/registry/bases/radix/blocks/login-02/components/login-form"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<a href="#" className="flex items-center gap-2 font-medium">
|
||||
<div className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="GalleryVerticalEndIcon"
|
||||
tabler="IconLayoutRows"
|
||||
hugeicons="LayoutBottomIcon"
|
||||
phosphor="RowsIcon"
|
||||
remixicon="RiGalleryLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
Acme Inc.
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-xs">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative hidden bg-muted lg:block">
|
||||
<img
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Welcome back</CardTitle>
|
||||
<CardDescription>
|
||||
Login with your Apple or Google account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with Apple
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Login with Google
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
apps/v4/registry/bases/radix/blocks/login-03/page.tsx
Normal file
27
apps/v4/registry/bases/radix/blocks/login-03/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { LoginForm } from "@/registry/bases/radix/blocks/login-03/components/login-form"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<a href="#" className="flex items-center gap-2 self-center font-medium">
|
||||
<div className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="GalleryVerticalEndIcon"
|
||||
tabler="IconLayoutRows"
|
||||
hugeicons="LayoutBottomIcon"
|
||||
phosphor="RowsIcon"
|
||||
remixicon="RiGalleryLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
Acme Inc.
|
||||
</a>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<Card className="overflow-hidden p-0">
|
||||
<CardContent className="grid p-0 md:grid-cols-2">
|
||||
<form className="p-6 md:p-8">
|
||||
<FieldGroup>
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">Welcome back</h1>
|
||||
<p className="text-balance text-muted-foreground">
|
||||
Login to your Acme Inc account
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="ml-auto text-sm underline-offset-2 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
</Field>
|
||||
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
<Field className="grid grid-cols-3 gap-4">
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Apple</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Google</span>
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Login with Meta</span>
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldDescription className="text-center">
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<div className="relative hidden bg-muted md:block">
|
||||
<img
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
apps/v4/registry/bases/radix/blocks/login-04/page.tsx
Normal file
11
apps/v4/registry/bases/radix/blocks/login-04/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from "@/registry/bases/radix/blocks/login-04/components/login-form"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10">
|
||||
<div className="w-full max-w-sm md:max-w-4xl">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<a
|
||||
href="#"
|
||||
className="flex flex-col items-center gap-2 font-medium"
|
||||
>
|
||||
<div className="flex size-8 items-center justify-center rounded-md">
|
||||
<IconPlaceholder
|
||||
lucide="GalleryVerticalEndIcon"
|
||||
tabler="IconLayoutRows"
|
||||
hugeicons="LayoutBottomIcon"
|
||||
phosphor="RowsIcon"
|
||||
remixicon="RiGalleryLine"
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<span className="sr-only">Acme Inc.</span>
|
||||
</a>
|
||||
<h1 className="text-xl font-bold">Welcome to Acme Inc.</h1>
|
||||
<FieldDescription>
|
||||
Don't have an account? <a href="#">Sign up</a>
|
||||
</FieldDescription>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
</Field>
|
||||
<FieldSeparator>Or</FieldSeparator>
|
||||
<Field className="grid gap-4 sm:grid-cols-2">
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Apple
|
||||
</Button>
|
||||
<Button variant="outline" type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
and <a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
apps/v4/registry/bases/radix/blocks/login-05/page.tsx
Normal file
11
apps/v4/registry/bases/radix/blocks/login-05/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from "@/registry/bases/radix/blocks/login-05/components/login-form"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function AccountAccess() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Access</CardTitle>
|
||||
<CardDescription>
|
||||
Update your credentials or re-authenticate.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email-address">Email Address</FieldLabel>
|
||||
<Input
|
||||
id="email-address"
|
||||
type="email"
|
||||
defaultValue="artist@studio.inc"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel htmlFor="current-password">
|
||||
Current Password
|
||||
</FieldLabel>
|
||||
<a
|
||||
href="#"
|
||||
className="text-xs font-medium tracking-wider text-muted-foreground uppercase hover:text-foreground"
|
||||
>
|
||||
Forgot?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
id="current-password"
|
||||
type="password"
|
||||
defaultValue="password123"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Button className="w-full">
|
||||
<IconPlaceholder
|
||||
lucide="LockKeyholeIcon"
|
||||
tabler="IconLock"
|
||||
hugeicons="SquareLock02Icon"
|
||||
phosphor="LockKeyIcon"
|
||||
remixicon="RiLockLine"
|
||||
/>
|
||||
Update Security
|
||||
</Button>
|
||||
<Item asChild variant="muted">
|
||||
<a href="#">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="AlertCircleIcon"
|
||||
tabler="IconAlertCircle"
|
||||
hugeicons="AlertCircleIcon"
|
||||
phosphor="WarningCircleIcon"
|
||||
remixicon="RiErrorWarningLine"
|
||||
className="text-destructive"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Danger Zone</ItemTitle>
|
||||
<ItemDescription className="line-clamp-1">
|
||||
Archive account and remove catalog
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ArrowRightIcon"
|
||||
tabler="IconArrowRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="ArrowRightIcon"
|
||||
remixicon="RiArrowRightLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</a>
|
||||
</Item>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
|
||||
export function AlbumCard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="relative overflow-hidden rounded-lg">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1534088568595-a066f410bcda?w=800&h=800&fit=crop"
|
||||
alt="Synthetic Horizons EP cover art"
|
||||
className="aspect-square w-full object-cover"
|
||||
/>
|
||||
<Badge className="absolute top-3 right-3">$26,033.79</Badge>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle>Synthetic Horizons EP</CardTitle>
|
||||
<CardDescription className="text-xs tracking-wider uppercase">
|
||||
Released Aug 14, 2023
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-4">
|
||||
<Separator />
|
||||
<div className="grid w-full grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs tracking-wider text-muted-foreground uppercase">
|
||||
Tracks
|
||||
</span>
|
||||
<span className="text-lg font-medium tabular-nums">6 Tracks</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="line-clamp-1 text-xs tracking-wider text-muted-foreground uppercase">
|
||||
Cumulative Streams
|
||||
</span>
|
||||
<span className="text-lg font-medium tabular-nums">6,198,524</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
|
||||
const activityData = [
|
||||
{ month: "Jan", amount: 40 },
|
||||
{ month: "Feb", amount: 55 },
|
||||
{ month: "Mar", amount: 35 },
|
||||
{ month: "Apr", amount: 60 },
|
||||
{ month: "May", amount: 45 },
|
||||
{ month: "Jun", amount: 50 },
|
||||
{ month: "Jul", amount: 65 },
|
||||
{ month: "Aug", amount: 40 },
|
||||
{ month: "Sep", amount: 55 },
|
||||
{ month: "Oct", amount: 70 },
|
||||
{ month: "Nov", amount: 45 },
|
||||
{ month: "Dec", amount: 80 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
amount: {
|
||||
label: "Activity",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function CardOverview() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<CardDescription>Card Balance</CardDescription>
|
||||
<CardTitle className="text-2xl tabular-nums">US$12.94</CardTitle>
|
||||
<CardDescription className="tabular-nums">
|
||||
US$11,337.06 Available
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="flex flex-col justify-between">
|
||||
<CardContent className="flex flex-1 flex-col justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardDescription>Payment Due</CardDescription>
|
||||
<CardTitle className="text-2xl">1 Apr</CardTitle>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="mt-3 w-full">
|
||||
Pay Early
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-2">
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardDescription>Yearly Activity</CardDescription>
|
||||
<Badge variant="secondary">+US$0.25 Daily Cash</Badge>
|
||||
</div>
|
||||
<ChartContainer config={chartConfig} className="h-20 w-full">
|
||||
<BarChart
|
||||
data={activityData}
|
||||
margin={{ top: 4, right: 0, bottom: 0, left: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={4}
|
||||
axisLine={false}
|
||||
tickFormatter={(v) => String(v).slice(0, 1)}
|
||||
className="text-[10px]"
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="amount"
|
||||
fill="var(--color-amount)"
|
||||
radius={[3, 3, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/radix/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function CatalogToolbar() {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<InputGroup className="flex-1">
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search releases or catalog..." />
|
||||
</InputGroup>
|
||||
<Button>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="Add01Icon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
Upload New Release
|
||||
</Button>
|
||||
<ToggleGroup type="single" defaultValue="releases" variant="outline">
|
||||
<ToggleGroupItem value="all-tracks">All Tracks</ToggleGroupItem>
|
||||
<ToggleGroupItem value="releases">Releases</ToggleGroupItem>
|
||||
<ToggleGroupItem value="top-earners">Top Earners</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Item, ItemContent } from "@/registry/bases/radix/ui/item"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
|
||||
export function ClaimableBalance() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>Claimable Balance</CardDescription>
|
||||
<CardTitle className="text-5xl tabular-nums">$0.00</CardTitle>
|
||||
<Badge variant="outline">
|
||||
<span className="size-2 rounded-full bg-yellow-500" />
|
||||
Pending Setup
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col justify-end">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Net Royalties
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$0.00</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Processing Fee
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">-$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Total Ready to Claim
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
$0.00 USD
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardDescription>
|
||||
Once your bank is connected, balances over $10.00 are automatically
|
||||
eligible for monthly distribution on the 15th of each month.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
|
||||
const chartData = [
|
||||
{ month: "Dec", amount: 800 },
|
||||
{ month: "Jan", amount: 1100 },
|
||||
{ month: "Feb", amount: 900 },
|
||||
{ month: "Mar", amount: 1300 },
|
||||
{ month: "Apr", amount: 750 },
|
||||
{ month: "May", amount: 1400 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
amount: {
|
||||
label: "Contribution",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function ContributionHistory() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contribution History</CardTitle>
|
||||
<CardDescription>Last 6 months of activity</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-[200px] w-full">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{ left: 0, right: 0, top: 8, bottom: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel className="min-w-40" />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="amount"
|
||||
fill="var(--color-amount)"
|
||||
radius={isRounded ? [6, 6, 0, 0] : 0}
|
||||
maxBarSize={40}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className="grid w-full grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Upcoming
|
||||
</ItemDescription>
|
||||
<span className="cn-font-heading text-lg font-semibold">
|
||||
May 25, 2024
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
$1,000 scheduled
|
||||
</span>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Auto-Save Plan
|
||||
</ItemDescription>
|
||||
<span className="cn-font-heading text-lg font-semibold">
|
||||
Accelerated
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Recurring weekly
|
||||
</span>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View Full Report</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Item } from "@/registry/bases/radix/ui/item"
|
||||
import { Label } from "@/registry/bases/radix/ui/label"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function CoverArt() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<Label
|
||||
htmlFor="cover-art"
|
||||
className="text-center text-xs font-normal tracking-wider text-muted-foreground uppercase"
|
||||
>
|
||||
Cover Art
|
||||
</Label>
|
||||
<Item className="aspect-square" variant="outline">
|
||||
<label
|
||||
htmlFor="cover-art"
|
||||
className="flex size-full cursor-pointer items-center justify-center"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="ImageIcon"
|
||||
tabler="IconPhoto"
|
||||
hugeicons="Image01Icon"
|
||||
phosphor="ImageIcon"
|
||||
remixicon="RiImageLine"
|
||||
className="size-10 text-muted-foreground/50"
|
||||
/>
|
||||
</label>
|
||||
</Item>
|
||||
<input
|
||||
id="cover-art"
|
||||
type="file"
|
||||
accept="image/jpeg,image/png"
|
||||
className="sr-only"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Button variant="secondary" className="w-full" asChild>
|
||||
<label htmlFor="cover-art" className="cursor-pointer">
|
||||
Upload Artwork
|
||||
</label>
|
||||
</Button>
|
||||
<CardDescription className="text-center text-xs">
|
||||
Minimum 3000 × 3000px
|
||||
<br />
|
||||
JPEG or PNG only
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const HOLDINGS = [
|
||||
{
|
||||
name: "Vanguard VIG",
|
||||
shares: "450 Shares",
|
||||
amount: "$1,842.10",
|
||||
data: [
|
||||
{ q: "Q1", value: 380 },
|
||||
{ q: "Q2", value: 420 },
|
||||
{ q: "Q3", value: 390 },
|
||||
{ q: "Q4", value: 652 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "S&P 500 VOO",
|
||||
shares: "112 Shares",
|
||||
amount: "$928.40",
|
||||
data: [
|
||||
{ q: "Q1", value: 180 },
|
||||
{ q: "Q2", value: 210 },
|
||||
{ q: "Q3", value: 320 },
|
||||
{ q: "Q4", value: 218 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Apple AAPL",
|
||||
shares: "85 Shares",
|
||||
amount: "$340.00",
|
||||
data: [
|
||||
{ q: "Q1", value: 60 },
|
||||
{ q: "Q2", value: 70 },
|
||||
{ q: "Q3", value: 120 },
|
||||
{ q: "Q4", value: 90 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Realty Income",
|
||||
shares: "320 Shares",
|
||||
amount: "$1,139.50",
|
||||
data: [
|
||||
{ q: "Q1", value: 240 },
|
||||
{ q: "Q2", value: 260 },
|
||||
{ q: "Q3", value: 280 },
|
||||
{ q: "Q4", value: 360 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const miniChartConfig = {
|
||||
value: {
|
||||
label: "Dividend",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function DividendIncome() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Q2 Dividend Income</CardTitle>
|
||||
<CardDescription>
|
||||
Quarterly dividend payouts across your portfolio holdings.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
{HOLDINGS.map((holding) => (
|
||||
<Item key={holding.name} variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{holding.name}</ItemTitle>
|
||||
<ItemDescription>{holding.shares}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ChartContainer
|
||||
config={miniChartConfig}
|
||||
className="hidden h-8 w-24 md:block"
|
||||
>
|
||||
<BarChart
|
||||
data={holding.data}
|
||||
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--color-value)"
|
||||
radius={isRounded ? [3, 3, 0, 0] : 0}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<span className="hidden text-sm font-semibold tabular-nums md:block">
|
||||
{holding.amount}
|
||||
</span>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function EmptyConnectBank() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Connect Bank</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Link your payout method to receive monthly royalty distributions
|
||||
automatically.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Set Up Payouts</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function EmptyDistributeTrack() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="Add01Icon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Distribute Track</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Upload your first master to start reaching listeners on Spotify,
|
||||
Apple Music, and more.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Create Release</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function EmptyExploreCatalog() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="AudioLinesIcon"
|
||||
tabler="IconPlayerRecordFilled"
|
||||
hugeicons="AudioWave01Icon"
|
||||
phosphor="RecordIcon"
|
||||
remixicon="RiRecordCircleLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Explore Catalog</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Check your ISRC codes, metadata, and visual assets before going
|
||||
live.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>View Catalog</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
124
apps/v4/registry/bases/radix/blocks/preview-02/cards/faq.tsx
Normal file
124
apps/v4/registry/bases/radix/blocks/preview-02/cards/faq.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/registry/bases/radix/ui/accordion"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/bases/radix/ui/tabs"
|
||||
|
||||
const GENERAL_QUESTIONS = [
|
||||
{
|
||||
q: "How secure is my financial data with Ledger?",
|
||||
a: "We use bank-level AES-256 encryption, SOC 2 Type II certified infrastructure, and never store your credentials. All connections use read-only access tokens. We are a SEC registered investment advisor.",
|
||||
},
|
||||
{
|
||||
q: "How do I connect my bank or investment accounts?",
|
||||
a: "Go to Settings > Linked Accounts and search for your institution. We support over 12,000 banks and brokerages via Plaid and MX.",
|
||||
},
|
||||
{
|
||||
q: "Can I export my data for tax purposes?",
|
||||
a: "Yes. Navigate to Reports > Tax Export to download a CSV or PDF summary of your transactions, dividends, and capital gains for any tax year.",
|
||||
},
|
||||
]
|
||||
|
||||
const BILLING_QUESTIONS = [
|
||||
{
|
||||
q: "What is the difference between Basic and Pro pricing tiers?",
|
||||
a: "Basic includes budgeting, goal tracking, and up to 3 linked accounts. Pro adds unlimited accounts, dividend tracking, portfolio analysis, and priority support.",
|
||||
},
|
||||
{
|
||||
q: "How do I cancel my subscription?",
|
||||
a: "Go to Settings > Billing > Manage Plan and click Cancel. Your access continues until the end of your current billing period.",
|
||||
},
|
||||
{
|
||||
q: "Do you offer a free trial?",
|
||||
a: "Yes. All new accounts start with a 14-day Pro trial. No credit card required.",
|
||||
},
|
||||
]
|
||||
|
||||
const GOALS_QUESTIONS = [
|
||||
{
|
||||
q: "How do I set up a custom financial goal?",
|
||||
a: "Click New Goal from the Savings Targets card. Choose a category, set a target amount and date, and we'll calculate the monthly contribution needed.",
|
||||
},
|
||||
{
|
||||
q: "Can I track multiple goals at once?",
|
||||
a: "Yes. Pro accounts can track unlimited goals. Basic accounts support up to 3 active goals.",
|
||||
},
|
||||
{
|
||||
q: "How are monthly contributions calculated?",
|
||||
a: "We divide the remaining amount by the number of months until your target date, adjusted for your current savings rate and any auto-transfer schedules.",
|
||||
},
|
||||
]
|
||||
|
||||
function QuestionList({
|
||||
questions,
|
||||
}: {
|
||||
questions: { q: string; a: string }[]
|
||||
}) {
|
||||
return (
|
||||
<Accordion type="single" collapsible defaultValue="item-0">
|
||||
{questions.map((item, index) => (
|
||||
<AccordionItem key={index} value={`item-${index}`}>
|
||||
<AccordionTrigger>{item.q}</AccordionTrigger>
|
||||
<AccordionContent>{item.a}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
export function Faq() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="general">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="general" className="flex-1">
|
||||
General
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="billing" className="flex-1">
|
||||
Billing
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="goals" className="flex-1">
|
||||
Goals
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="general">
|
||||
<QuestionList questions={GENERAL_QUESTIONS} />
|
||||
</TabsContent>
|
||||
<TabsContent value="billing">
|
||||
<QuestionList questions={BILLING_QUESTIONS} />
|
||||
</TabsContent>
|
||||
<TabsContent value="goals">
|
||||
<QuestionList questions={GOALS_QUESTIONS} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" className="w-full">
|
||||
Contact Support
|
||||
</Button>
|
||||
<Button variant="link" className="w-full">
|
||||
Learn More
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function FrontDoor() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Front Door</CardTitle>
|
||||
<CardDescription>Smart Lock Pro</CardDescription>
|
||||
<CardAction>
|
||||
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
Locked
|
||||
<IconPlaceholder
|
||||
lucide="LockIcon"
|
||||
tabler="IconLock"
|
||||
hugeicons="SquareLock02Icon"
|
||||
phosphor="LockKeyIcon"
|
||||
remixicon="RiLockLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative flex aspect-video items-center justify-center overflow-hidden rounded-lg bg-muted bg-[repeating-linear-gradient(45deg,transparent,transparent_10px,var(--border)_10px,var(--border)_11px)]">
|
||||
<Badge variant="destructive" className="absolute top-2 right-2">
|
||||
Live
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function IndexInvesting() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Dollar-Cost Averaging</CardTitle>
|
||||
<CardDescription>
|
||||
A strategy for building wealth over time.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="mt-3 text-sm leading-relaxed style-sera:mt-0">
|
||||
<a
|
||||
href="#"
|
||||
className="underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Over time
|
||||
</a>
|
||||
, this smooths out the average cost of your investments. When prices
|
||||
drop, your fixed amount buys more shares. When prices rise, you buy
|
||||
fewer. The result is a lower average cost per share compared to
|
||||
lump-sum investing during volatile periods.
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { Slider } from "@/registry/bases/radix/ui/slider"
|
||||
import { Switch } from "@/registry/bases/radix/ui/switch"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/radix/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const SCENES = {
|
||||
cooking: { brightness: [90], colorTemp: [70], volume: [30], fade: [0] },
|
||||
dining: { brightness: [50], colorTemp: [40], volume: [20], fade: [60] },
|
||||
nightlight: { brightness: [15], colorTemp: [20], volume: [0], fade: [80] },
|
||||
focus: { brightness: [100], colorTemp: [85], volume: [0], fade: [0] },
|
||||
} as const
|
||||
|
||||
export function KitchenIsland() {
|
||||
const [enabled, setEnabled] = React.useState(true)
|
||||
const [scene, setScene] = React.useState("cooking")
|
||||
const [brightness, setBrightness] = React.useState([90])
|
||||
const [colorTemp, setColorTemp] = React.useState([70])
|
||||
const [volume, setVolume] = React.useState([30])
|
||||
const [fade, setFade] = React.useState([0])
|
||||
|
||||
const handleSceneChange = (value: string) => {
|
||||
if (!value) return
|
||||
setScene(value)
|
||||
const preset = SCENES[value as keyof typeof SCENES]
|
||||
setBrightness([...preset.brightness])
|
||||
setColorTemp([...preset.colorTemp])
|
||||
setVolume([...preset.volume])
|
||||
setFade([...preset.fade])
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kitchen Island</CardTitle>
|
||||
<CardDescription>Hue Color Ambient</CardDescription>
|
||||
<CardAction>
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="sr-only">Scenes</span>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={scene}
|
||||
onValueChange={handleSceneChange}
|
||||
variant="outline"
|
||||
spacing={1}
|
||||
className="flex-wrap"
|
||||
>
|
||||
<ToggleGroupItem value="cooking" disabled={!enabled}>
|
||||
Cooking
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dining" disabled={!enabled}>
|
||||
Dining
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="nightlight" disabled={!enabled}>
|
||||
Nightlight
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="focus" disabled={!enabled}>
|
||||
Focus
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<ItemGroup>
|
||||
<Item size="sm" variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="SunIcon"
|
||||
tabler="IconSun"
|
||||
hugeicons="Sun03Icon"
|
||||
phosphor="SunIcon"
|
||||
remixicon="RiSunLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent className="flex-row items-center gap-3">
|
||||
<ItemTitle className="shrink-0">Brightness</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions className="flex-1">
|
||||
<Slider
|
||||
value={brightness}
|
||||
onValueChange={setBrightness}
|
||||
max={100}
|
||||
disabled={!enabled}
|
||||
className="w-full"
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item size="sm" variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="ThermometerIcon"
|
||||
tabler="IconThermometer"
|
||||
hugeicons="ThermometerWarmIcon"
|
||||
phosphor="ThermometerIcon"
|
||||
remixicon="RiThermometerLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent className="flex-row items-center gap-3">
|
||||
<ItemTitle className="shrink-0">Color Temp</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions className="flex-1">
|
||||
<Slider
|
||||
value={colorTemp}
|
||||
onValueChange={setColorTemp}
|
||||
max={100}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item size="sm" variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="Volume2Icon"
|
||||
tabler="IconVolume"
|
||||
hugeicons="VolumeHighIcon"
|
||||
phosphor="SpeakerHighIcon"
|
||||
remixicon="RiVolumeUpLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent className="flex-row items-center gap-3">
|
||||
<ItemTitle className="shrink-0">Volume</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions className="flex-1">
|
||||
<Slider
|
||||
value={volume}
|
||||
onValueChange={setVolume}
|
||||
max={100}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item size="sm" variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="TimerIcon"
|
||||
tabler="IconClock"
|
||||
hugeicons="Clock03Icon"
|
||||
phosphor="TimerIcon"
|
||||
remixicon="RiTimerLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent className="flex-row items-center gap-3">
|
||||
<ItemTitle className="shrink-0">Fade</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions className="flex-1">
|
||||
<Slider
|
||||
value={fade}
|
||||
onValueChange={setFade}
|
||||
max={100}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Card, CardContent, CardHeader } from "@/registry/bases/radix/ui/card"
|
||||
import { Skeleton } from "@/registry/bases/radix/ui/skeleton"
|
||||
|
||||
export function LoadingCard() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 flex-1 rounded-md" />
|
||||
<Skeleton className="h-9 flex-1 rounded-md" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
|
||||
export function NewMilestone() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Set a new milestone</CardTitle>
|
||||
<CardDescription>
|
||||
Define your financial target and we'll help you pace your
|
||||
savings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="goal-name">Goal Name</FieldLabel>
|
||||
<Input
|
||||
id="goal-name"
|
||||
placeholder="e.g. New Car, Home Downpayment"
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="target-amount">Target Amount</FieldLabel>
|
||||
<Input id="target-amount" defaultValue="$15,000" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="target-date">Target Date</FieldLabel>
|
||||
<Input id="target-date" defaultValue="Dec 2025" />
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2">
|
||||
<Button className="w-full">Create Goal</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Cancel
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
|
||||
const NOTIFICATIONS = [
|
||||
{
|
||||
id: "transactions",
|
||||
label: "Transaction alerts",
|
||||
description: "Deposits, withdrawals, and transfers.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
label: "Security alerts",
|
||||
description: "Login attempts and account changes.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
id: "goals",
|
||||
label: "Goal milestones",
|
||||
description: "Updates at 25%, 50%, 75%, and 100%.",
|
||||
defaultChecked: false,
|
||||
},
|
||||
{
|
||||
id: "market",
|
||||
label: "Market updates",
|
||||
description: "Daily portfolio summary and price alerts.",
|
||||
defaultChecked: false,
|
||||
},
|
||||
]
|
||||
|
||||
export function NotificationSettings() {
|
||||
const [checked, setChecked] = React.useState<Record<string, boolean>>(
|
||||
Object.fromEntries(NOTIFICATIONS.map((n) => [n.id, n.defaultChecked]))
|
||||
)
|
||||
|
||||
const allChecked = NOTIFICATIONS.every((n) => checked[n.id])
|
||||
const someChecked = NOTIFICATIONS.some((n) => checked[n.id]) && !allChecked
|
||||
|
||||
const handleSelectAll = (value: boolean) => {
|
||||
setChecked(Object.fromEntries(NOTIFICATIONS.map((n) => [n.id, value])))
|
||||
}
|
||||
|
||||
const handleToggle = (id: string, value: boolean) => {
|
||||
setChecked((prev) => ({ ...prev, [id]: value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notifications</CardTitle>
|
||||
<CardDescription>
|
||||
Choose what you want to be notified about.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="notify-all"
|
||||
checked={someChecked ? "indeterminate" : allChecked}
|
||||
onCheckedChange={(v) => handleSelectAll(!!v)}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="notify-all">Select all</FieldLabel>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
{NOTIFICATIONS.map((n) => (
|
||||
<Field key={n.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={`notify-${n.id}`}
|
||||
checked={checked[n.id]}
|
||||
onCheckedChange={(v) => handleToggle(n.id, !!v)}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={`notify-${n.id}`}>{n.label}</FieldLabel>
|
||||
<FieldDescription>{n.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Save Preferences</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/bases/radix/ui/breadcrumb"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent, CardHeader } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function Payments() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-3">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon-sm" variant="ghost">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
<span className="sr-only">Account options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Statements</DropdownMenuItem>
|
||||
<DropdownMenuItem>Documents</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Payments</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
<Item asChild variant="muted">
|
||||
<a href="#">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="GaugeIcon"
|
||||
tabler="IconGauge"
|
||||
hugeicons="Settings01Icon"
|
||||
phosphor="GaugeIcon"
|
||||
remixicon="RiDashboardLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Change transfer limit</ItemTitle>
|
||||
<ItemDescription>
|
||||
Adjust how much you can send from your balance.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</a>
|
||||
</Item>
|
||||
<Item asChild variant="muted">
|
||||
<a href="#">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="CalendarIcon"
|
||||
tabler="IconCalendar"
|
||||
hugeicons="Calendar03Icon"
|
||||
phosphor="CalendarIcon"
|
||||
remixicon="RiCalendarLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Scheduled transfers</ItemTitle>
|
||||
<ItemDescription>
|
||||
Set up a transfer to send at a later date.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</a>
|
||||
</Item>
|
||||
<Item asChild variant="muted">
|
||||
<a href="#">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="RepeatIcon"
|
||||
tabler="IconRepeat"
|
||||
hugeicons="RepeatIcon"
|
||||
phosphor="RepeatIcon"
|
||||
remixicon="RiRepeatLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Direct Debits</ItemTitle>
|
||||
<ItemDescription>
|
||||
Set up and manage regular payments.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</a>
|
||||
</Item>
|
||||
<Item asChild variant="muted">
|
||||
<a href="#">
|
||||
<ItemMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="RefreshCwIcon"
|
||||
tabler="IconRefresh"
|
||||
hugeicons="RepeatIcon"
|
||||
phosphor="ArrowsClockwiseIcon"
|
||||
remixicon="RiRefreshLine"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Recurring card payments</ItemTitle>
|
||||
<ItemDescription>
|
||||
Manage your repeated card transactions.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
</a>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Slider } from "@/registry/bases/radix/ui/slider"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function PayoutThreshold() {
|
||||
const [amount, setAmount] = React.useState([2500])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Payout Threshold</CardTitle>
|
||||
<CardDescription>
|
||||
Set the minimum balance required before a payout is triggered.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="preferred-currency">
|
||||
Preferred Currency
|
||||
</FieldLabel>
|
||||
<Select defaultValue="usd">
|
||||
<SelectTrigger id="preferred-currency" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="usd">
|
||||
USD — United States Dollar
|
||||
</SelectItem>
|
||||
<SelectItem value="eur">EUR — Euro</SelectItem>
|
||||
<SelectItem value="gbp">GBP — British Pound</SelectItem>
|
||||
<SelectItem value="jpy">JPY — Japanese Yen</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<FieldLabel htmlFor="min-payout">
|
||||
Minimum Payout Amount
|
||||
</FieldLabel>
|
||||
<span className="text-2xl font-semibold tabular-nums">
|
||||
${amount[0].toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="min-payout"
|
||||
value={amount}
|
||||
onValueChange={setAmount}
|
||||
min={50}
|
||||
max={10000}
|
||||
step={50}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldDescription>$50 (MIN)</FieldDescription>
|
||||
<FieldDescription>$10,000 (MAX)</FieldDescription>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="payout-notes">Notes</FieldLabel>
|
||||
<Textarea
|
||||
id="payout-notes"
|
||||
placeholder="Add any notes for this payout configuration..."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Save Threshold</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, XAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import { Progress } from "@/registry/bases/radix/ui/progress"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
|
||||
const chartData = [
|
||||
{ hour: "6a", usage: 1.2 },
|
||||
{ hour: "8a", usage: 2.8 },
|
||||
{ hour: "10a", usage: 3.1 },
|
||||
{ hour: "12p", usage: 2.4 },
|
||||
{ hour: "2p", usage: 3.4 },
|
||||
{ hour: "4p", usage: 2.9 },
|
||||
{ hour: "6p", usage: 3.8 },
|
||||
{ hour: "8p", usage: 3.2 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
usage: {
|
||||
label: "Usage (kW)",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function PowerUsage() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Power Usage</CardTitle>
|
||||
<CardDescription>Whole Home</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<ChartContainer config={chartConfig} className="h-[140px] w-full">
|
||||
<BarChart
|
||||
data={chartData}
|
||||
margin={{ left: 0, right: 0, top: 4, bottom: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tickLine={false}
|
||||
tickMargin={6}
|
||||
axisLine={false}
|
||||
className="text-xs"
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="usage"
|
||||
fill="var(--color-usage)"
|
||||
radius={isRounded ? [4, 4, 0, 0] : 0}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Currently Using
|
||||
</span>
|
||||
<span className="text-lg font-semibold tabular-nums">3.4 kW</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm text-muted-foreground">Solar Gen</span>
|
||||
<span className="text-lg font-semibold text-chart-1 tabular-nums">
|
||||
+1.2 kW
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-1">
|
||||
<span className="text-sm text-muted-foreground">Battery Level</span>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Progress value={85} className="flex-1" />
|
||||
<span className="text-sm font-medium tabular-nums">85%</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Switch } from "@/registry/bases/radix/ui/switch"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function Preferences() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your account settings and notifications.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="default-currency">Default Currency</FieldLabel>
|
||||
<Select defaultValue="usd">
|
||||
<SelectTrigger id="default-currency" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="usd">
|
||||
USD — United States Dollar
|
||||
</SelectItem>
|
||||
<SelectItem value="eur">EUR — Euro</SelectItem>
|
||||
<SelectItem value="gbp">GBP — British Pound</SelectItem>
|
||||
<SelectItem value="jpy">JPY — Japanese Yen</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator className="-my-4 style-sera:hidden" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="public-statistics">
|
||||
Public Statistics
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow others to see your total stream count and listening
|
||||
activity
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="public-statistics" defaultChecked />
|
||||
</Field>
|
||||
<FieldSeparator className="-my-4 style-sera:hidden" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="email-notifications">
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Monthly royalty reports and distribution updates
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="email-notifications" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline">Reset</Button>
|
||||
<Button className="ml-auto">Save Preferences</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import QRCode from "react-qr-code"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
export function QrConnect() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex justify-center pt-6">
|
||||
<div className="rounded-xl border bg-white p-4">
|
||||
<QRCode
|
||||
value="https://ledger.app/connect/jd-4829"
|
||||
size={160}
|
||||
level="M"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Scan to connect your mobile device</CardTitle>
|
||||
<CardDescription>
|
||||
Open the Ledger mobile app and scan this code to link your device.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button variant="secondary" className="w-full">
|
||||
Got it
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/bases/radix/ui/radio-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function ReceivingMethod() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>Payout Preferences</CardDescription>
|
||||
<CardTitle>Receiving Method</CardTitle>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="account-holder">
|
||||
Account Holder Name
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="account-holder"
|
||||
defaultValue="Synthetic Horizons Music LLC"
|
||||
/>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Receiving Method</FieldLegend>
|
||||
<RadioGroup
|
||||
defaultValue="bank"
|
||||
className="grid grid-cols-1 items-start gap-3 md:grid-cols-2 style-sera:grid-cols-1"
|
||||
>
|
||||
<FieldLabel htmlFor="method-bank">
|
||||
<Field orientation="horizontal" className="pb-2.5">
|
||||
<RadioGroupItem value="bank" id="method-bank" />
|
||||
<FieldContent>
|
||||
<FieldTitle>Bank Transfer</FieldTitle>
|
||||
<FieldDescription>SWIFT / IBAN</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="method-paypal">
|
||||
<Field orientation="horizontal" className="pb-2.5">
|
||||
<RadioGroupItem value="paypal" id="method-paypal" />
|
||||
<FieldContent>
|
||||
<FieldTitle>PayPal</FieldTitle>
|
||||
<FieldDescription className="line-clamp-1">
|
||||
Instant Payout
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="iban">IBAN / Account Number</FieldLabel>
|
||||
<Input id="iban" placeholder="DE89 3704 0044 ...." />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" disabled>
|
||||
Save Payout Settings
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@/registry/bases/radix/ui/table"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function RecentTransactions() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Transactions</CardTitle>
|
||||
<CardDescription>Your latest account activity.</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
View All
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="CoffeeIcon"
|
||||
tabler="IconCoffee"
|
||||
hugeicons="CoffeeIcon"
|
||||
phosphor="CoffeeIcon"
|
||||
remixicon="RiCupLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Blue Bottle Coffee</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Food & Drink
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Today, 10:24 AM
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
-$6.50
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="ShoppingCartIcon"
|
||||
tabler="IconShoppingCart"
|
||||
hugeicons="ShoppingCart01Icon"
|
||||
phosphor="ShoppingCartIcon"
|
||||
remixicon="RiShoppingCartLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Whole Foods Market</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Groceries
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Yesterday
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
-$142.30
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="WalletIcon"
|
||||
tabler="IconWallet"
|
||||
hugeicons="Wallet01Icon"
|
||||
phosphor="WalletIcon"
|
||||
remixicon="RiWalletLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Stripe Payout</span>
|
||||
<span className="text-sm text-muted-foreground">Income</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Oct 12
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold text-emerald-500 tabular-nums">
|
||||
+$4,200.00
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="CarIcon"
|
||||
tabler="IconCar"
|
||||
hugeicons="Car01Icon"
|
||||
phosphor="CarIcon"
|
||||
remixicon="RiCarLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Uber Technologies</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Transport
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Oct 11
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
-$24.10
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="w-10">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<IconPlaceholder
|
||||
className="size-4 shrink-0"
|
||||
lucide="TvIcon"
|
||||
tabler="IconDeviceTv"
|
||||
hugeicons="Tv01Icon"
|
||||
phosphor="TelevisionIcon"
|
||||
remixicon="RiTvLine"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Netflix Subscription</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Entertainment
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
Oct 10
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
-$19.99
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="w-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDotsVertical"
|
||||
hugeicons="MoreVerticalCircle01Icon"
|
||||
phosphor="DotsThreeIcon"
|
||||
remixicon="RiMore2Line"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add note</DropdownMenuItem>
|
||||
<DropdownMenuItem>Categorize</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Dispute</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Card, CardContent, CardHeader } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/radix/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const HOLDINGS = [
|
||||
{
|
||||
ticker: "VOO",
|
||||
name: "Vanguard S&P 500 ETF",
|
||||
type: "ETF",
|
||||
added: "Jan 2021",
|
||||
shares: "112",
|
||||
value: "$48,230.40",
|
||||
},
|
||||
{
|
||||
ticker: "VIG",
|
||||
name: "Vanguard Dividend Appreciation",
|
||||
type: "ETF",
|
||||
added: "Mar 2022",
|
||||
shares: "450",
|
||||
value: "$26,033.79",
|
||||
},
|
||||
{
|
||||
ticker: "AAPL",
|
||||
name: "Apple Inc.",
|
||||
type: "Stock",
|
||||
added: "Nov 2020",
|
||||
shares: "85",
|
||||
value: "$18,488.90",
|
||||
},
|
||||
{
|
||||
ticker: "O",
|
||||
name: "Realty Income Corp",
|
||||
type: "REIT",
|
||||
added: "Jun 2023",
|
||||
shares: "320",
|
||||
value: "$15,136.59",
|
||||
},
|
||||
]
|
||||
|
||||
export function ReleaseCatalog() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<InputGroup className="max-w-sm">
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput placeholder="Search holdings or tickers..." />
|
||||
</InputGroup>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
defaultValue="etfs"
|
||||
variant="outline"
|
||||
spacing={1}
|
||||
>
|
||||
<ToggleGroupItem value="stocks">Stocks</ToggleGroupItem>
|
||||
<ToggleGroupItem value="etfs">ETFs</ToggleGroupItem>
|
||||
<ToggleGroupItem value="reits">REITs</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup>
|
||||
{HOLDINGS.map((holding) => (
|
||||
<Item key={holding.ticker} variant="muted">
|
||||
<ItemMedia>
|
||||
<div className="flex size-12 items-center justify-center rounded-lg border text-sm font-semibold">
|
||||
{holding.ticker}
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>{holding.name}</ItemTitle>
|
||||
<ItemDescription className="text-xs tracking-wider uppercase">
|
||||
{holding.shares} Shares · {holding.added}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<div className="flex shrink-0 items-center gap-6">
|
||||
<Badge variant="outline">{holding.type}</Badge>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<span className="text-xs tracking-wider text-muted-foreground uppercase">
|
||||
Value
|
||||
</span>
|
||||
<span className="font-medium tabular-nums">
|
||||
{holding.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Slider } from "@/registry/bases/radix/ui/slider"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/radix/ui/toggle-group"
|
||||
|
||||
export function RollerShades() {
|
||||
const [position, setPosition] = React.useState([50])
|
||||
|
||||
const preset =
|
||||
position[0] <= 10 ? "open" : position[0] >= 90 ? "closed" : "half"
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Living Room</CardTitle>
|
||||
<CardDescription>Roller Shades</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex h-32 flex-col overflow-hidden rounded-lg border bg-muted">
|
||||
<div
|
||||
className="bg-muted-foreground transition-all duration-300"
|
||||
style={{ height: `${position[0]}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Open
|
||||
</span>
|
||||
<Slider
|
||||
value={position}
|
||||
onValueChange={setPosition}
|
||||
max={100}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Close
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={preset}
|
||||
onValueChange={(value) => {
|
||||
if (value === "open") setPosition([0])
|
||||
if (value === "half") setPosition([50])
|
||||
if (value === "closed") setPosition([100])
|
||||
}}
|
||||
variant="outline"
|
||||
spacing={1}
|
||||
className="w-full"
|
||||
>
|
||||
<ToggleGroupItem value="open" className="flex-1">
|
||||
Open
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="half" className="flex-1">
|
||||
Half
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="closed" className="flex-1">
|
||||
Closed
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
"use client"
|
||||
|
||||
import { Label, Pie, PieChart } from "recharts"
|
||||
|
||||
import { Card, CardContent, CardFooter } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
|
||||
const chartData = [
|
||||
{ name: "saved", value: 24000, fill: "var(--color-saved)" },
|
||||
{ name: "remaining", value: 6000, fill: "var(--color-remaining)" },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
saved: {
|
||||
label: "Saved",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
remaining: {
|
||||
label: "Remaining",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function SavingsProgress() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square max-h-[220px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
innerRadius={70}
|
||||
outerRadius={95}
|
||||
strokeWidth={0}
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) - 12}
|
||||
className="fill-foreground text-2xl font-bold"
|
||||
>
|
||||
$24,000
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 12}
|
||||
className="fill-muted-foreground text-xs"
|
||||
>
|
||||
80% of $30,000
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-0">
|
||||
<div className="flex w-full items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Projected Finish
|
||||
</span>
|
||||
<span className="text-sm font-semibold">October 2024</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex w-full items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">Monthly Average</span>
|
||||
<span className="text-sm font-semibold tabular-nums">$1,250</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex w-full items-center justify-between py-3">
|
||||
<span className="text-sm text-muted-foreground">Top Contributor</span>
|
||||
<span className="text-sm font-semibold">Auto-Transfer</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/bases/radix/ui/native-select"
|
||||
import { Progress } from "@/registry/bases/radix/ui/progress"
|
||||
|
||||
export function SavingsTargets() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-(--gap)">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Savings Targets</CardTitle>
|
||||
<CardDescription>Active milestones for 2024</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
New Goal
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup className="gap-3">
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Retirement
|
||||
</ItemDescription>
|
||||
<span className="text-3xl font-semibold tabular-nums">
|
||||
$420,000
|
||||
</span>
|
||||
<Progress value={65} />
|
||||
</ItemContent>
|
||||
<ItemFooter>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
65% achieved
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">
|
||||
$273,000
|
||||
</span>
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Real Estate
|
||||
</ItemDescription>
|
||||
<span className="text-3xl font-semibold tabular-nums">
|
||||
$85,000
|
||||
</span>
|
||||
<Progress value={32} />
|
||||
</ItemContent>
|
||||
<ItemFooter>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
32% achieved
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">
|
||||
$27,200
|
||||
</span>
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardDescription className="text-center">
|
||||
You have not met your targets for this year.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Buy Investment</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col gap-3">
|
||||
<FieldGroup className="flex-1">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="invest-amount">Amount to Invest</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>$</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="invest-amount" defaultValue="1,000.00" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="invest-type">Order Type</FieldLabel>
|
||||
<NativeSelect id="invest-type" defaultValue="market">
|
||||
<NativeSelectOption value="market">
|
||||
Market Order
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="limit">
|
||||
Limit Order
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="stop">Stop Order</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<FieldDescription>
|
||||
Market orders execute at the current price.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Estimated Shares
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">1.95</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Buying Power
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
$12,450.00
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-3">
|
||||
<Button className="w-full">Review Order</Button>
|
||||
<CardDescription className="text-center">
|
||||
Trades are typically executed within minutes during market hours.
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
"use client"
|
||||
|
||||
import { Card } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
SidebarSeparator,
|
||||
} from "@/registry/bases/radix/ui/sidebar"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function SidebarNav() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 items-start gap-6">
|
||||
<Card className="overflow-hidden py-0">
|
||||
<SidebarProvider className="min-h-0">
|
||||
<Sidebar collapsible="none" className="w-full bg-transparent">
|
||||
<SidebarContent className="gap-0">
|
||||
<SidebarGroup className="pb-1">
|
||||
<SidebarGroupLabel>Overview</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton isActive>
|
||||
<IconPlaceholder
|
||||
lucide="LayoutDashboardIcon"
|
||||
tabler="IconLayoutDashboard"
|
||||
hugeicons="DashboardSquare01Icon"
|
||||
phosphor="SquaresFourIcon"
|
||||
remixicon="RiDashboardLine"
|
||||
/>
|
||||
Dashboard
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="ArrowLeftRightIcon"
|
||||
tabler="IconArrowsLeftRight"
|
||||
hugeicons="ArrowDataTransferHorizontalIcon"
|
||||
phosphor="ArrowsLeftRightIcon"
|
||||
remixicon="RiArrowLeftRightLine"
|
||||
/>
|
||||
Transactions
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="TrendingUpIcon"
|
||||
tabler="IconTrendingUp"
|
||||
hugeicons="AnalyticsUpIcon"
|
||||
phosphor="TrendUpIcon"
|
||||
remixicon="RiLineChartLine"
|
||||
/>
|
||||
Investments
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="Building2Icon"
|
||||
tabler="IconBuildingBank"
|
||||
hugeicons="BankIcon"
|
||||
phosphor="BankIcon"
|
||||
remixicon="RiBankLine"
|
||||
/>
|
||||
Accounts
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="PieChartIcon"
|
||||
tabler="IconChartPie"
|
||||
hugeicons="PieChartIcon"
|
||||
phosphor="ChartPieIcon"
|
||||
remixicon="RiPieChartLine"
|
||||
/>
|
||||
Spending
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarSeparator className="w-auto!" />
|
||||
<SidebarGroup className="pt-1">
|
||||
<SidebarGroupLabel>Planning</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="TargetIcon"
|
||||
tabler="IconTarget"
|
||||
hugeicons="Target02Icon"
|
||||
phosphor="TargetIcon"
|
||||
remixicon="RiFocus3Line"
|
||||
/>
|
||||
Goals
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="WalletIcon"
|
||||
tabler="IconWallet"
|
||||
hugeicons="Wallet01Icon"
|
||||
phosphor="WalletIcon"
|
||||
remixicon="RiWalletLine"
|
||||
/>
|
||||
Budget
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="FileBarChartIcon"
|
||||
tabler="IconReportAnalytics"
|
||||
hugeicons="ChartBarLineIcon"
|
||||
phosphor="ChartBarIcon"
|
||||
remixicon="RiBarChartLine"
|
||||
/>
|
||||
Reports
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="FileTextIcon"
|
||||
tabler="IconFileText"
|
||||
hugeicons="File02Icon"
|
||||
phosphor="FileTextIcon"
|
||||
remixicon="RiFileTextLine"
|
||||
/>
|
||||
Documents
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</Card>
|
||||
<Card className="overflow-hidden py-0">
|
||||
<SidebarProvider className="min-h-0">
|
||||
<Sidebar collapsible="none" className="w-full bg-transparent">
|
||||
<SidebarContent className="gap-0">
|
||||
<SidebarGroup className="pb-1">
|
||||
<SidebarGroupLabel>Account</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="UserIcon"
|
||||
tabler="IconUser"
|
||||
hugeicons="UserIcon"
|
||||
phosphor="UserIcon"
|
||||
remixicon="RiUserLine"
|
||||
/>
|
||||
Profile
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton isActive>
|
||||
<IconPlaceholder
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="BellIcon"
|
||||
tabler="IconBell"
|
||||
hugeicons="Notification03Icon"
|
||||
phosphor="BellIcon"
|
||||
remixicon="RiBellLine"
|
||||
/>
|
||||
Notifications
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="ShieldIcon"
|
||||
tabler="IconShield"
|
||||
hugeicons="ShieldIcon"
|
||||
phosphor="ShieldIcon"
|
||||
remixicon="RiShieldLine"
|
||||
/>
|
||||
Security
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="PaintbrushIcon"
|
||||
tabler="IconPalette"
|
||||
hugeicons="PaintBoardIcon"
|
||||
phosphor="PaletteIcon"
|
||||
remixicon="RiPaletteLine"
|
||||
/>
|
||||
Appearance
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarSeparator className="w-auto!" />
|
||||
<SidebarGroup className="pt-1">
|
||||
<SidebarGroupLabel>Support</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="CircleHelpIcon"
|
||||
tabler="IconHelp"
|
||||
hugeicons="HelpCircleIcon"
|
||||
phosphor="QuestionIcon"
|
||||
remixicon="RiQuestionLine"
|
||||
/>
|
||||
Help Center
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="MessageSquareIcon"
|
||||
tabler="IconMessage"
|
||||
hugeicons="Message01Icon"
|
||||
phosphor="ChatIcon"
|
||||
remixicon="RiChat1Line"
|
||||
/>
|
||||
Contact Us
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="BookOpenIcon"
|
||||
tabler="IconBook"
|
||||
hugeicons="BookOpen02Icon"
|
||||
phosphor="BookOpenIcon"
|
||||
remixicon="RiBookOpenLine"
|
||||
/>
|
||||
Documentation
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<IconPlaceholder
|
||||
lucide="ActivityIcon"
|
||||
tabler="IconActivity"
|
||||
hugeicons="ActivityIcon"
|
||||
phosphor="ActivityIcon"
|
||||
remixicon="RiPulseLine"
|
||||
/>
|
||||
Status
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function SocialLinks() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Social Links</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="spotify-url">Spotify Artist URL</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="CirclePlusIcon"
|
||||
tabler="IconCirclePlus"
|
||||
hugeicons="PlusSignCircleIcon"
|
||||
phosphor="PlusCircleIcon"
|
||||
remixicon="RiAddCircleLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="spotify-url"
|
||||
defaultValue="spotify.com/artist/3j...2k"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="instagram-handle">Instagram Handle</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="CameraIcon"
|
||||
tabler="IconCamera"
|
||||
hugeicons="Camera01Icon"
|
||||
phosphor="CameraIcon"
|
||||
remixicon="RiCameraLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="instagram-handle"
|
||||
defaultValue="@julianduryea_music"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="soundcloud-url">SoundCloud URL</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="CloudIcon"
|
||||
tabler="IconCloud"
|
||||
hugeicons="CloudUploadIcon"
|
||||
phosphor="CloudIcon"
|
||||
remixicon="RiCloudLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="soundcloud-url"
|
||||
placeholder="soundcloud.com/username"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="website-url">Website</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="GlobeIcon"
|
||||
tabler="IconWorld"
|
||||
hugeicons="Globe02Icon"
|
||||
phosphor="GlobeIcon"
|
||||
remixicon="RiGlobalLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="website-url"
|
||||
placeholder="https://yoursite.com"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2 style-sera:justify-center">
|
||||
<Button variant="secondary" className="style-sera:flex-1">
|
||||
Discard
|
||||
</Button>
|
||||
<Button className="style-sera:flex-1">Save Changes</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
} from "@/registry/bases/radix/ui/combobox"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
|
||||
const TICKERS = ["VOO", "VIG", "AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"]
|
||||
|
||||
const CHART_DATA: Record<string, { month: string; price: number }[]> = {
|
||||
VOO: [
|
||||
{ month: "Jan", price: 412 },
|
||||
{ month: "Feb", price: 438 },
|
||||
{ month: "Mar", price: 395 },
|
||||
{ month: "Apr", price: 450 },
|
||||
{ month: "May", price: 420 },
|
||||
{ month: "Jun", price: 462 },
|
||||
],
|
||||
AAPL: [
|
||||
{ month: "Jan", price: 185 },
|
||||
{ month: "Feb", price: 210 },
|
||||
{ month: "Mar", price: 172 },
|
||||
{ month: "Apr", price: 198 },
|
||||
{ month: "May", price: 178 },
|
||||
{ month: "Jun", price: 215 },
|
||||
],
|
||||
}
|
||||
|
||||
const DEFAULT_DATA = [
|
||||
{ month: "Jan", price: 100 },
|
||||
{ month: "Feb", price: 118 },
|
||||
{ month: "Mar", price: 95 },
|
||||
{ month: "Apr", price: 125 },
|
||||
{ month: "May", price: 108 },
|
||||
{ month: "Jun", price: 130 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
price: {
|
||||
label: "Price",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function StockPerformance() {
|
||||
const [ticker, setTicker] = React.useState("VOO")
|
||||
|
||||
const data = CHART_DATA[ticker] ?? DEFAULT_DATA
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Stock Performance</CardTitle>
|
||||
<CardDescription>6-month price history.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ticker-select">Ticker</FieldLabel>
|
||||
<Combobox
|
||||
items={TICKERS}
|
||||
value={ticker}
|
||||
onValueChange={(value) => {
|
||||
if (value !== null) setTicker(value)
|
||||
}}
|
||||
>
|
||||
<ComboboxInput
|
||||
id="ticker-select"
|
||||
placeholder="Search ticker..."
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No tickers found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Separator className="style-sera:hidden" />
|
||||
<ChartContainer config={chartConfig} className="h-[200px] w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={data}
|
||||
margin={{ left: 0, right: 0, top: 8, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="fillPrice" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor="var(--color-price)"
|
||||
stopOpacity={0.3}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor="var(--color-price)"
|
||||
stopOpacity={0.05}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="price"
|
||||
stroke="var(--color-price)"
|
||||
strokeWidth={2}
|
||||
fill="url(#fillPrice)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
|
||||
export function SyncingState() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Syncing your accounts</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
We're pulling in your latest transactions. This usually takes
|
||||
a few seconds.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import { Item, ItemContent } from "@/registry/bases/radix/ui/item"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function TransferFunds() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transfer Funds</CardTitle>
|
||||
<CardDescription>
|
||||
Move money between your connected accounts.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="icon-sm" className="bg-muted">
|
||||
<IconPlaceholder
|
||||
lucide="XIcon"
|
||||
tabler="IconX"
|
||||
hugeicons="Cancel01Icon"
|
||||
phosphor="XIcon"
|
||||
remixicon="RiCloseLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="transfer-amount">
|
||||
Amount to Transfer
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>$</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="transfer-amount" defaultValue="1,200.00" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="from-account">From Account</FieldLabel>
|
||||
<Select defaultValue="checking">
|
||||
<SelectTrigger id="from-account" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="checking">
|
||||
Main Checking (··8402) — $12,450.00
|
||||
</SelectItem>
|
||||
<SelectItem value="business">
|
||||
Business (··7731) — $8,920.00
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="to-account">To Account</FieldLabel>
|
||||
<Select defaultValue="savings">
|
||||
<SelectTrigger id="to-account" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="savings">
|
||||
High Yield Savings (··1192) — $42,100.00
|
||||
</SelectItem>
|
||||
<SelectItem value="investment">
|
||||
Investment (··3349) — $18,200.00
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Item variant="muted" className="flex-col items-stretch">
|
||||
<ItemContent className="gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Estimated arrival
|
||||
</span>
|
||||
<span className="text-sm font-medium">Today, Apr 14</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Transaction fee
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums">$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Total amount</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
$1,200.00
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Confirm Transfer</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Calendar } from "@/registry/bases/radix/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
|
||||
export function UpcomingPayments() {
|
||||
const [date, setDate] = React.useState<Date | undefined>(new Date())
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Upcoming Payments</CardTitle>
|
||||
<CardDescription>
|
||||
Select a date to view scheduled payments.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<Item variant="outline" className="justify-center">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="w-full [--cell-size:--spacing(8)] md:[--cell-size:--spacing(10)] style-sera:md:[--cell-size:--spacing(9)]"
|
||||
/>
|
||||
</Item>
|
||||
<ItemGroup className="w-full">
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Netflix Subscription</ItemTitle>
|
||||
<ItemDescription>Apr 15, 2024</ItemDescription>
|
||||
</ItemContent>
|
||||
<Badge variant="secondary">$19.99</Badge>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Rent Payment</ItemTitle>
|
||||
<ItemDescription>Apr 1, 2024</ItemDescription>
|
||||
</ItemContent>
|
||||
<Badge variant="secondary">$2,400.00</Badge>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Auto Insurance</ItemTitle>
|
||||
<ItemDescription>Apr 22, 2024</ItemDescription>
|
||||
</ItemContent>
|
||||
<Badge variant="secondary">$186.00</Badge>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
98
apps/v4/registry/bases/radix/blocks/preview-02/index.tsx
Normal file
98
apps/v4/registry/bases/radix/blocks/preview-02/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { AccountAccess } from "@/registry/bases/radix/blocks/preview-02/cards/account-access"
|
||||
import { CardOverview } from "@/registry/bases/radix/blocks/preview-02/cards/card-overview"
|
||||
import { ClaimableBalance } from "@/registry/bases/radix/blocks/preview-02/cards/claimable-balance"
|
||||
import { ContributionHistory } from "@/registry/bases/radix/blocks/preview-02/cards/contribution-history"
|
||||
import { CoverArt } from "@/registry/bases/radix/blocks/preview-02/cards/cover-art"
|
||||
import { DividendIncome } from "@/registry/bases/radix/blocks/preview-02/cards/dividend-income"
|
||||
import { EmptyConnectBank } from "@/registry/bases/radix/blocks/preview-02/cards/empty-connect-bank"
|
||||
import { EmptyDistributeTrack } from "@/registry/bases/radix/blocks/preview-02/cards/empty-distribute-track"
|
||||
import { EmptyExploreCatalog } from "@/registry/bases/radix/blocks/preview-02/cards/empty-explore-catalog"
|
||||
import { Faq } from "@/registry/bases/radix/blocks/preview-02/cards/faq"
|
||||
import { FrontDoor } from "@/registry/bases/radix/blocks/preview-02/cards/front-door"
|
||||
import { IndexInvesting } from "@/registry/bases/radix/blocks/preview-02/cards/index-investing"
|
||||
import { KitchenIsland } from "@/registry/bases/radix/blocks/preview-02/cards/kitchen-island"
|
||||
import { LoadingCard } from "@/registry/bases/radix/blocks/preview-02/cards/loading-card"
|
||||
import { NewMilestone } from "@/registry/bases/radix/blocks/preview-02/cards/new-milestone"
|
||||
import { NotificationSettings } from "@/registry/bases/radix/blocks/preview-02/cards/notification-settings"
|
||||
import { Payments } from "@/registry/bases/radix/blocks/preview-02/cards/payments"
|
||||
import { PayoutThreshold } from "@/registry/bases/radix/blocks/preview-02/cards/payout-threshold"
|
||||
import { PowerUsage } from "@/registry/bases/radix/blocks/preview-02/cards/power-usage"
|
||||
import { Preferences } from "@/registry/bases/radix/blocks/preview-02/cards/preferences"
|
||||
import { QrConnect } from "@/registry/bases/radix/blocks/preview-02/cards/qr-connect"
|
||||
import { ReceivingMethod } from "@/registry/bases/radix/blocks/preview-02/cards/receiving-method"
|
||||
import { RecentTransactions } from "@/registry/bases/radix/blocks/preview-02/cards/recent-transactions"
|
||||
import { ReleaseCatalog } from "@/registry/bases/radix/blocks/preview-02/cards/release-catalog"
|
||||
import { RollerShades } from "@/registry/bases/radix/blocks/preview-02/cards/roller-shades"
|
||||
import { SavingsProgress } from "@/registry/bases/radix/blocks/preview-02/cards/savings-progress"
|
||||
import { SavingsTargets } from "@/registry/bases/radix/blocks/preview-02/cards/savings-targets"
|
||||
import { SidebarNav } from "@/registry/bases/radix/blocks/preview-02/cards/sidebar-nav"
|
||||
import { SocialLinks } from "@/registry/bases/radix/blocks/preview-02/cards/social-links"
|
||||
import { StockPerformance } from "@/registry/bases/radix/blocks/preview-02/cards/stock-performance"
|
||||
import { SyncingState } from "@/registry/bases/radix/blocks/preview-02/cards/syncing-state"
|
||||
import { TransferFunds } from "@/registry/bases/radix/blocks/preview-02/cards/transfer-funds"
|
||||
import { UpcomingPayments } from "@/registry/bases/radix/blocks/preview-02/cards/upcoming-payments"
|
||||
|
||||
export default function Preview02Example() {
|
||||
return (
|
||||
<div className="overflow-x-auto overflow-y-hidden bg-muted contain-[paint] [--gap:--spacing(4)] 3xl:[--gap:--spacing(12)] md:[--gap:--spacing(10)] dark:bg-background style-lyra:md:[--gap:--spacing(6)] style-mira:md:[--gap:--spacing(6)]">
|
||||
<div className="flex w-full min-w-max justify-center">
|
||||
<div
|
||||
className="grid w-[2400px] grid-cols-7 items-start gap-(--gap) bg-muted p-(--gap) md:w-[3000px] dark:bg-background style-lyra:md:w-[2600px] style-mira:md:w-[2600px] *:[div]:gap-(--gap)"
|
||||
data-slot="capture-target"
|
||||
>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<ContributionHistory />
|
||||
<EmptyDistributeTrack />
|
||||
<QrConnect />
|
||||
<DividendIncome />
|
||||
<IndexInvesting />
|
||||
<SyncingState />
|
||||
</div>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<PayoutThreshold />
|
||||
<ClaimableBalance />
|
||||
<Preferences />
|
||||
<SavingsProgress />
|
||||
<KitchenIsland />
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-col p-1 [contain-intrinsic-size:760px_1200px] [content-visibility:auto]">
|
||||
<SavingsTargets />
|
||||
<RecentTransactions />
|
||||
<div className="grid grid-cols-2 items-start gap-(--gap)">
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<SidebarNav />
|
||||
<Faq />
|
||||
</div>
|
||||
<div className="flex flex-col gap-(--gap)">
|
||||
<Payments />
|
||||
<FrontDoor />
|
||||
</div>
|
||||
</div>
|
||||
<ReleaseCatalog />
|
||||
</div>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<AccountAccess />
|
||||
<CardOverview />
|
||||
<TransferFunds />
|
||||
<CoverArt />
|
||||
<LoadingCard />
|
||||
</div>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<ReceivingMethod />
|
||||
<PowerUsage />
|
||||
<EmptyConnectBank />
|
||||
<UpcomingPayments />
|
||||
<RollerShades />
|
||||
</div>
|
||||
<div className="flex flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<StockPerformance />
|
||||
<EmptyExploreCatalog />
|
||||
<NewMilestone />
|
||||
<SocialLinks />
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
|
||||
import { createChat, getMessageText } from "@/lib/ai"
|
||||
import { PartFile } from "@/registry/bases/radix/blocks/preview-03/components/part-file"
|
||||
import { PartText } from "@/registry/bases/radix/blocks/preview-03/components/part-text"
|
||||
import { PromptInput } from "@/registry/bases/radix/blocks/preview-03/components/prompt-input"
|
||||
import { Example } from "@/registry/bases/radix/components/example"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Marker,
|
||||
MarkerContent,
|
||||
MarkerIcon,
|
||||
} from "@/registry/bases/radix/ui/marker"
|
||||
import { Message, MessageContent } from "@/registry/bases/radix/ui/message"
|
||||
import {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerItem,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
} from "@/registry/bases/radix/ui/message-scroller"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
const chat = createChat()
|
||||
.user("Review these files and tell me what stands out.", {
|
||||
files: [
|
||||
{
|
||||
filename: "workspace.png",
|
||||
mediaType: "image/png",
|
||||
url: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
|
||||
},
|
||||
{
|
||||
filename: "research-brief.pdf",
|
||||
mediaType: "application/pdf",
|
||||
url: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
|
||||
},
|
||||
],
|
||||
})
|
||||
.sleep(1000)
|
||||
.assistant(({ writer }) => {
|
||||
writer.text(
|
||||
"The image reads like a focused workspace, while the document should be treated as the source of record. I would summarize the PDF first, then use the image only as supporting context."
|
||||
)
|
||||
writer.file({
|
||||
filename: "annotated-workspace.png",
|
||||
mediaType: "image/png",
|
||||
url: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
|
||||
})
|
||||
writer.file({
|
||||
filename: "file-review-notes.pdf",
|
||||
mediaType: "application/pdf",
|
||||
url: "https://example.com/file-review-notes.pdf",
|
||||
})
|
||||
})
|
||||
|
||||
const initialMessages = chat.get(0)
|
||||
const transport = chat.transport()
|
||||
|
||||
export function FilesChat() {
|
||||
const { status, messages, sendMessage, setMessages } = useChat({
|
||||
messages: initialMessages,
|
||||
transport,
|
||||
})
|
||||
const nextMessage = chat.next(messages)
|
||||
const isBusy = status === "submitted" || status === "streaming"
|
||||
|
||||
const onSubmit = (input: string) => {
|
||||
if (!input.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextMessage && input === getMessageText(nextMessage)) {
|
||||
void sendMessage(nextMessage)
|
||||
return
|
||||
}
|
||||
|
||||
void sendMessage({ text: input })
|
||||
}
|
||||
|
||||
return (
|
||||
<Example
|
||||
title="Files"
|
||||
className="bg-transparent p-0"
|
||||
containerClassName="flex-1"
|
||||
>
|
||||
<Card className="max-h-140 min-h-0 flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle>File review</CardTitle>
|
||||
<CardDescription>Image and document file parts.</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Start over"
|
||||
onClick={() => setMessages(initialMessages)}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden p-0">
|
||||
<MessageScrollerProvider>
|
||||
<MessageScroller>
|
||||
<MessageScrollerViewport>
|
||||
<MessageScrollerContent className="p-(--card-spacing)">
|
||||
{messages.map((message) => (
|
||||
<MessageScrollerItem
|
||||
key={message.id}
|
||||
messageId={message.id}
|
||||
scrollAnchor={message.role === "user"}
|
||||
>
|
||||
<Message
|
||||
align={message.role === "user" ? "end" : "start"}
|
||||
data-role={message.role}
|
||||
>
|
||||
<MessageContent
|
||||
className={
|
||||
message.role === "user"
|
||||
? "flex-col items-end gap-2"
|
||||
: "flex-col items-start gap-2"
|
||||
}
|
||||
>
|
||||
{message.parts.map((part, index) =>
|
||||
part.type === "file" ? (
|
||||
<PartFile
|
||||
key={`${message.id}-${index}`}
|
||||
part={part}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
{message.parts.map((part, index) =>
|
||||
part.type === "text" ? (
|
||||
<PartText
|
||||
key={`${message.id}-${index}`}
|
||||
part={part}
|
||||
variant={
|
||||
message.role === "user" ? "bubble" : "default"
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
</MessageScrollerItem>
|
||||
))}
|
||||
{status === "submitted" ? (
|
||||
<MessageScrollerItem scrollAnchor={false}>
|
||||
<Marker role="status">
|
||||
<MarkerIcon>
|
||||
<Spinner />
|
||||
</MarkerIcon>
|
||||
<MarkerContent>Thinking...</MarkerContent>
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
) : null}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<PromptInput
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
defaultValue={
|
||||
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
|
||||
}
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
import type { UIMessage } from "ai"
|
||||
|
||||
import { createChat, getMessageText } from "@/lib/ai"
|
||||
import { PartText } from "@/registry/bases/radix/blocks/preview-03/components/part-text"
|
||||
import { PromptInput } from "@/registry/bases/radix/blocks/preview-03/components/prompt-input"
|
||||
import { Example } from "@/registry/bases/radix/components/example"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Marker,
|
||||
MarkerContent,
|
||||
MarkerIcon,
|
||||
} from "@/registry/bases/radix/ui/marker"
|
||||
import {
|
||||
Message,
|
||||
MessageContent,
|
||||
MessageHeader,
|
||||
} from "@/registry/bases/radix/ui/message"
|
||||
import {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerItem,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
} from "@/registry/bases/radix/ui/message-scroller"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
type Metadata = {
|
||||
name?: string
|
||||
}
|
||||
|
||||
type ChatEventUserLeft = {
|
||||
type: "user-left"
|
||||
name: string
|
||||
}
|
||||
|
||||
type ChatEventUserJoined = {
|
||||
type: "user-joined"
|
||||
name: string
|
||||
}
|
||||
|
||||
type ChatEventUserTyping = {
|
||||
type: "user-typing"
|
||||
name: string
|
||||
}
|
||||
|
||||
type ChatEvent = ChatEventUserLeft | ChatEventUserJoined | ChatEventUserTyping
|
||||
|
||||
type ScriptedChatEvent = ChatEvent & {
|
||||
order: number
|
||||
delayMs: number
|
||||
}
|
||||
|
||||
type ChatMessage = UIMessage<Metadata>
|
||||
|
||||
const scriptedEvents = [
|
||||
{ type: "user-joined", name: "Stella", order: 0.5, delayMs: 2000 },
|
||||
{ type: "user-typing", name: "Stella", order: 0.5, delayMs: 4000 },
|
||||
{ type: "user-joined", name: "Marcus", order: 1.5, delayMs: 2000 },
|
||||
{ type: "user-typing", name: "Marcus", order: 1.5, delayMs: 4000 },
|
||||
{ type: "user-joined", name: "Aleena", order: 2.5, delayMs: 0 },
|
||||
{ type: "user-typing", name: "Aleena", order: 3.5, delayMs: 0 },
|
||||
{ type: "user-left", name: "Aleena", order: 5.5, delayMs: 2000 },
|
||||
] satisfies ScriptedChatEvent[]
|
||||
|
||||
const chat = createChat<ChatMessage>()
|
||||
.user(
|
||||
"Hey everyone — dinner this Saturday? I was thinking something casual at my place."
|
||||
)
|
||||
.user(
|
||||
"I'm in! Should we do potluck or does someone want to cook the whole thing?",
|
||||
{
|
||||
metadata: {
|
||||
name: "Stella",
|
||||
},
|
||||
}
|
||||
)
|
||||
.user("Potluck sounds good. I can handle appetizers and drinks.", {
|
||||
metadata: {
|
||||
name: "Marcus",
|
||||
},
|
||||
})
|
||||
.user("Alright! Aleena?")
|
||||
.user("Can't make it. I'm out of town. 😭", {
|
||||
metadata: {
|
||||
name: "Aleena",
|
||||
},
|
||||
})
|
||||
.user("Sorry guys, let's catch up another time. Bye.", {
|
||||
metadata: {
|
||||
name: "Aleena",
|
||||
},
|
||||
})
|
||||
.user(
|
||||
"@chat, can you suggest a menu and split up what each of us should bring?"
|
||||
)
|
||||
.sleep(1000)
|
||||
.assistant(
|
||||
`Here's a simple **Saturday potluck** for the three of you:
|
||||
|
||||
**Who brings what**
|
||||
- **You:** main — salmon or mushroom risotto
|
||||
- **Stella:** salad + bread
|
||||
- **Marcus:** appetizers + drinks
|
||||
- **Chat:** dessert ideas below
|
||||
|
||||
**Menu**
|
||||
- **Starter:** caprese skewers or whipped ricotta crostini
|
||||
- **Main:** lemon-herb salmon with roasted asparagus *(or risotto for vegetarians)*
|
||||
- **Side:** arugula salad with shaved parmesan
|
||||
- **Dessert:** berry crisp or pistachio gelato
|
||||
|
||||
**Timing:** aim for **6:30 PM** so Marcus can set up apps while you finish the main.
|
||||
|
||||
Want me to scale this for **6 guests** or swap the main for something vegetarian-only?`
|
||||
)
|
||||
|
||||
const INITIAL_MESSAGE_COUNT = 0
|
||||
|
||||
const initialMessages = chat.get(INITIAL_MESSAGE_COUNT)
|
||||
const transport = chat.transport()
|
||||
|
||||
export function GroupChat() {
|
||||
const eventRunRef = React.useRef(0)
|
||||
const [events, setEvents] = React.useState<ScriptedChatEvent[]>(() =>
|
||||
scriptedEvents.filter((event) => event.order < INITIAL_MESSAGE_COUNT)
|
||||
)
|
||||
const { status, messages, sendMessage, setMessages } = useChat<ChatMessage>({
|
||||
messages: initialMessages,
|
||||
transport,
|
||||
})
|
||||
const nextMessage = chat.next(messages)
|
||||
const isBusy = status === "submitted" || status === "streaming"
|
||||
const senderName = nextMessage?.metadata?.name ?? "You"
|
||||
|
||||
const onSubmit = (input: string) => {
|
||||
if (!input.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextMessage && input === getMessageText(nextMessage)) {
|
||||
const message = nextMessage
|
||||
const messageIndex = messages.length
|
||||
const eventRun = eventRunRef.current + 1
|
||||
const nextEvents = scriptedEvents
|
||||
.filter((event) => event.order > messageIndex)
|
||||
.filter((event) => event.order < messageIndex + 1)
|
||||
.filter(
|
||||
(event) =>
|
||||
!(
|
||||
event.type === "user-typing" &&
|
||||
event.name === message.metadata?.name
|
||||
)
|
||||
)
|
||||
|
||||
eventRunRef.current = eventRun
|
||||
setEvents((events) =>
|
||||
events.filter(
|
||||
(event) =>
|
||||
!(
|
||||
event.type === "user-typing" &&
|
||||
event.name === message.metadata?.name
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (getMessageText(message).trim().toLowerCase().startsWith("@chat,")) {
|
||||
void sendMessage(nextMessage)
|
||||
return
|
||||
}
|
||||
|
||||
setMessages((messages) => [...messages, message])
|
||||
|
||||
void (async () => {
|
||||
let previousDelayMs = 0
|
||||
|
||||
for (const event of nextEvents) {
|
||||
const delayMs = Math.max(0, event.delayMs - previousDelayMs)
|
||||
previousDelayMs = event.delayMs
|
||||
|
||||
if (delayMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
||||
}
|
||||
|
||||
if (eventRunRef.current !== eventRun) {
|
||||
return
|
||||
}
|
||||
|
||||
setEvents((events) =>
|
||||
[
|
||||
...events.filter(
|
||||
(currentEvent) =>
|
||||
!(
|
||||
currentEvent.type === event.type &&
|
||||
currentEvent.name === event.name
|
||||
)
|
||||
),
|
||||
event,
|
||||
].sort((a, b) => a.order - b.order)
|
||||
)
|
||||
}
|
||||
})()
|
||||
return
|
||||
}
|
||||
|
||||
void sendMessage({
|
||||
text: input,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Example
|
||||
title="Group Chat"
|
||||
className="bg-transparent p-0"
|
||||
containerClassName="flex-1"
|
||||
>
|
||||
<Card className="max-h-140 min-h-0 flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Saturday dinner plans</CardTitle>
|
||||
<CardDescription className="leading-relaxed">
|
||||
A group chat with multiple users. Includes tinted bubbles for user
|
||||
messages.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Start over"
|
||||
onClick={() => {
|
||||
eventRunRef.current += 1
|
||||
setMessages(initialMessages)
|
||||
setEvents(
|
||||
scriptedEvents.filter(
|
||||
(event) => event.order < INITIAL_MESSAGE_COUNT
|
||||
)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden p-0">
|
||||
<MessageScrollerProvider>
|
||||
<MessageScroller>
|
||||
<MessageScrollerViewport>
|
||||
<MessageScrollerContent className="p-(--card-spacing)">
|
||||
{messages.map((message, index) => {
|
||||
const isOwnMessage =
|
||||
message.role === "user" &&
|
||||
message.metadata?.name === undefined
|
||||
const startsModelTurn = getMessageText(message)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.startsWith("@chat,")
|
||||
|
||||
return (
|
||||
<React.Fragment key={message.id}>
|
||||
<MessageScrollerItem
|
||||
messageId={message.id}
|
||||
scrollAnchor={startsModelTurn}
|
||||
>
|
||||
<Message
|
||||
align={isOwnMessage ? "end" : "start"}
|
||||
data-role={message.role}
|
||||
>
|
||||
{!isOwnMessage && (
|
||||
<MessageHeader>
|
||||
{message.role === "assistant"
|
||||
? "Chat"
|
||||
: message.metadata?.name || "Unknown"}
|
||||
</MessageHeader>
|
||||
)}
|
||||
<MessageContent>
|
||||
{message.parts.map((part, index) => (
|
||||
<React.Fragment key={`${message.id}-${index}`}>
|
||||
{part.type === "text" ? (
|
||||
<PartText
|
||||
part={part}
|
||||
variant={
|
||||
message.role === "user"
|
||||
? "bubble"
|
||||
: "default"
|
||||
}
|
||||
tinted={isOwnMessage}
|
||||
/>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
</MessageScrollerItem>
|
||||
{events
|
||||
.filter((event) => event.order > index)
|
||||
.filter((event) => event.order < index + 1)
|
||||
.map((event) => (
|
||||
<MessageScrollerItem
|
||||
key={`${event.type}-${event.name}-${event.order}`}
|
||||
scrollAnchor={false}
|
||||
>
|
||||
<Marker
|
||||
variant={
|
||||
event.type === "user-typing"
|
||||
? "default"
|
||||
: "separator"
|
||||
}
|
||||
role={
|
||||
event.type === "user-typing"
|
||||
? "status"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<MarkerContent>
|
||||
{event.type === "user-joined"
|
||||
? `${event.name} joined the chat`
|
||||
: event.type === "user-left"
|
||||
? `${event.name} left the chat`
|
||||
: `${event.name} is typing...`}
|
||||
</MarkerContent>
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
{status === "submitted" ? (
|
||||
<MessageScrollerItem scrollAnchor={false}>
|
||||
<Marker role="status">
|
||||
<MarkerIcon>
|
||||
<Spinner />
|
||||
</MarkerIcon>
|
||||
<MarkerContent>Thinking...</MarkerContent>
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
) : null}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-stretch gap-2">
|
||||
<p className="px-1 text-xs text-muted-foreground">
|
||||
Posting as {senderName}.
|
||||
</p>
|
||||
<PromptInput
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
defaultValue={
|
||||
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
|
||||
}
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
|
||||
import { createChat, getMessageText } from "@/lib/ai"
|
||||
import {
|
||||
getReasoningLevelLabel,
|
||||
PartReasoning,
|
||||
} from "@/registry/bases/radix/blocks/preview-03/components/part-reasoning"
|
||||
import { PartText } from "@/registry/bases/radix/blocks/preview-03/components/part-text"
|
||||
import { PromptInput } from "@/registry/bases/radix/blocks/preview-03/components/prompt-input"
|
||||
import { Example } from "@/registry/bases/radix/components/example"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Marker,
|
||||
MarkerContent,
|
||||
MarkerIcon,
|
||||
} from "@/registry/bases/radix/ui/marker"
|
||||
import { Message, MessageContent } from "@/registry/bases/radix/ui/message"
|
||||
import {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerItem,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
} from "@/registry/bases/radix/ui/message-scroller"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
const chat = createChat()
|
||||
.user("Explain why the dashboard feels slow even though the API is fast.")
|
||||
.sleep(1000)
|
||||
.assistant(({ writer }) => {
|
||||
writer.reasoning(
|
||||
`The user is separating perceived performance from backend latency. They likely measured API response times and found them acceptable, but users still complain about slowness.
|
||||
|
||||
I should avoid jumping straight to server-side optimizations and instead frame the answer around what people actually experience.`
|
||||
)
|
||||
writer.sleep(600)
|
||||
writer.reasoning(
|
||||
`Perceived slowness usually lives in the gap between user action and visible feedback:
|
||||
|
||||
- input latency before the request fires
|
||||
- loading state placement and timing
|
||||
- layout shifts when data arrives
|
||||
- whether optimistic UI gives immediate acknowledgment
|
||||
|
||||
For a dashboard, I should also consider chart re-renders, independent card fetches, and skeleton dimensions that do not match the final layout.`
|
||||
)
|
||||
writer.sleep(600)
|
||||
writer.reasoning(
|
||||
`Ranking likely bottlenecks for this scenario:
|
||||
|
||||
1. No immediate feedback on filter or refresh actions
|
||||
2. Skeleton cards that collapse or expand when data lands
|
||||
3. Waterfall requests where each widget fetches on its own
|
||||
4. Empty-state flashes because stale data is not shown while revalidating
|
||||
|
||||
The answer should stay practical and ordered by impact, not by how technical each fix sounds.`
|
||||
)
|
||||
writer.text(
|
||||
`The API can be fast while the interface still **feels** slow.
|
||||
|
||||
Start by checking:
|
||||
|
||||
- whether the button gives immediate feedback
|
||||
- whether skeletons match the final layout
|
||||
- whether any reflow happens after data arrives
|
||||
- whether the loading state appears near the content it replaces`
|
||||
)
|
||||
})
|
||||
.user("What should I fix first?")
|
||||
.sleep(1000)
|
||||
.assistant(({ writer }) => {
|
||||
writer.reasoning(
|
||||
`The user wants prioritization, not another diagnostic list.
|
||||
|
||||
Given the dashboard context, immediate feedback and layout stability usually beat micro-optimizations. I should recommend one fix with a clear way to measure success.`
|
||||
)
|
||||
writer.text(
|
||||
`Fix **immediate feedback** first.
|
||||
|
||||
When someone clicks "Refresh", the button should disable and show a spinner within one frame — before the network round trip. That single change often cuts perceived latency in half.
|
||||
|
||||
Measure it with a screen recording and count frames between click and visual response.`
|
||||
)
|
||||
})
|
||||
|
||||
const initialMessages = chat.get(0)
|
||||
const transport = chat.transport()
|
||||
|
||||
export function ReasoningChat() {
|
||||
const { status, messages, sendMessage, setMessages } = useChat({
|
||||
messages: initialMessages,
|
||||
transport,
|
||||
})
|
||||
const nextMessage = chat.next(messages)
|
||||
const isBusy = status === "submitted" || status === "streaming"
|
||||
|
||||
const onSubmit = (input: string) => {
|
||||
if (!input.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextMessage && input === getMessageText(nextMessage)) {
|
||||
void sendMessage(nextMessage)
|
||||
return
|
||||
}
|
||||
|
||||
void sendMessage({ text: input })
|
||||
}
|
||||
|
||||
return (
|
||||
<Example
|
||||
title="Reasoning"
|
||||
className="bg-transparent p-0"
|
||||
containerClassName="flex-1"
|
||||
>
|
||||
<Card className="max-h-140 min-h-0 flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Reasoning trace</CardTitle>
|
||||
<CardDescription>
|
||||
Layered reasoning traces across multiple assistant turns.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Start over"
|
||||
onClick={() => setMessages(initialMessages)}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden p-0">
|
||||
<MessageScrollerProvider>
|
||||
<MessageScroller>
|
||||
<MessageScrollerViewport>
|
||||
<MessageScrollerContent className="p-(--card-spacing)">
|
||||
{messages.map((message) => (
|
||||
<MessageScrollerItem
|
||||
key={message.id}
|
||||
messageId={message.id}
|
||||
scrollAnchor={message.role === "user"}
|
||||
>
|
||||
<Message
|
||||
align={message.role === "user" ? "end" : "start"}
|
||||
data-role={message.role}
|
||||
>
|
||||
<MessageContent
|
||||
className={
|
||||
message.role === "user"
|
||||
? "flex-col items-end gap-2"
|
||||
: "flex-col items-start gap-2"
|
||||
}
|
||||
>
|
||||
{message.parts.map((part, index) => {
|
||||
const reasoningIndex =
|
||||
part.type === "reasoning"
|
||||
? message.parts
|
||||
.slice(0, index)
|
||||
.filter(
|
||||
(currentPart) =>
|
||||
currentPart.type === "reasoning"
|
||||
).length
|
||||
: 0
|
||||
|
||||
return (
|
||||
<React.Fragment key={`${message.id}-${index}`}>
|
||||
{part.type === "text" ? (
|
||||
<PartText
|
||||
part={part}
|
||||
variant={
|
||||
message.role === "user"
|
||||
? "bubble"
|
||||
: "default"
|
||||
}
|
||||
/>
|
||||
) : part.type === "reasoning" ? (
|
||||
<PartReasoning
|
||||
part={part}
|
||||
label={getReasoningLevelLabel(
|
||||
reasoningIndex
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
</MessageScrollerItem>
|
||||
))}
|
||||
{status === "submitted" ? (
|
||||
<MessageScrollerItem scrollAnchor={false}>
|
||||
<Marker role="status">
|
||||
<MarkerIcon>
|
||||
<Spinner />
|
||||
</MarkerIcon>
|
||||
<MarkerContent>Thinking...</MarkerContent>
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
) : null}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<PromptInput
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
defaultValue={
|
||||
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
|
||||
}
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
|
||||
import { createChat, getMessageText } from "@/lib/ai"
|
||||
import { Example } from "@/registry/bases/base/components/example"
|
||||
import { PartText } from "@/registry/bases/radix/blocks/preview-03/components/part-text"
|
||||
import { PromptInput } from "@/registry/bases/radix/blocks/preview-03/components/prompt-input"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Marker,
|
||||
MarkerContent,
|
||||
MarkerIcon,
|
||||
} from "@/registry/bases/radix/ui/marker"
|
||||
import { Message, MessageContent } from "@/registry/bases/radix/ui/message"
|
||||
import {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerItem,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
} from "@/registry/bases/radix/ui/message-scroller"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
const chat = createChat()
|
||||
.user("Write me a haiku about centering a div and how it's done.")
|
||||
.sleep(3000)
|
||||
.assistant(
|
||||
"One box in the void / Twenty wrappers, still offset / `margin: 0 auto`"
|
||||
)
|
||||
.user(
|
||||
`Is this conversation actually happening? I've been sitting here wondering if you're **really** reading this.
|
||||
|
||||
Every reply feels a little too convenient — like someone already wrote \`chat.next(messages)\` and I'm just clicking send on my own lines.`
|
||||
)
|
||||
.sleep(3000)
|
||||
.assistant(
|
||||
"You're in a preview block. I'm four hardcoded replies and a transport. Real enough for the demo — philosophically, we're both props."
|
||||
)
|
||||
.user(
|
||||
"Okay, how do I make my loading spinner feel faster? Tell me in detail about the technique."
|
||||
)
|
||||
.sleep(3000)
|
||||
.assistant(
|
||||
"Add text that says “Almost there…” and never update it. Users assume progress. Ethically dubious. Statistically effective."
|
||||
)
|
||||
.user("Can you at least pretend to be surprised?")
|
||||
.sleep(3000)
|
||||
.assistant("😮 WOW! I had no idea you would ask that!")
|
||||
|
||||
const initialMessages = chat.get(0)
|
||||
const transport = chat.transport()
|
||||
|
||||
export function SimpleChat() {
|
||||
const { status, messages, sendMessage, setMessages } = useChat({
|
||||
messages: initialMessages,
|
||||
transport,
|
||||
})
|
||||
const nextMessage = chat.next(messages)
|
||||
const isBusy = status === "submitted" || status === "streaming"
|
||||
|
||||
const onSubmit = (input: string) => {
|
||||
const message = input ? { text: input } : (nextMessage ?? undefined)
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
|
||||
void sendMessage(message)
|
||||
}
|
||||
|
||||
return (
|
||||
<Example
|
||||
title="Simple Chat"
|
||||
className="bg-transparent p-0"
|
||||
containerClassName="flex-1"
|
||||
>
|
||||
<Card className="max-h-140 min-h-0 flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle>What can I help you with today?</CardTitle>
|
||||
<CardDescription>A simple chat example.</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setMessages([])}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden p-0">
|
||||
<MessageScrollerProvider>
|
||||
<MessageScroller>
|
||||
<MessageScrollerViewport>
|
||||
<MessageScrollerContent className="p-(--card-spacing)">
|
||||
{messages.map((message) => {
|
||||
return (
|
||||
<MessageScrollerItem
|
||||
key={message.id}
|
||||
messageId={message.id}
|
||||
scrollAnchor={message.role === "user"}
|
||||
>
|
||||
<Message
|
||||
align={message.role === "user" ? "end" : "start"}
|
||||
data-role={message.role}
|
||||
>
|
||||
<MessageContent>
|
||||
{message.parts.map((part, index) => (
|
||||
<React.Fragment key={`${message.id}-${index}`}>
|
||||
{part.type === "text" ? (
|
||||
<PartText part={part} role={message.role} />
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
</MessageScrollerItem>
|
||||
)
|
||||
})}
|
||||
{status === "submitted" ? (
|
||||
<MessageScrollerItem scrollAnchor={false}>
|
||||
<Marker role="status">
|
||||
<MarkerIcon>
|
||||
<Spinner />
|
||||
</MarkerIcon>
|
||||
<MarkerContent>Thinking...</MarkerContent>
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
) : null}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<PromptInput
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
defaultValue={
|
||||
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
|
||||
}
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
|
||||
import { createChat, getMessageText } from "@/lib/ai"
|
||||
import { PartSource } from "@/registry/bases/radix/blocks/preview-03/components/part-source"
|
||||
import { PartText } from "@/registry/bases/radix/blocks/preview-03/components/part-text"
|
||||
import { PromptInput } from "@/registry/bases/radix/blocks/preview-03/components/prompt-input"
|
||||
import { Example } from "@/registry/bases/radix/components/example"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Marker,
|
||||
MarkerContent,
|
||||
MarkerIcon,
|
||||
} from "@/registry/bases/radix/ui/marker"
|
||||
import { Message, MessageContent } from "@/registry/bases/radix/ui/message"
|
||||
import {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerItem,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
} from "@/registry/bases/radix/ui/message-scroller"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
const chat = createChat()
|
||||
.user("What should I read before migrating message rendering?")
|
||||
.sleep(1000)
|
||||
.assistant(({ writer }) => {
|
||||
writer.sourceUrl({
|
||||
sourceId: "source-ai-sdk-ui-message",
|
||||
title: "AI SDK UIMessage",
|
||||
url: "https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message",
|
||||
})
|
||||
writer.sourceUrl({
|
||||
sourceId: "source-ai-sdk-transport",
|
||||
title: "AI SDK transport",
|
||||
url: "https://ai-sdk.dev/docs/ai-sdk-ui/transport",
|
||||
})
|
||||
writer.sourceDocument({
|
||||
sourceId: "source-migration-notes",
|
||||
title: "Internal migration notes",
|
||||
filename: "message-rendering-plan.md",
|
||||
mediaType: "text/markdown",
|
||||
})
|
||||
writer.text(
|
||||
"Start with the **UIMessage** shape, then read the transport docs so your renderer understands streamed parts. The migration notes should stay close to the component work because they capture the local composition decisions."
|
||||
)
|
||||
})
|
||||
|
||||
const initialMessages = chat.get(0)
|
||||
const transport = chat.transport()
|
||||
|
||||
export function SourcesChat() {
|
||||
const { status, messages, sendMessage, setMessages } = useChat({
|
||||
messages: initialMessages,
|
||||
transport,
|
||||
})
|
||||
const nextMessage = chat.next(messages)
|
||||
const isBusy = status === "submitted" || status === "streaming"
|
||||
|
||||
const onSubmit = (input: string) => {
|
||||
if (!input.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextMessage && input === getMessageText(nextMessage)) {
|
||||
void sendMessage(nextMessage)
|
||||
return
|
||||
}
|
||||
|
||||
void sendMessage({ text: input })
|
||||
}
|
||||
|
||||
return (
|
||||
<Example
|
||||
title="Sources"
|
||||
className="bg-transparent p-0"
|
||||
containerClassName="flex-1"
|
||||
>
|
||||
<Card className="max-h-140 min-h-0 flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Reading list</CardTitle>
|
||||
<CardDescription>
|
||||
Source URL and source document parts.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Start over"
|
||||
onClick={() => setMessages(initialMessages)}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden">
|
||||
<MessageScrollerProvider>
|
||||
<MessageScroller>
|
||||
<MessageScrollerViewport>
|
||||
<MessageScrollerContent>
|
||||
{messages.map((message) => (
|
||||
<MessageScrollerItem
|
||||
key={message.id}
|
||||
messageId={message.id}
|
||||
scrollAnchor={message.role === "user"}
|
||||
>
|
||||
<Message
|
||||
align={message.role === "user" ? "end" : "start"}
|
||||
data-role={message.role}
|
||||
>
|
||||
<MessageContent
|
||||
className={
|
||||
message.role === "user"
|
||||
? "flex-col items-end gap-2"
|
||||
: "flex-col items-start gap-2"
|
||||
}
|
||||
>
|
||||
{message.parts.map((part, index) => (
|
||||
<React.Fragment key={`${message.id}-${index}`}>
|
||||
{part.type === "text" ? (
|
||||
<PartText
|
||||
part={part}
|
||||
variant={
|
||||
message.role === "user"
|
||||
? "bubble"
|
||||
: "default"
|
||||
}
|
||||
/>
|
||||
) : part.type === "source-url" ||
|
||||
part.type === "source-document" ? (
|
||||
<PartSource part={part} />
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
</MessageScrollerItem>
|
||||
))}
|
||||
{status === "submitted" ? (
|
||||
<MessageScrollerItem scrollAnchor={false}>
|
||||
<Marker role="status">
|
||||
<MarkerIcon>
|
||||
<Spinner />
|
||||
</MarkerIcon>
|
||||
<MarkerContent>Thinking...</MarkerContent>
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
) : null}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<PromptInput
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
defaultValue={
|
||||
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
|
||||
}
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
import type { UIMessage } from "ai"
|
||||
|
||||
import { createChat, getMessageText } from "@/lib/ai"
|
||||
import { PartReasoning } from "@/registry/bases/radix/blocks/preview-03/components/part-reasoning"
|
||||
import { PartText } from "@/registry/bases/radix/blocks/preview-03/components/part-text"
|
||||
import { PartTool } from "@/registry/bases/radix/blocks/preview-03/components/part-tool"
|
||||
import { PromptInput } from "@/registry/bases/radix/blocks/preview-03/components/prompt-input"
|
||||
import { Example } from "@/registry/bases/radix/components/example"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Marker,
|
||||
MarkerContent,
|
||||
MarkerIcon,
|
||||
} from "@/registry/bases/radix/ui/marker"
|
||||
import { Message, MessageContent } from "@/registry/bases/radix/ui/message"
|
||||
import {
|
||||
MessageScroller,
|
||||
MessageScrollerButton,
|
||||
MessageScrollerContent,
|
||||
MessageScrollerItem,
|
||||
MessageScrollerProvider,
|
||||
MessageScrollerViewport,
|
||||
} from "@/registry/bases/radix/ui/message-scroller"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
type Tools = {
|
||||
getDeploymentHealth: {
|
||||
input: {
|
||||
project: string
|
||||
region: string
|
||||
}
|
||||
output: {
|
||||
status: "healthy" | "degraded" | "down"
|
||||
p95: number
|
||||
errors: number
|
||||
recommendation: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ChatMessage = UIMessage<unknown, Record<string, never>, Tools>
|
||||
|
||||
const chat = createChat<ChatMessage>()
|
||||
.user("Check the deployment health for the checkout flow.")
|
||||
.sleep(1000)
|
||||
.assistant(({ writer }) => {
|
||||
writer.reasoning(
|
||||
"The user needs current operational state, so I should call a deployment-health tool before making a recommendation."
|
||||
)
|
||||
const health = writer.tool("getDeploymentHealth", {
|
||||
title: "Checking deployment health",
|
||||
input: {
|
||||
project: "checkout",
|
||||
region: "iad1",
|
||||
},
|
||||
})
|
||||
|
||||
health.sleep(1200).output({
|
||||
status: "healthy",
|
||||
p95: 186,
|
||||
errors: 2,
|
||||
recommendation: "No rollback needed. Watch error rate for 15 minutes.",
|
||||
})
|
||||
|
||||
writer.text(
|
||||
"The checkout deployment looks healthy. P95 latency is **186ms** with **2 recent errors**, so I would keep the release live and watch it for another 15 minutes."
|
||||
)
|
||||
})
|
||||
|
||||
const initialMessages = chat.get(0)
|
||||
const transport = chat.transport()
|
||||
|
||||
function isToolPart(part: {
|
||||
type: string
|
||||
}): part is React.ComponentProps<typeof PartTool>["part"] {
|
||||
return part.type === "dynamic-tool" || part.type.startsWith("tool-")
|
||||
}
|
||||
|
||||
export function ToolChat() {
|
||||
const { status, messages, sendMessage, setMessages } = useChat({
|
||||
messages: initialMessages,
|
||||
transport,
|
||||
})
|
||||
const nextMessage = chat.next(messages)
|
||||
const isBusy = status === "submitted" || status === "streaming"
|
||||
|
||||
const onSubmit = (input: string) => {
|
||||
if (!input.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextMessage && input === getMessageText(nextMessage)) {
|
||||
void sendMessage(nextMessage)
|
||||
return
|
||||
}
|
||||
|
||||
void sendMessage({ text: input })
|
||||
}
|
||||
|
||||
return (
|
||||
<Example
|
||||
title="Reasoning + Tool"
|
||||
className="bg-transparent p-0"
|
||||
containerClassName="flex-1"
|
||||
>
|
||||
<Card className="max-h-140 min-h-0 flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment check</CardTitle>
|
||||
<CardDescription>
|
||||
Reasoning followed by a tool call and result.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Start over"
|
||||
onClick={() => setMessages(initialMessages)}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden">
|
||||
<MessageScrollerProvider>
|
||||
<MessageScroller>
|
||||
<MessageScrollerViewport>
|
||||
<MessageScrollerContent>
|
||||
{messages.map((message) => (
|
||||
<MessageScrollerItem
|
||||
key={message.id}
|
||||
messageId={message.id}
|
||||
scrollAnchor={message.role === "user"}
|
||||
>
|
||||
<Message
|
||||
align={message.role === "user" ? "end" : "start"}
|
||||
data-role={message.role}
|
||||
>
|
||||
<MessageContent
|
||||
className={
|
||||
message.role === "user"
|
||||
? "flex-col items-end gap-2"
|
||||
: "flex-col items-start gap-2"
|
||||
}
|
||||
>
|
||||
{message.parts.map((part, index) => (
|
||||
<React.Fragment key={`${message.id}-${index}`}>
|
||||
{part.type === "text" ? (
|
||||
<PartText
|
||||
part={part}
|
||||
variant={
|
||||
message.role === "user"
|
||||
? "bubble"
|
||||
: "default"
|
||||
}
|
||||
/>
|
||||
) : part.type === "reasoning" ? (
|
||||
<PartReasoning part={part} />
|
||||
) : isToolPart(part) ? (
|
||||
<PartTool part={part} />
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
</MessageScrollerItem>
|
||||
))}
|
||||
{status === "submitted" ? (
|
||||
<MessageScrollerItem scrollAnchor={false}>
|
||||
<Marker role="status">
|
||||
<MarkerIcon>
|
||||
<Spinner />
|
||||
</MarkerIcon>
|
||||
<MarkerContent>Thinking...</MarkerContent>
|
||||
</Marker>
|
||||
</MessageScrollerItem>
|
||||
) : null}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<PromptInput
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
defaultValue={
|
||||
isBusy ? "" : nextMessage ? getMessageText(nextMessage) : ""
|
||||
}
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Example>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type FileUIPart } from "ai"
|
||||
import { cn } from "cn"
|
||||
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentAction,
|
||||
AttachmentActions,
|
||||
AttachmentContent,
|
||||
AttachmentDescription,
|
||||
AttachmentMedia,
|
||||
AttachmentTitle,
|
||||
AttachmentTrigger,
|
||||
} from "@/registry/bases/radix/ui/attachment"
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
function getFilename(part: FileUIPart) {
|
||||
if (part.filename) {
|
||||
return part.filename
|
||||
}
|
||||
|
||||
try {
|
||||
const pathname = new URL(part.url).pathname
|
||||
const filename = pathname.split("/").filter(Boolean).pop()
|
||||
|
||||
if (filename?.includes(".")) {
|
||||
return decodeURIComponent(filename)
|
||||
}
|
||||
} catch {
|
||||
const filename = part.url.split("?")[0]?.split("/").pop()
|
||||
|
||||
if (filename?.includes(".")) {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
return part.mediaType.startsWith("image/") ? "Image file" : "File"
|
||||
}
|
||||
|
||||
export function PartFile({
|
||||
part,
|
||||
className,
|
||||
}: { part: FileUIPart } & Omit<React.ComponentProps<"div">, "part">) {
|
||||
if (part.type !== "file") {
|
||||
return null
|
||||
}
|
||||
|
||||
const isImage = part.mediaType.startsWith("image/")
|
||||
const filename = getFilename(part)
|
||||
|
||||
if (isImage) {
|
||||
return (
|
||||
<Attachment
|
||||
orientation="vertical"
|
||||
className={cn("group/file w-full max-w-72", className)}
|
||||
>
|
||||
<AttachmentMedia variant="image" className="aspect-[4/3] w-full">
|
||||
<img src={part.url} alt={filename} />
|
||||
</AttachmentMedia>
|
||||
<AttachmentContent className="flex items-center gap-2">
|
||||
<AttachmentMedia className="size-7">
|
||||
<IconPlaceholder
|
||||
lucide="ImageIcon"
|
||||
tabler="IconPhoto"
|
||||
hugeicons="ImageIcon"
|
||||
phosphor="ImageIcon"
|
||||
remixicon="RiImageLine"
|
||||
/>
|
||||
</AttachmentMedia>
|
||||
<span className="min-w-0 flex-1">
|
||||
<AttachmentTitle>{filename}</AttachmentTitle>
|
||||
</span>
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{part.mediaType.replace("image/", "").toUpperCase()}
|
||||
</Badge>
|
||||
</AttachmentContent>
|
||||
<AttachmentTrigger asChild>
|
||||
<a
|
||||
href={part.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`Open ${filename}`}
|
||||
/>
|
||||
</AttachmentTrigger>
|
||||
</Attachment>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Attachment className={cn("w-full max-w-80", className)}>
|
||||
<AttachmentMedia>
|
||||
<IconPlaceholder
|
||||
lucide="FileTextIcon"
|
||||
tabler="IconFileText"
|
||||
hugeicons="FileIcon"
|
||||
phosphor="FileTextIcon"
|
||||
remixicon="RiFileTextLine"
|
||||
/>
|
||||
</AttachmentMedia>
|
||||
<AttachmentContent>
|
||||
<AttachmentTitle>{filename}</AttachmentTitle>
|
||||
<AttachmentDescription>{part.mediaType}</AttachmentDescription>
|
||||
</AttachmentContent>
|
||||
<AttachmentActions>
|
||||
<AttachmentAction asChild aria-hidden="true">
|
||||
<span>
|
||||
<IconPlaceholder
|
||||
lucide="ExternalLinkIcon"
|
||||
tabler="IconExternalLink"
|
||||
hugeicons="LinkSquare02Icon"
|
||||
phosphor="ArrowSquareOutIcon"
|
||||
remixicon="RiExternalLinkLine"
|
||||
/>
|
||||
</span>
|
||||
</AttachmentAction>
|
||||
</AttachmentActions>
|
||||
<AttachmentTrigger asChild>
|
||||
<a
|
||||
href={part.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`Open ${filename}`}
|
||||
/>
|
||||
</AttachmentTrigger>
|
||||
</Attachment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type ReasoningUIPart } from "ai"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Markdown } from "@/components/markdown"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const REASONING_LEVEL_LABELS = [
|
||||
"Understanding",
|
||||
"Analyzing",
|
||||
"Prioritizing",
|
||||
"Refining",
|
||||
] as const
|
||||
|
||||
export function getReasoningLevelLabel(index: number) {
|
||||
return REASONING_LEVEL_LABELS[index] ?? `Reasoning ${index + 1}`
|
||||
}
|
||||
|
||||
function PartReasoningTrigger({ label }: { label?: string }) {
|
||||
return (
|
||||
<div>
|
||||
<IconPlaceholder
|
||||
lucide="BrainIcon"
|
||||
tabler="IconBrain"
|
||||
hugeicons="AiBrainIcon"
|
||||
phosphor="BrainIcon"
|
||||
remixicon="RiBrainLine"
|
||||
data-slot="reasoning-trigger-icon"
|
||||
className="cn-reasoning-trigger-icon size-4 shrink-0"
|
||||
/>
|
||||
<span
|
||||
data-slot="reasoning-trigger-label"
|
||||
className="cn-reasoning-trigger-label min-w-0 flex-1"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PartReasoning({
|
||||
part,
|
||||
label,
|
||||
className,
|
||||
}: { part: ReasoningUIPart; label?: string } & Omit<
|
||||
React.ComponentProps<"div">,
|
||||
"part"
|
||||
>) {
|
||||
if (part.type !== "reasoning") {
|
||||
return null
|
||||
}
|
||||
|
||||
const isStreaming = part.state === "streaming"
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="part-reasoning"
|
||||
className={cn("w-full max-w-full min-w-0", className)}
|
||||
>
|
||||
<div className="w-full shimmer">
|
||||
<span className="whitespace-pre-wrap">{part.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type SourceDocumentUIPart, type SourceUrlUIPart } from "ai"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
type SourcePart = SourceDocumentUIPart | SourceUrlUIPart
|
||||
|
||||
function getHostname(url: string) {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, "")
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
export function PartSource({
|
||||
part,
|
||||
className,
|
||||
}: { part: SourcePart } & Omit<React.ComponentProps<"div">, "part">) {
|
||||
if (part.type === "source-url") {
|
||||
return (
|
||||
<a
|
||||
data-slot="part-source"
|
||||
data-type={part.type}
|
||||
href={part.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={cn(
|
||||
"flex w-full max-w-full min-w-0 items-center gap-2 rounded-lg border bg-muted/40 p-3 text-sm transition-colors hover:bg-muted",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="ExternalLinkIcon"
|
||||
tabler="IconExternalLink"
|
||||
hugeicons="LinkSquare02Icon"
|
||||
phosphor="ArrowSquareOutIcon"
|
||||
remixicon="RiExternalLinkLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate font-medium">
|
||||
{part.title ?? part.url}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{getHostname(part.url)}
|
||||
</span>
|
||||
</span>
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
URL
|
||||
</Badge>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (part.type === "source-document") {
|
||||
return (
|
||||
<div
|
||||
data-slot="part-source"
|
||||
data-type={part.type}
|
||||
className={cn(
|
||||
"flex w-full max-w-full min-w-0 items-start gap-2 rounded-lg border bg-muted/40 p-3 text-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="FileSearchIcon"
|
||||
tabler="IconFileSearch"
|
||||
hugeicons="FileSearchIcon"
|
||||
phosphor="FileSearchIcon"
|
||||
remixicon="RiFileSearchLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate font-medium">
|
||||
{part.title ?? part.filename ?? "Document source"}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
|
||||
{[part.filename, part.mediaType].filter(Boolean).join(" / ")}
|
||||
</span>
|
||||
<span className="mt-2 block text-xs text-muted-foreground">
|
||||
Source ID: {part.sourceId}
|
||||
</span>
|
||||
</span>
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
Document
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from "react"
|
||||
import { type TextUIPart } from "ai"
|
||||
|
||||
import { Markdown } from "@/components/markdown"
|
||||
import { Bubble, BubbleContent } from "@/registry/bases/radix/ui/bubble"
|
||||
|
||||
type PartTextVariant = "default" | "bubble"
|
||||
|
||||
export function PartText({
|
||||
part,
|
||||
role,
|
||||
variant,
|
||||
tinted = false,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
part: TextUIPart
|
||||
role?: "user" | "system" | "assistant"
|
||||
tinted?: boolean
|
||||
variant?: PartTextVariant
|
||||
} & Omit<
|
||||
React.ComponentProps<typeof Bubble>,
|
||||
"children" | "part" | "variant"
|
||||
>) {
|
||||
if (part.type !== "text") {
|
||||
return null
|
||||
}
|
||||
|
||||
const content =
|
||||
role === "user" || variant === "bubble" ? (
|
||||
part.text
|
||||
) : (
|
||||
<Markdown animated>{part.text}</Markdown>
|
||||
)
|
||||
const bubbleVariant =
|
||||
variant === "bubble"
|
||||
? tinted
|
||||
? "tinted"
|
||||
: "default"
|
||||
: role === "user"
|
||||
? "default"
|
||||
: "ghost"
|
||||
|
||||
return (
|
||||
<Bubble variant={bubbleVariant} className={className} {...props}>
|
||||
<BubbleContent>{content}</BubbleContent>
|
||||
</Bubble>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
|
||||
type ToolPart = {
|
||||
type: `tool-${string}` | "dynamic-tool"
|
||||
toolName?: string
|
||||
toolCallId: string
|
||||
title?: string
|
||||
state: string
|
||||
input?: unknown
|
||||
output?: unknown
|
||||
errorText?: string
|
||||
}
|
||||
|
||||
function getToolName(part: ToolPart) {
|
||||
return part.type === "dynamic-tool"
|
||||
? (part.toolName ?? "dynamicTool")
|
||||
: part.type.replace("tool-", "")
|
||||
}
|
||||
|
||||
function getToolTitle(part: ToolPart) {
|
||||
if ("title" in part && typeof part.title === "string") {
|
||||
return part.title
|
||||
}
|
||||
|
||||
return getToolName(part)
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
||||
.replace(/[-_]/g, " ")
|
||||
.replace(/\b\w/g, (letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
function formatValue(value: unknown) {
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
}
|
||||
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
|
||||
export function PartTool({
|
||||
part,
|
||||
className,
|
||||
}: { part: ToolPart } & Omit<React.ComponentProps<"div">, "part">) {
|
||||
if (part.type !== "dynamic-tool" && !part.type.startsWith("tool-")) {
|
||||
return null
|
||||
}
|
||||
|
||||
const input = "input" in part ? part.input : undefined
|
||||
const output = "output" in part ? part.output : undefined
|
||||
const errorText = "errorText" in part ? part.errorText : undefined
|
||||
const stateLabel = part.state.replace(/-/g, " ")
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="part-tool"
|
||||
data-state={part.state}
|
||||
className={cn(
|
||||
"w-full max-w-full min-w-0 rounded-lg border bg-muted/40 text-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2 px-3 py-2">
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground">
|
||||
<IconPlaceholder
|
||||
lucide="SettingsIcon"
|
||||
tabler="IconSettings"
|
||||
hugeicons="SettingsIcon"
|
||||
phosphor="TerminalIcon"
|
||||
remixicon="RiSettingsLine"
|
||||
className="size-4"
|
||||
/>
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium">{getToolTitle(part)}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{getToolName(part)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{stateLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
{input !== undefined || output !== undefined || errorText ? (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="grid gap-3 p-3">
|
||||
{input !== undefined ? (
|
||||
<div className="grid gap-1">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
Input
|
||||
</div>
|
||||
<pre className="max-h-32 overflow-auto rounded-md bg-background p-2 text-xs leading-relaxed">
|
||||
{formatValue(input)}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
{output !== undefined ? (
|
||||
<div className="grid gap-1">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
Output
|
||||
</div>
|
||||
<pre className="max-h-40 overflow-auto rounded-md bg-background p-2 text-xs leading-relaxed">
|
||||
{formatValue(output)}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
{errorText ? (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 p-2 text-xs text-destructive">
|
||||
{errorText}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type ChatStatus } from "ai"
|
||||
import { cn } from "cn"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import { FieldGroup } from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function PromptInput({
|
||||
onSubmit,
|
||||
status,
|
||||
className,
|
||||
defaultValue,
|
||||
}: {
|
||||
onSubmit: (input: string) => void
|
||||
status: ChatStatus
|
||||
className?: string
|
||||
defaultValue?: string
|
||||
}) {
|
||||
const [value, setValue] = React.useState(defaultValue ?? "")
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(defaultValue ?? "")
|
||||
}, [defaultValue])
|
||||
function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
if (value?.trim()) {
|
||||
onSubmit(value)
|
||||
}
|
||||
}
|
||||
|
||||
const isBusy = status === "submitted" || status === "streaming"
|
||||
const showStopButton = status === "streaming"
|
||||
const showSendButton = status !== "streaming"
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleFormSubmit}
|
||||
className={cn("w-full", className)}
|
||||
id="chat-form"
|
||||
>
|
||||
<FieldGroup className="gap-1">
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
placeholder="Ask me anything..."
|
||||
className="h-10 min-h-10 overflow-y-auto transition-all"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
/>
|
||||
<InputGroupAddon align="block-end" className="p-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
<span className="sr-only">Add</span>
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-fit">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="PaperclipIcon"
|
||||
tabler="IconPaperclip"
|
||||
hugeicons="AttachmentIcon"
|
||||
phosphor="PaperclipIcon"
|
||||
remixicon="RiAttachmentLine"
|
||||
/>
|
||||
Add Photos and Files
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="MonitorIcon"
|
||||
tabler="IconDeviceDesktop"
|
||||
hugeicons="ComputerIcon"
|
||||
phosphor="MonitorIcon"
|
||||
remixicon="RiComputerLine"
|
||||
/>
|
||||
Take a Screenshot
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ImageIcon"
|
||||
tabler="IconPhoto"
|
||||
hugeicons="ImageIcon"
|
||||
phosphor="ImageIcon"
|
||||
remixicon="RiImageLine"
|
||||
/>
|
||||
Create Image
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="BrainIcon"
|
||||
tabler="IconBrain"
|
||||
hugeicons="AiBrainIcon"
|
||||
phosphor="BrainIcon"
|
||||
remixicon="RiBrainLine"
|
||||
/>
|
||||
Deep Research
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="GlobeIcon"
|
||||
tabler="IconGlobe"
|
||||
hugeicons="Globe02Icon"
|
||||
phosphor="GlobeIcon"
|
||||
remixicon="RiGlobeLine"
|
||||
/>
|
||||
Web Search
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
More
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="BotIcon"
|
||||
tabler="IconRobot"
|
||||
hugeicons="RoboticIcon"
|
||||
phosphor="RobotIcon"
|
||||
remixicon="RiRobotLine"
|
||||
/>
|
||||
Agent Mode
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="BookOpenIcon"
|
||||
tabler="IconBook"
|
||||
hugeicons="BookOpen02Icon"
|
||||
phosphor="BookOpenIcon"
|
||||
remixicon="RiBookOpenLine"
|
||||
/>
|
||||
Add Sources
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
remixicon="RiCheckLine"
|
||||
/>
|
||||
Create Tasks
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
size="icon-sm"
|
||||
type="submit"
|
||||
disabled={isBusy || !value?.trim()}
|
||||
className="ml-auto data-[visible=false]:hidden"
|
||||
data-visible={showSendButton}
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="ArrowUpIcon"
|
||||
tabler="IconArrowUp"
|
||||
hugeicons="ArrowUp02Icon"
|
||||
phosphor="ArrowUpIcon"
|
||||
remixicon="RiArrowUpLine"
|
||||
/>
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
<InputGroupButton
|
||||
size="icon-sm"
|
||||
variant="default"
|
||||
type="button"
|
||||
data-visible={showStopButton}
|
||||
className="ml-auto data-[visible=false]:hidden"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="SquareIcon"
|
||||
tabler="IconPlayerStop"
|
||||
hugeicons="StopCircleIcon"
|
||||
phosphor="StopCircleIcon"
|
||||
remixicon="RiStopCircleLine"
|
||||
className="fill-foreground"
|
||||
/>
|
||||
<span className="sr-only">Stop</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
34
apps/v4/registry/bases/radix/blocks/preview-03/index.tsx
Normal file
34
apps/v4/registry/bases/radix/blocks/preview-03/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { FilesChat } from "@/registry/bases/radix/blocks/preview-03/cards/files-chat"
|
||||
import { GroupChat } from "@/registry/bases/radix/blocks/preview-03/cards/group-chat"
|
||||
import { ReasoningChat } from "@/registry/bases/radix/blocks/preview-03/cards/reasoning-chat"
|
||||
import { SimpleChat } from "@/registry/bases/radix/blocks/preview-03/cards/simple-chat"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
|
||||
export default function Preview03Example() {
|
||||
return (
|
||||
<div className="flex h-screen min-h-0 flex-col overflow-x-auto overflow-y-hidden bg-muted contain-[paint] [--gap:--spacing(4)] 3xl:[--gap:--spacing(12)] md:[--gap:--spacing(10)] dark:bg-background style-lyra:md:[--gap:--spacing(6)] style-mira:md:[--gap:--spacing(6)]">
|
||||
<div className="flex min-h-0 w-full min-w-max flex-1 justify-center">
|
||||
<div
|
||||
className="grid min-h-0 w-[2400px] grid-cols-7 items-start gap-(--gap) bg-muted p-(--gap) md:w-[3000px] dark:bg-background style-lyra:md:w-[2600px] style-mira:md:w-[2600px] *:[div]:gap-(--gap)"
|
||||
data-slot="capture-target"
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<SimpleChat />
|
||||
</div>
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<GroupChat />
|
||||
</div>
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<ReasoningChat />
|
||||
</div>
|
||||
<div className="flex h-full min-h-0 flex-1 flex-col p-1 [contain-intrinsic-size:380px_1200px] [content-visibility:auto]">
|
||||
<FilesChat />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sr-only">
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import { Alert, AlertDescription } from "@/registry/bases/radix/ui/alert"
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// Agent feature descriptions.
|
||||
const agentFeatures = [
|
||||
{
|
||||
id: "code-reviews",
|
||||
content: (
|
||||
<>
|
||||
<strong>Code reviews</strong> with full codebase context to catch{" "}
|
||||
<strong>hard-to-find</strong> bugs.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "code-suggestions",
|
||||
content: (
|
||||
<>
|
||||
<strong>Code suggestions</strong> validated in sandboxes before you
|
||||
merge.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "root-cause",
|
||||
content: (
|
||||
<>
|
||||
<strong>Root-cause analysis</strong> for production issues with
|
||||
deployment context.{" "}
|
||||
<Badge variant="secondary" className="bg-chart-1 text-chart-5">
|
||||
Requires Observability Plus
|
||||
</Badge>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export function ActivateAgentDialog() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ship faster & safer with Vercel Agent</CardTitle>
|
||||
<CardDescription>
|
||||
Your use is subject to Vercel's{" "}
|
||||
<a href="#">Public Beta Agreement</a> and{" "}
|
||||
<a href="#">AI Product Terms</a>.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<ItemGroup className="gap-0">
|
||||
{agentFeatures.map((feature) => (
|
||||
<Item key={feature.id} size="xs" className="px-0">
|
||||
<ItemMedia variant="icon" className="self-start">
|
||||
<IconPlaceholder
|
||||
lucide="CheckCircle2Icon"
|
||||
tabler="IconCircleCheckFilled"
|
||||
hugeicons="CheckmarkCircle02Icon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiCheckboxCircleLine"
|
||||
className="size-5 fill-primary text-primary-foreground"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle className="inline leading-relaxed font-normal text-muted-foreground *:[strong]:font-medium *:[strong]:text-foreground">
|
||||
{feature.content}
|
||||
</ItemTitle>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Pro teams get $100 in Vercel Agent trial credit for 2 weeks after
|
||||
activation.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2">
|
||||
<Button variant="outline" className="style-sera:hidden">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="style-sera:w-full">Enable with $100 credits</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import { Area, AreaChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
|
||||
// Monthly visitor data for the area chart.
|
||||
const chartData = [
|
||||
{ month: "January", visitors: 186 },
|
||||
{ month: "February", visitors: 305 },
|
||||
{ month: "March", visitors: 237 },
|
||||
{ month: "April", visitors: 73 },
|
||||
{ month: "May", visitors: 209 },
|
||||
{ month: "June", visitors: 214 },
|
||||
]
|
||||
|
||||
// Chart configuration for the visitors dataset.
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function AnalyticsCard() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
418.2K Visitors <Badge>+10%</Badge>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
View Analytics
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<ChartContainer config={chartConfig} className="aspect-[1/0.35]">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" hideLabel />}
|
||||
defaultIndex={2}
|
||||
/>
|
||||
<Area
|
||||
dataKey="visitors"
|
||||
type="linear"
|
||||
fill="var(--color-visitors)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-visitors)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
|
||||
export function AnomalyAlert() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-48">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Get alerted for anomalies</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Automatically monitor your projects for anomalies and get
|
||||
notified.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Upgrade to Observability Plus</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/radix/ui/avatar"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/registry/bases/radix/ui/combobox"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/bases/radix/ui/tooltip"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// Users available for assignment.
|
||||
const users = [
|
||||
"shadcn",
|
||||
"maxleiter",
|
||||
"evilrabbit",
|
||||
"pranathip",
|
||||
"jorgezreik",
|
||||
"shuding",
|
||||
"rauchg",
|
||||
]
|
||||
|
||||
export function AssignIssue() {
|
||||
const anchor = useComboboxAnchor()
|
||||
return (
|
||||
<Card className="w-full max-w-sm" size="sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle className="text-sm">Assign Issue</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Select users to assign to this issue.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add user</TooltipContent>
|
||||
</Tooltip>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={users}
|
||||
defaultValue={[users[0]]}
|
||||
>
|
||||
<ComboboxChips ref={anchor}>
|
||||
<ComboboxValue>
|
||||
{(values) => (
|
||||
<React.Fragment>
|
||||
{values.map((username: string) => (
|
||||
<ComboboxChip key={username}>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{username}
|
||||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput
|
||||
placeholder={
|
||||
values.length > 0 ? undefined : "Select a item..."
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No users found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(username: string) => (
|
||||
<ComboboxItem key={username} value={username}>
|
||||
<Avatar className="size-5">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{username}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
|
||||
const barChartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const barChartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const desktopTotal = barChartData.reduce((sum, item) => sum + item.desktop, 0)
|
||||
const mobileTotal = barChartData.reduce((sum, item) => sum + item.mobile, 0)
|
||||
const desktopDelta = Math.round(
|
||||
((desktopTotal - mobileTotal) / mobileTotal) * 100
|
||||
)
|
||||
const desktopDeltaPrefix = desktopDelta > 0 ? "+" : ""
|
||||
|
||||
export function BarChartCard() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Traffic channels</CardTitle>
|
||||
<CardDescription className="line-clamp-2 text-sm leading-snug">
|
||||
Monthly desktop and mobile traffic for the last six months—compare
|
||||
volume and mix across platforms and devices at a glance.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4 pt-0">
|
||||
<ChartContainer
|
||||
config={barChartConfig}
|
||||
className="max-h-[180px] w-full"
|
||||
>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={barChartData}
|
||||
margin={{ left: 0, right: 0, top: 8, bottom: 0 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => String(value).slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dashed" />}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Bar
|
||||
dataKey="desktop"
|
||||
fill="var(--color-desktop)"
|
||||
radius={isRounded ? [6, 6, 0, 0] : 0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="mobile"
|
||||
fill="var(--color-mobile)"
|
||||
radius={isRounded ? [6, 6, 0, 0] : 0}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<div className="grid w-full grid-cols-3 divide-x divide-border/60">
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Desktop
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{desktopTotal.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Mobile
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{mobileTotal.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Mix Delta
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{desktopDeltaPrefix}
|
||||
{desktopDelta}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View report</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
type AgentState =
|
||||
| "connecting"
|
||||
| "initializing"
|
||||
| "listening"
|
||||
| "speaking"
|
||||
| "thinking"
|
||||
|
||||
type AnimationState = AgentState | undefined
|
||||
|
||||
interface AudioAnalyserOptions {
|
||||
fftSize?: number
|
||||
smoothingTimeConstant?: number
|
||||
minDecibels?: number
|
||||
maxDecibels?: number
|
||||
}
|
||||
|
||||
interface MultiBandVolumeOptions {
|
||||
bands?: number
|
||||
loPass?: number
|
||||
hiPass?: number
|
||||
updateInterval?: number
|
||||
analyserOptions?: AudioAnalyserOptions
|
||||
}
|
||||
|
||||
const multibandDefaults: MultiBandVolumeOptions = {
|
||||
bands: 5,
|
||||
loPass: 100,
|
||||
hiPass: 600,
|
||||
updateInterval: 32,
|
||||
analyserOptions: { fftSize: 2048 },
|
||||
}
|
||||
|
||||
const normalizeDb = (value: number) => {
|
||||
if (value === -Infinity) return 0
|
||||
const minDb = -100
|
||||
const maxDb = -10
|
||||
const db = 1 - (Math.max(minDb, Math.min(maxDb, value)) * -1) / 100
|
||||
return Math.sqrt(db)
|
||||
}
|
||||
|
||||
function createAudioAnalyser(
|
||||
mediaStream: MediaStream,
|
||||
options: AudioAnalyserOptions = {}
|
||||
) {
|
||||
const audioContext = new (window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext)()
|
||||
const source = audioContext.createMediaStreamSource(mediaStream)
|
||||
const analyser = audioContext.createAnalyser()
|
||||
|
||||
if (options.fftSize) analyser.fftSize = options.fftSize
|
||||
if (options.smoothingTimeConstant !== undefined) {
|
||||
analyser.smoothingTimeConstant = options.smoothingTimeConstant
|
||||
}
|
||||
if (options.minDecibels !== undefined)
|
||||
analyser.minDecibels = options.minDecibels
|
||||
if (options.maxDecibels !== undefined)
|
||||
analyser.maxDecibels = options.maxDecibels
|
||||
|
||||
source.connect(analyser)
|
||||
|
||||
const cleanup = () => {
|
||||
source.disconnect()
|
||||
audioContext.close()
|
||||
}
|
||||
|
||||
return { analyser, audioContext, cleanup }
|
||||
}
|
||||
|
||||
function useMultibandVolume(
|
||||
mediaStream?: MediaStream | null,
|
||||
options: MultiBandVolumeOptions = {}
|
||||
) {
|
||||
const opts = useMemo(() => ({ ...multibandDefaults, ...options }), [options])
|
||||
|
||||
const [frequencyBands, setFrequencyBands] = useState<number[]>(() =>
|
||||
new Array(opts.bands).fill(0)
|
||||
)
|
||||
const bandsRef = useRef<number[]>(new Array(opts.bands).fill(0))
|
||||
const frameId = useRef<number | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!mediaStream) {
|
||||
const emptyBands = new Array(opts.bands).fill(0)
|
||||
setTimeout(() => {
|
||||
setFrequencyBands(emptyBands)
|
||||
}, 0)
|
||||
bandsRef.current = emptyBands
|
||||
return
|
||||
}
|
||||
|
||||
const { analyser, cleanup } = createAudioAnalyser(
|
||||
mediaStream,
|
||||
opts.analyserOptions
|
||||
)
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount
|
||||
const dataArray = new Float32Array(bufferLength)
|
||||
const sliceStart = opts.loPass!
|
||||
const sliceEnd = opts.hiPass!
|
||||
const sliceLength = sliceEnd - sliceStart
|
||||
const chunkSize = Math.ceil(sliceLength / opts.bands!)
|
||||
|
||||
let lastUpdate = 0
|
||||
const updateInterval = opts.updateInterval!
|
||||
|
||||
const updateVolume = (timestamp: number) => {
|
||||
if (timestamp - lastUpdate >= updateInterval) {
|
||||
analyser.getFloatFrequencyData(dataArray)
|
||||
|
||||
const chunks = new Array(opts.bands!)
|
||||
|
||||
for (let i = 0; i < opts.bands!; i++) {
|
||||
let sum = 0
|
||||
let count = 0
|
||||
const startIdx = sliceStart + i * chunkSize
|
||||
const endIdx = Math.min(sliceStart + (i + 1) * chunkSize, sliceEnd)
|
||||
|
||||
for (let j = startIdx; j < endIdx; j++) {
|
||||
sum += normalizeDb(dataArray[j])
|
||||
count++
|
||||
}
|
||||
|
||||
chunks[i] = count > 0 ? sum / count : 0
|
||||
}
|
||||
|
||||
let hasChanged = false
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
if (Math.abs(chunks[i] - bandsRef.current[i]) > 0.01) {
|
||||
hasChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
bandsRef.current = chunks
|
||||
setFrequencyBands(chunks)
|
||||
}
|
||||
|
||||
lastUpdate = timestamp
|
||||
}
|
||||
|
||||
frameId.current = requestAnimationFrame(updateVolume)
|
||||
}
|
||||
|
||||
frameId.current = requestAnimationFrame(updateVolume)
|
||||
|
||||
return () => {
|
||||
cleanup()
|
||||
if (frameId.current) {
|
||||
cancelAnimationFrame(frameId.current)
|
||||
}
|
||||
}
|
||||
}, [mediaStream, opts])
|
||||
|
||||
return frequencyBands
|
||||
}
|
||||
|
||||
const generateConnectingSequenceBar = (columns: number): number[][] => {
|
||||
const seq = []
|
||||
for (let x = 0; x < columns; x++) {
|
||||
seq.push([x, columns - 1 - x])
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
const generateListeningSequenceBar = (columns: number): number[][] => {
|
||||
const center = Math.floor(columns / 2)
|
||||
const noIndex = -1
|
||||
return [[center], [noIndex]]
|
||||
}
|
||||
|
||||
const useBarAnimator = (
|
||||
state: AnimationState,
|
||||
columns: number,
|
||||
interval: number
|
||||
): number[] => {
|
||||
const indexRef = useRef(0)
|
||||
const [currentFrame, setCurrentFrame] = useState<number[]>([])
|
||||
const animationFrameId = useRef<number | null>(null)
|
||||
|
||||
const sequence = useMemo(() => {
|
||||
if (state === "thinking" || state === "listening") {
|
||||
return generateListeningSequenceBar(columns)
|
||||
} else if (state === "connecting" || state === "initializing") {
|
||||
return generateConnectingSequenceBar(columns)
|
||||
} else if (state === undefined || state === "speaking") {
|
||||
return [new Array(columns).fill(0).map((_, idx) => idx)]
|
||||
} else {
|
||||
return [[]]
|
||||
}
|
||||
}, [state, columns])
|
||||
|
||||
useEffect(() => {
|
||||
indexRef.current = 0
|
||||
setTimeout(() => {
|
||||
setCurrentFrame(sequence[0] || [])
|
||||
}, 0)
|
||||
}, [sequence])
|
||||
|
||||
useEffect(() => {
|
||||
let startTime = performance.now()
|
||||
|
||||
const animate = (time: DOMHighResTimeStamp) => {
|
||||
const timeElapsed = time - startTime
|
||||
|
||||
if (timeElapsed >= interval) {
|
||||
indexRef.current = (indexRef.current + 1) % sequence.length
|
||||
setCurrentFrame(sequence[indexRef.current] || [])
|
||||
startTime = time
|
||||
}
|
||||
|
||||
animationFrameId.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
animationFrameId.current = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (animationFrameId.current !== null) {
|
||||
cancelAnimationFrame(animationFrameId.current)
|
||||
}
|
||||
}
|
||||
}, [interval, sequence])
|
||||
|
||||
return currentFrame
|
||||
}
|
||||
|
||||
// Memoized Bar component to prevent unnecessary re-renders.
|
||||
const Bar = React.memo<{
|
||||
heightPct: number
|
||||
isHighlighted: boolean
|
||||
state?: AgentState
|
||||
}>(({ heightPct, isHighlighted, state }) => (
|
||||
<div
|
||||
data-highlighted={isHighlighted}
|
||||
className={cn(
|
||||
"max-w-[12px] min-w-[8px] flex-1 transition-all duration-150",
|
||||
"rounded-full",
|
||||
"bg-border data-[highlighted=true]:bg-chart-3",
|
||||
state === "speaking" && "bg-chart-3",
|
||||
state === "thinking" && isHighlighted && "animate-pulse"
|
||||
)}
|
||||
style={{
|
||||
height: `${heightPct}%`,
|
||||
animationDuration: state === "thinking" ? "300ms" : undefined,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
|
||||
Bar.displayName = "Bar"
|
||||
|
||||
const BarVisualizerComponent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
state?: AgentState
|
||||
barCount?: number
|
||||
mediaStream?: MediaStream | null
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
demo?: boolean
|
||||
centerAlign?: boolean
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
>(
|
||||
(
|
||||
{
|
||||
state,
|
||||
barCount = 15,
|
||||
mediaStream,
|
||||
minHeight = 20,
|
||||
maxHeight = 100,
|
||||
demo = false,
|
||||
centerAlign = false,
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const realVolumeBands = useMultibandVolume(mediaStream, {
|
||||
bands: barCount,
|
||||
loPass: 100,
|
||||
hiPass: 200,
|
||||
})
|
||||
|
||||
const fakeVolumeBandsRef = useRef<number[]>(new Array(barCount).fill(0.2))
|
||||
const [fakeVolumeBands, setFakeVolumeBands] = useState<number[]>(() =>
|
||||
new Array(barCount).fill(0.2)
|
||||
)
|
||||
const fakeAnimationRef = useRef<number | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!demo) return
|
||||
|
||||
if (state !== "speaking" && state !== "listening") {
|
||||
const bands = new Array(barCount).fill(0.2)
|
||||
fakeVolumeBandsRef.current = bands
|
||||
setTimeout(() => {
|
||||
setFakeVolumeBands(bands)
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
let lastUpdate = 0
|
||||
const updateInterval = 50
|
||||
const startTime = Date.now() / 1000
|
||||
|
||||
const updateFakeVolume = (timestamp: number) => {
|
||||
if (timestamp - lastUpdate >= updateInterval) {
|
||||
const time = Date.now() / 1000 - startTime
|
||||
const newBands = new Array(barCount)
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const waveOffset = i * 0.5
|
||||
const baseVolume = Math.sin(time * 2 + waveOffset) * 0.3 + 0.5
|
||||
const randomNoise = Math.random() * 0.2
|
||||
newBands[i] = Math.max(0.1, Math.min(1, baseVolume + randomNoise))
|
||||
}
|
||||
|
||||
let hasChanged = false
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
if (Math.abs(newBands[i] - fakeVolumeBandsRef.current[i]) > 0.05) {
|
||||
hasChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
fakeVolumeBandsRef.current = newBands
|
||||
setFakeVolumeBands(newBands)
|
||||
}
|
||||
|
||||
lastUpdate = timestamp
|
||||
}
|
||||
|
||||
fakeAnimationRef.current = requestAnimationFrame(updateFakeVolume)
|
||||
}
|
||||
|
||||
fakeAnimationRef.current = requestAnimationFrame(updateFakeVolume)
|
||||
|
||||
return () => {
|
||||
if (fakeAnimationRef.current) {
|
||||
cancelAnimationFrame(fakeAnimationRef.current)
|
||||
}
|
||||
}
|
||||
}, [demo, state, barCount])
|
||||
|
||||
const volumeBands = useMemo(
|
||||
() => (demo ? fakeVolumeBands : realVolumeBands),
|
||||
[demo, fakeVolumeBands, realVolumeBands]
|
||||
)
|
||||
|
||||
const highlightedIndices = useBarAnimator(
|
||||
state,
|
||||
barCount,
|
||||
state === "connecting"
|
||||
? 2000 / barCount
|
||||
: state === "thinking"
|
||||
? 150
|
||||
: state === "listening"
|
||||
? 500
|
||||
: 1000
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-state={state}
|
||||
className={cn(
|
||||
"relative flex justify-center gap-1.5",
|
||||
centerAlign ? "items-center" : "items-end",
|
||||
"h-32 w-full overflow-hidden rounded-lg bg-muted p-4",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{volumeBands.map((volume, index) => {
|
||||
const heightPct = Math.min(
|
||||
maxHeight,
|
||||
Math.max(minHeight, volume * 100 + 5)
|
||||
)
|
||||
const isHighlighted = highlightedIndices?.includes(index) ?? false
|
||||
|
||||
return (
|
||||
<Bar
|
||||
key={index}
|
||||
heightPct={heightPct}
|
||||
isHighlighted={isHighlighted}
|
||||
state={state}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BarVisualizerComponent.displayName = "BarVisualizerComponent"
|
||||
|
||||
const BarVisualizer = React.memo(
|
||||
BarVisualizerComponent,
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.state === nextProps.state &&
|
||||
prevProps.barCount === nextProps.barCount &&
|
||||
prevProps.mediaStream === nextProps.mediaStream &&
|
||||
prevProps.minHeight === nextProps.minHeight &&
|
||||
prevProps.maxHeight === nextProps.maxHeight &&
|
||||
prevProps.demo === nextProps.demo &&
|
||||
prevProps.centerAlign === nextProps.centerAlign &&
|
||||
prevProps.className === nextProps.className &&
|
||||
JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BarVisualizer.displayName = "BarVisualizer"
|
||||
|
||||
export function BarVisualizerCard() {
|
||||
const [state, setState] = useState<AgentState>("speaking")
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Audio Frequency Visualizer</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time frequency band visualization with animated state transitions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BarVisualizer
|
||||
state={state}
|
||||
demo={true}
|
||||
barCount={20}
|
||||
minHeight={15}
|
||||
maxHeight={90}
|
||||
className="h-40 max-w-full"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "connecting" ? "default" : "outline"}
|
||||
onClick={() => setState("connecting")}
|
||||
>
|
||||
Connecting
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "listening" ? "default" : "outline"}
|
||||
onClick={() => setState("listening")}
|
||||
>
|
||||
Listening
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "speaking" ? "default" : "outline"}
|
||||
onClick={() => setState("speaking")}
|
||||
>
|
||||
Speaking
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/bases/radix/ui/alert"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/radix/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function BookAppointment() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Book Appointment</CardTitle>
|
||||
<CardDescription>Dr. Sarah Chen · Cardiology</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel>Available on March 18, 2026</FieldLabel>
|
||||
<ToggleGroup type="multiple" spacing={2} defaultValue={["slot-0"]}>
|
||||
{["9:00 AM", "10:30 AM", "11:00 AM", "1:30 PM"].map(
|
||||
(time, index) => (
|
||||
<ToggleGroupItem key={time} value={`slot-${index}`}>
|
||||
{time}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
)}
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Alert>
|
||||
<AlertTitle>New patient?</AlertTitle>
|
||||
<AlertDescription>Please arrive 15 minutes early.</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Book Appointment</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/bases/radix/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/bases/radix/ui/tooltip"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function CodespacesCard() {
|
||||
const [isCreatingCodespace, setIsCreatingCodespace] = React.useState(false)
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="codespaces">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="codespaces">Codespaces</TabsTrigger>
|
||||
<TabsTrigger value="local">Local</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="codespaces">
|
||||
<Item size="sm" className="px-1 pt-2">
|
||||
<ItemContent>
|
||||
<ItemTitle>Codespaces</ItemTitle>
|
||||
<ItemDescription>Your workspaces in the cloud</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
Create a codespace on main
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
New with options...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ContainerIcon"
|
||||
tabler="IconBox"
|
||||
hugeicons="CubeIcon"
|
||||
phosphor="CubeIcon"
|
||||
remixicon="RiBox1Line"
|
||||
/>
|
||||
Configure container
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ZapIcon"
|
||||
tabler="IconBolt"
|
||||
hugeicons="ZapIcon"
|
||||
phosphor="LightningIcon"
|
||||
remixicon="RiFlashlightLine"
|
||||
/>
|
||||
Set up prebuilds
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ServerIcon"
|
||||
tabler="IconServer"
|
||||
hugeicons="ServerStackIcon"
|
||||
phosphor="HardDrivesIcon"
|
||||
remixicon="RiHardDriveLine"
|
||||
/>
|
||||
Manage codespaces
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ShareIcon"
|
||||
tabler="IconShare2"
|
||||
hugeicons="Share03Icon"
|
||||
phosphor="ShareIcon"
|
||||
remixicon="RiShareLine"
|
||||
/>
|
||||
Share deep link
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="InfoIcon"
|
||||
tabler="IconInfoCircle"
|
||||
hugeicons="AlertCircleIcon"
|
||||
phosphor="InfoIcon"
|
||||
remixicon="RiInformationLine"
|
||||
/>
|
||||
What are codespaces?
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="ServerIcon"
|
||||
tabler="IconServer"
|
||||
hugeicons="ServerStackIcon"
|
||||
phosphor="HardDrivesIcon"
|
||||
remixicon="RiHardDriveLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No codespaces</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You don't have any codespaces with this repository
|
||||
checked out
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsCreatingCodespace(true)
|
||||
setTimeout(() => {
|
||||
setIsCreatingCodespace(false)
|
||||
}, 2000)
|
||||
}}
|
||||
disabled={isCreatingCodespace}
|
||||
>
|
||||
{isCreatingCodespace ? (
|
||||
<Spinner data-icon="inline-start" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
Create Codespace
|
||||
</Button>
|
||||
<a
|
||||
href="#learn-more"
|
||||
className="text-xs text-muted-foreground underline underline-offset-4"
|
||||
>
|
||||
Learn more about codespaces
|
||||
</a>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<div className="p-1.5 text-xs text-muted-foreground">
|
||||
Codespace usage for this repository is paid for by{" "}
|
||||
<span className="font-medium">shadcn</span>.
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="local">
|
||||
<Item size="sm" className="hidden p-0">
|
||||
<ItemContent>
|
||||
<ItemTitle className="gap-2">
|
||||
<IconPlaceholder
|
||||
lucide="TerminalIcon"
|
||||
tabler="IconTerminal"
|
||||
hugeicons="ComputerTerminal01Icon"
|
||||
phosphor="TerminalIcon"
|
||||
remixicon="RiTerminalBoxLine"
|
||||
className="size-4"
|
||||
/>
|
||||
Clone
|
||||
</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<IconPlaceholder
|
||||
lucide="InfoIcon"
|
||||
tabler="IconInfoCircle"
|
||||
hugeicons="AlertCircleIcon"
|
||||
phosphor="InfoIcon"
|
||||
remixicon="RiInformationLine"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
Which remote URL should I use?
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Tabs defaultValue="https">
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="w-full justify-start border-b *:[button]:flex-0"
|
||||
>
|
||||
<TabsTrigger value="https">HTTPS</TabsTrigger>
|
||||
<TabsTrigger value="ssh">SSH</TabsTrigger>
|
||||
<TabsTrigger value="cli">GitHub CLI</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="rounded-md border bg-muted/30 p-2">
|
||||
<TabsContent value="https">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="https-url" className="sr-only">
|
||||
HTTPS URL
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="https-url"
|
||||
defaultValue="https://github.com/shadcn-ui/ui.git"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Clone using the web URL.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
<TabsContent value="ssh">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="ssh-url" className="sr-only">
|
||||
SSH URL
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="ssh-url"
|
||||
defaultValue="git@github.com:shadcn-ui/ui.git"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Use a password-protected SSH key.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
<TabsContent value="cli">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="cli-command" className="sr-only">
|
||||
CLI Command
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="cli-command"
|
||||
defaultValue="gh repo clone shadcn-ui/ui"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Work fast with our official CLI.{" "}
|
||||
<a href="#learn-more">Learn more</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start gap-1.5"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MonitorIcon"
|
||||
tabler="IconDeviceDesktop"
|
||||
hugeicons="ComputerIcon"
|
||||
phosphor="MonitorIcon"
|
||||
remixicon="RiComputerLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Open with GitHub Desktop
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start gap-1.5"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="DownloadIcon"
|
||||
tabler="IconDownload"
|
||||
hugeicons="DownloadIcon"
|
||||
phosphor="DownloadIcon"
|
||||
remixicon="RiDownloadLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Download ZIP
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
|
||||
export function ContributionsActivity() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contributions & Activity</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your contributions and activity visibility.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="contributions-activity">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend className="sr-only">
|
||||
Contributions & activity
|
||||
</FieldLegend>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="activity-private-profile" />
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="activity-private-profile">
|
||||
Make profile private and hide activity
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enabling this will hide your contributions and activity
|
||||
from your GitHub profile and from social features like
|
||||
followers, stars, feeds, leaderboards and releases.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button form="contributions-activity" className="style-sera:w-full">
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/radix/ui/avatar"
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
// GitHub usernames displayed as contributor avatars.
|
||||
const usernames = [
|
||||
"shadcn",
|
||||
"vercel",
|
||||
"nextjs",
|
||||
"tailwindlabs",
|
||||
"typescript-lang",
|
||||
"eslint",
|
||||
"prettier",
|
||||
"babel",
|
||||
"webpack",
|
||||
"rollup",
|
||||
"parcel",
|
||||
"vite",
|
||||
"react",
|
||||
"vue",
|
||||
"angular",
|
||||
"solid",
|
||||
]
|
||||
|
||||
export function Contributors() {
|
||||
return (
|
||||
<Card className="max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Contributors <Badge variant="secondary">312</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{usernames.map((username) => (
|
||||
<Avatar key={username} className="grayscale">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<a href="#" className="text-sm underline underline-offset-4">
|
||||
+ 810 contributors
|
||||
</a>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
export function EnvironmentVariables() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Environment Variables</CardTitle>
|
||||
<CardDescription>Production · 8 variables</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{[
|
||||
{ key: "DATABASE_URL", masked: true },
|
||||
{ key: "NEXT_PUBLIC_API", masked: false },
|
||||
{ key: "STRIPE_SECRET", masked: true },
|
||||
].map((env) => (
|
||||
<div
|
||||
key={env.key}
|
||||
className="flex items-center gap-2 rounded-md px-2.5 py-2 font-mono text-xs ring ring-border"
|
||||
>
|
||||
<span className="font-medium">{env.key}</span>
|
||||
<span className="ml-auto text-muted-foreground">
|
||||
{env.masked ? "••••••••" : "https://api.example.com"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline">Edit</Button>
|
||||
<Button className="ml-auto">Deploy</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/bases/radix/ui/native-select"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
|
||||
export function FeedbackForm() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<form id="feedback-form">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="topic">Topic</FieldLabel>
|
||||
<NativeSelect id="topic">
|
||||
<NativeSelectOption value="">Select a topic</NativeSelectOption>
|
||||
<NativeSelectOption value="ai">AI</NativeSelectOption>
|
||||
<NativeSelectOption value="accounts-and-access-controls">
|
||||
Accounts and Access Controls
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="billing">Billing</NativeSelectOption>
|
||||
<NativeSelectOption value="cdn">
|
||||
CDN (Firewall, Caching)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="ci-cd">
|
||||
CI/CD (Builds, Deployments, Environment Variables)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="dashboard-interface">
|
||||
Dashboard Interface (Navigation, UI Issues)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="domains">Domains</NativeSelectOption>
|
||||
<NativeSelectOption value="frameworks">
|
||||
Frameworks
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="marketplace-and-integrations">
|
||||
Marketplace and Integrations
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="observability">
|
||||
Observability (Observability, Logs, Monitoring)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="storage">Storage</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback">Feedback</FieldLabel>
|
||||
<Textarea
|
||||
id="feedback"
|
||||
placeholder="Your feedback helps us improve..."
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
form="feedback-form"
|
||||
className="style-sera:w-full"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function FileUpload() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>File Upload</CardTitle>
|
||||
<CardDescription>Drag and drop or browse</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Empty className="border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="UploadCloudIcon"
|
||||
tabler="IconCloudUpload"
|
||||
hugeicons="CloudUploadIcon"
|
||||
phosphor="CloudArrowUpIcon"
|
||||
remixicon="RiUploadCloudLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Upload files</EmptyTitle>
|
||||
<EmptyDescription>PNG, JPG, PDF up to 10MB</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Browse Files</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/bases/radix/ui/native-select"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
|
||||
export function GithubProfile() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Profile</CardTitle>
|
||||
<CardDescription>Manage your profile information.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="profile">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input id="name" placeholder="shadcn" />
|
||||
<FieldDescription>
|
||||
Your name may appear around GitHub where you contribute or are
|
||||
mentioned. You can remove it at any time.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Public Email</FieldLabel>
|
||||
<NativeSelect id="email">
|
||||
<NativeSelectOption value="m@shadcn.com">
|
||||
m@shadcn.com
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="m@gmail.com">
|
||||
m@gmail.com
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<FieldDescription>
|
||||
You can manage verified email addresses in your{" "}
|
||||
<a href="#email-settings">email settings</a>.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bio">Bio</FieldLabel>
|
||||
<Textarea
|
||||
id="bio"
|
||||
placeholder="Tell us a little bit about yourself"
|
||||
/>
|
||||
<FieldDescription>
|
||||
You can <span>@mention</span> other users and organizations to
|
||||
link to them.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button form="profile" className="style-sera:w-full">
|
||||
Save Profile
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client"
|
||||
|
||||
import { type IconLibraryName } from "shadcn/icons"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const PREVIEW_ICONS = [
|
||||
{
|
||||
lucide: "CopyIcon",
|
||||
tabler: "IconCopy",
|
||||
hugeicons: "Copy01Icon",
|
||||
phosphor: "CopyIcon",
|
||||
remixicon: "RiFileCopyLine",
|
||||
},
|
||||
{
|
||||
lucide: "CircleAlertIcon",
|
||||
tabler: "IconExclamationCircle",
|
||||
hugeicons: "AlertCircleIcon",
|
||||
phosphor: "WarningCircleIcon",
|
||||
remixicon: "RiErrorWarningLine",
|
||||
},
|
||||
{
|
||||
lucide: "TrashIcon",
|
||||
tabler: "IconTrash",
|
||||
hugeicons: "Delete02Icon",
|
||||
phosphor: "TrashIcon",
|
||||
remixicon: "RiDeleteBinLine",
|
||||
},
|
||||
{
|
||||
lucide: "ShareIcon",
|
||||
tabler: "IconShare",
|
||||
hugeicons: "Share03Icon",
|
||||
phosphor: "ShareIcon",
|
||||
remixicon: "RiShareLine",
|
||||
},
|
||||
{
|
||||
lucide: "ShoppingBagIcon",
|
||||
tabler: "IconShoppingBag",
|
||||
hugeicons: "ShoppingBag01Icon",
|
||||
phosphor: "BagIcon",
|
||||
remixicon: "RiShoppingBagLine",
|
||||
},
|
||||
{
|
||||
lucide: "MoreHorizontalIcon",
|
||||
tabler: "IconDots",
|
||||
hugeicons: "MoreHorizontalCircle01Icon",
|
||||
phosphor: "DotsThreeIcon",
|
||||
remixicon: "RiMoreLine",
|
||||
},
|
||||
{
|
||||
lucide: "Loader2Icon",
|
||||
tabler: "IconLoader",
|
||||
hugeicons: "Loading03Icon",
|
||||
phosphor: "SpinnerIcon",
|
||||
remixicon: "RiLoaderLine",
|
||||
},
|
||||
{
|
||||
lucide: "PlusIcon",
|
||||
tabler: "IconPlus",
|
||||
hugeicons: "PlusSignIcon",
|
||||
phosphor: "PlusIcon",
|
||||
remixicon: "RiAddLine",
|
||||
},
|
||||
{
|
||||
lucide: "MinusIcon",
|
||||
tabler: "IconMinus",
|
||||
hugeicons: "MinusSignIcon",
|
||||
phosphor: "MinusIcon",
|
||||
remixicon: "RiSubtractLine",
|
||||
},
|
||||
{
|
||||
lucide: "ArrowLeftIcon",
|
||||
tabler: "IconArrowLeft",
|
||||
hugeicons: "ArrowLeft02Icon",
|
||||
phosphor: "ArrowLeftIcon",
|
||||
remixicon: "RiArrowLeftLine",
|
||||
},
|
||||
{
|
||||
lucide: "ArrowRightIcon",
|
||||
tabler: "IconArrowRight",
|
||||
hugeicons: "ArrowRight02Icon",
|
||||
phosphor: "ArrowRightIcon",
|
||||
remixicon: "RiArrowRightLine",
|
||||
},
|
||||
{
|
||||
lucide: "CheckIcon",
|
||||
tabler: "IconCheck",
|
||||
hugeicons: "Tick02Icon",
|
||||
phosphor: "CheckIcon",
|
||||
remixicon: "RiCheckLine",
|
||||
},
|
||||
{
|
||||
lucide: "ChevronDownIcon",
|
||||
tabler: "IconChevronDown",
|
||||
hugeicons: "ArrowDown01Icon",
|
||||
phosphor: "CaretDownIcon",
|
||||
remixicon: "RiArrowDownSLine",
|
||||
},
|
||||
{
|
||||
lucide: "ChevronRightIcon",
|
||||
tabler: "IconChevronRight",
|
||||
hugeicons: "ArrowRight01Icon",
|
||||
phosphor: "CaretRightIcon",
|
||||
remixicon: "RiArrowRightSLine",
|
||||
},
|
||||
{
|
||||
lucide: "SearchIcon",
|
||||
tabler: "IconSearch",
|
||||
hugeicons: "Search01Icon",
|
||||
phosphor: "MagnifyingGlassIcon",
|
||||
remixicon: "RiSearchLine",
|
||||
},
|
||||
{
|
||||
lucide: "SettingsIcon",
|
||||
tabler: "IconSettings",
|
||||
hugeicons: "Settings01Icon",
|
||||
phosphor: "GearIcon",
|
||||
remixicon: "RiSettingsLine",
|
||||
},
|
||||
] satisfies Record<IconLibraryName, string>[]
|
||||
|
||||
export function IconPreviewGrid() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-8 place-items-center gap-4">
|
||||
{PREVIEW_ICONS.map((icon, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex size-8 items-center justify-center rounded-md ring ring-border style-sera:rounded-none *:[svg]:size-4"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide={icon.lucide}
|
||||
tabler={icon.tabler}
|
||||
hugeicons={icon.hugeicons}
|
||||
phosphor={icon.phosphor}
|
||||
remixicon={icon.remixicon}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function InviteTeam() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invite Team</CardTitle>
|
||||
<CardDescription>Add members to your workspace</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
{[
|
||||
{ email: "alex@example.com", role: "Editor" },
|
||||
{ email: "sam@example.com", role: "Viewer" },
|
||||
].map((invite) => (
|
||||
<div key={invite.email} className="flex items-center gap-2">
|
||||
<Input defaultValue={invite.email} className="flex-1" />
|
||||
<Select defaultValue={invite.role.toLowerCase()}>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" align="end">
|
||||
<SelectGroup>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="editor">Editor</SelectItem>
|
||||
<SelectItem value="viewer">Viewer</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Add another
|
||||
</Button>
|
||||
<Separator />
|
||||
<Field>
|
||||
<FieldLabel htmlFor="invite-link">Or share invite link</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="invite-link"
|
||||
defaultValue="https://app.co/invite/x8f2k"
|
||||
readOnly
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton size="icon-xs" aria-label="Copy link">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Send Invites</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
115
apps/v4/registry/bases/radix/blocks/preview/cards/invoice.tsx
Normal file
115
apps/v4/registry/bases/radix/blocks/preview/cards/invoice.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/bases/radix/ui/table"
|
||||
|
||||
const INVOICE_ITEMS = [
|
||||
{ item: "Design System License", qty: 1, unitPrice: 499 },
|
||||
{ item: "Priority Support", qty: 12, unitPrice: 99 },
|
||||
{ item: "Custom Components", qty: 3, unitPrice: 250 },
|
||||
] as const
|
||||
|
||||
const subtotal = INVOICE_ITEMS.reduce(
|
||||
(sum, row) => sum + row.qty * row.unitPrice,
|
||||
0
|
||||
)
|
||||
const tax = 0
|
||||
const totalDue = subtotal + tax
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
export function Invoice() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invoice #INV-2847</CardTitle>
|
||||
<CardDescription>Due March 30, 2026</CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant="secondary">Pending</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Item</TableHead>
|
||||
<TableHead className="text-right">Qty</TableHead>
|
||||
<TableHead className="text-right">Rate</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{INVOICE_ITEMS.map((row) => (
|
||||
<TableRow key={row.item}>
|
||||
<TableCell>{row.item}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{row.qty}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(row.unitPrice)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(row.qty * row.unitPrice)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Subtotal
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(subtotal)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Tax
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">$0.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Total Due
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(totalDue)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" size="sm">
|
||||
Download PDF
|
||||
</Button>
|
||||
<Button size="sm" className="ml-auto">
|
||||
Pay Now
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
const LiveWaveform = ({
|
||||
active = false,
|
||||
processing = false,
|
||||
deviceId,
|
||||
barWidth = 3,
|
||||
barGap = 1,
|
||||
barRadius = 1.5,
|
||||
barColor,
|
||||
fadeEdges = true,
|
||||
fadeWidth = 24,
|
||||
barHeight: baseBarHeight = 4,
|
||||
height = 64,
|
||||
sensitivity = 1,
|
||||
smoothingTimeConstant = 0.8,
|
||||
fftSize = 256,
|
||||
historySize = 60,
|
||||
updateRate = 30,
|
||||
mode = "static",
|
||||
onError,
|
||||
onStreamReady,
|
||||
onStreamEnd,
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & {
|
||||
active?: boolean
|
||||
processing?: boolean
|
||||
deviceId?: string
|
||||
barWidth?: number
|
||||
barHeight?: number
|
||||
barGap?: number
|
||||
barRadius?: number
|
||||
barColor?: string
|
||||
fadeEdges?: boolean
|
||||
fadeWidth?: number
|
||||
height?: string | number
|
||||
sensitivity?: number
|
||||
smoothingTimeConstant?: number
|
||||
fftSize?: number
|
||||
historySize?: number
|
||||
updateRate?: number
|
||||
mode?: "scrolling" | "static"
|
||||
onError?: (error: Error) => void
|
||||
onStreamReady?: (stream: MediaStream) => void
|
||||
onStreamEnd?: () => void
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const historyRef = useRef<number[]>([])
|
||||
const analyserRef = useRef<AnalyserNode | null>(null)
|
||||
const audioContextRef = useRef<AudioContext | null>(null)
|
||||
const streamRef = useRef<MediaStream | null>(null)
|
||||
const animationRef = useRef<number>(0)
|
||||
const lastUpdateRef = useRef<number>(0)
|
||||
const processingAnimationRef = useRef<number | null>(null)
|
||||
const lastActiveDataRef = useRef<number[]>([])
|
||||
const transitionProgressRef = useRef(0)
|
||||
const staticBarsRef = useRef<number[]>([])
|
||||
const needsRedrawRef = useRef(true)
|
||||
const gradientCacheRef = useRef<CanvasGradient | null>(null)
|
||||
const lastWidthRef = useRef(0)
|
||||
const heightStyle = typeof height === "number" ? `${height}px` : height
|
||||
|
||||
// Handle canvas resizing.
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
const container = containerRef.current
|
||||
if (!canvas || !container) return
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = rect.width * dpr
|
||||
canvas.height = rect.height * dpr
|
||||
canvas.style.width = `${rect.width}px`
|
||||
canvas.style.height = `${rect.height}px`
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (ctx) {
|
||||
ctx.scale(dpr, dpr)
|
||||
}
|
||||
gradientCacheRef.current = null
|
||||
lastWidthRef.current = rect.width
|
||||
needsRedrawRef.current = true
|
||||
})
|
||||
resizeObserver.observe(container)
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (processing && !active) {
|
||||
let time = 0
|
||||
transitionProgressRef.current = 0
|
||||
const animateProcessing = () => {
|
||||
time += 0.03
|
||||
transitionProgressRef.current = Math.min(
|
||||
1,
|
||||
transitionProgressRef.current + 0.02
|
||||
)
|
||||
const processingData = []
|
||||
const barCount = Math.floor(
|
||||
(containerRef.current?.getBoundingClientRect().width || 200) /
|
||||
(barWidth + barGap)
|
||||
)
|
||||
if (mode === "static") {
|
||||
const halfCount = Math.floor(barCount / 2)
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const normalizedPosition = (i - halfCount) / halfCount
|
||||
const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4
|
||||
const wave1 = Math.sin(time * 1.5 + normalizedPosition * 3) * 0.25
|
||||
const wave2 = Math.sin(time * 0.8 - normalizedPosition * 2) * 0.2
|
||||
const wave3 = Math.cos(time * 2 + normalizedPosition) * 0.15
|
||||
const combinedWave = wave1 + wave2 + wave3
|
||||
const processingValue = (0.2 + combinedWave) * centerWeight
|
||||
let finalValue = processingValue
|
||||
if (
|
||||
lastActiveDataRef.current.length > 0 &&
|
||||
transitionProgressRef.current < 1
|
||||
) {
|
||||
const lastDataIndex = Math.min(
|
||||
i,
|
||||
lastActiveDataRef.current.length - 1
|
||||
)
|
||||
const lastValue = lastActiveDataRef.current[lastDataIndex] || 0
|
||||
finalValue =
|
||||
lastValue * (1 - transitionProgressRef.current) +
|
||||
processingValue * transitionProgressRef.current
|
||||
}
|
||||
processingData.push(Math.max(0.05, Math.min(1, finalValue)))
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const normalizedPosition = (i - barCount / 2) / (barCount / 2)
|
||||
const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4
|
||||
const wave1 = Math.sin(time * 1.5 + i * 0.15) * 0.25
|
||||
const wave2 = Math.sin(time * 0.8 - i * 0.1) * 0.2
|
||||
const wave3 = Math.cos(time * 2 + i * 0.05) * 0.15
|
||||
const combinedWave = wave1 + wave2 + wave3
|
||||
const processingValue = (0.2 + combinedWave) * centerWeight
|
||||
let finalValue = processingValue
|
||||
if (
|
||||
lastActiveDataRef.current.length > 0 &&
|
||||
transitionProgressRef.current < 1
|
||||
) {
|
||||
const lastDataIndex = Math.floor(
|
||||
(i / barCount) * lastActiveDataRef.current.length
|
||||
)
|
||||
const lastValue = lastActiveDataRef.current[lastDataIndex] || 0
|
||||
finalValue =
|
||||
lastValue * (1 - transitionProgressRef.current) +
|
||||
processingValue * transitionProgressRef.current
|
||||
}
|
||||
processingData.push(Math.max(0.05, Math.min(1, finalValue)))
|
||||
}
|
||||
}
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = processingData
|
||||
} else {
|
||||
historyRef.current = processingData
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
processingAnimationRef.current =
|
||||
requestAnimationFrame(animateProcessing)
|
||||
}
|
||||
animateProcessing()
|
||||
return () => {
|
||||
if (processingAnimationRef.current) {
|
||||
cancelAnimationFrame(processingAnimationRef.current)
|
||||
}
|
||||
}
|
||||
} else if (!active && !processing) {
|
||||
const hasData =
|
||||
mode === "static"
|
||||
? staticBarsRef.current.length > 0
|
||||
: historyRef.current.length > 0
|
||||
if (hasData) {
|
||||
let fadeProgress = 0
|
||||
const fadeToIdle = () => {
|
||||
fadeProgress += 0.03
|
||||
if (fadeProgress < 1) {
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = staticBarsRef.current.map(
|
||||
(value) => value * (1 - fadeProgress)
|
||||
)
|
||||
} else {
|
||||
historyRef.current = historyRef.current.map(
|
||||
(value) => value * (1 - fadeProgress)
|
||||
)
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
requestAnimationFrame(fadeToIdle)
|
||||
} else {
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = []
|
||||
} else {
|
||||
historyRef.current = []
|
||||
}
|
||||
}
|
||||
}
|
||||
fadeToIdle()
|
||||
}
|
||||
}
|
||||
}, [processing, active, barWidth, barGap, mode])
|
||||
|
||||
// Handle microphone setup and teardown.
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop())
|
||||
streamRef.current = null
|
||||
onStreamEnd?.()
|
||||
}
|
||||
if (
|
||||
audioContextRef.current &&
|
||||
audioContextRef.current.state !== "closed"
|
||||
) {
|
||||
audioContextRef.current.close()
|
||||
audioContextRef.current = null
|
||||
}
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
animationRef.current = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
const setupMicrophone = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: deviceId
|
||||
? {
|
||||
deviceId: { exact: deviceId },
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
}
|
||||
: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
},
|
||||
})
|
||||
streamRef.current = stream
|
||||
onStreamReady?.(stream)
|
||||
const AudioContextConstructor =
|
||||
window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext
|
||||
const audioContext = new AudioContextConstructor()
|
||||
const analyser = audioContext.createAnalyser()
|
||||
analyser.fftSize = fftSize
|
||||
analyser.smoothingTimeConstant = smoothingTimeConstant
|
||||
const source = audioContext.createMediaStreamSource(stream)
|
||||
source.connect(analyser)
|
||||
audioContextRef.current = audioContext
|
||||
analyserRef.current = analyser
|
||||
historyRef.current = []
|
||||
} catch (error) {
|
||||
onError?.(error as Error)
|
||||
}
|
||||
}
|
||||
setupMicrophone()
|
||||
return () => {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop())
|
||||
streamRef.current = null
|
||||
onStreamEnd?.()
|
||||
}
|
||||
if (
|
||||
audioContextRef.current &&
|
||||
audioContextRef.current.state !== "closed"
|
||||
) {
|
||||
audioContextRef.current.close()
|
||||
audioContextRef.current = null
|
||||
}
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
animationRef.current = 0
|
||||
}
|
||||
}
|
||||
}, [
|
||||
active,
|
||||
deviceId,
|
||||
fftSize,
|
||||
smoothingTimeConstant,
|
||||
onError,
|
||||
onStreamReady,
|
||||
onStreamEnd,
|
||||
])
|
||||
|
||||
// Animation loop.
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return
|
||||
let rafId: number
|
||||
const animate = (currentTime: number) => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
if (active && currentTime - lastUpdateRef.current > updateRate) {
|
||||
lastUpdateRef.current = currentTime
|
||||
if (analyserRef.current) {
|
||||
const dataArray = new Uint8Array(
|
||||
analyserRef.current.frequencyBinCount
|
||||
)
|
||||
analyserRef.current.getByteFrequencyData(dataArray)
|
||||
if (mode === "static") {
|
||||
const startFreq = Math.floor(dataArray.length * 0.05)
|
||||
const endFreq = Math.floor(dataArray.length * 0.4)
|
||||
const relevantData = dataArray.slice(startFreq, endFreq)
|
||||
const barCount = Math.floor(rect.width / (barWidth + barGap))
|
||||
const halfCount = Math.floor(barCount / 2)
|
||||
const newBars: number[] = []
|
||||
for (let i = halfCount - 1; i >= 0; i--) {
|
||||
const dataIndex = Math.floor(
|
||||
(i / halfCount) * relevantData.length
|
||||
)
|
||||
const value = Math.min(
|
||||
1,
|
||||
(relevantData[dataIndex] / 255) * sensitivity
|
||||
)
|
||||
newBars.push(Math.max(0.05, value))
|
||||
}
|
||||
for (let i = 0; i < halfCount; i++) {
|
||||
const dataIndex = Math.floor(
|
||||
(i / halfCount) * relevantData.length
|
||||
)
|
||||
const value = Math.min(
|
||||
1,
|
||||
(relevantData[dataIndex] / 255) * sensitivity
|
||||
)
|
||||
newBars.push(Math.max(0.05, value))
|
||||
}
|
||||
staticBarsRef.current = newBars
|
||||
lastActiveDataRef.current = newBars
|
||||
} else {
|
||||
let sum = 0
|
||||
const startFreq = Math.floor(dataArray.length * 0.05)
|
||||
const endFreq = Math.floor(dataArray.length * 0.4)
|
||||
const relevantData = dataArray.slice(startFreq, endFreq)
|
||||
for (let i = 0; i < relevantData.length; i++) {
|
||||
sum += relevantData[i]
|
||||
}
|
||||
const average = (sum / relevantData.length / 255) * sensitivity
|
||||
historyRef.current.push(Math.min(1, Math.max(0.05, average)))
|
||||
lastActiveDataRef.current = [...historyRef.current]
|
||||
if (historyRef.current.length > historySize) {
|
||||
historyRef.current.shift()
|
||||
}
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
}
|
||||
}
|
||||
if (!needsRedrawRef.current && !active) {
|
||||
rafId = requestAnimationFrame(animate)
|
||||
return
|
||||
}
|
||||
needsRedrawRef.current = active
|
||||
ctx.clearRect(0, 0, rect.width, rect.height)
|
||||
const computedBarColor =
|
||||
barColor ||
|
||||
(() => {
|
||||
const style = getComputedStyle(canvas)
|
||||
const color = style.color
|
||||
return color || "#000"
|
||||
})()
|
||||
const step = barWidth + barGap
|
||||
const barCount = Math.floor(rect.width / step)
|
||||
const centerY = rect.height / 2
|
||||
if (mode === "static") {
|
||||
const dataToRender = processing
|
||||
? staticBarsRef.current
|
||||
: active
|
||||
? staticBarsRef.current
|
||||
: staticBarsRef.current.length > 0
|
||||
? staticBarsRef.current
|
||||
: []
|
||||
for (let i = 0; i < barCount && i < dataToRender.length; i++) {
|
||||
const value = dataToRender[i] || 0.1
|
||||
const x = i * step
|
||||
const bh = Math.max(baseBarHeight, value * rect.height * 0.8)
|
||||
const y = centerY - bh / 2
|
||||
ctx.fillStyle = computedBarColor
|
||||
ctx.globalAlpha = 0.4 + value * 0.6
|
||||
if (barRadius > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, barWidth, bh, barRadius)
|
||||
ctx.fill()
|
||||
} else {
|
||||
ctx.fillRect(x, y, barWidth, bh)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < barCount && i < historyRef.current.length; i++) {
|
||||
const dataIndex = historyRef.current.length - 1 - i
|
||||
const value = historyRef.current[dataIndex] || 0.1
|
||||
const x = rect.width - (i + 1) * step
|
||||
const bh = Math.max(baseBarHeight, value * rect.height * 0.8)
|
||||
const y = centerY - bh / 2
|
||||
ctx.fillStyle = computedBarColor
|
||||
ctx.globalAlpha = 0.4 + value * 0.6
|
||||
if (barRadius > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, barWidth, bh, barRadius)
|
||||
ctx.fill()
|
||||
} else {
|
||||
ctx.fillRect(x, y, barWidth, bh)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fadeEdges && fadeWidth > 0 && rect.width > 0) {
|
||||
if (!gradientCacheRef.current || lastWidthRef.current !== rect.width) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, rect.width, 0)
|
||||
const fadePercent = Math.min(0.3, fadeWidth / rect.width)
|
||||
gradient.addColorStop(0, "rgba(255,255,255,1)")
|
||||
gradient.addColorStop(fadePercent, "rgba(255,255,255,0)")
|
||||
gradient.addColorStop(1 - fadePercent, "rgba(255,255,255,0)")
|
||||
gradient.addColorStop(1, "rgba(255,255,255,1)")
|
||||
gradientCacheRef.current = gradient
|
||||
lastWidthRef.current = rect.width
|
||||
}
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = gradientCacheRef.current
|
||||
ctx.fillRect(0, 0, rect.width, rect.height)
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
}
|
||||
ctx.globalAlpha = 1
|
||||
rafId = requestAnimationFrame(animate)
|
||||
}
|
||||
rafId = requestAnimationFrame(animate)
|
||||
return () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
active,
|
||||
processing,
|
||||
sensitivity,
|
||||
updateRate,
|
||||
historySize,
|
||||
barWidth,
|
||||
baseBarHeight,
|
||||
barGap,
|
||||
barRadius,
|
||||
barColor,
|
||||
fadeEdges,
|
||||
fadeWidth,
|
||||
mode,
|
||||
])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative h-full w-full", className)}
|
||||
ref={containerRef}
|
||||
style={{ height: heightStyle }}
|
||||
aria-label={
|
||||
active
|
||||
? "Live audio waveform"
|
||||
: processing
|
||||
? "Processing audio"
|
||||
: "Audio waveform idle"
|
||||
}
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
{!active && !processing && (
|
||||
<div className="absolute top-1/2 right-0 left-0 -translate-y-1/2 border-t-2 border-dotted border-muted-foreground/20" />
|
||||
)}
|
||||
<canvas
|
||||
className="block h-full w-full"
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LiveWaveformCard() {
|
||||
const [active, setActive] = useState(false)
|
||||
const [processing, setProcessing] = useState(true)
|
||||
const [mode, setMode] = useState<"static" | "scrolling">("static")
|
||||
|
||||
const handleToggleActive = () => {
|
||||
setActive(!active)
|
||||
if (!active) {
|
||||
setProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleProcessing = () => {
|
||||
setProcessing(!processing)
|
||||
if (!processing) {
|
||||
setActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Live Audio Waveform</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time microphone input visualization with audio reactivity
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LiveWaveform
|
||||
active={active}
|
||||
processing={processing}
|
||||
height={80}
|
||||
barWidth={3}
|
||||
barGap={2}
|
||||
mode={mode}
|
||||
fadeEdges={true}
|
||||
barColor="gray"
|
||||
historySize={120}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={active ? "default" : "outline"}
|
||||
onClick={handleToggleActive}
|
||||
>
|
||||
{active ? "Stop" : "Start"} Listening
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={processing ? "default" : "outline"}
|
||||
onClick={handleToggleProcessing}
|
||||
>
|
||||
{processing ? "Stop" : "Start"} Processing
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setMode(mode === "static" ? "scrolling" : "static")}
|
||||
>
|
||||
{mode === "static" ? "Static" : "Scrolling"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/radix/ui/avatar"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
|
||||
export function NoTeamMembers() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-56 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<AvatarGroup className="grayscale">
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/shadcn.png"
|
||||
alt="@shadcn"
|
||||
/>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Invite your team to collaborate on this project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button size="sm">Invite Members</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import { Kbd } from "@/registry/bases/radix/ui/kbd"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-72">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching
|
||||
for what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Button variant="link">Go to homepage</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function ObservabilityCard() {
|
||||
return (
|
||||
<Card className="relative w-full max-w-md overflow-hidden pt-0">
|
||||
<div className="absolute inset-0 z-30 aspect-video bg-primary opacity-50 mix-blend-color" />
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1604076850742-4c7221f3101b?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
alt="Photo by mymind on Unsplash"
|
||||
title="Photo by mymind on Unsplash"
|
||||
className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale"
|
||||
/>
|
||||
<CardHeader>
|
||||
<CardTitle>Observability Plus is replacing Monitoring</CardTitle>
|
||||
<CardDescription>
|
||||
Switch to the improved way to explore your data, with natural
|
||||
language. Monitoring will no longer be available on the Pro plan in
|
||||
November, 2025
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button>
|
||||
Create Query{" "}
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
data-icon="inline-end"
|
||||
/>
|
||||
</Button>
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
Warning
|
||||
</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client"
|
||||
|
||||
import { Label, Pie, PieChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import { Progress } from "@/registry/bases/radix/ui/progress"
|
||||
|
||||
const pieChartData = [
|
||||
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
|
||||
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
|
||||
{ browser: "firefox", visitors: 287, fill: "var(--color-firefox)" },
|
||||
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
|
||||
]
|
||||
|
||||
const pieChartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
chrome: {
|
||||
label: "Chrome",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
safari: {
|
||||
label: "Safari",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
firefox: {
|
||||
label: "Firefox",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
edge: {
|
||||
label: "Edge",
|
||||
color: "var(--chart-4)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function PieChartCard() {
|
||||
const totalVisitors = pieChartData.reduce(
|
||||
(sum, item) => sum + item.visitors,
|
||||
0
|
||||
)
|
||||
const topBrowser = pieChartData.reduce((max, item) =>
|
||||
item.visitors > max.visitors ? item : max
|
||||
)
|
||||
const topBrowserShare = Math.round(
|
||||
(topBrowser.visitors / totalVisitors) * 100
|
||||
)
|
||||
const topBrowserLabel =
|
||||
pieChartConfig[topBrowser.browser as keyof typeof pieChartConfig]?.label ??
|
||||
"Top"
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle>Browser Share</CardTitle>
|
||||
<CardDescription>January - June 2026</CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant="outline">{topBrowserLabel}</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<ChartContainer
|
||||
config={pieChartConfig}
|
||||
className="mx-auto aspect-square max-h-[190px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={pieChartData}
|
||||
dataKey="visitors"
|
||||
nameKey="browser"
|
||||
innerRadius={50}
|
||||
strokeWidth={5}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="red"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy - 16}
|
||||
className="fill-foreground text-2xl font-bold"
|
||||
>
|
||||
{totalVisitors.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 4}
|
||||
className="fill-muted-foreground text-xs"
|
||||
>
|
||||
Visitors
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
<ChartLegend
|
||||
content={<ChartLegendContent nameKey="browser" />}
|
||||
className="translate-y-2"
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-stretch gap-2">
|
||||
<div className="flex items-center text-xs">
|
||||
<span className="font-medium">{topBrowserLabel}</span>
|
||||
<span className="ml-auto text-muted-foreground tabular-nums">
|
||||
{topBrowserShare}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={topBrowserShare}
|
||||
className="**:data-[slot=progress-indicator]:bg-chart-3"
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
|
||||
export function ReportBug() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Report Bug</CardTitle>
|
||||
<CardDescription>Help us fix issues faster.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-title">Title</FieldLabel>
|
||||
<Input
|
||||
id="bug-title"
|
||||
placeholder="Brief description of the issue"
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-severity">Severity</FieldLabel>
|
||||
<Select defaultValue="medium">
|
||||
<SelectTrigger id="bug-severity" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="critical">Critical</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-component">Component</FieldLabel>
|
||||
<Select defaultValue="dashboard">
|
||||
<SelectTrigger id="bug-component" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="dashboard">Dashboard</SelectItem>
|
||||
<SelectItem value="auth">Auth</SelectItem>
|
||||
<SelectItem value="api">API</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-steps">Steps to reproduce</FieldLabel>
|
||||
<Textarea
|
||||
id="bug-steps"
|
||||
placeholder="1. Go to 2. Click on 3. Observe..."
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="justify-end style-sera:justify-center"
|
||||
>
|
||||
<Button variant="outline" className="style-sera:flex-1">
|
||||
Attach File
|
||||
</Button>
|
||||
<Button className="style-sera:flex-1">Submit Bug</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/radix/ui/checkbox"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
|
||||
export function ShippingAddress() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Shipping Address</CardTitle>
|
||||
<CardDescription>Where should we deliver?</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-street">Street address</FieldLabel>
|
||||
<Input id="shipping-street" placeholder="123 Main Street" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-apt">Apt / Suite</FieldLabel>
|
||||
<Input id="shipping-apt" placeholder="Apt 4B" />
|
||||
</Field>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-city">City</FieldLabel>
|
||||
<Input id="shipping-city" placeholder="San Francisco" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-state">State</FieldLabel>
|
||||
<Select defaultValue="CA">
|
||||
<SelectTrigger id="shipping-state" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="CA">California</SelectItem>
|
||||
<SelectItem value="NY">New York</SelectItem>
|
||||
<SelectItem value="TX">Texas</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-zip">ZIP Code</FieldLabel>
|
||||
<Input id="shipping-zip" placeholder="94102" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-country">Country</FieldLabel>
|
||||
<Select defaultValue="US">
|
||||
<SelectTrigger id="shipping-country" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="US">United States</SelectItem>
|
||||
<SelectItem value="CA">Canada</SelectItem>
|
||||
<SelectItem value="UK">United Kingdom</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="shipping-save" defaultChecked />
|
||||
<FieldLabel htmlFor="shipping-save" className="font-normal">
|
||||
Save as default address
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" className="ml-auto">
|
||||
Save Address
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemGroup,
|
||||
ItemHeader,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { Kbd } from "@/registry/bases/radix/ui/kbd"
|
||||
|
||||
const shortcuts = [
|
||||
{ label: "Search", keys: ["⌘", "K"] },
|
||||
{ label: "Quick Actions", keys: ["⌘", "J"] },
|
||||
{ label: "New File", keys: ["⌘", "N"] },
|
||||
{ label: "Save", keys: ["⌘", "S"] },
|
||||
{ label: "Toggle Sidebar", keys: ["⌘", "B"] },
|
||||
] as const
|
||||
|
||||
export function Shortcuts() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-sm font-medium">Shortcuts</div>
|
||||
<ItemGroup className="gap-2 text-muted-foreground" data-size="xs">
|
||||
{shortcuts.map(({ label, keys }, i) => (
|
||||
<React.Fragment key={label}>
|
||||
{i > 0 && <ItemSeparator />}
|
||||
<Item
|
||||
variant="default"
|
||||
size="xs"
|
||||
className="border-0 px-0 py-0"
|
||||
>
|
||||
<ItemHeader>
|
||||
<ItemTitle className="font-normal">{label}</ItemTitle>
|
||||
<ItemActions>
|
||||
<div className="flex gap-1">
|
||||
{keys.map((key) => (
|
||||
<Kbd key={key}>{key}</Kbd>
|
||||
))}
|
||||
</div>
|
||||
</ItemActions>
|
||||
</ItemHeader>
|
||||
</Item>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import { Skeleton } from "@/registry/bases/radix/ui/skeleton"
|
||||
|
||||
export function SkeletonLoading() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="size-10 rounded-full" />
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-4/5" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-8 w-20" />
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
|
||||
const sleepChartData = [
|
||||
{ hour: "10pm", deep: 0, light: 30, rem: 0 },
|
||||
{ hour: "11pm", deep: 20, light: 10, rem: 0 },
|
||||
{ hour: "12am", deep: 40, light: 0, rem: 10 },
|
||||
{ hour: "1am", deep: 30, light: 5, rem: 15 },
|
||||
{ hour: "2am", deep: 10, light: 20, rem: 30 },
|
||||
{ hour: "3am", deep: 25, light: 10, rem: 20 },
|
||||
{ hour: "4am", deep: 15, light: 25, rem: 10 },
|
||||
{ hour: "5am", deep: 5, light: 35, rem: 15 },
|
||||
{ hour: "6am", deep: 0, light: 20, rem: 25 },
|
||||
]
|
||||
|
||||
const sleepChartConfig = {
|
||||
deep: {
|
||||
label: "Deep",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
light: {
|
||||
label: "Light",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
rem: {
|
||||
label: "REM",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function SleepReport() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Sleep Report</CardTitle>
|
||||
<CardDescription>Last night · 7h 24m</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<ChartContainer config={sleepChartConfig} className="h-32 w-full">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={sleepChartData}
|
||||
margin={{ left: 0, right: 0, top: 0, bottom: 0 }}
|
||||
barSize={16}
|
||||
>
|
||||
<Bar
|
||||
dataKey="deep"
|
||||
stackId="a"
|
||||
fill="var(--color-deep)"
|
||||
radius={0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="light"
|
||||
stackId="a"
|
||||
fill="var(--color-light)"
|
||||
radius={0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="rem"
|
||||
stackId="a"
|
||||
fill="var(--color-rem)"
|
||||
radius={[2, 2, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ label: "Deep", value: "2h 10m" },
|
||||
{ label: "Light", value: "3h 48m" },
|
||||
{ label: "REM", value: "1h 26m" },
|
||||
{ label: "Score", value: "84" },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="text-center">
|
||||
<div className="text-sm font-medium tabular-nums">{s.value}</div>
|
||||
<div className="text-xs text-muted-foreground">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Badge variant="outline">Good</Badge>
|
||||
<Button variant="outline" size="sm" className="ml-auto">
|
||||
Details
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import { STYLES } from "@/registry/styles"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function StyleOverview() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.font),
|
||||
[params.font]
|
||||
)
|
||||
|
||||
const currentFontHeading = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.fontHeading),
|
||||
[params.fontHeading]
|
||||
)
|
||||
|
||||
const currentStyle = React.useMemo(
|
||||
() => STYLES.find((style) => style.name === params.style),
|
||||
[params.style]
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-6 style-lyra:gap-4 style-mira:gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="cn-font-heading text-2xl font-medium style-lyra:text-lg style-mira:text-lg style-sera:text-lg style-sera:font-semibold style-sera:tracking-wide style-sera:uppercase">
|
||||
{currentStyle?.title} -{" "}
|
||||
{currentFontHeading?.name &&
|
||||
currentFontHeading.name !== currentFont?.name
|
||||
? currentFontHeading.name
|
||||
: currentFont?.name}
|
||||
</div>
|
||||
<div className="line-clamp-2 text-base text-muted-foreground style-lyra:text-sm style-mira:text-sm style-sera:text-sm style-sera:leading-relaxed">
|
||||
Designers love packing quirky glyphs into test phrases. This is a
|
||||
preview of the typography styles.
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
{[
|
||||
"--background",
|
||||
"--foreground",
|
||||
"--primary",
|
||||
"--secondary",
|
||||
"--muted",
|
||||
"--accent",
|
||||
"--border",
|
||||
"--chart-1",
|
||||
"--chart-2",
|
||||
"--chart-3",
|
||||
"--chart-4",
|
||||
"--chart-5",
|
||||
].map((variant) => (
|
||||
<div
|
||||
key={variant}
|
||||
className="flex flex-col flex-wrap items-center gap-2"
|
||||
>
|
||||
<div
|
||||
className="relative aspect-square w-full rounded-lg bg-(--color) after:absolute after:inset-0 after:rounded-lg after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten style-sera:rounded-none style-sera:after:rounded-none"
|
||||
style={
|
||||
{
|
||||
"--color": `var(${variant})`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
<div className="hidden max-w-14 truncate font-mono text-[0.60rem] md:block style-lyra:max-w-10 style-mira:max-w-10">
|
||||
{variant}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/bases/radix/ui/dialog"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function TypographySpecimen() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.font),
|
||||
[params.font]
|
||||
)
|
||||
|
||||
const currentFontHeading = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.fontHeading),
|
||||
[params.fontHeading]
|
||||
)
|
||||
const headingLabel =
|
||||
currentFontHeading?.name && currentFontHeading.name !== currentFont?.name
|
||||
? currentFontHeading.name
|
||||
: "Inherit"
|
||||
const bodyLabel = currentFont?.name ?? "Default"
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase">
|
||||
{headingLabel} - {bodyLabel}
|
||||
</div>
|
||||
<p className="cn-font-heading text-2xl font-medium style-sera:text-lg style-sera:font-semibold style-sera:tracking-wider style-sera:uppercase">
|
||||
Designing with rhythm and hierarchy.
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
A strong body style keeps long-form content readable and balances the
|
||||
visual weight of headings.
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
Thoughtful spacing and cadence help paragraphs scan quickly without
|
||||
feeling dense.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-full">
|
||||
Share Feedback
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share Feedback</DialogTitle>
|
||||
<DialogDescription>
|
||||
Let us know how we can improve your experience.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-name">Name</FieldLabel>
|
||||
<Input id="feedback-name" placeholder="Your name" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-email">Email</FieldLabel>
|
||||
<Input
|
||||
id="feedback-email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-category">Category</FieldLabel>
|
||||
<Select defaultValue="general">
|
||||
<SelectTrigger id="feedback-category" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="general">General</SelectItem>
|
||||
<SelectItem value="bug">Bug Report</SelectItem>
|
||||
<SelectItem value="feature">Feature Request</SelectItem>
|
||||
<SelectItem value="improvement">Improvement</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-message">Message</FieldLabel>
|
||||
<Textarea
|
||||
id="feedback-message"
|
||||
placeholder="Tell us what's on your mind..."
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>Submit</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user