mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-22 04:05:48 +00:00
Compare commits
81 Commits
shadcn@4.1
...
shadcn@4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d718a8045f | ||
|
|
2c4678c8c8 | ||
|
|
2466a300f4 | ||
|
|
66fcf1e853 | ||
|
|
5ebd54198d | ||
|
|
3a2d812510 | ||
|
|
7811557088 | ||
|
|
575f1602a1 | ||
|
|
ae70ecc2f3 | ||
|
|
42284f4e64 | ||
|
|
abc65a4871 | ||
|
|
7d5af61468 | ||
|
|
2badcdc31f | ||
|
|
64b8263450 | ||
|
|
13b4593f37 | ||
|
|
7dc65da6b2 | ||
|
|
98e56b773c | ||
|
|
7ff9778ff0 | ||
|
|
4af7bbf4ba | ||
|
|
f00a94d9e5 | ||
|
|
187ae44fa7 | ||
|
|
034178bf7d | ||
|
|
4064c78bc7 | ||
|
|
943b023b7c | ||
|
|
e3d654fd26 | ||
|
|
71d0470be1 | ||
|
|
53bbdc738f | ||
|
|
97707ec08e | ||
|
|
b9ce2f10c3 | ||
|
|
7cb3b13a33 | ||
|
|
e3d2b14911 | ||
|
|
58c9dc2a7e | ||
|
|
3bdf60340d | ||
|
|
c1e29824cd | ||
|
|
62f6df75f2 | ||
|
|
62bae86e86 | ||
|
|
aa69fbf85a | ||
|
|
8d41295f2c | ||
|
|
2b053d916d | ||
|
|
0d1309f322 | ||
|
|
c26250dcfe | ||
|
|
07c5c36be8 | ||
|
|
21c9cc5246 | ||
|
|
058960046a | ||
|
|
be80c18ea9 | ||
|
|
3c59a0cd95 | ||
|
|
26d0228ee9 | ||
|
|
9050646893 | ||
|
|
3ca09b9647 | ||
|
|
720ccca653 | ||
|
|
1e3dff8daa | ||
|
|
c116b325ab | ||
|
|
5b266d3fc9 | ||
|
|
6095e6272d | ||
|
|
f3fc5a62f2 | ||
|
|
ef7507cc9a | ||
|
|
16b7bea50d | ||
|
|
ccc4caad9c | ||
|
|
ba2c4fc586 | ||
|
|
bb5afb2df1 | ||
|
|
53f45f5f6f | ||
|
|
990040691c | ||
|
|
83857679cb | ||
|
|
61989da8ec | ||
|
|
768d8a808f | ||
|
|
95479a06bb | ||
|
|
4289d5fe02 | ||
|
|
5a6702845d | ||
|
|
ebf2192d98 | ||
|
|
44c09a19b0 | ||
|
|
4101ec98af | ||
|
|
a7c3300d7a | ||
|
|
b50acc9d21 | ||
|
|
fc76a9ada2 | ||
|
|
d6b4bf8ddc | ||
|
|
2c334c3c2d | ||
|
|
d3de6aa760 | ||
|
|
23b2ac4dcf | ||
|
|
e56c476105 | ||
|
|
0c25e712e1 | ||
|
|
4f421aba65 |
@@ -63,11 +63,7 @@ export default function IndexPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageNav className="hidden md:flex">
|
<div className="container-wrapper flex-1 pb-6">
|
||||||
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
|
||||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
|
||||||
</PageNav>
|
|
||||||
<div className="container-wrapper flex-1 section-soft pb-6">
|
|
||||||
<div className="container overflow-hidden">
|
<div className="container overflow-hidden">
|
||||||
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
|
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function MenuAccentPicker({
|
export function MenuAccentPicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/styles/base-nova/ui/command"
|
} from "@/styles/base-nova/ui/command"
|
||||||
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
|
import { useActionMenu } from "@/app/(app)/create/hooks/use-action-menu"
|
||||||
|
|
||||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { useMounted } from "@/hooks/use-mounted"
|
import { useMounted } from "@/hooks/use-mounted"
|
||||||
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
|
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function BaseColorPicker({
|
export function BaseColorPicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function BasePicker({
|
export function BasePicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
getThemesForBaseColor,
|
getThemesForBaseColor,
|
||||||
type ChartColorName,
|
type ChartColorName,
|
||||||
} from "@/registry/config"
|
} from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function ChartColorPicker({
|
export function ChartColorPicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -5,7 +5,7 @@ import * as React from "react"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { Button } from "@/styles/base-nova/ui/button"
|
||||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||||
|
|
||||||
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||||
const presetCode = usePresetCode()
|
const presetCode = usePresetCode()
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import dynamic from "next/dynamic"
|
||||||
import { type RegistryItem } from "shadcn/schema"
|
import { type RegistryItem } from "shadcn/schema"
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
@@ -12,24 +13,31 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
} from "@/styles/base-nova/ui/card"
|
} from "@/styles/base-nova/ui/card"
|
||||||
import { FieldGroup, FieldSeparator } from "@/styles/base-nova/ui/field"
|
import { FieldGroup, FieldSeparator } from "@/styles/base-nova/ui/field"
|
||||||
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
|
import { MenuAccentPicker } from "@/app/(app)/create/components/accent-picker"
|
||||||
import { ActionMenu } from "@/app/(create)/components/action-menu"
|
import { ActionMenu } from "@/app/(app)/create/components/action-menu"
|
||||||
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
|
import { BaseColorPicker } from "@/app/(app)/create/components/base-color-picker"
|
||||||
import { BasePicker } from "@/app/(create)/components/base-picker"
|
import { BasePicker } from "@/app/(app)/create/components/base-picker"
|
||||||
import { ChartColorPicker } from "@/app/(create)/components/chart-color-picker"
|
import { ChartColorPicker } from "@/app/(app)/create/components/chart-color-picker"
|
||||||
import { CopyPreset } from "@/app/(create)/components/copy-preset"
|
import { CopyPreset } from "@/app/(app)/create/components/copy-preset"
|
||||||
import { FontPicker } from "@/app/(create)/components/font-picker"
|
import { FontPicker } from "@/app/(app)/create/components/font-picker"
|
||||||
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
|
import { IconLibraryPicker } from "@/app/(app)/create/components/icon-library-picker"
|
||||||
import { MainMenu } from "@/app/(create)/components/main-menu"
|
import { MainMenu } from "@/app/(app)/create/components/main-menu"
|
||||||
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
|
import { MenuColorPicker } from "@/app/(app)/create/components/menu-picker"
|
||||||
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
|
import { RadiusPicker } from "@/app/(app)/create/components/radius-picker"
|
||||||
import { RandomButton } from "@/app/(create)/components/random-button"
|
import { RandomButton } from "@/app/(app)/create/components/random-button"
|
||||||
import { ResetDialog } from "@/app/(create)/components/reset-button"
|
import { ResetDialog } from "@/app/(app)/create/components/reset-button"
|
||||||
import { StylePicker } from "@/app/(create)/components/style-picker"
|
import { StylePicker } from "@/app/(app)/create/components/style-picker"
|
||||||
import { ThemePicker } from "@/app/(create)/components/theme-picker"
|
import { ThemePicker } from "@/app/(app)/create/components/theme-picker"
|
||||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
import { V0Button } from "@/app/(app)/create/components/v0-button"
|
||||||
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
|
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/create/lib/fonts"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
|
// Only visible when user clicks "Create Project".
|
||||||
|
const ProjectForm = dynamic(() =>
|
||||||
|
import("@/app/(app)/create/components/project-form").then(
|
||||||
|
(m) => m.ProjectForm
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export function Customizer({
|
export function Customizer({
|
||||||
itemsByBase,
|
itemsByBase,
|
||||||
@@ -56,7 +64,6 @@ export function Customizer({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden md:overflow-y-auto">
|
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden md:overflow-y-auto">
|
||||||
<FieldGroup className="flex-row gap-2.5 py-px **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25">
|
<FieldGroup className="flex-row gap-2.5 py-px **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25">
|
||||||
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
|
|
||||||
<StylePicker
|
<StylePicker
|
||||||
styles={STYLES}
|
styles={STYLES}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
@@ -91,14 +98,18 @@ export function Customizer({
|
|||||||
<FieldSeparator className="hidden md:block" />
|
<FieldSeparator className="hidden md:block" />
|
||||||
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
|
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:**:[button,a]:w-full">
|
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:rounded-b-none md:**:[button,a]:w-full">
|
||||||
<CopyPreset className="flex-1 md:flex-none" />
|
<CopyPreset className="flex-1 md:flex-none" />
|
||||||
<RandomButton className="flex-1 md:flex-none" />
|
<RandomButton className="flex-1 md:flex-none" />
|
||||||
<ActionMenu itemsByBase={itemsByBase} />
|
<ActionMenu itemsByBase={itemsByBase} />
|
||||||
<ResetDialog />
|
<ResetDialog />
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
<CardFooter className="-mt-3 hidden min-w-0 gap-2 md:flex md:flex-col md:**:[button,a]:w-full">
|
||||||
|
<ProjectForm />
|
||||||
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
type DesignSystemConfig,
|
type DesignSystemConfig,
|
||||||
} from "@/registry/config"
|
} from "@/registry/config"
|
||||||
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
|
import { useIframeMessageListener } from "@/app/(app)/create/hooks/use-iframe-sync"
|
||||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
||||||
import {
|
import {
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
type DesignSystemSearchParams,
|
type DesignSystemSearchParams,
|
||||||
} from "@/app/(create)/lib/search-params"
|
} from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
|
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
|
||||||
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
|
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -12,12 +12,12 @@ import {
|
|||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
||||||
import {
|
import {
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
type DesignSystemSearchParams,
|
type DesignSystemSearchParams,
|
||||||
} from "@/app/(create)/lib/search-params"
|
} from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
type FontPickerOption = {
|
type FontPickerOption = {
|
||||||
name: string
|
name: string
|
||||||
@@ -97,7 +97,7 @@ export function FontPicker({
|
|||||||
<PickerTrigger>
|
<PickerTrigger>
|
||||||
<div className="flex flex-col justify-start text-left">
|
<div className="flex flex-col justify-start text-left">
|
||||||
<div className="text-xs text-muted-foreground">{label}</div>
|
<div className="text-xs text-muted-foreground">{label}</div>
|
||||||
<div className="text-sm font-medium text-foreground">
|
<div className="line-clamp-1 max-w-[80%] truncate text-sm font-medium text-foreground">
|
||||||
{displayFontName}
|
{displayFontName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -5,7 +5,7 @@ import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
|
|||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
import { HugeiconsIcon } from "@hugeicons/react"
|
||||||
|
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { Button } from "@/styles/base-nova/ui/button"
|
||||||
import { useHistory } from "@/app/(create)/hooks/use-history"
|
import { useHistory } from "@/app/(app)/create/hooks/use-history"
|
||||||
|
|
||||||
export const UNDO_FORWARD_TYPE = "undo-forward"
|
export const UNDO_FORWARD_TYPE = "undo-forward"
|
||||||
export const REDO_FORWARD_TYPE = "redo-forward"
|
export const REDO_FORWARD_TYPE = "redo-forward"
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { iconLibraries, type IconLibraryName } from "@/registry/config"
|
import { iconLibraries, type IconLibraryName } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
const logos = {
|
const logos = {
|
||||||
lucide: (
|
lucide: (
|
||||||
75
apps/v4/app/(app)/create/components/icon-placeholder.tsx
Normal file
75
apps/v4/app/(app)/create/components/icon-placeholder.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { lazy, Suspense } from "react"
|
||||||
|
import { SquareIcon } from "lucide-react"
|
||||||
|
import type { IconLibraryName } from "shadcn/icons"
|
||||||
|
|
||||||
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
|
const IconLucide = lazy(() =>
|
||||||
|
import("@/registry/icons/icon-lucide").then((mod) => ({
|
||||||
|
default: mod.IconLucide,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const IconTabler = lazy(() =>
|
||||||
|
import("@/registry/icons/icon-tabler").then((mod) => ({
|
||||||
|
default: mod.IconTabler,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const IconHugeicons = lazy(() =>
|
||||||
|
import("@/registry/icons/icon-hugeicons").then((mod) => ({
|
||||||
|
default: mod.IconHugeicons,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const IconPhosphor = lazy(() =>
|
||||||
|
import("@/registry/icons/icon-phosphor").then((mod) => ({
|
||||||
|
default: mod.IconPhosphor,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const IconRemixicon = lazy(() =>
|
||||||
|
import("@/registry/icons/icon-remixicon").then((mod) => ({
|
||||||
|
default: mod.IconRemixicon,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Preload all icon renderer modules so switching libraries is instant.
|
||||||
|
// These warm the browser module cache; React.lazy resolves immediately
|
||||||
|
// for modules that are already loaded.
|
||||||
|
void import("@/registry/icons/icon-lucide")
|
||||||
|
void import("@/registry/icons/icon-tabler")
|
||||||
|
void import("@/registry/icons/icon-hugeicons")
|
||||||
|
void import("@/registry/icons/icon-phosphor")
|
||||||
|
void import("@/registry/icons/icon-remixicon")
|
||||||
|
|
||||||
|
export function IconPlaceholder({
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
[K in IconLibraryName]: string
|
||||||
|
} & React.ComponentProps<"svg">) {
|
||||||
|
const [{ iconLibrary }] = useDesignSystemSearchParams()
|
||||||
|
const iconName = props[iconLibrary]
|
||||||
|
|
||||||
|
if (!iconName) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<SquareIcon {...props} />}>
|
||||||
|
{iconLibrary === "lucide" && <IconLucide name={iconName} {...props} />}
|
||||||
|
{iconLibrary === "tabler" && <IconTabler name={iconName} {...props} />}
|
||||||
|
{iconLibrary === "hugeicons" && (
|
||||||
|
<IconHugeicons name={iconName} {...props} />
|
||||||
|
)}
|
||||||
|
{iconLibrary === "phosphor" && (
|
||||||
|
<IconPhosphor name={iconName} {...props} />
|
||||||
|
)}
|
||||||
|
{iconLibrary === "remixicon" && (
|
||||||
|
<IconRemixicon name={iconName} {...props} />
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/styles/base-nova/ui/sidebar"
|
} from "@/styles/base-nova/ui/sidebar"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
|
||||||
|
|
||||||
const cachedGroupedItems = React.cache(
|
const cachedGroupedItems = React.cache(
|
||||||
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
|
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
|
||||||
@@ -7,7 +7,10 @@ import {
|
|||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
import { HugeiconsIcon } from "@hugeicons/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useLocks, type LockableParam } from "@/app/(create)/hooks/use-locks"
|
import {
|
||||||
|
useLocks,
|
||||||
|
type LockableParam,
|
||||||
|
} from "@/app/(app)/create/hooks/use-locks"
|
||||||
|
|
||||||
export function LockButton({
|
export function LockButton({
|
||||||
param,
|
param,
|
||||||
@@ -14,12 +14,12 @@ import {
|
|||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerShortcut,
|
PickerShortcut,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useActionMenuTrigger } from "@/app/(create)/hooks/use-action-menu"
|
import { useActionMenuTrigger } from "@/app/(app)/create/hooks/use-action-menu"
|
||||||
import { useHistory } from "@/app/(create)/hooks/use-history"
|
import { useHistory } from "@/app/(app)/create/hooks/use-history"
|
||||||
import { useRandom } from "@/app/(create)/hooks/use-random"
|
import { useRandom } from "@/app/(app)/create/hooks/use-random"
|
||||||
import { useReset } from "@/app/(create)/hooks/use-reset"
|
import { useReset } from "@/app/(app)/create/hooks/use-reset"
|
||||||
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
|
||||||
|
|
||||||
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
|
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import { useTheme } from "next-themes"
|
|||||||
|
|
||||||
import { useMounted } from "@/hooks/use-mounted"
|
import { useMounted } from "@/hooks/use-mounted"
|
||||||
import { type MenuColorValue } from "@/registry/config"
|
import { type MenuColorValue } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -17,11 +17,11 @@ import {
|
|||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import {
|
import {
|
||||||
isTranslucentMenuColor,
|
isTranslucentMenuColor,
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
} from "@/app/(create)/lib/search-params"
|
} from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
type ColorChoice = "default" | "inverted"
|
type ColorChoice = "default" | "inverted"
|
||||||
type SurfaceChoice = "solid" | "translucent"
|
type SurfaceChoice = "solid" | "translucent"
|
||||||
@@ -5,7 +5,7 @@ import Script from "next/script"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { Button } from "@/styles/base-nova/ui/button"
|
||||||
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
|
||||||
|
|
||||||
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
||||||
|
|
||||||
import { cn } from "@/registry/bases/base/lib/utils"
|
import { cn } from "@/registry/bases/base/lib/utils"
|
||||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||||
|
|
||||||
function Picker({ ...props }: MenuPrimitive.Root.Props) {
|
function Picker({ ...props }: MenuPrimitive.Root.Props) {
|
||||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
@@ -19,7 +19,7 @@ function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
|
|||||||
<MenuPrimitive.Trigger
|
<MenuPrimitive.Trigger
|
||||||
data-slot="dropdown-menu-trigger"
|
data-slot="dropdown-menu-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-40 shrink-0 touch-manipulation rounded-xl p-3 ring-1 ring-foreground/10 select-none hover:bg-muted focus-visible:ring-foreground/50 focus-visible:outline-none disabled:opacity-50 data-popup-open:bg-muted md:w-full md:rounded-lg md:px-2.5 md:py-2",
|
"relative w-36 shrink-0 touch-manipulation rounded-xl p-3 ring-1 ring-foreground/10 select-none hover:bg-muted focus-visible:ring-foreground/50 focus-visible:outline-none disabled:opacity-50 data-popup-open:bg-muted md:w-full md:rounded-lg md:px-2.5 md:py-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
|
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
|
||||||
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function PresetHandler() {
|
export function PresetHandler() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function PresetPicker({
|
export function PresetPicker({
|
||||||
presets,
|
presets,
|
||||||
@@ -112,13 +112,6 @@ export function PresetPicker({
|
|||||||
closeOnClick={isMobile}
|
closeOnClick={isMobile}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{style?.icon && (
|
|
||||||
<div className="flex size-4 shrink-0 items-center justify-center">
|
|
||||||
{React.cloneElement(style.icon, {
|
|
||||||
className: "size-4",
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{preset.description}
|
{preset.description}
|
||||||
</div>
|
</div>
|
||||||
</PickerRadioItem>
|
</PickerRadioItem>
|
||||||
37
apps/v4/app/(app)/create/components/preview-switcher.tsx
Normal file
37
apps/v4/app/(app)/create/components/preview-switcher.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||||
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
|
const PREVIEW_ITEMS = [
|
||||||
|
{ label: "01", value: "preview-02" },
|
||||||
|
{ label: "02", value: "preview" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function PreviewSwitcher() {
|
||||||
|
const [params, setParams] = useDesignSystemSearchParams()
|
||||||
|
|
||||||
|
const isPreview =
|
||||||
|
params.item === "preview" || params.item.startsWith("preview-0")
|
||||||
|
|
||||||
|
if (!isPreview) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dark absolute right-3 bottom-3 z-20 flex items-center gap-1 rounded-xl bg-card/90 p-1 shadow-xl backdrop-blur-xl">
|
||||||
|
{PREVIEW_ITEMS.map((item) => (
|
||||||
|
<Button
|
||||||
|
key={item.value}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
data-active={params.item === item.value}
|
||||||
|
className="h-7 min-w-8 cursor-pointer rounded-lg px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
|
||||||
|
onClick={() => setParams({ item: item.value })}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,19 +2,20 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/action-menu"
|
import { CMD_K_FORWARD_TYPE } from "@/app/(app)/create/components/action-menu"
|
||||||
import {
|
import {
|
||||||
REDO_FORWARD_TYPE,
|
REDO_FORWARD_TYPE,
|
||||||
UNDO_FORWARD_TYPE,
|
UNDO_FORWARD_TYPE,
|
||||||
} from "@/app/(create)/components/history-buttons"
|
} from "@/app/(app)/create/components/history-buttons"
|
||||||
import { DARK_MODE_FORWARD_TYPE } from "@/app/(create)/components/mode-switcher"
|
import { DARK_MODE_FORWARD_TYPE } from "@/app/(app)/create/components/mode-switcher"
|
||||||
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
|
import { PreviewSwitcher } from "@/app/(app)/create/components/preview-switcher"
|
||||||
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
|
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(app)/create/components/random-button"
|
||||||
import { RESET_FORWARD_TYPE } from "@/app/(create)/hooks/use-reset"
|
import { sendToIframe } from "@/app/(app)/create/hooks/use-iframe-sync"
|
||||||
|
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
|
||||||
import {
|
import {
|
||||||
serializeDesignSystemSearchParams,
|
serializeDesignSystemSearchParams,
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
} from "@/app/(create)/lib/search-params"
|
} from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
|
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
|
||||||
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
|
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
|
||||||
@@ -147,6 +148,7 @@ export function Preview() {
|
|||||||
title="Preview"
|
title="Preview"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<PreviewSwitcher />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -36,17 +36,17 @@ import {
|
|||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@/styles/base-nova/ui/tabs"
|
} from "@/styles/base-nova/ui/tabs"
|
||||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||||
import {
|
import {
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
type DesignSystemSearchParams,
|
type DesignSystemSearchParams,
|
||||||
} from "@/app/(create)/lib/search-params"
|
} from "@/app/(app)/create/lib/search-params"
|
||||||
import {
|
import {
|
||||||
getFramework,
|
getFramework,
|
||||||
getTemplateValue,
|
getTemplateValue,
|
||||||
NO_MONOREPO_FRAMEWORKS,
|
NO_MONOREPO_FRAMEWORKS,
|
||||||
TEMPLATES,
|
TEMPLATES,
|
||||||
} from "@/app/(create)/lib/templates"
|
} from "@/app/(app)/create/lib/templates"
|
||||||
|
|
||||||
const TURBOREPO_LOGO =
|
const TURBOREPO_LOGO =
|
||||||
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Turborepo</title><path d="M11.9906 4.1957c-4.2998 0-7.7981 3.501-7.7981 7.8043s3.4983 7.8043 7.7981 7.8043c4.2999 0 7.7982-3.501 7.7982-7.8043s-3.4983-7.8043-7.7982-7.8043m0 11.843c-2.229 0-4.0356-1.8079-4.0356-4.0387s1.8065-4.0387 4.0356-4.0387S16.0262 9.7692 16.0262 12s-1.8065 4.0388-4.0356 4.0388m.6534-13.1249V0C18.9726.3386 24 5.5822 24 12s-5.0274 11.66-11.356 12v-2.9139c4.7167-.3372 8.4516-4.2814 8.4516-9.0861s-3.735-8.749-8.4516-9.0861M5.113 17.9586c-1.2502-1.4446-2.0562-3.2845-2.2-5.3046H0c.151 2.8266 1.2808 5.3917 3.051 7.3668l2.0606-2.0622zM11.3372 24v-2.9139c-2.02-.1439-3.8584-.949-5.3019-2.2018l-2.0606 2.0623c1.975 1.773 4.538 2.9022 7.361 3.0534z"/></svg>'
|
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Turborepo</title><path d="M11.9906 4.1957c-4.2998 0-7.7981 3.501-7.7981 7.8043s3.4983 7.8043 7.7981 7.8043c4.2999 0 7.7982-3.501 7.7982-7.8043s-3.4983-7.8043-7.7982-7.8043m0 11.843c-2.229 0-4.0356-1.8079-4.0356-4.0387s1.8065-4.0387 4.0356-4.0387S16.0262 9.7692 16.0262 12s-1.8065 4.0388-4.0356 4.0388m.6534-13.1249V0C18.9726.3386 24 5.5822 24 12s-5.0274 11.66-11.356 12v-2.9139c4.7167-.3372 8.4516-4.2814 8.4516-9.0861s-3.735-8.749-8.4516-9.0861M5.113 17.9586c-1.2502-1.4446-2.0562-3.2845-2.2-5.3046H0c.151 2.8266 1.2808 5.3917 3.051 7.3668l2.0606-2.0622zM11.3372 24v-2.9139c-2.02-.1439-3.8584-.949-5.3019-2.2018l-2.0606 2.0623c1.975 1.773 4.538 2.9022 7.361 3.0534z"/></svg>'
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { RADII, type RadiusValue } from "@/registry/config"
|
import { RADII, type RadiusValue } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function RadiusPicker({
|
export function RadiusPicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { Button } from "@/styles/base-nova/ui/button"
|
||||||
import { useRandom } from "@/app/(create)/hooks/use-random"
|
import { useRandom } from "@/app/(app)/create/hooks/use-random"
|
||||||
import { RESET_FORWARD_TYPE } from "@/app/(create)/hooks/use-reset"
|
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
|
||||||
|
|
||||||
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
|
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/styles/base-nova/ui/alert-dialog"
|
} from "@/styles/base-nova/ui/alert-dialog"
|
||||||
import { useReset } from "@/app/(create)/hooks/use-reset"
|
import { useReset } from "@/app/(app)/create/hooks/use-reset"
|
||||||
|
|
||||||
export function ResetDialog() {
|
export function ResetDialog() {
|
||||||
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()
|
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()
|
||||||
@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react"
|
|||||||
|
|
||||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { Button } from "@/styles/base-nova/ui/button"
|
||||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function ShareButton() {
|
export function ShareButton() {
|
||||||
const [params] = useDesignSystemSearchParams()
|
const [params] = useDesignSystemSearchParams()
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { type Style, type StyleName } from "@/registry/config"
|
import { type Style, type StyleName } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function StylePicker({
|
export function StylePicker({
|
||||||
styles,
|
styles,
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { useMounted } from "@/hooks/use-mounted"
|
import { useMounted } from "@/hooks/use-mounted"
|
||||||
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
|
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(create)/components/picker"
|
} from "@/app/(app)/create/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function ThemePicker({
|
export function ThemePicker({
|
||||||
themes,
|
themes,
|
||||||
@@ -8,7 +8,7 @@ import { useMounted } from "@/hooks/use-mounted"
|
|||||||
import { Icons } from "@/components/icons"
|
import { Icons } from "@/components/icons"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { Button } from "@/styles/base-nova/ui/button"
|
||||||
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
export function V0Button({ className }: { className?: string }) {
|
export function V0Button({ className }: { className?: string }) {
|
||||||
const [params] = useDesignSystemSearchParams()
|
const [params] = useDesignSystemSearchParams()
|
||||||
@@ -4,8 +4,8 @@ import * as React from "react"
|
|||||||
import { type RegistryItem } from "shadcn/schema"
|
import { type RegistryItem } from "shadcn/schema"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
|
||||||
|
|
||||||
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"
|
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { getPresetCode } from "@/app/(create)/lib/preset-code"
|
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
// Returns the canonical preset code derived from the current search params.
|
// Returns the canonical preset code derived from the current search params.
|
||||||
export function usePresetCode() {
|
export function usePresetCode() {
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
import { Suspense } from "react"
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
|
||||||
type HistoryContextValue = {
|
type HistoryContextValue = {
|
||||||
canGoBack: boolean
|
canGoBack: boolean
|
||||||
@@ -12,12 +13,28 @@ type HistoryContextValue = {
|
|||||||
|
|
||||||
const HistoryContext = React.createContext<HistoryContextValue | null>(null)
|
const HistoryContext = React.createContext<HistoryContextValue | null>(null)
|
||||||
|
|
||||||
export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
// Reads useSearchParams() in its own Suspense boundary so the
|
||||||
const router = useRouter()
|
// provider never blanks out children while search params resolve.
|
||||||
const pathname = usePathname()
|
function PresetSync({
|
||||||
|
onPresetChange,
|
||||||
|
}: {
|
||||||
|
onPresetChange: (preset: string) => void
|
||||||
|
}) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const preset = searchParams.get("preset") ?? ""
|
const preset = searchParams.get("preset") ?? ""
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onPresetChange(preset)
|
||||||
|
}, [preset, onPresetChange])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [preset, setPreset] = React.useState("")
|
||||||
|
|
||||||
const entriesRef = React.useRef<string[]>([preset])
|
const entriesRef = React.useRef<string[]>([preset])
|
||||||
const indexRef = React.useRef(0)
|
const indexRef = React.useRef(0)
|
||||||
const maxIndexRef = React.useRef(0)
|
const maxIndexRef = React.useRef(0)
|
||||||
@@ -26,6 +43,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const [index, setIndex] = React.useState(0)
|
const [index, setIndex] = React.useState(0)
|
||||||
const [maxIndex, setMaxIndex] = React.useState(0)
|
const [maxIndex, setMaxIndex] = React.useState(0)
|
||||||
|
|
||||||
|
const onPresetChange = React.useCallback((nextPreset: string) => {
|
||||||
|
setPreset(nextPreset)
|
||||||
|
}, [])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isNavigatingRef.current) {
|
if (isNavigatingRef.current) {
|
||||||
isNavigatingRef.current = false
|
isNavigatingRef.current = false
|
||||||
@@ -67,9 +88,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
|||||||
} else {
|
} else {
|
||||||
params.delete("preset")
|
params.delete("preset")
|
||||||
}
|
}
|
||||||
|
const pathname = window.location.pathname
|
||||||
const query = params.toString()
|
const query = params.toString()
|
||||||
router.replace(query ? `${pathname}?${query}` : pathname)
|
router.replace(query ? `${pathname}?${query}` : pathname)
|
||||||
}, [pathname, router])
|
}, [router])
|
||||||
|
|
||||||
const goForward = React.useCallback(() => {
|
const goForward = React.useCallback(() => {
|
||||||
if (indexRef.current >= maxIndexRef.current) {
|
if (indexRef.current >= maxIndexRef.current) {
|
||||||
@@ -88,9 +110,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
|||||||
} else {
|
} else {
|
||||||
params.delete("preset")
|
params.delete("preset")
|
||||||
}
|
}
|
||||||
|
const pathname = window.location.pathname
|
||||||
const query = params.toString()
|
const query = params.toString()
|
||||||
router.replace(query ? `${pathname}?${query}` : pathname)
|
router.replace(query ? `${pathname}?${query}` : pathname)
|
||||||
}, [pathname, router])
|
}, [router])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
@@ -133,7 +156,14 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
|||||||
[canGoBack, canGoForward, goBack, goForward]
|
[canGoBack, canGoForward, goBack, goForward]
|
||||||
)
|
)
|
||||||
|
|
||||||
return <HistoryContext value={value}>{children}</HistoryContext>
|
return (
|
||||||
|
<HistoryContext value={value}>
|
||||||
|
<Suspense>
|
||||||
|
<PresetSync onPresetChange={onPresetChange} />
|
||||||
|
</Suspense>
|
||||||
|
{children}
|
||||||
|
</HistoryContext>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useHistory() {
|
export function useHistory() {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import type { DesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import type { DesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
type ParentToIframeMessage = {
|
type ParentToIframeMessage = {
|
||||||
type: "design-system-params"
|
type: "design-system-params"
|
||||||
@@ -24,10 +24,15 @@ const LocksContext = React.createContext<LocksContextValue | null>(null)
|
|||||||
|
|
||||||
export function LocksProvider({ children }: { children: React.ReactNode }) {
|
export function LocksProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [locks, setLocks] = React.useState<Set<LockableParam>>(new Set())
|
const [locks, setLocks] = React.useState<Set<LockableParam>>(new Set())
|
||||||
|
const locksRef = React.useRef(locks)
|
||||||
|
React.useEffect(() => {
|
||||||
|
locksRef.current = locks
|
||||||
|
}, [locks])
|
||||||
|
|
||||||
|
// Stable callback — reads from ref so it doesn't change on every lock toggle.
|
||||||
const isLocked = React.useCallback(
|
const isLocked = React.useCallback(
|
||||||
(param: LockableParam) => locks.has(param),
|
(param: LockableParam) => locksRef.current.has(param),
|
||||||
[locks]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggleLock = React.useCallback((param: LockableParam) => {
|
const toggleLock = React.useCallback((param: LockableParam) => {
|
||||||
@@ -12,17 +12,17 @@ import {
|
|||||||
STYLES,
|
STYLES,
|
||||||
type FontHeadingValue,
|
type FontHeadingValue,
|
||||||
} from "@/registry/config"
|
} from "@/registry/config"
|
||||||
import { useLocks } from "@/app/(create)/hooks/use-locks"
|
import { useLocks } from "@/app/(app)/create/hooks/use-locks"
|
||||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
||||||
import {
|
import {
|
||||||
applyBias,
|
applyBias,
|
||||||
RANDOMIZE_BIASES,
|
RANDOMIZE_BIASES,
|
||||||
type RandomizeContext,
|
type RandomizeContext,
|
||||||
} from "@/app/(create)/lib/randomize-biases"
|
} from "@/app/(app)/create/lib/randomize-biases"
|
||||||
import {
|
import {
|
||||||
isTranslucentMenuColor,
|
isTranslucentMenuColor,
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
} from "@/app/(create)/lib/search-params"
|
} from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
function randomItem<T>(array: readonly T[]): T {
|
function randomItem<T>(array: readonly T[]): T {
|
||||||
return array[Math.floor(Math.random() * array.length)]
|
return array[Math.floor(Math.random() * array.length)]
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
import { DEFAULT_CONFIG, PRESETS } from "@/registry/config"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||||
|
|
||||||
const RESET_DIALOG_KEY = "create:reset-dialog-open"
|
const RESET_DIALOG_KEY = "create:reset-dialog-open"
|
||||||
export const RESET_FORWARD_TYPE = "reset-forward"
|
export const RESET_FORWARD_TYPE = "reset-forward"
|
||||||
@@ -20,22 +20,27 @@ export function useReset() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const reset = React.useCallback(() => {
|
const reset = React.useCallback(() => {
|
||||||
|
const preset =
|
||||||
|
PRESETS.find(
|
||||||
|
(preset) => preset.base === params.base && preset.style === params.style
|
||||||
|
) ?? DEFAULT_CONFIG
|
||||||
|
|
||||||
setParams({
|
setParams({
|
||||||
base: params.base,
|
base: params.base,
|
||||||
style: DEFAULT_CONFIG.style,
|
style: params.style,
|
||||||
baseColor: DEFAULT_CONFIG.baseColor,
|
baseColor: preset.baseColor,
|
||||||
theme: DEFAULT_CONFIG.theme,
|
theme: preset.theme,
|
||||||
chartColor: DEFAULT_CONFIG.chartColor,
|
chartColor: preset.chartColor,
|
||||||
iconLibrary: DEFAULT_CONFIG.iconLibrary,
|
iconLibrary: preset.iconLibrary,
|
||||||
font: DEFAULT_CONFIG.font,
|
font: preset.font,
|
||||||
fontHeading: DEFAULT_CONFIG.fontHeading,
|
fontHeading: preset.fontHeading,
|
||||||
menuAccent: DEFAULT_CONFIG.menuAccent,
|
menuAccent: preset.menuAccent,
|
||||||
menuColor: DEFAULT_CONFIG.menuColor,
|
menuColor: preset.menuColor,
|
||||||
radius: DEFAULT_CONFIG.radius,
|
radius: preset.radius,
|
||||||
template: DEFAULT_CONFIG.template,
|
template: DEFAULT_CONFIG.template,
|
||||||
item: params.item,
|
item: params.item,
|
||||||
})
|
})
|
||||||
}, [setParams, params.base, params.item])
|
}, [setParams, params.base, params.style, params.item])
|
||||||
|
|
||||||
const handleShowResetDialogChange = React.useCallback(
|
const handleShowResetDialogChange = React.useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
import { HistoryProvider } from "@/app/(create)/hooks/use-history"
|
import { HistoryProvider } from "@/app/(app)/create/hooks/use-history"
|
||||||
import { LocksProvider } from "@/app/(create)/hooks/use-locks"
|
import { LocksProvider } from "@/app/(app)/create/hooks/use-locks"
|
||||||
|
|
||||||
export default function CreateLayout({
|
export default function CreateLayout({
|
||||||
children,
|
children,
|
||||||
@@ -6,7 +6,7 @@ import { BASES, getThemesForBaseColor, type BaseName } from "@/registry/config"
|
|||||||
import {
|
import {
|
||||||
ALLOWED_ITEM_TYPES,
|
ALLOWED_ITEM_TYPES,
|
||||||
EXCLUDED_ITEMS,
|
EXCLUDED_ITEMS,
|
||||||
} from "@/app/(create)/lib/constants"
|
} from "@/app/(app)/create/lib/constants"
|
||||||
|
|
||||||
export async function getItemsForBase(base: BaseName) {
|
export async function getItemsForBase(base: BaseName) {
|
||||||
const { Index } = await import("@/registry/bases/__index__")
|
const { Index } = await import("@/registry/bases/__index__")
|
||||||
233
apps/v4/app/(app)/create/lib/fonts.ts
Normal file
233
apps/v4/app/(app)/create/lib/fonts.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import {
|
||||||
|
DM_Sans,
|
||||||
|
Figtree,
|
||||||
|
Geist,
|
||||||
|
Geist_Mono,
|
||||||
|
IBM_Plex_Sans,
|
||||||
|
Instrument_Sans,
|
||||||
|
Inter,
|
||||||
|
JetBrains_Mono,
|
||||||
|
Lora,
|
||||||
|
Manrope,
|
||||||
|
Merriweather,
|
||||||
|
Montserrat,
|
||||||
|
Noto_Sans,
|
||||||
|
Noto_Serif,
|
||||||
|
Nunito_Sans,
|
||||||
|
Outfit,
|
||||||
|
Oxanium,
|
||||||
|
Playfair_Display,
|
||||||
|
Public_Sans,
|
||||||
|
Raleway,
|
||||||
|
Roboto,
|
||||||
|
Roboto_Slab,
|
||||||
|
Source_Sans_3,
|
||||||
|
Space_Grotesk,
|
||||||
|
} from "next/font/google"
|
||||||
|
|
||||||
|
import { FONT_DEFINITIONS, type FontName } from "@/lib/font-definitions"
|
||||||
|
|
||||||
|
type PreviewFont = ReturnType<typeof Inter>
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
})
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-inter",
|
||||||
|
})
|
||||||
|
|
||||||
|
const notoSans = Noto_Sans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-noto-sans",
|
||||||
|
})
|
||||||
|
|
||||||
|
const nunitoSans = Nunito_Sans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-nunito-sans",
|
||||||
|
})
|
||||||
|
|
||||||
|
const figtree = Figtree({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-figtree",
|
||||||
|
})
|
||||||
|
|
||||||
|
const roboto = Roboto({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-roboto",
|
||||||
|
})
|
||||||
|
|
||||||
|
const raleway = Raleway({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-raleway",
|
||||||
|
})
|
||||||
|
|
||||||
|
const dmSans = DM_Sans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-dm-sans",
|
||||||
|
})
|
||||||
|
|
||||||
|
const publicSans = Public_Sans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-public-sans",
|
||||||
|
})
|
||||||
|
|
||||||
|
const outfit = Outfit({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-outfit",
|
||||||
|
})
|
||||||
|
|
||||||
|
const oxanium = Oxanium({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-oxanium",
|
||||||
|
})
|
||||||
|
|
||||||
|
const manrope = Manrope({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-manrope",
|
||||||
|
})
|
||||||
|
|
||||||
|
const spaceGrotesk = Space_Grotesk({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-space-grotesk",
|
||||||
|
})
|
||||||
|
|
||||||
|
const montserrat = Montserrat({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-montserrat",
|
||||||
|
})
|
||||||
|
|
||||||
|
const ibmPlexSans = IBM_Plex_Sans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-ibm-plex-sans",
|
||||||
|
})
|
||||||
|
|
||||||
|
const sourceSans3 = Source_Sans_3({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-source-sans-3",
|
||||||
|
})
|
||||||
|
|
||||||
|
const instrumentSans = Instrument_Sans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-instrument-sans",
|
||||||
|
})
|
||||||
|
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-jetbrains-mono",
|
||||||
|
})
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
})
|
||||||
|
|
||||||
|
const notoSerif = Noto_Serif({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-noto-serif",
|
||||||
|
})
|
||||||
|
|
||||||
|
const robotoSlab = Roboto_Slab({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-roboto-slab",
|
||||||
|
})
|
||||||
|
|
||||||
|
const merriweather = Merriweather({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-merriweather",
|
||||||
|
})
|
||||||
|
|
||||||
|
const lora = Lora({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-lora",
|
||||||
|
})
|
||||||
|
|
||||||
|
const playfairDisplay = Playfair_Display({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-playfair-display",
|
||||||
|
})
|
||||||
|
|
||||||
|
const PREVIEW_FONTS = {
|
||||||
|
geist: geistSans,
|
||||||
|
inter,
|
||||||
|
"noto-sans": notoSans,
|
||||||
|
"nunito-sans": nunitoSans,
|
||||||
|
figtree,
|
||||||
|
roboto,
|
||||||
|
raleway,
|
||||||
|
"dm-sans": dmSans,
|
||||||
|
"public-sans": publicSans,
|
||||||
|
outfit,
|
||||||
|
oxanium,
|
||||||
|
manrope,
|
||||||
|
"space-grotesk": spaceGrotesk,
|
||||||
|
montserrat,
|
||||||
|
"ibm-plex-sans": ibmPlexSans,
|
||||||
|
"source-sans-3": sourceSans3,
|
||||||
|
"instrument-sans": instrumentSans,
|
||||||
|
"jetbrains-mono": jetbrainsMono,
|
||||||
|
"geist-mono": geistMono,
|
||||||
|
"noto-serif": notoSerif,
|
||||||
|
"roboto-slab": robotoSlab,
|
||||||
|
merriweather,
|
||||||
|
lora,
|
||||||
|
"playfair-display": playfairDisplay,
|
||||||
|
} satisfies Record<FontName, PreviewFont>
|
||||||
|
|
||||||
|
function createFontOption(name: FontName) {
|
||||||
|
const definition = FONT_DEFINITIONS.find((font) => font.name === name)
|
||||||
|
|
||||||
|
if (!definition) {
|
||||||
|
throw new Error(`Unknown font definition: ${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: definition.title,
|
||||||
|
value: definition.name,
|
||||||
|
font: PREVIEW_FONTS[name],
|
||||||
|
type: definition.type,
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FONTS = [
|
||||||
|
createFontOption("geist"),
|
||||||
|
createFontOption("inter"),
|
||||||
|
createFontOption("noto-sans"),
|
||||||
|
createFontOption("nunito-sans"),
|
||||||
|
createFontOption("figtree"),
|
||||||
|
createFontOption("roboto"),
|
||||||
|
createFontOption("raleway"),
|
||||||
|
createFontOption("dm-sans"),
|
||||||
|
createFontOption("public-sans"),
|
||||||
|
createFontOption("outfit"),
|
||||||
|
createFontOption("oxanium"),
|
||||||
|
createFontOption("manrope"),
|
||||||
|
createFontOption("space-grotesk"),
|
||||||
|
createFontOption("montserrat"),
|
||||||
|
createFontOption("ibm-plex-sans"),
|
||||||
|
createFontOption("source-sans-3"),
|
||||||
|
createFontOption("instrument-sans"),
|
||||||
|
createFontOption("geist-mono"),
|
||||||
|
createFontOption("jetbrains-mono"),
|
||||||
|
createFontOption("noto-serif"),
|
||||||
|
createFontOption("roboto-slab"),
|
||||||
|
createFontOption("merriweather"),
|
||||||
|
createFontOption("lora"),
|
||||||
|
createFontOption("playfair-display"),
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type Font = (typeof FONTS)[number]
|
||||||
|
|
||||||
|
export const FONT_HEADING_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: "Inherit",
|
||||||
|
value: "inherit",
|
||||||
|
font: null,
|
||||||
|
type: "default",
|
||||||
|
},
|
||||||
|
...FONTS,
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type FontHeadingOption = (typeof FONT_HEADING_OPTIONS)[number]
|
||||||
309
apps/v4/app/(app)/create/lib/search-params.ts
Normal file
309
apps/v4/app/(app)/create/lib/search-params.ts
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { useQueryStates } from "nuqs"
|
||||||
|
import {
|
||||||
|
createLoader,
|
||||||
|
createSerializer,
|
||||||
|
parseAsBoolean,
|
||||||
|
parseAsInteger,
|
||||||
|
parseAsString,
|
||||||
|
parseAsStringLiteral,
|
||||||
|
type inferParserType,
|
||||||
|
type Options,
|
||||||
|
} from "nuqs/server"
|
||||||
|
import { decodePreset, isPresetCode } from "shadcn/preset"
|
||||||
|
|
||||||
|
import {
|
||||||
|
BASE_COLORS,
|
||||||
|
BASES,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
getThemesForBaseColor,
|
||||||
|
iconLibraries,
|
||||||
|
MENU_ACCENTS,
|
||||||
|
MENU_COLORS,
|
||||||
|
RADII,
|
||||||
|
STYLES,
|
||||||
|
THEMES,
|
||||||
|
type BaseColorName,
|
||||||
|
type BaseName,
|
||||||
|
type ChartColorName,
|
||||||
|
type FontHeadingValue,
|
||||||
|
type FontValue,
|
||||||
|
type IconLibraryName,
|
||||||
|
type MenuAccentValue,
|
||||||
|
type MenuColorValue,
|
||||||
|
type RadiusValue,
|
||||||
|
type StyleName,
|
||||||
|
type ThemeName,
|
||||||
|
} from "@/registry/config"
|
||||||
|
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
||||||
|
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||||
|
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
|
||||||
|
|
||||||
|
const designSystemSearchParams = {
|
||||||
|
preset: parseAsString.withDefault("b0"),
|
||||||
|
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
|
||||||
|
DEFAULT_CONFIG.base
|
||||||
|
),
|
||||||
|
item: parseAsString.withDefault("preview-02").withOptions({ shallow: true }),
|
||||||
|
iconLibrary: parseAsStringLiteral<IconLibraryName>(
|
||||||
|
Object.values(iconLibraries).map((i) => i.name)
|
||||||
|
).withDefault(DEFAULT_CONFIG.iconLibrary),
|
||||||
|
style: parseAsStringLiteral<StyleName>(STYLES.map((s) => s.name)).withDefault(
|
||||||
|
DEFAULT_CONFIG.style
|
||||||
|
),
|
||||||
|
theme: parseAsStringLiteral<ThemeName>(THEMES.map((t) => t.name)).withDefault(
|
||||||
|
DEFAULT_CONFIG.theme
|
||||||
|
),
|
||||||
|
chartColor: parseAsStringLiteral<ChartColorName>(
|
||||||
|
THEMES.map((t) => t.name)
|
||||||
|
).withDefault(DEFAULT_CONFIG.chartColor ?? "neutral"),
|
||||||
|
font: parseAsStringLiteral<FontValue>(FONTS.map((f) => f.value)).withDefault(
|
||||||
|
DEFAULT_CONFIG.font
|
||||||
|
),
|
||||||
|
fontHeading: parseAsStringLiteral<FontHeadingValue>([
|
||||||
|
"inherit",
|
||||||
|
...FONTS.map((f) => f.value),
|
||||||
|
]).withDefault(DEFAULT_CONFIG.fontHeading),
|
||||||
|
baseColor: parseAsStringLiteral<BaseColorName>(
|
||||||
|
BASE_COLORS.map((b) => b.name)
|
||||||
|
).withDefault(DEFAULT_CONFIG.baseColor),
|
||||||
|
menuAccent: parseAsStringLiteral<MenuAccentValue>(
|
||||||
|
MENU_ACCENTS.map((a) => a.value)
|
||||||
|
).withDefault(DEFAULT_CONFIG.menuAccent),
|
||||||
|
menuColor: parseAsStringLiteral<MenuColorValue>(
|
||||||
|
MENU_COLORS.map((m) => m.value)
|
||||||
|
).withDefault(DEFAULT_CONFIG.menuColor),
|
||||||
|
radius: parseAsStringLiteral<RadiusValue>(
|
||||||
|
RADII.map((r) => r.name)
|
||||||
|
).withDefault("default"),
|
||||||
|
template: parseAsStringLiteral([
|
||||||
|
"next",
|
||||||
|
"next-monorepo",
|
||||||
|
"start",
|
||||||
|
"start-monorepo",
|
||||||
|
"react-router",
|
||||||
|
"react-router-monorepo",
|
||||||
|
"vite",
|
||||||
|
"vite-monorepo",
|
||||||
|
"astro",
|
||||||
|
"astro-monorepo",
|
||||||
|
"laravel",
|
||||||
|
] as const).withDefault("next"),
|
||||||
|
rtl: parseAsBoolean.withDefault(false),
|
||||||
|
size: parseAsInteger.withDefault(100),
|
||||||
|
custom: parseAsBoolean.withDefault(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Design system param keys that get encoded into the preset code.
|
||||||
|
const DESIGN_SYSTEM_KEYS = [
|
||||||
|
"style",
|
||||||
|
"baseColor",
|
||||||
|
"theme",
|
||||||
|
"chartColor",
|
||||||
|
"iconLibrary",
|
||||||
|
"font",
|
||||||
|
"fontHeading",
|
||||||
|
"radius",
|
||||||
|
"menuAccent",
|
||||||
|
"menuColor",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function normalizeFontHeading(
|
||||||
|
font: FontValue,
|
||||||
|
fontHeading: FontHeadingValue
|
||||||
|
): FontHeadingValue {
|
||||||
|
// Persist "same as body" as an explicit inherit sentinel so the body font
|
||||||
|
// can change later without freezing headings to a concrete previous value.
|
||||||
|
return fontHeading === font ? "inherit" : fontHeading
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-design-system keys that get passed through as-is.
|
||||||
|
// `base` is not encoded in preset codes — it's an architectural choice, not visual.
|
||||||
|
const NON_DESIGN_SYSTEM_KEYS = [
|
||||||
|
"base",
|
||||||
|
"item",
|
||||||
|
"preset",
|
||||||
|
"template",
|
||||||
|
"rtl",
|
||||||
|
"size",
|
||||||
|
"custom",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const loadDesignSystemSearchParams = createLoader(
|
||||||
|
designSystemSearchParams
|
||||||
|
)
|
||||||
|
|
||||||
|
export const serializeDesignSystemSearchParams = createSerializer(
|
||||||
|
designSystemSearchParams
|
||||||
|
)
|
||||||
|
|
||||||
|
export type DesignSystemSearchParams = inferParserType<
|
||||||
|
typeof designSystemSearchParams
|
||||||
|
>
|
||||||
|
|
||||||
|
export function isTranslucentMenuColor(
|
||||||
|
menuColor?: MenuColorValue | null
|
||||||
|
): menuColor is "default-translucent" | "inverted-translucent" {
|
||||||
|
return (
|
||||||
|
menuColor === "default-translucent" || menuColor === "inverted-translucent"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePartialDesignSystemParams(
|
||||||
|
params: Partial<DesignSystemSearchParams>
|
||||||
|
): Partial<DesignSystemSearchParams> {
|
||||||
|
if (
|
||||||
|
params.menuAccent === "bold" &&
|
||||||
|
isTranslucentMenuColor(params.menuColor ?? undefined)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...params,
|
||||||
|
menuAccent: "subtle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDesignSystemParams(
|
||||||
|
params: DesignSystemSearchParams
|
||||||
|
): DesignSystemSearchParams {
|
||||||
|
let result = {
|
||||||
|
...params,
|
||||||
|
fontHeading: normalizeFontHeading(params.font, params.fontHeading),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate theme and chartColor against baseColor.
|
||||||
|
if (result.baseColor) {
|
||||||
|
const available = getThemesForBaseColor(result.baseColor)
|
||||||
|
const themeValid = available.some((t) => t.name === result.theme)
|
||||||
|
const chartColorValid = available.some((t) => t.name === result.chartColor)
|
||||||
|
|
||||||
|
if (!themeValid || !chartColorValid) {
|
||||||
|
const fallback = (available[0]?.name ?? result.baseColor) as ThemeName
|
||||||
|
result = {
|
||||||
|
...result,
|
||||||
|
...(!themeValid && { theme: fallback }),
|
||||||
|
...(!chartColorValid && { chartColor: fallback as ChartColorName }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.menuAccent === "bold" &&
|
||||||
|
isTranslucentMenuColor(result.menuColor)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
menuAccent: "subtle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// If preset param exists, decode it and overlay on raw params.
|
||||||
|
// V1 presets don't encode chartColor — fall back to the colored
|
||||||
|
// theme that base-color themes originally borrowed charts from.
|
||||||
|
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
|
||||||
|
|
||||||
|
function resolvePresetParams(
|
||||||
|
rawParams: DesignSystemSearchParams,
|
||||||
|
searchParams: SearchParamsLike
|
||||||
|
) {
|
||||||
|
if (rawParams.preset && isPresetCode(rawParams.preset)) {
|
||||||
|
const decoded = decodePreset(rawParams.preset)
|
||||||
|
if (decoded) {
|
||||||
|
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
|
||||||
|
return normalizeDesignSystemParams({
|
||||||
|
...decoded,
|
||||||
|
...presetOverrides,
|
||||||
|
base: rawParams.base,
|
||||||
|
item: rawParams.item,
|
||||||
|
preset: rawParams.preset,
|
||||||
|
template: rawParams.template,
|
||||||
|
rtl: rawParams.rtl,
|
||||||
|
size: rawParams.size,
|
||||||
|
custom: rawParams.custom,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalizeDesignSystemParams(rawParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wraps nuqs useQueryStates with transparent preset encoding/decoding.
|
||||||
|
// - Reads: if ?preset=CODE is in the URL, decodes it and returns individual values.
|
||||||
|
// - Writes: when design system params are set, encodes them into a preset code.
|
||||||
|
export function useDesignSystemSearchParams(options: Options = {}) {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const [rawParams, rawSetParams] = useQueryStates(designSystemSearchParams, {
|
||||||
|
shallow: false,
|
||||||
|
history: "push",
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = React.useMemo(
|
||||||
|
() => resolvePresetParams(rawParams, searchParams),
|
||||||
|
[rawParams, searchParams]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use ref so setParams callback stays stable across renders.
|
||||||
|
const paramsRef = React.useRef(params)
|
||||||
|
React.useEffect(() => {
|
||||||
|
paramsRef.current = params
|
||||||
|
}, [params])
|
||||||
|
|
||||||
|
type RawSetParamsInput = Parameters<typeof rawSetParams>[0]
|
||||||
|
|
||||||
|
const setParams = React.useCallback(
|
||||||
|
(
|
||||||
|
updates:
|
||||||
|
| Partial<DesignSystemSearchParams>
|
||||||
|
| ((
|
||||||
|
old: DesignSystemSearchParams
|
||||||
|
) => Partial<DesignSystemSearchParams>),
|
||||||
|
setOptions?: Options
|
||||||
|
) => {
|
||||||
|
const resolvedUpdates = normalizePartialDesignSystemParams(
|
||||||
|
typeof updates === "function" ? updates(paramsRef.current) : updates
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasDesignSystemUpdate = DESIGN_SYSTEM_KEYS.some(
|
||||||
|
(key) => key in resolvedUpdates
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasDesignSystemUpdate) {
|
||||||
|
// No design system change, pass through directly.
|
||||||
|
return rawSetParams(resolvedUpdates as RawSetParamsInput, setOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge current decoded values with updates.
|
||||||
|
const merged = normalizeDesignSystemParams({
|
||||||
|
...paramsRef.current,
|
||||||
|
...resolvedUpdates,
|
||||||
|
})
|
||||||
|
// Encode design system fields into a preset code.
|
||||||
|
// Cast needed: merged values may include null from nuqs resets,
|
||||||
|
// but encodePreset handles missing values by falling back to defaults.
|
||||||
|
const code = getPresetCode(merged)
|
||||||
|
// Build update: set preset, clear individual DS params from URL.
|
||||||
|
const rawUpdate: Record<string, unknown> = { preset: code }
|
||||||
|
for (const key of DESIGN_SYSTEM_KEYS) {
|
||||||
|
rawUpdate[key] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass through non-DS params that were explicitly in the update.
|
||||||
|
for (const key of NON_DESIGN_SYSTEM_KEYS) {
|
||||||
|
if (key in resolvedUpdates) {
|
||||||
|
rawUpdate[key] = (resolvedUpdates as Record<string, unknown>)[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawSetParams(rawUpdate as RawSetParamsInput, setOptions)
|
||||||
|
},
|
||||||
|
[rawSetParams]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [params, setParams] as const
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
import { DEFAULT_CONFIG } from "@/registry/config"
|
||||||
import { buildV0Payload } from "@/app/(create)/lib/v0"
|
import { buildV0Payload } from "@/app/(app)/create/lib/v0"
|
||||||
|
|
||||||
vi.mock("shadcn/schema", async () => {
|
vi.mock("shadcn/schema", async () => {
|
||||||
return await vi.importActual("shadcn/schema")
|
return await vi.importActual("shadcn/schema")
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
|
import { Suspense } from "react"
|
||||||
import { type Metadata } from "next"
|
import { type Metadata } from "next"
|
||||||
|
import dynamic from "next/dynamic"
|
||||||
|
|
||||||
import { siteConfig } from "@/lib/config"
|
import { siteConfig } from "@/lib/config"
|
||||||
import { absoluteUrl } from "@/lib/utils"
|
import { absoluteUrl } from "@/lib/utils"
|
||||||
import { SiteHeader } from "@/components/site-header"
|
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
||||||
import { Customizer } from "@/app/(create)/components/customizer"
|
import { Customizer } from "@/app/(app)/create/components/customizer"
|
||||||
import { PresetHandler } from "@/app/(create)/components/preset-handler"
|
import { PresetHandler } from "@/app/(app)/create/components/preset-handler"
|
||||||
import { Preview } from "@/app/(create)/components/preview"
|
import { Preview } from "@/app/(app)/create/components/preview"
|
||||||
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
|
import { getAllItems } from "@/app/(app)/create/lib/api"
|
||||||
import { getAllItems } from "@/app/(create)/lib/api"
|
|
||||||
|
// Only shown on first visit (checks localStorage).
|
||||||
|
const WelcomeDialog = dynamic(() =>
|
||||||
|
import("@/app/(app)/create/components/welcome-dialog").then(
|
||||||
|
(m) => m.WelcomeDialog
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "New Project",
|
title: "New Project",
|
||||||
@@ -38,24 +46,29 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CreatePage() {
|
export default function CreatePage() {
|
||||||
const itemsByBase = await getAllItems()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden section-soft [--customizer-width:--spacing(48)] [--gap:--spacing(4)] md:[--gap:--spacing(6)] 2xl:[--customizer-width:--spacing(56)]">
|
||||||
data-slot="layout"
|
<div
|
||||||
className="group/layout relative z-10 flex h-svh flex-col overflow-hidden section-soft [--customizer-width:--spacing(48)] [--gap:--spacing(4)] md:[--gap:--spacing(6)] 2xl:[--customizer-width:--spacing(56)]"
|
|
||||||
>
|
|
||||||
<SiteHeader />
|
|
||||||
<main
|
|
||||||
data-slot="designer"
|
data-slot="designer"
|
||||||
className="flex min-h-0 flex-1 flex-col gap-(--gap) p-(--gap) pt-[calc(var(--gap)*0.25)] md:flex-row-reverse"
|
className="flex min-h-0 flex-1 flex-col gap-(--gap) p-(--gap) pt-[calc(var(--gap)*0.25)] md:flex-row-reverse"
|
||||||
>
|
>
|
||||||
<Preview />
|
<Preview />
|
||||||
<Customizer itemsByBase={itemsByBase} />
|
<Suspense
|
||||||
<PresetHandler />
|
fallback={
|
||||||
<WelcomeDialog />
|
<Skeleton className="isolate min-h-[151px] w-full self-start rounded-2xl md:h-full md:max-h-full md:min-h-0 md:w-(--customizer-width)" />
|
||||||
</main>
|
}
|
||||||
|
>
|
||||||
|
<CustomizerLoader />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
<PresetHandler />
|
||||||
|
<WelcomeDialog />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function CustomizerLoader() {
|
||||||
|
const itemsByBase = await getAllItems()
|
||||||
|
return <Customizer itemsByBase={itemsByBase} />
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import { Button } from "@/styles/radix-nova/ui/button"
|
|||||||
export const revalidate = false
|
export const revalidate = false
|
||||||
export const dynamic = "force-static"
|
export const dynamic = "force-static"
|
||||||
|
|
||||||
|
const NUMBER_OF_LATEST_PAGES = 2
|
||||||
|
|
||||||
export function generateMetadata() {
|
export function generateMetadata() {
|
||||||
return {
|
return {
|
||||||
title: "Changelog",
|
title: "Changelog",
|
||||||
@@ -34,8 +36,8 @@ export function generateMetadata() {
|
|||||||
|
|
||||||
export default function ChangelogPage() {
|
export default function ChangelogPage() {
|
||||||
const pages = getChangelogPages()
|
const pages = getChangelogPages()
|
||||||
const latestPages = pages.slice(0, 5)
|
const latestPages = pages.slice(0, NUMBER_OF_LATEST_PAGES)
|
||||||
const olderPages = pages.slice(5)
|
const olderPages = pages.slice(NUMBER_OF_LATEST_PAGES)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -44,7 +46,7 @@ export default function ChangelogPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<div className="h-(--top-spacing) shrink-0" />
|
<div className="h-(--top-spacing) shrink-0" />
|
||||||
<div className="mx-auto flex w-full max-w-[40rem] min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
|
<div className="mx-auto flex w-full max-w-160 min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">
|
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="layout"
|
data-slot="layout"
|
||||||
className="relative z-10 flex min-h-svh flex-col bg-background"
|
className="group/layout relative z-10 flex min-h-svh flex-col bg-background has-data-[slot=designer]:h-svh has-data-[slot=designer]:overflow-hidden"
|
||||||
>
|
>
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<main className="flex flex-1 flex-col">{children}</main>
|
<main className="flex min-h-0 flex-1 flex-col">{children}</main>
|
||||||
<SiteFooter />
|
<SiteFooter />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { type Metadata } from "next"
|
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
import { Announcement } from "@/components/announcement"
|
|
||||||
import {
|
|
||||||
PageActions,
|
|
||||||
PageHeader,
|
|
||||||
PageHeaderDescription,
|
|
||||||
PageHeaderHeading,
|
|
||||||
} from "@/components/page-header"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
|
|
||||||
const title = "Pick a Color. Make it yours."
|
|
||||||
const description =
|
|
||||||
"Try our hand-picked themes. Copy and paste them into your project. New theme editor coming soon."
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
openGraph: {
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: `/og?title=${encodeURIComponent(
|
|
||||||
title
|
|
||||||
)}&description=${encodeURIComponent(description)}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: `/og?title=${encodeURIComponent(
|
|
||||||
title
|
|
||||||
)}&description=${encodeURIComponent(description)}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ThemesLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageHeader>
|
|
||||||
<Announcement />
|
|
||||||
<PageHeaderHeading>{title}</PageHeaderHeading>
|
|
||||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
|
||||||
<PageActions>
|
|
||||||
<Button asChild size="sm">
|
|
||||||
<a href="#themes">Browse Themes</a>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="ghost" size="sm">
|
|
||||||
<Link href="/docs/theming">Documentation</Link>
|
|
||||||
</Button>
|
|
||||||
</PageActions>
|
|
||||||
</PageHeader>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { CardsDemo } from "@/components/cards"
|
|
||||||
import { ThemeCustomizer } from "@/components/theme-customizer"
|
|
||||||
|
|
||||||
export const dynamic = "force-static"
|
|
||||||
export const revalidate = false
|
|
||||||
|
|
||||||
export default function ThemesPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div id="themes" className="container-wrapper scroll-mt-20">
|
|
||||||
<div className="container flex items-center justify-between gap-8 px-6 py-4 md:px-8">
|
|
||||||
<ThemeCustomizer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
|
|
||||||
<div className="container flex flex-1 flex-col theme-container">
|
|
||||||
<CardsDemo />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,75 +1,3 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { lazy, Suspense } from "react"
|
export { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||||
import { SquareIcon } from "lucide-react"
|
|
||||||
import type { IconLibraryName } from "shadcn/icons"
|
|
||||||
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
|
||||||
|
|
||||||
const IconLucide = lazy(() =>
|
|
||||||
import("@/registry/icons/icon-lucide").then((mod) => ({
|
|
||||||
default: mod.IconLucide,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const IconTabler = lazy(() =>
|
|
||||||
import("@/registry/icons/icon-tabler").then((mod) => ({
|
|
||||||
default: mod.IconTabler,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const IconHugeicons = lazy(() =>
|
|
||||||
import("@/registry/icons/icon-hugeicons").then((mod) => ({
|
|
||||||
default: mod.IconHugeicons,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const IconPhosphor = lazy(() =>
|
|
||||||
import("@/registry/icons/icon-phosphor").then((mod) => ({
|
|
||||||
default: mod.IconPhosphor,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const IconRemixicon = lazy(() =>
|
|
||||||
import("@/registry/icons/icon-remixicon").then((mod) => ({
|
|
||||||
default: mod.IconRemixicon,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Preload all icon renderer modules so switching libraries is instant.
|
|
||||||
// These warm the browser module cache; React.lazy resolves immediately
|
|
||||||
// for modules that are already loaded.
|
|
||||||
void import("@/registry/icons/icon-lucide")
|
|
||||||
void import("@/registry/icons/icon-tabler")
|
|
||||||
void import("@/registry/icons/icon-hugeicons")
|
|
||||||
void import("@/registry/icons/icon-phosphor")
|
|
||||||
void import("@/registry/icons/icon-remixicon")
|
|
||||||
|
|
||||||
export function IconPlaceholder({
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
[K in IconLibraryName]: string
|
|
||||||
} & React.ComponentProps<"svg">) {
|
|
||||||
const [{ iconLibrary }] = useDesignSystemSearchParams()
|
|
||||||
const iconName = props[iconLibrary]
|
|
||||||
|
|
||||||
if (!iconName) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<SquareIcon {...props} />}>
|
|
||||||
{iconLibrary === "lucide" && <IconLucide name={iconName} {...props} />}
|
|
||||||
{iconLibrary === "tabler" && <IconTabler name={iconName} {...props} />}
|
|
||||||
{iconLibrary === "hugeicons" && (
|
|
||||||
<IconHugeicons name={iconName} {...props} />
|
|
||||||
)}
|
|
||||||
{iconLibrary === "phosphor" && (
|
|
||||||
<IconPhosphor name={iconName} {...props} />
|
|
||||||
)}
|
|
||||||
{iconLibrary === "remixicon" && (
|
|
||||||
<IconRemixicon name={iconName} {...props} />
|
|
||||||
)}
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
designSystemConfigSchema,
|
designSystemConfigSchema,
|
||||||
type DesignSystemConfig,
|
type DesignSystemConfig,
|
||||||
} from "@/registry/config"
|
} from "@/registry/config"
|
||||||
import { resolvePresetOverrides } from "@/app/(create)/lib/preset-query"
|
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
|
||||||
|
|
||||||
// Parses design system config from URL search params.
|
// Parses design system config from URL search params.
|
||||||
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { isPresetCode } from "shadcn/preset"
|
|||||||
import { registryItemSchema } from "shadcn/schema"
|
import { registryItemSchema } from "shadcn/schema"
|
||||||
|
|
||||||
import { buildRegistryBase } from "@/registry/config"
|
import { buildRegistryBase } from "@/registry/config"
|
||||||
|
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||||
import { getPresetCode } from "@/app/(create)/lib/preset-code"
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { after, NextResponse, type NextRequest } from "next/server"
|
|||||||
import { track } from "@vercel/analytics/server"
|
import { track } from "@vercel/analytics/server"
|
||||||
import { isPresetCode } from "shadcn/preset"
|
import { isPresetCode } from "shadcn/preset"
|
||||||
|
|
||||||
|
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||||
|
import { buildV0Payload } from "@/app/(app)/create/lib/v0"
|
||||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||||
import { getPresetCode } from "@/app/(create)/lib/preset-code"
|
|
||||||
import { buildV0Payload } from "@/app/(create)/lib/v0"
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,233 +1 @@
|
|||||||
import {
|
export * from "@/app/(app)/create/lib/fonts"
|
||||||
DM_Sans,
|
|
||||||
Figtree,
|
|
||||||
Geist,
|
|
||||||
Geist_Mono,
|
|
||||||
IBM_Plex_Sans,
|
|
||||||
Instrument_Sans,
|
|
||||||
Inter,
|
|
||||||
JetBrains_Mono,
|
|
||||||
Lora,
|
|
||||||
Manrope,
|
|
||||||
Merriweather,
|
|
||||||
Montserrat,
|
|
||||||
Noto_Sans,
|
|
||||||
Noto_Serif,
|
|
||||||
Nunito_Sans,
|
|
||||||
Outfit,
|
|
||||||
Oxanium,
|
|
||||||
Playfair_Display,
|
|
||||||
Public_Sans,
|
|
||||||
Raleway,
|
|
||||||
Roboto,
|
|
||||||
Roboto_Slab,
|
|
||||||
Source_Sans_3,
|
|
||||||
Space_Grotesk,
|
|
||||||
} from "next/font/google"
|
|
||||||
|
|
||||||
import { FONT_DEFINITIONS, type FontName } from "@/lib/font-definitions"
|
|
||||||
|
|
||||||
type PreviewFont = ReturnType<typeof Inter>
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const inter = Inter({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-inter",
|
|
||||||
})
|
|
||||||
|
|
||||||
const notoSans = Noto_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-noto-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const nunitoSans = Nunito_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-nunito-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const figtree = Figtree({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-figtree",
|
|
||||||
})
|
|
||||||
|
|
||||||
const roboto = Roboto({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-roboto",
|
|
||||||
})
|
|
||||||
|
|
||||||
const raleway = Raleway({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-raleway",
|
|
||||||
})
|
|
||||||
|
|
||||||
const dmSans = DM_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-dm-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const publicSans = Public_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-public-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const outfit = Outfit({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-outfit",
|
|
||||||
})
|
|
||||||
|
|
||||||
const oxanium = Oxanium({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-oxanium",
|
|
||||||
})
|
|
||||||
|
|
||||||
const manrope = Manrope({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-manrope",
|
|
||||||
})
|
|
||||||
|
|
||||||
const spaceGrotesk = Space_Grotesk({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-space-grotesk",
|
|
||||||
})
|
|
||||||
|
|
||||||
const montserrat = Montserrat({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-montserrat",
|
|
||||||
})
|
|
||||||
|
|
||||||
const ibmPlexSans = IBM_Plex_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-ibm-plex-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const sourceSans3 = Source_Sans_3({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-source-sans-3",
|
|
||||||
})
|
|
||||||
|
|
||||||
const instrumentSans = Instrument_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-instrument-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-jetbrains-mono",
|
|
||||||
})
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
})
|
|
||||||
|
|
||||||
const notoSerif = Noto_Serif({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-noto-serif",
|
|
||||||
})
|
|
||||||
|
|
||||||
const robotoSlab = Roboto_Slab({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-roboto-slab",
|
|
||||||
})
|
|
||||||
|
|
||||||
const merriweather = Merriweather({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-merriweather",
|
|
||||||
})
|
|
||||||
|
|
||||||
const lora = Lora({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-lora",
|
|
||||||
})
|
|
||||||
|
|
||||||
const playfairDisplay = Playfair_Display({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-playfair-display",
|
|
||||||
})
|
|
||||||
|
|
||||||
const PREVIEW_FONTS = {
|
|
||||||
geist: geistSans,
|
|
||||||
inter,
|
|
||||||
"noto-sans": notoSans,
|
|
||||||
"nunito-sans": nunitoSans,
|
|
||||||
figtree,
|
|
||||||
roboto,
|
|
||||||
raleway,
|
|
||||||
"dm-sans": dmSans,
|
|
||||||
"public-sans": publicSans,
|
|
||||||
outfit,
|
|
||||||
oxanium,
|
|
||||||
manrope,
|
|
||||||
"space-grotesk": spaceGrotesk,
|
|
||||||
montserrat,
|
|
||||||
"ibm-plex-sans": ibmPlexSans,
|
|
||||||
"source-sans-3": sourceSans3,
|
|
||||||
"instrument-sans": instrumentSans,
|
|
||||||
"jetbrains-mono": jetbrainsMono,
|
|
||||||
"geist-mono": geistMono,
|
|
||||||
"noto-serif": notoSerif,
|
|
||||||
"roboto-slab": robotoSlab,
|
|
||||||
merriweather,
|
|
||||||
lora,
|
|
||||||
"playfair-display": playfairDisplay,
|
|
||||||
} satisfies Record<FontName, PreviewFont>
|
|
||||||
|
|
||||||
function createFontOption(name: FontName) {
|
|
||||||
const definition = FONT_DEFINITIONS.find((font) => font.name === name)
|
|
||||||
|
|
||||||
if (!definition) {
|
|
||||||
throw new Error(`Unknown font definition: ${name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: definition.title,
|
|
||||||
value: definition.name,
|
|
||||||
font: PREVIEW_FONTS[name],
|
|
||||||
type: definition.type,
|
|
||||||
} as const
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FONTS = [
|
|
||||||
createFontOption("geist"),
|
|
||||||
createFontOption("inter"),
|
|
||||||
createFontOption("noto-sans"),
|
|
||||||
createFontOption("nunito-sans"),
|
|
||||||
createFontOption("figtree"),
|
|
||||||
createFontOption("roboto"),
|
|
||||||
createFontOption("raleway"),
|
|
||||||
createFontOption("dm-sans"),
|
|
||||||
createFontOption("public-sans"),
|
|
||||||
createFontOption("outfit"),
|
|
||||||
createFontOption("oxanium"),
|
|
||||||
createFontOption("manrope"),
|
|
||||||
createFontOption("space-grotesk"),
|
|
||||||
createFontOption("montserrat"),
|
|
||||||
createFontOption("ibm-plex-sans"),
|
|
||||||
createFontOption("source-sans-3"),
|
|
||||||
createFontOption("instrument-sans"),
|
|
||||||
createFontOption("geist-mono"),
|
|
||||||
createFontOption("jetbrains-mono"),
|
|
||||||
createFontOption("noto-serif"),
|
|
||||||
createFontOption("roboto-slab"),
|
|
||||||
createFontOption("merriweather"),
|
|
||||||
createFontOption("lora"),
|
|
||||||
createFontOption("playfair-display"),
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type Font = (typeof FONTS)[number]
|
|
||||||
|
|
||||||
export const FONT_HEADING_OPTIONS = [
|
|
||||||
{
|
|
||||||
name: "Inherit",
|
|
||||||
value: "inherit",
|
|
||||||
font: null,
|
|
||||||
type: "default",
|
|
||||||
},
|
|
||||||
...FONTS,
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type FontHeadingOption = (typeof FONT_HEADING_OPTIONS)[number]
|
|
||||||
|
|||||||
@@ -1,309 +1 @@
|
|||||||
import * as React from "react"
|
export * from "@/app/(app)/create/lib/search-params"
|
||||||
import { useSearchParams } from "next/navigation"
|
|
||||||
import { useQueryStates } from "nuqs"
|
|
||||||
import {
|
|
||||||
createLoader,
|
|
||||||
createSerializer,
|
|
||||||
parseAsBoolean,
|
|
||||||
parseAsInteger,
|
|
||||||
parseAsString,
|
|
||||||
parseAsStringLiteral,
|
|
||||||
type inferParserType,
|
|
||||||
type Options,
|
|
||||||
} from "nuqs/server"
|
|
||||||
import { decodePreset, isPresetCode } from "shadcn/preset"
|
|
||||||
|
|
||||||
import {
|
|
||||||
BASE_COLORS,
|
|
||||||
BASES,
|
|
||||||
DEFAULT_CONFIG,
|
|
||||||
getThemesForBaseColor,
|
|
||||||
iconLibraries,
|
|
||||||
MENU_ACCENTS,
|
|
||||||
MENU_COLORS,
|
|
||||||
RADII,
|
|
||||||
STYLES,
|
|
||||||
THEMES,
|
|
||||||
type BaseColorName,
|
|
||||||
type BaseName,
|
|
||||||
type ChartColorName,
|
|
||||||
type FontHeadingValue,
|
|
||||||
type FontValue,
|
|
||||||
type IconLibraryName,
|
|
||||||
type MenuAccentValue,
|
|
||||||
type MenuColorValue,
|
|
||||||
type RadiusValue,
|
|
||||||
type StyleName,
|
|
||||||
type ThemeName,
|
|
||||||
} from "@/registry/config"
|
|
||||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
|
||||||
import { getPresetCode } from "@/app/(create)/lib/preset-code"
|
|
||||||
import { resolvePresetOverrides } from "@/app/(create)/lib/preset-query"
|
|
||||||
|
|
||||||
const designSystemSearchParams = {
|
|
||||||
preset: parseAsString.withDefault("b0"),
|
|
||||||
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
|
|
||||||
DEFAULT_CONFIG.base
|
|
||||||
),
|
|
||||||
item: parseAsString.withDefault("preview").withOptions({ shallow: true }),
|
|
||||||
iconLibrary: parseAsStringLiteral<IconLibraryName>(
|
|
||||||
Object.values(iconLibraries).map((i) => i.name)
|
|
||||||
).withDefault(DEFAULT_CONFIG.iconLibrary),
|
|
||||||
style: parseAsStringLiteral<StyleName>(STYLES.map((s) => s.name)).withDefault(
|
|
||||||
DEFAULT_CONFIG.style
|
|
||||||
),
|
|
||||||
theme: parseAsStringLiteral<ThemeName>(THEMES.map((t) => t.name)).withDefault(
|
|
||||||
DEFAULT_CONFIG.theme
|
|
||||||
),
|
|
||||||
chartColor: parseAsStringLiteral<ChartColorName>(
|
|
||||||
THEMES.map((t) => t.name)
|
|
||||||
).withDefault(DEFAULT_CONFIG.chartColor ?? "neutral"),
|
|
||||||
font: parseAsStringLiteral<FontValue>(FONTS.map((f) => f.value)).withDefault(
|
|
||||||
DEFAULT_CONFIG.font
|
|
||||||
),
|
|
||||||
fontHeading: parseAsStringLiteral<FontHeadingValue>([
|
|
||||||
"inherit",
|
|
||||||
...FONTS.map((f) => f.value),
|
|
||||||
]).withDefault(DEFAULT_CONFIG.fontHeading),
|
|
||||||
baseColor: parseAsStringLiteral<BaseColorName>(
|
|
||||||
BASE_COLORS.map((b) => b.name)
|
|
||||||
).withDefault(DEFAULT_CONFIG.baseColor),
|
|
||||||
menuAccent: parseAsStringLiteral<MenuAccentValue>(
|
|
||||||
MENU_ACCENTS.map((a) => a.value)
|
|
||||||
).withDefault(DEFAULT_CONFIG.menuAccent),
|
|
||||||
menuColor: parseAsStringLiteral<MenuColorValue>(
|
|
||||||
MENU_COLORS.map((m) => m.value)
|
|
||||||
).withDefault(DEFAULT_CONFIG.menuColor),
|
|
||||||
radius: parseAsStringLiteral<RadiusValue>(
|
|
||||||
RADII.map((r) => r.name)
|
|
||||||
).withDefault("default"),
|
|
||||||
template: parseAsStringLiteral([
|
|
||||||
"next",
|
|
||||||
"next-monorepo",
|
|
||||||
"start",
|
|
||||||
"start-monorepo",
|
|
||||||
"react-router",
|
|
||||||
"react-router-monorepo",
|
|
||||||
"vite",
|
|
||||||
"vite-monorepo",
|
|
||||||
"astro",
|
|
||||||
"astro-monorepo",
|
|
||||||
"laravel",
|
|
||||||
] as const).withDefault("next"),
|
|
||||||
rtl: parseAsBoolean.withDefault(false),
|
|
||||||
size: parseAsInteger.withDefault(100),
|
|
||||||
custom: parseAsBoolean.withDefault(false),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Design system param keys that get encoded into the preset code.
|
|
||||||
const DESIGN_SYSTEM_KEYS = [
|
|
||||||
"style",
|
|
||||||
"baseColor",
|
|
||||||
"theme",
|
|
||||||
"chartColor",
|
|
||||||
"iconLibrary",
|
|
||||||
"font",
|
|
||||||
"fontHeading",
|
|
||||||
"radius",
|
|
||||||
"menuAccent",
|
|
||||||
"menuColor",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
function normalizeFontHeading(
|
|
||||||
font: FontValue,
|
|
||||||
fontHeading: FontHeadingValue
|
|
||||||
): FontHeadingValue {
|
|
||||||
// Persist "same as body" as an explicit inherit sentinel so the body font
|
|
||||||
// can change later without freezing headings to a concrete previous value.
|
|
||||||
return fontHeading === font ? "inherit" : fontHeading
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-design-system keys that get passed through as-is.
|
|
||||||
// `base` is not encoded in preset codes — it's an architectural choice, not visual.
|
|
||||||
const NON_DESIGN_SYSTEM_KEYS = [
|
|
||||||
"base",
|
|
||||||
"item",
|
|
||||||
"preset",
|
|
||||||
"template",
|
|
||||||
"rtl",
|
|
||||||
"size",
|
|
||||||
"custom",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export const loadDesignSystemSearchParams = createLoader(
|
|
||||||
designSystemSearchParams
|
|
||||||
)
|
|
||||||
|
|
||||||
export const serializeDesignSystemSearchParams = createSerializer(
|
|
||||||
designSystemSearchParams
|
|
||||||
)
|
|
||||||
|
|
||||||
export type DesignSystemSearchParams = inferParserType<
|
|
||||||
typeof designSystemSearchParams
|
|
||||||
>
|
|
||||||
|
|
||||||
export function isTranslucentMenuColor(
|
|
||||||
menuColor?: MenuColorValue | null
|
|
||||||
): menuColor is "default-translucent" | "inverted-translucent" {
|
|
||||||
return (
|
|
||||||
menuColor === "default-translucent" || menuColor === "inverted-translucent"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePartialDesignSystemParams(
|
|
||||||
params: Partial<DesignSystemSearchParams>
|
|
||||||
): Partial<DesignSystemSearchParams> {
|
|
||||||
if (
|
|
||||||
params.menuAccent === "bold" &&
|
|
||||||
isTranslucentMenuColor(params.menuColor ?? undefined)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...params,
|
|
||||||
menuAccent: "subtle",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeDesignSystemParams(
|
|
||||||
params: DesignSystemSearchParams
|
|
||||||
): DesignSystemSearchParams {
|
|
||||||
let result = {
|
|
||||||
...params,
|
|
||||||
fontHeading: normalizeFontHeading(params.font, params.fontHeading),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate theme and chartColor against baseColor.
|
|
||||||
if (result.baseColor) {
|
|
||||||
const available = getThemesForBaseColor(result.baseColor)
|
|
||||||
const themeValid = available.some((t) => t.name === result.theme)
|
|
||||||
const chartColorValid = available.some((t) => t.name === result.chartColor)
|
|
||||||
|
|
||||||
if (!themeValid || !chartColorValid) {
|
|
||||||
const fallback = (available[0]?.name ?? result.baseColor) as ThemeName
|
|
||||||
result = {
|
|
||||||
...result,
|
|
||||||
...(!themeValid && { theme: fallback }),
|
|
||||||
...(!chartColorValid && { chartColor: fallback as ChartColorName }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
result.menuAccent === "bold" &&
|
|
||||||
isTranslucentMenuColor(result.menuColor)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
menuAccent: "subtle",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// If preset param exists, decode it and overlay on raw params.
|
|
||||||
// V1 presets don't encode chartColor — fall back to the colored
|
|
||||||
// theme that base-color themes originally borrowed charts from.
|
|
||||||
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
|
|
||||||
|
|
||||||
function resolvePresetParams(
|
|
||||||
rawParams: DesignSystemSearchParams,
|
|
||||||
searchParams: SearchParamsLike
|
|
||||||
) {
|
|
||||||
if (rawParams.preset && isPresetCode(rawParams.preset)) {
|
|
||||||
const decoded = decodePreset(rawParams.preset)
|
|
||||||
if (decoded) {
|
|
||||||
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
|
|
||||||
return normalizeDesignSystemParams({
|
|
||||||
...decoded,
|
|
||||||
...presetOverrides,
|
|
||||||
base: rawParams.base,
|
|
||||||
item: rawParams.item,
|
|
||||||
preset: rawParams.preset,
|
|
||||||
template: rawParams.template,
|
|
||||||
rtl: rawParams.rtl,
|
|
||||||
size: rawParams.size,
|
|
||||||
custom: rawParams.custom,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return normalizeDesignSystemParams(rawParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wraps nuqs useQueryStates with transparent preset encoding/decoding.
|
|
||||||
// - Reads: if ?preset=CODE is in the URL, decodes it and returns individual values.
|
|
||||||
// - Writes: when design system params are set, encodes them into a preset code.
|
|
||||||
export function useDesignSystemSearchParams(options: Options = {}) {
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const [rawParams, rawSetParams] = useQueryStates(designSystemSearchParams, {
|
|
||||||
shallow: false,
|
|
||||||
history: "push",
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
|
|
||||||
const params = React.useMemo(
|
|
||||||
() => resolvePresetParams(rawParams, searchParams),
|
|
||||||
[rawParams, searchParams]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Use ref so setParams callback stays stable across renders.
|
|
||||||
const paramsRef = React.useRef(params)
|
|
||||||
React.useEffect(() => {
|
|
||||||
paramsRef.current = params
|
|
||||||
}, [params])
|
|
||||||
|
|
||||||
type RawSetParamsInput = Parameters<typeof rawSetParams>[0]
|
|
||||||
|
|
||||||
const setParams = React.useCallback(
|
|
||||||
(
|
|
||||||
updates:
|
|
||||||
| Partial<DesignSystemSearchParams>
|
|
||||||
| ((
|
|
||||||
old: DesignSystemSearchParams
|
|
||||||
) => Partial<DesignSystemSearchParams>),
|
|
||||||
setOptions?: Options
|
|
||||||
) => {
|
|
||||||
const resolvedUpdates = normalizePartialDesignSystemParams(
|
|
||||||
typeof updates === "function" ? updates(paramsRef.current) : updates
|
|
||||||
)
|
|
||||||
|
|
||||||
const hasDesignSystemUpdate = DESIGN_SYSTEM_KEYS.some(
|
|
||||||
(key) => key in resolvedUpdates
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!hasDesignSystemUpdate) {
|
|
||||||
// No design system change, pass through directly.
|
|
||||||
return rawSetParams(resolvedUpdates as RawSetParamsInput, setOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge current decoded values with updates.
|
|
||||||
const merged = normalizeDesignSystemParams({
|
|
||||||
...paramsRef.current,
|
|
||||||
...resolvedUpdates,
|
|
||||||
})
|
|
||||||
// Encode design system fields into a preset code.
|
|
||||||
// Cast needed: merged values may include null from nuqs resets,
|
|
||||||
// but encodePreset handles missing values by falling back to defaults.
|
|
||||||
const code = getPresetCode(merged)
|
|
||||||
// Build update: set preset, clear individual DS params from URL.
|
|
||||||
const rawUpdate: Record<string, unknown> = { preset: code }
|
|
||||||
for (const key of DESIGN_SYSTEM_KEYS) {
|
|
||||||
rawUpdate[key] = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass through non-DS params that were explicitly in the update.
|
|
||||||
for (const key of NON_DESIGN_SYSTEM_KEYS) {
|
|
||||||
if (key in resolvedUpdates) {
|
|
||||||
rawUpdate[key] = (resolvedUpdates as Record<string, unknown>)[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawSetParams(rawUpdate as RawSetParamsInput, setOptions)
|
|
||||||
},
|
|
||||||
[rawSetParams]
|
|
||||||
)
|
|
||||||
|
|
||||||
return [params, setParams] as const
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import { siteConfig } from "@/lib/config"
|
|||||||
import { absoluteUrl } from "@/lib/utils"
|
import { absoluteUrl } from "@/lib/utils"
|
||||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||||
import { BASES, type Base, type BaseName } from "@/registry/config"
|
import { BASES, type Base, type BaseName } from "@/registry/config"
|
||||||
import { ActionMenuScript } from "@/app/(create)/components/action-menu"
|
import { ActionMenuScript } from "@/app/(app)/create/components/action-menu"
|
||||||
import { DesignSystemProvider } from "@/app/(create)/components/design-system-provider"
|
import { DesignSystemProvider } from "@/app/(app)/create/components/design-system-provider"
|
||||||
import { HistoryScript } from "@/app/(create)/components/history-buttons"
|
import { HistoryScript } from "@/app/(app)/create/components/history-buttons"
|
||||||
import { DarkModeScript } from "@/app/(create)/components/mode-switcher"
|
import { DarkModeScript } from "@/app/(app)/create/components/mode-switcher"
|
||||||
import { PreviewStyle } from "@/app/(create)/components/preview-style"
|
import { PreviewStyle } from "@/app/(app)/create/components/preview-style"
|
||||||
import { RandomizeScript } from "@/app/(create)/components/random-button"
|
import { RandomizeScript } from "@/app/(app)/create/components/random-button"
|
||||||
import {
|
import {
|
||||||
getBaseComponent,
|
getBaseComponent,
|
||||||
getBaseItem,
|
getBaseItem,
|
||||||
getItemsForBase,
|
getItemsForBase,
|
||||||
} from "@/app/(create)/lib/api"
|
} from "@/app/(app)/create/lib/api"
|
||||||
|
|
||||||
export const revalidate = false
|
export const revalidate = false
|
||||||
export const dynamic = "force-static"
|
export const dynamic = "force-static"
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { addDays, format } from "date-fns"
|
|
||||||
import { CalendarIcon } from "lucide-react"
|
|
||||||
import { type DateRange } from "react-day-picker"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/popover"
|
|
||||||
|
|
||||||
export function AnalyticsDatePicker() {
|
|
||||||
const [date, setDate] = React.useState<DateRange | undefined>({
|
|
||||||
from: new Date(new Date().getFullYear(), 0, 20),
|
|
||||||
to: addDays(new Date(new Date().getFullYear(), 0, 20), 20),
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
id="date"
|
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
"w-fit justify-start px-2 font-normal",
|
|
||||||
!date && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="text-muted-foreground" />
|
|
||||||
{date?.from ? (
|
|
||||||
date.to ? (
|
|
||||||
<>
|
|
||||||
{format(date.from, "LLL dd, y")} -{" "}
|
|
||||||
{format(date.to, "LLL dd, y")}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
format(date.from, "LLL dd, y")
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span>Pick a date</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="end">
|
|
||||||
<Calendar
|
|
||||||
initialFocus
|
|
||||||
mode="range"
|
|
||||||
defaultMonth={date?.from}
|
|
||||||
selected={date}
|
|
||||||
onSelect={setDate}
|
|
||||||
numberOfMonths={2}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
ChartLineIcon,
|
|
||||||
FileIcon,
|
|
||||||
HomeIcon,
|
|
||||||
LifeBuoy,
|
|
||||||
Send,
|
|
||||||
Settings2Icon,
|
|
||||||
ShoppingBagIcon,
|
|
||||||
ShoppingCartIcon,
|
|
||||||
UserIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import { Sidebar, SidebarContent } from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
import { NavMain } from "@/app/(examples)/dashboard-03/components/nav-main"
|
|
||||||
import { NavSecondary } from "@/app/(examples)/dashboard-03/components/nav-secondary"
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
navMain: [
|
|
||||||
{
|
|
||||||
title: "Dashboard",
|
|
||||||
url: "/dashboard",
|
|
||||||
icon: HomeIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Analytics",
|
|
||||||
url: "/dashboard/analytics",
|
|
||||||
icon: ChartLineIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Orders",
|
|
||||||
url: "/dashboard/orders",
|
|
||||||
icon: ShoppingBagIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Products",
|
|
||||||
url: "/dashboard/products",
|
|
||||||
icon: ShoppingCartIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Invoices",
|
|
||||||
url: "/dashboard/invoices",
|
|
||||||
icon: FileIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Customers",
|
|
||||||
url: "/dashboard/customers",
|
|
||||||
icon: UserIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
url: "/dashboard/settings",
|
|
||||||
icon: Settings2Icon,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
navSecondary: [
|
|
||||||
{
|
|
||||||
title: "Support",
|
|
||||||
url: "#",
|
|
||||||
icon: LifeBuoy,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Feedback",
|
|
||||||
url: "#",
|
|
||||||
icon: Send,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
||||||
return (
|
|
||||||
<Sidebar
|
|
||||||
className="top-(--header-height) h-[calc(100svh-var(--header-height))]!"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SidebarContent>
|
|
||||||
<NavMain items={data.navMain} />
|
|
||||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
|
||||||
</SidebarContent>
|
|
||||||
</Sidebar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { TrendingUp } from "lucide-react"
|
|
||||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/card"
|
|
||||||
import {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
type ChartConfig,
|
|
||||||
} from "@/registry/new-york-v4/ui/chart"
|
|
||||||
|
|
||||||
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: 346, mobile: 140 },
|
|
||||||
{ month: "July", desktop: 321, mobile: 275 },
|
|
||||||
{ month: "August", desktop: 132, mobile: 95 },
|
|
||||||
{ month: "September", desktop: 189, mobile: 225 },
|
|
||||||
{ month: "October", desktop: 302, mobile: 248 },
|
|
||||||
{ month: "November", desktop: 342, mobile: 285 },
|
|
||||||
{ month: "December", desktop: 328, mobile: 290 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
desktop: {
|
|
||||||
label: "Desktop",
|
|
||||||
color: "var(--chart-1)",
|
|
||||||
},
|
|
||||||
mobile: {
|
|
||||||
label: "Mobile",
|
|
||||||
color: "var(--chart-2)",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig
|
|
||||||
|
|
||||||
export function ChartRevenue() {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardDescription>January - June 2024</CardDescription>
|
|
||||||
<CardTitle className="text-3xl font-bold tracking-tight">
|
|
||||||
$45,231.89
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer config={chartConfig} className="aspect-[3/1]">
|
|
||||||
<BarChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={chartData}
|
|
||||||
margin={{
|
|
||||||
left: -16,
|
|
||||||
right: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="month"
|
|
||||||
tickLine={false}
|
|
||||||
tickMargin={10}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => value.slice(0, 3)}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
tickLine={false}
|
|
||||||
tickMargin={10}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => value.toLocaleString()}
|
|
||||||
domain={[0, "dataMax"]}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={<ChartTooltipContent hideIndicator />}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="desktop"
|
|
||||||
fill="var(--color-desktop)"
|
|
||||||
radius={[0, 0, 4, 4]}
|
|
||||||
stackId={1}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="mobile"
|
|
||||||
fill="var(--color-mobile)"
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
stackId={1}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
|
||||||
<div className="flex gap-2 leading-none font-medium">
|
|
||||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
<div className="leading-none text-muted-foreground">
|
|
||||||
Showing total visitors for the last 6 months
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Label, Pie, PieChart, Sector } from "recharts"
|
|
||||||
import type {
|
|
||||||
PieSectorDataItem,
|
|
||||||
PieSectorShapeProps,
|
|
||||||
} from "recharts/types/polar/Pie"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/card"
|
|
||||||
import {
|
|
||||||
ChartContainer,
|
|
||||||
ChartStyle,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
type ChartConfig,
|
|
||||||
} from "@/registry/new-york-v4/ui/chart"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
|
|
||||||
const desktopData = [
|
|
||||||
{ month: "january", desktop: 186, fill: "var(--color-january)" },
|
|
||||||
{ month: "february", desktop: 305, fill: "var(--color-february)" },
|
|
||||||
{ month: "march", desktop: 237, fill: "var(--color-march)" },
|
|
||||||
{ month: "april", desktop: 173, fill: "var(--color-april)" },
|
|
||||||
{ month: "may", desktop: 209, fill: "var(--color-may)" },
|
|
||||||
]
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
visitors: {
|
|
||||||
label: "Visitors",
|
|
||||||
},
|
|
||||||
desktop: {
|
|
||||||
label: "Desktop",
|
|
||||||
},
|
|
||||||
mobile: {
|
|
||||||
label: "Mobile",
|
|
||||||
},
|
|
||||||
january: {
|
|
||||||
label: "January",
|
|
||||||
color: "var(--chart-1)",
|
|
||||||
},
|
|
||||||
february: {
|
|
||||||
label: "February",
|
|
||||||
color: "var(--chart-2)",
|
|
||||||
},
|
|
||||||
march: {
|
|
||||||
label: "March",
|
|
||||||
color: "var(--chart-3)",
|
|
||||||
},
|
|
||||||
april: {
|
|
||||||
label: "April",
|
|
||||||
color: "var(--chart-4)",
|
|
||||||
},
|
|
||||||
may: {
|
|
||||||
label: "May",
|
|
||||||
color: "var(--chart-5)",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig
|
|
||||||
|
|
||||||
export function ChartVisitors() {
|
|
||||||
const id = "pie-interactive"
|
|
||||||
const [activeMonth, setActiveMonth] = React.useState(desktopData[0].month)
|
|
||||||
|
|
||||||
const activeIndex = React.useMemo(
|
|
||||||
() => desktopData.findIndex((item) => item.month === activeMonth),
|
|
||||||
[activeMonth]
|
|
||||||
)
|
|
||||||
const months = React.useMemo(() => desktopData.map((item) => item.month), [])
|
|
||||||
|
|
||||||
const renderPieShape = React.useCallback(
|
|
||||||
({ index, outerRadius = 0, ...props }: PieSectorShapeProps) => {
|
|
||||||
if (index === activeIndex) {
|
|
||||||
return (
|
|
||||||
<g>
|
|
||||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
|
||||||
<Sector
|
|
||||||
{...props}
|
|
||||||
outerRadius={outerRadius + 25}
|
|
||||||
innerRadius={outerRadius + 12}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Sector {...props} outerRadius={outerRadius} />
|
|
||||||
},
|
|
||||||
[activeIndex]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card data-chart={id}>
|
|
||||||
<ChartStyle id={id} config={chartConfig} />
|
|
||||||
<CardHeader>
|
|
||||||
<CardDescription>January - June 2024</CardDescription>
|
|
||||||
<CardTitle className="text-2xl font-bold">1,234 visitors</CardTitle>
|
|
||||||
<CardAction>
|
|
||||||
<Select value={activeMonth} onValueChange={setActiveMonth}>
|
|
||||||
<SelectTrigger
|
|
||||||
className="ml-auto h-8 w-[120px]"
|
|
||||||
aria-label="Select a value"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Select month" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
{months.map((key) => {
|
|
||||||
const config = chartConfig[key as keyof typeof chartConfig]
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = "color" in config ? config.color : undefined
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectItem key={key} value={key}>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<span
|
|
||||||
className="flex h-3 w-3 shrink-0 rounded-sm"
|
|
||||||
style={{
|
|
||||||
backgroundColor: color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{config?.label}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</CardAction>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-1 justify-center pb-0">
|
|
||||||
<ChartContainer
|
|
||||||
id={id}
|
|
||||||
config={chartConfig}
|
|
||||||
className="mx-auto aspect-square w-full max-w-[300px]"
|
|
||||||
>
|
|
||||||
<PieChart>
|
|
||||||
<ChartTooltip
|
|
||||||
cursor={false}
|
|
||||||
content={<ChartTooltipContent hideLabel />}
|
|
||||||
/>
|
|
||||||
<Pie
|
|
||||||
data={desktopData}
|
|
||||||
dataKey="desktop"
|
|
||||||
nameKey="month"
|
|
||||||
innerRadius={60}
|
|
||||||
strokeWidth={5}
|
|
||||||
shape={renderPieShape}
|
|
||||||
>
|
|
||||||
<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}
|
|
||||||
className="fill-foreground text-3xl font-bold"
|
|
||||||
>
|
|
||||||
{desktopData[activeIndex].desktop.toLocaleString()}
|
|
||||||
</tspan>
|
|
||||||
<tspan
|
|
||||||
x={viewBox.cx}
|
|
||||||
y={(viewBox.cy || 0) + 24}
|
|
||||||
className="fill-muted-foreground"
|
|
||||||
>
|
|
||||||
Visitors
|
|
||||||
</tspan>
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { MoonIcon, SunIcon } from "lucide-react"
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
|
|
||||||
export function ModeToggle() {
|
|
||||||
const { setTheme, resolvedTheme } = useTheme()
|
|
||||||
|
|
||||||
const toggleTheme = React.useCallback(() => {
|
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
|
||||||
}, [resolvedTheme, setTheme])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="icon"
|
|
||||||
className="group/toggle size-8"
|
|
||||||
onClick={toggleTheme}
|
|
||||||
>
|
|
||||||
<SunIcon className="hidden [html.dark_&]:block" />
|
|
||||||
<MoonIcon className="hidden [html.light_&]:block" />
|
|
||||||
<span className="sr-only">Toggle theme</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { usePathname } from "next/navigation"
|
|
||||||
import { ChevronRight, type LucideIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/collapsible"
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
|
|
||||||
export function NavMain({
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
icon: LucideIcon
|
|
||||||
isActive?: boolean
|
|
||||||
items?: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
}[]
|
|
||||||
disabled?: boolean
|
|
||||||
}[]
|
|
||||||
}) {
|
|
||||||
const pathname = usePathname()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel>Dashboard</SidebarGroupLabel>
|
|
||||||
<SidebarMenu>
|
|
||||||
{items.map((item) => (
|
|
||||||
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton
|
|
||||||
asChild
|
|
||||||
tooltip={item.title}
|
|
||||||
isActive={pathname === item.url}
|
|
||||||
disabled={item.disabled}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={item.disabled ? "#" : item.url}
|
|
||||||
data-disabled={item.disabled}
|
|
||||||
className="data-[disabled=true]:opacity-50"
|
|
||||||
>
|
|
||||||
<item.icon className="text-muted-foreground" />
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
{item.items?.length ? (
|
|
||||||
<>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
|
||||||
<ChevronRight />
|
|
||||||
<span className="sr-only">Toggle</span>
|
|
||||||
</SidebarMenuAction>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<SidebarMenuSub>
|
|
||||||
{item.items?.map((subItem) => (
|
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
|
||||||
<SidebarMenuSubButton asChild>
|
|
||||||
<a href={subItem.url}>
|
|
||||||
<span>{subItem.title}</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuSubButton>
|
|
||||||
</SidebarMenuSubItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenuSub>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</Collapsible>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { type LucideIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
|
|
||||||
export function NavSecondary({
|
|
||||||
items,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
icon: LucideIcon
|
|
||||||
}[]
|
|
||||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
|
||||||
return (
|
|
||||||
<SidebarGroup {...props}>
|
|
||||||
<SidebarGroupContent>
|
|
||||||
<SidebarMenu>
|
|
||||||
{items.map((item) => (
|
|
||||||
<SidebarMenuItem key={item.title}>
|
|
||||||
<SidebarMenuButton asChild size="sm">
|
|
||||||
<a href={item.url}>
|
|
||||||
<item.icon />
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { BadgeCheck, Bell, CreditCard, LogOut, Sparkles } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
} from "@/registry/new-york-v4/ui/avatar"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
|
||||||
|
|
||||||
export function NavUser({
|
|
||||||
user,
|
|
||||||
}: {
|
|
||||||
user: {
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
avatar: string
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<Avatar className="size-8 rounded-md">
|
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
||||||
side="bottom"
|
|
||||||
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>
|
|
||||||
<Sparkles />
|
|
||||||
Upgrade to Pro
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<BadgeCheck />
|
|
||||||
Account
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<CreditCard />
|
|
||||||
Billing
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Bell />
|
|
||||||
Notifications
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<LogOut />
|
|
||||||
Log out
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
import {
|
|
||||||
ArrowUpDownIcon,
|
|
||||||
EllipsisVerticalIcon,
|
|
||||||
ListFilterIcon,
|
|
||||||
PlusIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
} from "@/registry/new-york-v4/ui/card"
|
|
||||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
|
||||||
import {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationEllipsis,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
|
||||||
PaginationPrevious,
|
|
||||||
} from "@/registry/new-york-v4/ui/pagination"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/registry/new-york-v4/ui/table"
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/registry/new-york-v4/ui/tabs"
|
|
||||||
|
|
||||||
export function ProductsTable({
|
|
||||||
products,
|
|
||||||
}: {
|
|
||||||
products: {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
price: number
|
|
||||||
stock: number
|
|
||||||
dateAdded: string
|
|
||||||
status: string
|
|
||||||
}[]
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Card className="flex w-full flex-col gap-4">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
|
||||||
<Tabs defaultValue="all">
|
|
||||||
<TabsList className="w-full @3xl/page:w-fit">
|
|
||||||
<TabsTrigger value="all">All Products</TabsTrigger>
|
|
||||||
<TabsTrigger value="in-stock">In Stock</TabsTrigger>
|
|
||||||
<TabsTrigger value="low-stock">Low Stock</TabsTrigger>
|
|
||||||
<TabsTrigger value="add-product" asChild>
|
|
||||||
<button>
|
|
||||||
<PlusIcon />
|
|
||||||
</button>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
<div className="hidden items-center gap-2 **:data-[slot=button]:size-8 **:data-[slot=select-trigger]:h-8 @3xl/page:flex">
|
|
||||||
<Select defaultValue="all">
|
|
||||||
<SelectTrigger>
|
|
||||||
<span className="text-sm text-muted-foreground">Category:</span>
|
|
||||||
<SelectValue placeholder="Select a product" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All</SelectItem>
|
|
||||||
<SelectItem value="in-stock">In Stock</SelectItem>
|
|
||||||
<SelectItem value="low-stock">Low Stock</SelectItem>
|
|
||||||
<SelectItem value="archived">Archived</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select defaultValue="all">
|
|
||||||
<SelectTrigger>
|
|
||||||
<span className="text-sm text-muted-foreground">Price:</span>
|
|
||||||
<SelectValue placeholder="Select a product" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">$100-$200</SelectItem>
|
|
||||||
<SelectItem value="in-stock">$200-$300</SelectItem>
|
|
||||||
<SelectItem value="low-stock">$300-$400</SelectItem>
|
|
||||||
<SelectItem value="archived">$400-$500</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select defaultValue="all">
|
|
||||||
<SelectTrigger>
|
|
||||||
<span className="text-sm text-muted-foreground">Status:</span>
|
|
||||||
<SelectValue placeholder="Select a product" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">In Stock</SelectItem>
|
|
||||||
<SelectItem value="in-stock">Low Stock</SelectItem>
|
|
||||||
<SelectItem value="low-stock">Archived</SelectItem>
|
|
||||||
<SelectItem value="archived">Archived</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<ListFilterIcon />
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<ArrowUpDownIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-12 px-4">
|
|
||||||
<Checkbox />
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>Product</TableHead>
|
|
||||||
<TableHead className="text-right">Price</TableHead>
|
|
||||||
<TableHead className="text-right">Stock</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Date Added</TableHead>
|
|
||||||
<TableHead />
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody className="**:data-[slot=table-cell]:py-2.5">
|
|
||||||
{products.map((product) => (
|
|
||||||
<TableRow key={product.id}>
|
|
||||||
<TableCell className="px-4">
|
|
||||||
<Checkbox />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-medium">{product.name}</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
${product.price.toFixed(2)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">{product.stock}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className={
|
|
||||||
product.status === "Low Stock"
|
|
||||||
? "border-orange-700 bg-transparent text-orange-700 dark:border-orange-700 dark:bg-transparent dark:text-orange-700"
|
|
||||||
: "bg-green-100 text-green-800 dark:bg-green-950 dark:text-green-100"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{product.status}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{new Date(product.dateAdded).toLocaleDateString("en-US", {
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="size-6">
|
|
||||||
<EllipsisVerticalIcon />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem variant="destructive">
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex flex-col items-center justify-between border-t pt-6 @3xl/page:flex-row">
|
|
||||||
<div className="hidden text-sm text-muted-foreground @3xl/page:block">
|
|
||||||
Showing 1-10 of 100 products
|
|
||||||
</div>
|
|
||||||
<Pagination className="mx-0 w-fit">
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationPrevious href="#" />
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationLink href="#">1</PaginationLink>
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationLink href="#" isActive>
|
|
||||||
2
|
|
||||||
</PaginationLink>
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationLink href="#">3</PaginationLink>
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationEllipsis />
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationNext href="#" />
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Search } from "lucide-react"
|
|
||||||
|
|
||||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
|
||||||
import { SidebarInput } from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
|
|
||||||
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
|
|
||||||
return (
|
|
||||||
<form {...props}>
|
|
||||||
<div className="relative">
|
|
||||||
<Label htmlFor="search" className="sr-only">
|
|
||||||
Search
|
|
||||||
</Label>
|
|
||||||
<SidebarInput
|
|
||||||
id="search"
|
|
||||||
placeholder="Type to search..."
|
|
||||||
className="h-8 pl-7"
|
|
||||||
/>
|
|
||||||
<Search className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Fragment, useMemo } from "react"
|
|
||||||
import { usePathname } from "next/navigation"
|
|
||||||
import { SidebarIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { ThemeSelector } from "@/components/theme-selector"
|
|
||||||
import { SearchForm } from "@/registry/new-york-v4/blocks/sidebar-16/components/search-form"
|
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from "@/registry/new-york-v4/ui/breadcrumb"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
|
||||||
import { useSidebar } from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
import { ModeToggle } from "@/app/(examples)/dashboard-03/components/mode-toggle"
|
|
||||||
import { NavUser } from "@/app/(examples)/dashboard-03/components/nav-user"
|
|
||||||
|
|
||||||
export function SiteHeader() {
|
|
||||||
const { toggleSidebar } = useSidebar()
|
|
||||||
const pathname = usePathname()
|
|
||||||
|
|
||||||
// Faux breadcrumbs for demo.
|
|
||||||
const breadcrumbs = useMemo(() => {
|
|
||||||
return pathname
|
|
||||||
.split("/")
|
|
||||||
.filter((path) => path !== "")
|
|
||||||
.map((path, index, array) => ({
|
|
||||||
label: path,
|
|
||||||
href: `/${array.slice(0, index + 1).join("/")}`,
|
|
||||||
}))
|
|
||||||
}, [pathname])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header
|
|
||||||
data-slot="site-header"
|
|
||||||
className="sticky top-0 z-50 flex w-full items-center border-b bg-background"
|
|
||||||
>
|
|
||||||
<div className="flex h-(--header-height) w-full items-center gap-2 px-2 pr-4">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
className="gap-2.5 has-[>svg]:px-2"
|
|
||||||
>
|
|
||||||
<SidebarIcon />
|
|
||||||
<span className="truncate font-medium">Acme Inc</span>
|
|
||||||
</Button>
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="mr-2 data-[orientation=vertical]:h-4"
|
|
||||||
/>
|
|
||||||
<Breadcrumb className="hidden sm:block">
|
|
||||||
<BreadcrumbList>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink href="/" className="capitalize">
|
|
||||||
Home
|
|
||||||
</BreadcrumbLink>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
{breadcrumbs.map((breadcrumb, index) =>
|
|
||||||
index === breadcrumbs.length - 1 ? (
|
|
||||||
<BreadcrumbItem key={index}>
|
|
||||||
<BreadcrumbPage className="capitalize">
|
|
||||||
{breadcrumb.label}
|
|
||||||
</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
) : (
|
|
||||||
<Fragment key={index}>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbLink
|
|
||||||
href={breadcrumb.href}
|
|
||||||
className="capitalize"
|
|
||||||
>
|
|
||||||
{breadcrumb.label}
|
|
||||||
</BreadcrumbLink>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator />
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
<div className="ml-auto flex items-center gap-2">
|
|
||||||
<SearchForm className="w-fullsm:w-auto" />
|
|
||||||
<ThemeSelector />
|
|
||||||
<ModeToggle />
|
|
||||||
<NavUser
|
|
||||||
user={{
|
|
||||||
name: "shadcn",
|
|
||||||
email: "m@example.com",
|
|
||||||
avatar: "/avatars/shadcn.jpg",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export default function CustomersPage() {
|
|
||||||
return (
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="bg-input p-4">Input</div>
|
|
||||||
<div className="bg-input/30 p-4">Input 50</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { cookies } from "next/headers"
|
|
||||||
|
|
||||||
import {
|
|
||||||
SidebarInset,
|
|
||||||
SidebarProvider,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
import { AppSidebar } from "@/app/(examples)/dashboard-03/components/app-sidebar"
|
|
||||||
import { SiteHeader } from "@/app/(examples)/dashboard-03/components/site-header"
|
|
||||||
|
|
||||||
export default async function DashboardLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const cookieStore = await cookies()
|
|
||||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="[--header-height:calc(theme(spacing.14))]">
|
|
||||||
<SidebarProvider defaultOpen={defaultOpen} className="flex flex-col">
|
|
||||||
<SiteHeader />
|
|
||||||
<div className="flex flex-1">
|
|
||||||
<AppSidebar />
|
|
||||||
<SidebarInset>{children}</SidebarInset>
|
|
||||||
</div>
|
|
||||||
</SidebarProvider>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
import { type Metadata } from "next"
|
|
||||||
import {
|
|
||||||
DownloadIcon,
|
|
||||||
FilterIcon,
|
|
||||||
TrendingDownIcon,
|
|
||||||
TrendingUpIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/card"
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/tabs"
|
|
||||||
import { AnalyticsDatePicker } from "@/app/(examples)/dashboard-03/components/analytics-date-picker"
|
|
||||||
import { ChartRevenue } from "@/app/(examples)/dashboard-03/components/chart-revenue"
|
|
||||||
import { ChartVisitors } from "@/app/(examples)/dashboard-03/components/chart-visitors"
|
|
||||||
import { ProductsTable } from "@/app/(examples)/dashboard-03/components/products-table"
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Dashboard",
|
|
||||||
description: "An example dashboard to test the new components.",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load from database.
|
|
||||||
const products = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
name: "BJÖRKSNÄS Dining Table",
|
|
||||||
price: 599.99,
|
|
||||||
stock: 12,
|
|
||||||
dateAdded: "2023-06-15",
|
|
||||||
status: "In Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
name: "POÄNG Armchair",
|
|
||||||
price: 249.99,
|
|
||||||
stock: 28,
|
|
||||||
dateAdded: "2023-07-22",
|
|
||||||
status: "In Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
name: "MALM Bed Frame",
|
|
||||||
price: 399.99,
|
|
||||||
stock: 15,
|
|
||||||
dateAdded: "2023-08-05",
|
|
||||||
status: "In Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
name: "KALLAX Shelf Unit",
|
|
||||||
price: 179.99,
|
|
||||||
stock: 32,
|
|
||||||
dateAdded: "2023-09-12",
|
|
||||||
status: "In Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5",
|
|
||||||
name: "STOCKHOLM Rug",
|
|
||||||
price: 299.99,
|
|
||||||
stock: 8,
|
|
||||||
dateAdded: "2023-10-18",
|
|
||||||
status: "Low Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "6",
|
|
||||||
name: "KIVIK Sofa",
|
|
||||||
price: 899.99,
|
|
||||||
stock: 6,
|
|
||||||
dateAdded: "2023-11-02",
|
|
||||||
status: "Low Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "7",
|
|
||||||
name: "LISABO Coffee Table",
|
|
||||||
price: 149.99,
|
|
||||||
stock: 22,
|
|
||||||
dateAdded: "2023-11-29",
|
|
||||||
status: "In Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "8",
|
|
||||||
name: "HEMNES Bookcase",
|
|
||||||
price: 249.99,
|
|
||||||
stock: 17,
|
|
||||||
dateAdded: "2023-12-10",
|
|
||||||
status: "In Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "9",
|
|
||||||
name: "EKEDALEN Dining Chairs (Set of 2)",
|
|
||||||
price: 199.99,
|
|
||||||
stock: 14,
|
|
||||||
dateAdded: "2024-01-05",
|
|
||||||
status: "In Stock",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "10",
|
|
||||||
name: "FRIHETEN Sleeper Sofa",
|
|
||||||
price: 799.99,
|
|
||||||
stock: 9,
|
|
||||||
dateAdded: "2024-01-18",
|
|
||||||
status: "Low Stock",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function DashboardPage() {
|
|
||||||
return (
|
|
||||||
<div className="@container/page flex flex-1 flex-col gap-8 p-6">
|
|
||||||
<Tabs defaultValue="overview" className="gap-6">
|
|
||||||
<div
|
|
||||||
data-slot="dashboard-header"
|
|
||||||
className="flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<TabsList className="w-full @3xl/page:w-fit">
|
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
||||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
|
||||||
<TabsTrigger value="reports">Reports</TabsTrigger>
|
|
||||||
<TabsTrigger value="exports" disabled>
|
|
||||||
Exports
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="hidden items-center gap-2 @3xl/page:flex">
|
|
||||||
<AnalyticsDatePicker />
|
|
||||||
<Button variant="outline">
|
|
||||||
<FilterIcon />
|
|
||||||
Filter
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline">
|
|
||||||
<DownloadIcon />
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TabsContent value="overview" className="flex flex-col gap-4">
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Total Revenue</CardTitle>
|
|
||||||
<CardDescription>$1,250.00 in the last 30 days</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
<Badge variant="outline">
|
|
||||||
<TrendingUpIcon />
|
|
||||||
+12.5%
|
|
||||||
</Badge>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>New Customers</CardTitle>
|
|
||||||
<CardDescription>-12 customers from last month</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
<Badge variant="outline">
|
|
||||||
<TrendingDownIcon />
|
|
||||||
-20%
|
|
||||||
</Badge>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Active Accounts</CardTitle>
|
|
||||||
<CardDescription>+2,345 users from last month</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
<Badge variant="outline">
|
|
||||||
<TrendingUpIcon />
|
|
||||||
+12.5%
|
|
||||||
</Badge>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Growth Rate</CardTitle>
|
|
||||||
<CardDescription>+12.5% increase per month</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
<Badge variant="outline">
|
|
||||||
<TrendingUpIcon />
|
|
||||||
+4.5%
|
|
||||||
</Badge>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 gap-4 @4xl/page:grid-cols-[2fr_1fr]">
|
|
||||||
<ChartRevenue />
|
|
||||||
<ChartVisitors />
|
|
||||||
</div>
|
|
||||||
<ProductsTable products={products} />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,500 +0,0 @@
|
|||||||
import { type Metadata } from "next"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/card"
|
|
||||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
|
||||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
|
||||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/registry/new-york-v4/ui/table"
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/tabs"
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Settings",
|
|
||||||
description: "Manage your account settings",
|
|
||||||
}
|
|
||||||
|
|
||||||
const timezones = [
|
|
||||||
{
|
|
||||||
label: "Americas",
|
|
||||||
timezones: [
|
|
||||||
{ value: "America/New_York", label: "(GMT-5) New York" },
|
|
||||||
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
|
|
||||||
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
|
|
||||||
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
|
|
||||||
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
|
|
||||||
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Europe",
|
|
||||||
timezones: [
|
|
||||||
{ value: "Europe/London", label: "(GMT+0) London" },
|
|
||||||
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
|
|
||||||
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
|
|
||||||
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
|
|
||||||
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
|
|
||||||
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Asia/Pacific",
|
|
||||||
timezones: [
|
|
||||||
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
|
|
||||||
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
|
|
||||||
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
|
|
||||||
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
|
|
||||||
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
|
|
||||||
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const loginHistory = [
|
|
||||||
{
|
|
||||||
date: "2024-01-01",
|
|
||||||
ip: "192.168.1.1",
|
|
||||||
location: "New York, USA",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2023-12-29",
|
|
||||||
ip: "172.16.0.100",
|
|
||||||
location: "London, UK",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2023-12-28",
|
|
||||||
ip: "10.0.0.50",
|
|
||||||
location: "Toronto, Canada",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2023-12-25",
|
|
||||||
ip: "192.168.2.15",
|
|
||||||
location: "Sydney, Australia",
|
|
||||||
},
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const activeSessions = [
|
|
||||||
{
|
|
||||||
device: "MacBook Pro",
|
|
||||||
browser: "Chrome",
|
|
||||||
os: "macOS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
device: "iPhone",
|
|
||||||
browser: "Safari",
|
|
||||||
os: "iOS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
device: "iPad",
|
|
||||||
browser: "Safari",
|
|
||||||
os: "iOS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
device: "Android Phone",
|
|
||||||
browser: "Chrome",
|
|
||||||
os: "Android",
|
|
||||||
},
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
|
||||||
return (
|
|
||||||
<div className="@container/page flex flex-1 flex-col gap-8 p-6">
|
|
||||||
<Tabs defaultValue="account" className="gap-6">
|
|
||||||
<div
|
|
||||||
data-slot="dashboard-header"
|
|
||||||
className="flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="account">Account</TabsTrigger>
|
|
||||||
<TabsTrigger value="security">Security</TabsTrigger>
|
|
||||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
|
||||||
<TabsTrigger value="privacy">Privacy</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</div>
|
|
||||||
<TabsContent value="account" className="grid gap-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Account Settings</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Make changes to your account here.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form id="form-account" className="@container">
|
|
||||||
<FieldGroup>
|
|
||||||
<Field>
|
|
||||||
<Label htmlFor="name">Name</Label>
|
|
||||||
<FieldControl>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
placeholder="First and last name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</FieldControl>
|
|
||||||
<FieldDescription>
|
|
||||||
This is your public display name.
|
|
||||||
</FieldDescription>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<Label htmlFor="email">Email</Label>
|
|
||||||
<FieldControl>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
placeholder="you@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</FieldControl>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<Label htmlFor="timezone">Timezone</Label>
|
|
||||||
<FieldControl>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id="timezone">
|
|
||||||
<SelectValue placeholder="Select a timezone" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{timezones.map((timezone) => (
|
|
||||||
<SelectGroup key={timezone.label}>
|
|
||||||
<SelectLabel>{timezone.label}</SelectLabel>
|
|
||||||
{timezone.timezones.map((time) => (
|
|
||||||
<SelectItem key={time.value} value={time.value}>
|
|
||||||
{time.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FieldControl>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="border-t">
|
|
||||||
<Button type="submit" form="form-account" variant="secondary">
|
|
||||||
Save changes
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Notifications</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Manage how you receive notifications.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form id="form-notifications" className="@container">
|
|
||||||
<FieldGroup>
|
|
||||||
<Field>
|
|
||||||
<Label htmlFor="channels">Notification Channels</Label>
|
|
||||||
<FieldControl className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox id="notification-email" />
|
|
||||||
<Label htmlFor="notification-email">Email</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox id="notification-sms" />
|
|
||||||
<Label htmlFor="notification-sms">SMS</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox id="notification-push" />
|
|
||||||
<Label htmlFor="notification-push">Push</Label>
|
|
||||||
</div>
|
|
||||||
</FieldControl>
|
|
||||||
<FieldDescription>
|
|
||||||
Choose how you want to receive notifications.
|
|
||||||
</FieldDescription>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<Label htmlFor="types">Notification Types</Label>
|
|
||||||
<FieldControl className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox id="notification-account" />
|
|
||||||
<Label htmlFor="notification-account">
|
|
||||||
Account Activity
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id="notification-security"
|
|
||||||
defaultChecked
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<Label htmlFor="notification-security">
|
|
||||||
Security Alerts
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox id="notification-marketing" />
|
|
||||||
<Label htmlFor="notification-marketing">
|
|
||||||
Marketing & Promotions
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FieldControl>
|
|
||||||
<FieldDescription>
|
|
||||||
Choose how you want to receive notifications.
|
|
||||||
</FieldDescription>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="border-t">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
form="form-notifications"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
Save changes
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent
|
|
||||||
value="security"
|
|
||||||
className="grid gap-6 @3xl/page:grid-cols-2"
|
|
||||||
>
|
|
||||||
<Card className="@3xl/page:col-span-2">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Security Settings</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Make changes to your security settings here.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="@container">
|
|
||||||
<form id="form-security">
|
|
||||||
<FieldGroup>
|
|
||||||
<Field>
|
|
||||||
<Label htmlFor="current-password">Current Password</Label>
|
|
||||||
<FieldControl>
|
|
||||||
<Input
|
|
||||||
id="current-password"
|
|
||||||
placeholder="Current password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</FieldControl>
|
|
||||||
<FieldDescription>
|
|
||||||
This is your current password.
|
|
||||||
</FieldDescription>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<Label htmlFor="new-password">New Password</Label>
|
|
||||||
<FieldControl>
|
|
||||||
<Input
|
|
||||||
id="new-password"
|
|
||||||
placeholder="New password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</FieldControl>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<Label htmlFor="confirm-password">Confirm Password</Label>
|
|
||||||
<FieldControl>
|
|
||||||
<Input
|
|
||||||
id="confirm-password"
|
|
||||||
placeholder="Confirm password"
|
|
||||||
/>
|
|
||||||
</FieldControl>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldControl>
|
|
||||||
<Switch
|
|
||||||
id="enable-two-factor-auth"
|
|
||||||
className="self-start"
|
|
||||||
/>
|
|
||||||
</FieldControl>
|
|
||||||
<Label htmlFor="enable-two-factor-auth">
|
|
||||||
Enable two-factor authentication
|
|
||||||
</Label>
|
|
||||||
<FieldDescription>
|
|
||||||
This will add an extra layer of security to your account.
|
|
||||||
Make this an extra long description to test the layout.
|
|
||||||
</FieldDescription>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="border-t">
|
|
||||||
<Button type="submit" form="form-security" variant="secondary">
|
|
||||||
Save changes
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Login History</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Recent login activities on your account.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Date</TableHead>
|
|
||||||
<TableHead className="hidden @md/page:table-cell">
|
|
||||||
IP
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>Location</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{loginHistory.map((login) => (
|
|
||||||
<TableRow key={login.date}>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{new Date(login.date).toLocaleDateString("en-US", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
<span className="flex @md/page:hidden">
|
|
||||||
{login.ip}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="hidden @md/page:table-cell">
|
|
||||||
{login.ip}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{login.location}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Active Sessions</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Current active sessions on your account.
|
|
||||||
</CardDescription>
|
|
||||||
<CardAction>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<span className="hidden @md/card-header:block">
|
|
||||||
Manage Sessions
|
|
||||||
</span>
|
|
||||||
<span className="block @md/card-header:hidden">Manage</span>
|
|
||||||
</Button>
|
|
||||||
</CardAction>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Device</TableHead>
|
|
||||||
<TableHead>Browser</TableHead>
|
|
||||||
<TableHead>OS</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{activeSessions.map((session) => (
|
|
||||||
<TableRow key={session.device}>
|
|
||||||
<TableCell>{session.device}</TableCell>
|
|
||||||
<TableCell>{session.browser}</TableCell>
|
|
||||||
<TableCell>{session.os}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldGroup({ children }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-group"
|
|
||||||
className="@container/field-group flex max-w-4xl min-w-0 flex-col gap-8 @3xl:gap-6"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Field({ children, className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field"
|
|
||||||
className={cn(
|
|
||||||
"grid auto-rows-min items-start gap-3 *:data-[slot=label]:col-start-1 *:data-[slot=label]:row-start-1 @3xl/field-group:grid-cols-2 @3xl/field-group:gap-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldControl({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-control"
|
|
||||||
className={cn(
|
|
||||||
"@3xl/field-group:col-start-2 @3xl/field-group:row-span-2 @3xl/field-group:row-start-1 @3xl/field-group:self-start",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldDescription({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="field-description"
|
|
||||||
className={cn(
|
|
||||||
"text-sm text-muted-foreground @3xl/field-group:col-start-1 @3xl/field-group:row-start-1 @3xl/field-group:translate-y-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
IconCamera,
|
|
||||||
IconChartBar,
|
|
||||||
IconDashboard,
|
|
||||||
IconDatabase,
|
|
||||||
IconFileAi,
|
|
||||||
IconFileDescription,
|
|
||||||
IconFileWord,
|
|
||||||
IconFolder,
|
|
||||||
IconHelp,
|
|
||||||
IconInnerShadowTop,
|
|
||||||
IconListDetails,
|
|
||||||
IconReport,
|
|
||||||
IconSearch,
|
|
||||||
IconSettings,
|
|
||||||
IconUsers,
|
|
||||||
} from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
import { NavDocuments } from "@/app/(examples)/dashboard/components/nav-documents"
|
|
||||||
import { NavMain } from "@/app/(examples)/dashboard/components/nav-main"
|
|
||||||
import { NavSecondary } from "@/app/(examples)/dashboard/components/nav-secondary"
|
|
||||||
import { NavUser } from "@/app/(examples)/dashboard/components/nav-user"
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
user: {
|
|
||||||
name: "shadcn",
|
|
||||||
email: "m@example.com",
|
|
||||||
avatar: "/avatars/shadcn.jpg",
|
|
||||||
},
|
|
||||||
navMain: [
|
|
||||||
{
|
|
||||||
title: "Dashboard",
|
|
||||||
url: "#",
|
|
||||||
icon: IconDashboard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Lifecycle",
|
|
||||||
url: "#",
|
|
||||||
icon: IconListDetails,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Analytics",
|
|
||||||
url: "#",
|
|
||||||
icon: IconChartBar,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Projects",
|
|
||||||
url: "#",
|
|
||||||
icon: IconFolder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Team",
|
|
||||||
url: "#",
|
|
||||||
icon: IconUsers,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
navClouds: [
|
|
||||||
{
|
|
||||||
title: "Capture",
|
|
||||||
icon: IconCamera,
|
|
||||||
isActive: true,
|
|
||||||
url: "#",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Active Proposals",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Archived",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Proposal",
|
|
||||||
icon: IconFileDescription,
|
|
||||||
url: "#",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Active Proposals",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Archived",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Prompts",
|
|
||||||
icon: IconFileAi,
|
|
||||||
url: "#",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Active Proposals",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Archived",
|
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
navSecondary: [
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
url: "#",
|
|
||||||
icon: IconSettings,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Get Help",
|
|
||||||
url: "#",
|
|
||||||
icon: IconHelp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Search",
|
|
||||||
url: "#",
|
|
||||||
icon: IconSearch,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
name: "Data Library",
|
|
||||||
url: "#",
|
|
||||||
icon: IconDatabase,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Reports",
|
|
||||||
url: "#",
|
|
||||||
icon: IconReport,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Word Assistant",
|
|
||||||
url: "#",
|
|
||||||
icon: IconFileWord,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
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="#">
|
|
||||||
<IconInnerShadowTop 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
|
||||||
|
|
||||||
import { useIsMobile } from "@/registry/new-york-v4/hooks/use-mobile"
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/card"
|
|
||||||
import {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
type ChartConfig,
|
|
||||||
} from "@/registry/new-york-v4/ui/chart"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
import {
|
|
||||||
ToggleGroup,
|
|
||||||
ToggleGroupItem,
|
|
||||||
} from "@/registry/new-york-v4/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}
|
|
||||||
defaultIndex={isMobile ? -1 : 10}
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,807 +0,0 @@
|
|||||||
"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 {
|
|
||||||
IconChevronDown,
|
|
||||||
IconChevronLeft,
|
|
||||||
IconChevronRight,
|
|
||||||
IconChevronsLeft,
|
|
||||||
IconChevronsRight,
|
|
||||||
IconCircleCheckFilled,
|
|
||||||
IconDotsVertical,
|
|
||||||
IconGripVertical,
|
|
||||||
IconLayoutColumns,
|
|
||||||
IconLoader,
|
|
||||||
IconPlus,
|
|
||||||
IconTrendingUp,
|
|
||||||
} from "@tabler/icons-react"
|
|
||||||
import {
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getFacetedRowModel,
|
|
||||||
getFacetedUniqueValues,
|
|
||||||
getFilteredRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
useReactTable,
|
|
||||||
type ColumnDef,
|
|
||||||
type ColumnFiltersState,
|
|
||||||
type Row,
|
|
||||||
type SortingState,
|
|
||||||
type VisibilityState,
|
|
||||||
} from "@tanstack/react-table"
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
import { useIsMobile } from "@/registry/new-york-v4/hooks/use-mobile"
|
|
||||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
type ChartConfig,
|
|
||||||
} from "@/registry/new-york-v4/ui/chart"
|
|
||||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/drawer"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
|
||||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
|
||||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/registry/new-york-v4/ui/table"
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/tabs"
|
|
||||||
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<IconGripVertical className="size-3 text-muted-foreground" />
|
|
||||||
<span className="sr-only">Drag to reorder</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
|
||||||
{
|
|
||||||
id: "drag",
|
|
||||||
header: () => null,
|
|
||||||
cell: ({ row }) => <DragHandle id={row.original.id} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "header",
|
|
||||||
header: "Header",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return <TableCellViewer item={row.original} />
|
|
||||||
},
|
|
||||||
enableHiding: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "status",
|
|
||||||
header: "Status",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
|
||||||
{row.original.status === "Done" ? (
|
|
||||||
<IconCircleCheckFilled className="fill-green-500 dark:fill-green-400" />
|
|
||||||
) : (
|
|
||||||
<IconLoader />
|
|
||||||
)}
|
|
||||||
{row.original.status}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "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">
|
|
||||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
|
||||||
<SelectItem value="Jamik Tashpulatov">
|
|
||||||
Jamik Tashpulatov
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "actions",
|
|
||||||
cell: () => (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
|
|
||||||
size="icon"
|
|
||||||
>
|
|
||||||
<IconDotsVertical />
|
|
||||||
<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<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.column.columnDef.cell, cell.getContext())}
|
|
||||||
</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<VisibilityState>({})
|
|
||||||
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 = useReactTable({
|
|
||||||
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,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
getFacetedRowModel: getFacetedRowModel(),
|
|
||||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
||||||
})
|
|
||||||
|
|
||||||
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>
|
|
||||||
<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>
|
|
||||||
</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">
|
|
||||||
<IconLayoutColumns />
|
|
||||||
<span className="hidden lg:inline">Customize Columns</span>
|
|
||||||
<span className="lg:hidden">Columns</span>
|
|
||||||
<IconChevronDown />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-56">
|
|
||||||
{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">
|
|
||||||
<IconPlus />
|
|
||||||
<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.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</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.getState().pagination.pageSize}`}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
table.setPageSize(Number(value))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={table.getState().pagination.pageSize}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent side="top">
|
|
||||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
|
||||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
||||||
{pageSize}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
|
||||||
Page {table.getState().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>
|
|
||||||
<IconChevronsLeft />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="size-8"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to previous page</span>
|
|
||||||
<IconChevronLeft />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="size-8"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Go to next page</span>
|
|
||||||
<IconChevronRight />
|
|
||||||
</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>
|
|
||||||
<IconChevronsRight />
|
|
||||||
</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{" "}
|
|
||||||
<IconTrendingUp 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>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
<SelectItem value="Done">Done</SelectItem>
|
|
||||||
<SelectItem value="In Progress">In Progress</SelectItem>
|
|
||||||
<SelectItem value="Not Started">Not Started</SelectItem>
|
|
||||||
</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>
|
|
||||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
|
||||||
<SelectItem value="Jamik Tashpulatov">
|
|
||||||
Jamik Tashpulatov
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<DrawerFooter>
|
|
||||||
<Button>Submit</Button>
|
|
||||||
<DrawerClose asChild>
|
|
||||||
<Button variant="outline">Done</Button>
|
|
||||||
</DrawerClose>
|
|
||||||
</DrawerFooter>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { IconBrightness } from "@tabler/icons-react"
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
|
|
||||||
export function ModeToggle() {
|
|
||||||
const { setTheme, resolvedTheme } = useTheme()
|
|
||||||
|
|
||||||
const toggleTheme = React.useCallback(() => {
|
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
|
||||||
}, [resolvedTheme, setTheme])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="icon"
|
|
||||||
className="group/toggle size-8"
|
|
||||||
onClick={toggleTheme}
|
|
||||||
>
|
|
||||||
<IconBrightness />
|
|
||||||
<span className="sr-only">Toggle theme</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
|
||||||
IconDots,
|
|
||||||
IconFolder,
|
|
||||||
IconShare3,
|
|
||||||
IconTrash,
|
|
||||||
type Icon,
|
|
||||||
} from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
|
|
||||||
export function NavDocuments({
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
icon: Icon
|
|
||||||
}[]
|
|
||||||
}) {
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<IconDots />
|
|
||||||
<span className="sr-only">More</span>
|
|
||||||
</SidebarMenuAction>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-24 rounded-lg"
|
|
||||||
side={isMobile ? "bottom" : "right"}
|
|
||||||
align={isMobile ? "end" : "start"}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<IconFolder />
|
|
||||||
<span>Open</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<IconShare3 />
|
|
||||||
<span>Share</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem variant="destructive">
|
|
||||||
<IconTrash />
|
|
||||||
<span>Delete</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
|
||||||
<IconDots className="text-sidebar-foreground/70" />
|
|
||||||
<span>More</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
|
|
||||||
export function NavMain({
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
icon?: Icon
|
|
||||||
}[]
|
|
||||||
}) {
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<IconCirclePlusFilled />
|
|
||||||
<span>Quick Create</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
className="size-8 group-data-[collapsible=icon]:opacity-0"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
<IconMail />
|
|
||||||
<span className="sr-only">Inbox</span>
|
|
||||||
</Button>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
<SidebarMenu>
|
|
||||||
{items.map((item) => (
|
|
||||||
<SidebarMenuItem key={item.title}>
|
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
|
||||||
{item.icon && <item.icon />}
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { IconBrightness, type Icon } from "@tabler/icons-react"
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
import { Skeleton } from "@/registry/new-york-v4/ui/skeleton"
|
|
||||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
|
||||||
|
|
||||||
export function NavSecondary({
|
|
||||||
items,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
icon: Icon
|
|
||||||
}[]
|
|
||||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
|
||||||
const { resolvedTheme, setTheme } = useTheme()
|
|
||||||
const [mounted, setMounted] = React.useState(false)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setMounted(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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>
|
|
||||||
))}
|
|
||||||
<SidebarMenuItem className="group-data-[collapsible=icon]:hidden">
|
|
||||||
<SidebarMenuButton asChild>
|
|
||||||
<label>
|
|
||||||
<IconBrightness />
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
{mounted ? (
|
|
||||||
<Switch
|
|
||||||
className="ml-auto"
|
|
||||||
checked={resolvedTheme !== "light"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Skeleton className="ml-auto h-4 w-8 rounded-full" />
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
|
||||||
IconCreditCard,
|
|
||||||
IconDotsVertical,
|
|
||||||
IconLogout,
|
|
||||||
IconNotification,
|
|
||||||
IconUserCircle,
|
|
||||||
} from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
} from "@/registry/new-york-v4/ui/avatar"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
|
||||||
import {
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
|
|
||||||
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>
|
|
||||||
<IconDotsVertical 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>
|
|
||||||
<IconUserCircle />
|
|
||||||
Account
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<IconCreditCard />
|
|
||||||
Billing
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<IconNotification />
|
|
||||||
Notifications
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<IconLogout />
|
|
||||||
Log out
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/card"
|
|
||||||
|
|
||||||
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">
|
|
||||||
<IconTrendingUp />
|
|
||||||
+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 <IconTrendingUp 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">
|
|
||||||
<IconTrendingDown />
|
|
||||||
-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 <IconTrendingDown 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">
|
|
||||||
<IconTrendingUp />
|
|
||||||
+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 <IconTrendingUp 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">
|
|
||||||
<IconTrendingUp />
|
|
||||||
+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 <IconTrendingUp className="size-4" />
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground">Meets growth projections</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
|
||||||
import { SidebarTrigger } from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
import { ModeToggle } from "@/app/(examples)/dashboard/components/mode-toggle"
|
|
||||||
import { ThemeSelector } from "@/app/(examples)/dashboard/components/theme-selector"
|
|
||||||
|
|
||||||
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 className="ml-auto flex items-center gap-2">
|
|
||||||
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
|
|
||||||
<a
|
|
||||||
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
className="dark:text-foreground"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
<ThemeSelector />
|
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useThemeConfig } from "@/components/active-theme"
|
|
||||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
|
|
||||||
const DEFAULT_THEMES = [
|
|
||||||
{
|
|
||||||
name: "Default",
|
|
||||||
value: "default",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Blue",
|
|
||||||
value: "blue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Green",
|
|
||||||
value: "green",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Amber",
|
|
||||||
value: "amber",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const SCALED_THEMES = [
|
|
||||||
{
|
|
||||||
name: "Default",
|
|
||||||
value: "default-scaled",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Blue",
|
|
||||||
value: "blue-scaled",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const MONO_THEMES = [
|
|
||||||
{
|
|
||||||
name: "Mono",
|
|
||||||
value: "mono-scaled",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function ThemeSelector() {
|
|
||||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label htmlFor="theme-selector" className="sr-only">
|
|
||||||
Theme
|
|
||||||
</Label>
|
|
||||||
<Select value={activeTheme} onValueChange={setActiveTheme}>
|
|
||||||
<SelectTrigger
|
|
||||||
id="theme-selector"
|
|
||||||
size="sm"
|
|
||||||
className="justify-start *:data-[slot=select-value]:w-12"
|
|
||||||
>
|
|
||||||
<span className="hidden text-muted-foreground sm:block">
|
|
||||||
Select a theme:
|
|
||||||
</span>
|
|
||||||
<span className="block text-muted-foreground sm:hidden">Theme</span>
|
|
||||||
<SelectValue placeholder="Select a theme" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Default</SelectLabel>
|
|
||||||
{DEFAULT_THEMES.map((theme) => (
|
|
||||||
<SelectItem key={theme.name} value={theme.value}>
|
|
||||||
{theme.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
<SelectSeparator />
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Scaled</SelectLabel>
|
|
||||||
{SCALED_THEMES.map((theme) => (
|
|
||||||
<SelectItem key={theme.name} value={theme.value}>
|
|
||||||
{theme.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Monospaced</SelectLabel>
|
|
||||||
{MONO_THEMES.map((theme) => (
|
|
||||||
<SelectItem key={theme.name} value={theme.value}>
|
|
||||||
{theme.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,614 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { cookies } from "next/headers"
|
|
||||||
|
|
||||||
import {
|
|
||||||
SidebarInset,
|
|
||||||
SidebarProvider,
|
|
||||||
} from "@/registry/new-york-v4/ui/sidebar"
|
|
||||||
import { AppSidebar } from "@/app/(examples)/dashboard/components/app-sidebar"
|
|
||||||
import { SiteHeader } from "@/app/(examples)/dashboard/components/site-header"
|
|
||||||
|
|
||||||
import "@/app/(examples)/dashboard/theme.css"
|
|
||||||
|
|
||||||
export default async function DashboardLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const cookieStore = await cookies()
|
|
||||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarProvider
|
|
||||||
defaultOpen={defaultOpen}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AppSidebar variant="inset" />
|
|
||||||
<SidebarInset>
|
|
||||||
<SiteHeader />
|
|
||||||
<div className="flex flex-1 flex-col">{children}</div>
|
|
||||||
</SidebarInset>
|
|
||||||
</SidebarProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { ChartAreaInteractive } from "@/app/(examples)/dashboard/components/chart-area-interactive"
|
|
||||||
import { DataTable } from "@/app/(examples)/dashboard/components/data-table"
|
|
||||||
import { SectionCards } from "@/app/(examples)/dashboard/components/section-cards"
|
|
||||||
import data from "@/app/(examples)/dashboard/data.json"
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
body {
|
|
||||||
@apply overscroll-none bg-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--font-sans: var(--font-inter);
|
|
||||||
--header-height: calc(var(--spacing) * 12 + 1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-scaled {
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
--radius: 0.6rem;
|
|
||||||
--text-lg: 1.05rem;
|
|
||||||
--text-base: 0.85rem;
|
|
||||||
--text-sm: 0.8rem;
|
|
||||||
--spacing: 0.222222rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="card"] {
|
|
||||||
--spacing: 0.16rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="select-trigger"],
|
|
||||||
[data-slot="toggle-group-item"] {
|
|
||||||
--spacing: 0.222222rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-default,
|
|
||||||
.theme-default-scaled {
|
|
||||||
--primary: var(--color-neutral-600);
|
|
||||||
--primary-foreground: var(--color-neutral-50);
|
|
||||||
|
|
||||||
@variant dark {
|
|
||||||
--primary: var(--color-neutral-500);
|
|
||||||
--primary-foreground: var(--color-neutral-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-blue,
|
|
||||||
.theme-blue-scaled {
|
|
||||||
--primary: var(--color-blue-600);
|
|
||||||
--primary-foreground: var(--color-blue-50);
|
|
||||||
|
|
||||||
@variant dark {
|
|
||||||
--primary: var(--color-blue-500);
|
|
||||||
--primary-foreground: var(--color-blue-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-green,
|
|
||||||
.theme-green-scaled {
|
|
||||||
--primary: var(--color-lime-600);
|
|
||||||
--primary-foreground: var(--color-lime-50);
|
|
||||||
|
|
||||||
@variant dark {
|
|
||||||
--primary: var(--color-lime-600);
|
|
||||||
--primary-foreground: var(--color-lime-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-amber,
|
|
||||||
.theme-amber-scaled {
|
|
||||||
--primary: var(--color-amber-600);
|
|
||||||
--primary-foreground: var(--color-amber-50);
|
|
||||||
|
|
||||||
@variant dark {
|
|
||||||
--primary: var(--color-amber-500);
|
|
||||||
--primary-foreground: var(--color-amber-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mono,
|
|
||||||
.theme-mono-scaled {
|
|
||||||
--font-sans: var(--font-mono);
|
|
||||||
--primary: var(--color-neutral-600);
|
|
||||||
--primary-foreground: var(--color-neutral-50);
|
|
||||||
|
|
||||||
@variant dark {
|
|
||||||
--primary: var(--color-neutral-500);
|
|
||||||
--primary-foreground: var(--color-neutral-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rounded-xs,
|
|
||||||
.rounded-sm,
|
|
||||||
.rounded-md,
|
|
||||||
.rounded-lg,
|
|
||||||
.rounded-xl {
|
|
||||||
@apply !rounded-none;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-xs,
|
|
||||||
.shadow-sm,
|
|
||||||
.shadow-md,
|
|
||||||
.shadow-lg,
|
|
||||||
.shadow-xl {
|
|
||||||
@apply !shadow-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="toggle-group"],
|
|
||||||
[data-slot="toggle-group-item"] {
|
|
||||||
@apply !rounded-none !shadow-none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import Image from "next/image"
|
|
||||||
import { CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/field"
|
|
||||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
|
||||||
import {
|
|
||||||
RadioGroup,
|
|
||||||
RadioGroupItem,
|
|
||||||
} from "@/registry/new-york-v4/ui/radio-group"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
|
||||||
|
|
||||||
const modes = [
|
|
||||||
{
|
|
||||||
name: "Light",
|
|
||||||
value: "light",
|
|
||||||
image: "/placeholder.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Dark",
|
|
||||||
value: "dark",
|
|
||||||
image: "/placeholder.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "System",
|
|
||||||
value: "system",
|
|
||||||
image: "/placeholder.svg",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const accents = [
|
|
||||||
{
|
|
||||||
name: "Blue",
|
|
||||||
value: "#007AFF",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Purple",
|
|
||||||
value: "#6A4695",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Red",
|
|
||||||
value: "#FF3B30",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Orange",
|
|
||||||
value: "#FF9500",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function AppearanceSettings() {
|
|
||||||
return (
|
|
||||||
<FieldSet>
|
|
||||||
<FieldLegend>Appearance</FieldLegend>
|
|
||||||
<FieldDescription>
|
|
||||||
Configure appearance. accent, scroll bar, and more.
|
|
||||||
</FieldDescription>
|
|
||||||
<FieldGroup>
|
|
||||||
<FieldSet>
|
|
||||||
<FieldLegend variant="label">Mode</FieldLegend>
|
|
||||||
<FieldDescription>
|
|
||||||
Select the mode to use for the appearance.
|
|
||||||
</FieldDescription>
|
|
||||||
<RadioGroup
|
|
||||||
className="flex flex-col gap-4 @min-[28rem]/field-group:grid @min-[28rem]/field-group:grid-cols-3"
|
|
||||||
defaultValue="light"
|
|
||||||
>
|
|
||||||
{modes.map((mode) => (
|
|
||||||
<FieldLabel
|
|
||||||
htmlFor={mode.value}
|
|
||||||
className="gap-0 overflow-hidden"
|
|
||||||
key={mode.value}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={mode.image}
|
|
||||||
alt={mode.name}
|
|
||||||
width={160}
|
|
||||||
height={90}
|
|
||||||
className="hidden aspect-video w-full object-cover @min-[28rem]/field-group:block dark:brightness-[0.2] dark:grayscale"
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
orientation="horizontal"
|
|
||||||
className="@min-[28rem]/field-group:border-t @min-[28rem]/field-group:border-t-input"
|
|
||||||
>
|
|
||||||
<FieldTitle>{mode.name}</FieldTitle>
|
|
||||||
<RadioGroupItem id={mode.value} value={mode.value} />
|
|
||||||
</Field>
|
|
||||||
</FieldLabel>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</FieldSet>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldTitle>Accent</FieldTitle>
|
|
||||||
<FieldDescription>
|
|
||||||
Select the accent color to use for the appearance.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<FieldSet aria-label="Accent">
|
|
||||||
<RadioGroup className="flex flex-wrap gap-2" defaultValue="#007AFF">
|
|
||||||
{accents.map((accent) => (
|
|
||||||
<Label
|
|
||||||
htmlFor={accent.value}
|
|
||||||
key={accent.value}
|
|
||||||
className="flex size-6 items-center justify-center rounded-full"
|
|
||||||
style={{ backgroundColor: accent.value }}
|
|
||||||
>
|
|
||||||
<RadioGroupItem
|
|
||||||
id={accent.value}
|
|
||||||
value={accent.value}
|
|
||||||
aria-label={accent.name}
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
|
|
||||||
</Label>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</FieldSet>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="responsive">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="icon-size">Sidebar Icon Size</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Select the size of the sidebar icons.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id="icon-size" className="ml-auto">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
<SelectItem value="small">Small</SelectItem>
|
|
||||||
<SelectItem value="medium">Medium</SelectItem>
|
|
||||||
<SelectItem value="large">Large</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Allow the wallpaper to be tinted with the accent color.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Switch id="tinting" defaultChecked />
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
import { CircleIcon, InfoIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/field"
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupInput,
|
|
||||||
} from "@/registry/new-york-v4/ui/input-group"
|
|
||||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/tabs"
|
|
||||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/registry/new-york-v4/ui/tooltip"
|
|
||||||
|
|
||||||
const spokenLanguages = [
|
|
||||||
{ label: "English", value: "en" },
|
|
||||||
{ label: "Spanish", value: "es" },
|
|
||||||
{ label: "French", value: "fr" },
|
|
||||||
{ label: "German", value: "de" },
|
|
||||||
{ label: "Italian", value: "it" },
|
|
||||||
{ label: "Portuguese", value: "pt" },
|
|
||||||
{ label: "Russian", value: "ru" },
|
|
||||||
{ label: "Chinese", value: "zh" },
|
|
||||||
{ label: "Japanese", value: "ja" },
|
|
||||||
{ label: "Korean", value: "ko" },
|
|
||||||
{ label: "Arabic", value: "ar" },
|
|
||||||
{ label: "Hindi", value: "hi" },
|
|
||||||
{ label: "Bengali", value: "bn" },
|
|
||||||
{ label: "Telugu", value: "te" },
|
|
||||||
{ label: "Marathi", value: "mr" },
|
|
||||||
{ label: "Kannada", value: "kn" },
|
|
||||||
{ label: "Malayalam", value: "ml" },
|
|
||||||
]
|
|
||||||
|
|
||||||
const voices = [
|
|
||||||
{ label: "Samantha", value: "samantha" },
|
|
||||||
{ label: "Alex", value: "alex" },
|
|
||||||
{ label: "Fred", value: "fred" },
|
|
||||||
{ label: "Victoria", value: "victoria" },
|
|
||||||
{ label: "Tom", value: "tom" },
|
|
||||||
{ label: "Karen", value: "karen" },
|
|
||||||
{ label: "Sam", value: "sam" },
|
|
||||||
{ label: "Daniel", value: "daniel" },
|
|
||||||
]
|
|
||||||
|
|
||||||
const personalities = [
|
|
||||||
{
|
|
||||||
label: "Friendly",
|
|
||||||
value: "friendly",
|
|
||||||
description: "Friendly and approachable.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Professional",
|
|
||||||
value: "professional",
|
|
||||||
description: "Professional and authoritative.",
|
|
||||||
},
|
|
||||||
{ label: "Funny", value: "funny", description: "Funny and light-hearted." },
|
|
||||||
{
|
|
||||||
label: "Sarcastic",
|
|
||||||
value: "sarcastic",
|
|
||||||
description: "Sarcastic and witty.",
|
|
||||||
},
|
|
||||||
{ label: "Cynical", value: "cynical", description: "Cynical and skeptical." },
|
|
||||||
]
|
|
||||||
|
|
||||||
const instructions = [
|
|
||||||
{
|
|
||||||
label: "Witty",
|
|
||||||
value: "witty",
|
|
||||||
description: "Use quick and clever responses when appropriate.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Professional",
|
|
||||||
value: "professional",
|
|
||||||
description: "Have a professional and authoritative tone.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Funny",
|
|
||||||
value: "funny",
|
|
||||||
description: "Use humor and wit to engage the user.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Sarcastic",
|
|
||||||
value: "sarcastic",
|
|
||||||
description: "Use sarcasm and wit to engage the user.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Cynical",
|
|
||||||
value: "cynical",
|
|
||||||
description: "Use cynicism and skepticism to engage the user.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function ChatSettings() {
|
|
||||||
const [tab, setTab] = useState("general")
|
|
||||||
const [theme, setTheme] = useState("system")
|
|
||||||
const [accentColor, setAccentColor] = useState("default")
|
|
||||||
const [spokenLanguage, setSpokenLanguage] = useState("en")
|
|
||||||
const [voice, setVoice] = useState("samantha")
|
|
||||||
const [personality, setPersonality] = useState("friendly")
|
|
||||||
const [customInstructions, setCustomInstructions] = useState("")
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Button variant="outline" asChild className="w-full md:hidden">
|
|
||||||
<select
|
|
||||||
value={tab}
|
|
||||||
onChange={(e) => setTab(e.target.value)}
|
|
||||||
className="appearance-none"
|
|
||||||
>
|
|
||||||
<option value="general">General</option>
|
|
||||||
<option value="notifications">Notifications</option>
|
|
||||||
<option value="personalization">Personalization</option>
|
|
||||||
<option value="security">Security</option>
|
|
||||||
</select>
|
|
||||||
</Button>
|
|
||||||
<Tabs value={tab} onValueChange={setTab}>
|
|
||||||
<TabsList className="hidden md:flex">
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
|
||||||
<TabsTrigger value="personalization">Personalization</TabsTrigger>
|
|
||||||
<TabsTrigger value="security">Security</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="rounded-lg border p-6 [&_[data-slot=select-trigger]]:min-w-[125px]">
|
|
||||||
<TabsContent value="general">
|
|
||||||
<FieldSet>
|
|
||||||
<FieldGroup>
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldLabel htmlFor="theme">Theme</FieldLabel>
|
|
||||||
<Select value={theme} onValueChange={setTheme}>
|
|
||||||
<SelectTrigger id="theme">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
<SelectItem value="light">Light</SelectItem>
|
|
||||||
<SelectItem value="dark">Dark</SelectItem>
|
|
||||||
<SelectItem value="system">System</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldLabel htmlFor="accent-color">Accent Color</FieldLabel>
|
|
||||||
<Select value={accentColor} onValueChange={setAccentColor}>
|
|
||||||
<SelectTrigger id="accent-color">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
<SelectItem value="default">
|
|
||||||
<CircleIcon className="fill-neutral-500 stroke-neutral-500 dark:fill-neutral-400 dark:stroke-neutral-400" />
|
|
||||||
Default
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="red">
|
|
||||||
<CircleIcon className="fill-red-500 stroke-red-500 dark:fill-red-400 dark:stroke-red-400" />
|
|
||||||
Red
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="blue">
|
|
||||||
<CircleIcon className="fill-blue-500 stroke-blue-500 dark:fill-blue-400 dark:stroke-blue-400" />
|
|
||||||
Blue
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="green">
|
|
||||||
<CircleIcon className="fill-green-500 stroke-green-500 dark:fill-green-400 dark:stroke-green-400" />
|
|
||||||
Green
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="purple">
|
|
||||||
<CircleIcon className="fill-purple-500 stroke-purple-500 dark:fill-purple-400 dark:stroke-purple-400" />
|
|
||||||
Purple
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="pink">
|
|
||||||
<CircleIcon className="fill-pink-500 stroke-pink-500 dark:fill-pink-400 dark:stroke-pink-400" />
|
|
||||||
Pink
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="responsive">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="spoken-language">
|
|
||||||
Spoken Language
|
|
||||||
</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
For best results, select the language you mainly speak. If
|
|
||||||
it's not listed, it may still be supported via
|
|
||||||
auto-detection.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Select
|
|
||||||
value={spokenLanguage}
|
|
||||||
onValueChange={setSpokenLanguage}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="spoken-language">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end" position="item-aligned">
|
|
||||||
<SelectItem value="auto">Auto</SelectItem>
|
|
||||||
<SelectSeparator />
|
|
||||||
{spokenLanguages.map((language) => (
|
|
||||||
<SelectItem key={language.value} value={language.value}>
|
|
||||||
{language.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldLabel htmlFor="voice">Voice</FieldLabel>
|
|
||||||
<Select value={voice} onValueChange={setVoice}>
|
|
||||||
<SelectTrigger id="voice">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end" position="item-aligned">
|
|
||||||
{voices.map((voice) => (
|
|
||||||
<SelectItem key={voice.value} value={voice.value}>
|
|
||||||
{voice.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="notifications">
|
|
||||||
<FieldGroup>
|
|
||||||
<FieldSet>
|
|
||||||
<FieldLabel>Responses</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Get notified when ChatGPT responds to requests that take time,
|
|
||||||
like research or image generation.
|
|
||||||
</FieldDescription>
|
|
||||||
<FieldGroup data-slot="checkbox-group">
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<Checkbox id="push" defaultChecked disabled />
|
|
||||||
<FieldLabel htmlFor="push" className="font-normal">
|
|
||||||
Push notifications
|
|
||||||
</FieldLabel>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
<FieldSeparator />
|
|
||||||
<FieldSet>
|
|
||||||
<FieldLabel>Tasks</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Get notified when tasks you've created have updates.{" "}
|
|
||||||
<a href="#">Manage tasks</a>
|
|
||||||
</FieldDescription>
|
|
||||||
<FieldGroup data-slot="checkbox-group">
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<Checkbox id="push-tasks" />
|
|
||||||
<FieldLabel htmlFor="push-tasks" className="font-normal">
|
|
||||||
Push notifications
|
|
||||||
</FieldLabel>
|
|
||||||
</Field>
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<Checkbox id="email-tasks" />
|
|
||||||
<FieldLabel htmlFor="email-tasks" className="font-normal">
|
|
||||||
Email notifications
|
|
||||||
</FieldLabel>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
</FieldGroup>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="personalization">
|
|
||||||
<FieldGroup>
|
|
||||||
<Field orientation="responsive">
|
|
||||||
<FieldLabel htmlFor="nickname">Nickname</FieldLabel>
|
|
||||||
<InputGroup>
|
|
||||||
<InputGroupInput
|
|
||||||
id="nickname"
|
|
||||||
placeholder="Broski"
|
|
||||||
className="@md/field-group:max-w-[200px]"
|
|
||||||
/>
|
|
||||||
<InputGroupAddon align="inline-end">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<InputGroupButton size="icon-xs">
|
|
||||||
<InfoIcon />
|
|
||||||
</InputGroupButton>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="flex items-center gap-2">
|
|
||||||
Used to identify you in the chat. <Kbd>N</Kbd>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</InputGroupAddon>
|
|
||||||
</InputGroup>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field
|
|
||||||
orientation="responsive"
|
|
||||||
className="@md/field-group:flex-col @2xl/field-group:flex-row"
|
|
||||||
>
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="about">More about you</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Tell us more about yourself. This will be used to help us
|
|
||||||
personalize your experience.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Textarea
|
|
||||||
id="about"
|
|
||||||
placeholder="I'm a software engineer..."
|
|
||||||
className="min-h-[120px] @md/field-group:min-w-full @2xl/field-group:min-w-[300px]"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<FieldLabel>
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="customization">
|
|
||||||
Enable customizations
|
|
||||||
</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Enable customizations to make ChatGPT more personalized.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Switch id="customization" defaultChecked />
|
|
||||||
</Field>
|
|
||||||
</FieldLabel>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="responsive">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="personality">
|
|
||||||
ChatGPT Personality
|
|
||||||
</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Set the style and tone ChatGPT should use when responding.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Select value={personality} onValueChange={setPersonality}>
|
|
||||||
<SelectTrigger id="personality">
|
|
||||||
{personalities.find((p) => p.value === personality)?.label}
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
{personalities.map((personality) => (
|
|
||||||
<SelectItem
|
|
||||||
key={personality.value}
|
|
||||||
value={personality.value}
|
|
||||||
>
|
|
||||||
<FieldContent className="gap-0.5">
|
|
||||||
<FieldLabel>{personality.label}</FieldLabel>
|
|
||||||
<FieldDescription className="text-xs">
|
|
||||||
{personality.description}
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="instructions">
|
|
||||||
Custom Instructions
|
|
||||||
</FieldLabel>
|
|
||||||
<Textarea
|
|
||||||
id="instructions"
|
|
||||||
value={customInstructions}
|
|
||||||
onChange={(e) => setCustomInstructions(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{instructions.map((instruction) => (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
key={instruction.value}
|
|
||||||
value={instruction.value}
|
|
||||||
className="rounded-full"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
setCustomInstructions(
|
|
||||||
`${customInstructions} ${instruction.description}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{instruction.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="security">
|
|
||||||
<FieldGroup>
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="2fa">
|
|
||||||
Multi-factor authentication
|
|
||||||
</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Enable multi-factor authentication to secure your account.
|
|
||||||
If you do not have a two-factor authentication device, you
|
|
||||||
can use a one-time code sent to your email.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Switch id="2fa" />
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldTitle>Log out</FieldTitle>
|
|
||||||
<FieldDescription>
|
|
||||||
Log out of your account on this device.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
Log Out
|
|
||||||
</Button>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldTitle>Log out of all devices</FieldTitle>
|
|
||||||
<FieldDescription>
|
|
||||||
This will log you out of all devices, including the current
|
|
||||||
session. It may take up to 30 minutes for the changes to
|
|
||||||
take effect.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
Log Out All
|
|
||||||
</Button>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { SunDimIcon, SunIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldDescription,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldTitle,
|
|
||||||
} from "@/registry/new-york-v4/ui/field"
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/registry/new-york-v4/ui/select"
|
|
||||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
|
||||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
|
||||||
|
|
||||||
export function DisplaySettings() {
|
|
||||||
return (
|
|
||||||
<FieldSet>
|
|
||||||
<FieldLegend>Display</FieldLegend>
|
|
||||||
<FieldDescription>
|
|
||||||
Configure display settings, brightness, refresh rate, and more.
|
|
||||||
</FieldDescription>
|
|
||||||
<FieldGroup>
|
|
||||||
<Field orientation="responsive">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="resolution">Resolution</FieldLabel>
|
|
||||||
<FieldDescription>Select the display resolution.</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id="resolution" className="ml-auto">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
<SelectItem value="1920x1080">1920 x 1080</SelectItem>
|
|
||||||
<SelectItem value="2560x1440">2560 x 1440</SelectItem>
|
|
||||||
<SelectItem value="3840x2160">3840 x 2160</SelectItem>
|
|
||||||
<SelectItem value="auto">Auto</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="responsive">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldTitle>Brightness</FieldTitle>
|
|
||||||
<FieldDescription>
|
|
||||||
Adjust the display brightness level.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<div className="flex min-w-[150px] items-center gap-2">
|
|
||||||
<SunDimIcon className="size-4 shrink-0" />
|
|
||||||
<Slider
|
|
||||||
id="brightness"
|
|
||||||
defaultValue={[75]}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
aria-label="Brightness"
|
|
||||||
/>
|
|
||||||
<SunIcon className="size-4 shrink-0" />
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="auto-brightness">
|
|
||||||
Automatically Adjust Brightness
|
|
||||||
</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Automatically adjust brightness based on ambient light.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Checkbox id="auto-brightness" defaultChecked />
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="horizontal">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="true-tone">True Tone</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Automatically adjust colors to match ambient lighting.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Switch id="true-tone" />
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="responsive">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="refresh-rate">Refresh Rate</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Select the display refresh rate.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id="refresh-rate" className="ml-auto min-w-[200px]">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
<SelectItem value="60hz">60 Hz</SelectItem>
|
|
||||||
<SelectItem value="120hz">120 Hz</SelectItem>
|
|
||||||
<SelectItem value="144hz">144 Hz</SelectItem>
|
|
||||||
<SelectItem value="240hz">240 Hz</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator />
|
|
||||||
<Field orientation="responsive">
|
|
||||||
<FieldContent>
|
|
||||||
<FieldLabel htmlFor="tv-connection">
|
|
||||||
When connected to TV
|
|
||||||
</FieldLabel>
|
|
||||||
<FieldDescription>
|
|
||||||
Choose display behavior when connected to a TV.
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger id="tv-connection" className="ml-auto">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent align="end">
|
|
||||||
<SelectItem value="mirror">Mirror Display</SelectItem>
|
|
||||||
<SelectItem value="extend">Extend Display</SelectItem>
|
|
||||||
<SelectItem value="tv-only">TV Only</SelectItem>
|
|
||||||
<SelectItem value="auto">Auto</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user