Compare commits

...

109 Commits

Author SHA1 Message Date
shadcn
ecbace99d9 feat: shadcn info preset code 2026-04-27 11:05:25 +04:00
shadcn
84d1d476b1 Merge pull request #9728 from htmujahid/main
Update URL for @shadcn-editor in registries.json
2026-04-14 21:03:02 +04:00
shadcn
fc62d5781d Merge pull request #10337 from ramonclaudio/docs/llms-txt-drift
docs(llms.txt): fix 404 and backfill missing routes
2026-04-09 05:13:18 +04:00
shadcn
d86c5e5939 Merge pull request #9484 from ramonclaudio/fix/docs-copy-page-components-list
fix(docs): replace <ComponentsList /> in copy-page and markdown output
2026-04-08 22:02:29 +04:00
shadcn
8006dd1c93 Merge branch 'main' into fix/docs-copy-page-components-list 2026-04-08 21:43:08 +04:00
Ray
1dcbb4c88a docs(llms.txt): fix 404 and backfill missing routes
llms.txt was added in #8460 and hasn't kept up with the docs tree.
Audited every URL against apps/v4/content/docs and fixed the drift
in one pass.

Removed:
- About (/docs/about): returns 404, no about.mdx exists
- Form (/docs/components/form): points at a phantom. No radix/form.mdx
  exists post-#9304. URL only resolves because of a redirect in
  next.config.mjs, which lands at /docs/forms. That page is already
  listed as 'Forms Overview' in the ## Forms section, and the real
  form library docs (React Hook Form, TanStack Form, Next.js) are
  listed there too. The Form component entry is a stale duplicate.

Added to Overview:
- Skills (/docs/skills)
- Directory (/docs/directory)

Added whole RTL section (new since #8460):
- RTL (/docs/rtl)
- RTL - Next.js
- RTL - Vite
- RTL - TanStack Start

Added to Components:
- Direction (Misc)
- Native Select (Form & Input, after Select)
- Sonner (Feedback & Status, after Toast, since Sonner has its own
  docs page even though Toast already uses it under the hood)

Added to Registry:
- Namespaces
- Add a Registry (open source registry index)
- Open in v0 integration
- registry.json schema docs
- registry-item.json spec docs

Descriptions match the short curated style of the rest of the file.
Noticed while working on #9484.
2026-04-08 12:36:44 -04:00
shadcn
4f4ffde4aa chore: update registries 2026-04-08 20:01:13 +04:00
Ray
6d7a0ed93b fix(docs): replace <ComponentsList /> in copy-page and markdown output
The <ComponentsList /> tag on /docs/components was emitted as-is by
the Copy Page button and the /llm/[slug] markdown endpoint because
getComponentsList() walked components.children for pages directly.
After #9304 restructured the folder into components/radix/ and
components/base/ subfolders, the filter always returned [] and the
tag was replaced with an empty string (or, in the copy-page case,
never replaced at all).

- Reuse getPagesFromFolder() from lib/page-tree so the walker stays
  in sync with the on-screen ComponentsList React component.
- Match the existing llms.txt format: flat absolute URLs (the
  /docs/components/:name -> /docs/components/radix/:name redirect in
  next.config.mjs is the canonical form) plus the frontmatter
  description pulled via source.getPage() on each page.
- Export replaceComponentsList() and call it from
  docs/[[...slug]]/page.tsx so the Copy Page button goes through the
  same replacement path as processMdxForLLMs.
2026-04-08 11:50:07 -04:00
shadcn
b909b0363f Merge pull request #10324 from wrappixelTeam/feat/added-shadcn-dashboard
feat(registry): added new registry ( @shadcn-dashboard )
2026-04-08 19:15:16 +04:00
shadcn
a6fa6893eb Merge pull request #10333 from kapishdima/feat/remocn
feat: added @remocn to directory.json
2026-04-08 19:08:48 +04:00
KapishDima
561586bd98 Merge branch 'main' into feat/remocn 2026-04-08 16:41:56 +03:00
kapishdima
7ddb30aade feat: added @remocn to directory.json 2026-04-08 16:38:33 +03:00
shadcn
024425d45a fix: directory pager 2026-04-08 17:05:50 +04:00
Mihir Koshti
4bdaf48f9b Merge branch 'main' into feat/added-shadcn-dashboard 2026-04-08 18:15:55 +05:30
shadcn
e9546e87ff Merge pull request #10332 from shadcn-ui/shadcn/open-preset
feat: add open preset
2026-04-08 16:43:39 +04:00
shadcn
0b34d581f9 feat: add open preset 2026-04-08 16:33:32 +04:00
shadcn
5c2ed5e90e Merge branch 'main' of github.com:shadcn-ui/ui 2026-04-08 14:42:57 +04:00
shadcn
e9443ccd4a docs: add apply changelog 2026-04-08 14:42:52 +04:00
shadcn
1fe0fe65e8 Merge pull request #10331 from shadcn-ui/shadcn/directory-refactor
refactor: directory
2026-04-08 12:31:58 +04:00
shadcn
6823bad998 refactor: directory 2026-04-08 12:23:34 +04:00
shadcn
398e6c3406 fix: formatting in registries.json 2026-04-08 11:52:03 +04:00
shadcn
710cc27de7 Merge pull request #10330 from Aniket-508/feat/termcn-directory
feat(registry): add @termcn
2026-04-08 10:42:26 +04:00
Aniket Pawar
08212a478d feat(registry): add @termcn 2026-04-08 02:47:14 +00:00
shadcn
d718a8045f Merge pull request #10328 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-04-07 22:13:37 +04:00
github-actions[bot]
2c4678c8c8 chore(release): version packages 2026-04-07 17:48:58 +00:00
shadcn
2466a300f4 Merge pull request #10313 from shadcn-ui/shadcn/apply-preset
feat: add apply
2026-04-07 21:47:54 +04:00
shadcn
66fcf1e853 Merge branch 'shadcn/apply-preset' of github.com:shadcn-ui/ui into shadcn/apply-preset 2026-04-07 21:36:15 +04:00
shadcn
5ebd54198d fix 2026-04-07 21:36:09 +04:00
shadcn
3a2d812510 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 21:24:14 +04:00
shadcn
7811557088 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 21:23:59 +04:00
shadcn
575f1602a1 fix 2026-04-07 21:05:08 +04:00
htmujahid
50dc9b506b fix: update URL for @shadcn-editor to point to raw GitHub content 2026-04-07 19:10:12 +05:00
shadcn
ae70ecc2f3 Merge branch 'shadcn/apply-preset' of github.com:shadcn-ui/ui into shadcn/apply-preset 2026-04-07 18:02:20 +04:00
shadcn
42284f4e64 test: update rtl 2026-04-07 18:02:05 +04:00
Mihir Koshti
6b5aa16668 updated name to @shadcndashboard 2026-04-07 18:31:07 +05:30
Mihir Koshti
706806a207 feat(registry): added new registry ( @shadcn-dashboard ) 2026-04-07 18:07:26 +05:30
Talha Mujahid
8a7502d7fa Merge branch 'shadcn-ui:main' into main 2026-04-07 17:34:11 +05:00
shadcn
abc65a4871 Merge branch 'main' into shadcn/apply-preset 2026-04-07 16:27:29 +04:00
shadcn
7d5af61468 style: fix code block 2026-04-07 16:11:26 +04:00
shadcn
2badcdc31f Merge pull request #10323 from shadcn-ui/docs/component-composition-sections
docs: add composition section
2026-04-07 15:57:44 +04:00
shadcn
64b8263450 docs: add changelog 2026-04-07 15:49:54 +04:00
shadcn
13b4593f37 fix 2026-04-07 15:49:26 +04:00
shadcn
7dc65da6b2 fix 2026-04-07 15:28:19 +04:00
shadcn
98e56b773c Merge branch 'shadcn/apply-preset' of github.com:shadcn-ui/ui into shadcn/apply-preset 2026-04-07 15:27:56 +04:00
shadcn
7ff9778ff0 fix 2026-04-07 15:27:33 +04:00
shadcn
4af7bbf4ba Merge branch 'main' into docs/component-composition-sections 2026-04-07 15:25:02 +04:00
shadcn
f00a94d9e5 docs: add composition section 2026-04-07 15:23:27 +04:00
shadcn
187ae44fa7 Merge pull request #10318 from shadcn-ui/dependabot/npm_and_yarn/templates/start-monorepo/vite-7.3.2
chore(deps-dev): bump vite from 7.3.1 to 7.3.2 in /templates/start-monorepo
2026-04-07 14:40:02 +04:00
shadcn
034178bf7d Merge pull request #10317 from shadcn-ui/dependabot/npm_and_yarn/templates/react-router-monorepo/vite-7.3.2
chore(deps-dev): bump vite from 7.3.1 to 7.3.2 in /templates/react-router-monorepo
2026-04-07 14:39:46 +04:00
shadcn
4064c78bc7 Merge pull request #10316 from shadcn-ui/dependabot/npm_and_yarn/templates/vite-monorepo/vite-7.3.2
chore(deps-dev): bump vite from 7.3.1 to 7.3.2 in /templates/vite-monorepo
2026-04-07 14:39:28 +04:00
shadcn
943b023b7c Merge pull request #10314 from shadcn-ui/dependabot/npm_and_yarn/vite-7.3.2
chore(deps): bump vite from 7.1.12 to 7.3.2
2026-04-07 14:39:12 +04:00
shadcn
e3d654fd26 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 14:01:23 +04:00
dependabot[bot]
71d0470be1 chore(deps-dev): bump vite in /templates/start-monorepo
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 22:12:00 +00:00
dependabot[bot]
53bbdc738f chore(deps-dev): bump vite in /templates/react-router-monorepo
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 21:45:55 +00:00
dependabot[bot]
97707ec08e chore(deps-dev): bump vite in /templates/vite-monorepo
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 21:45:51 +00:00
dependabot[bot]
b9ce2f10c3 chore(deps): bump vite from 7.1.12 to 7.3.2
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.12 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:17:13 +00:00
shadcn
7cb3b13a33 fix 2026-04-06 23:32:48 +04:00
shadcn
e3d2b14911 fix 2026-04-06 23:27:15 +04:00
shadcn
58c9dc2a7e fix 2026-04-06 23:27:09 +04:00
shadcn
3bdf60340d fix 2026-04-06 23:19:37 +04:00
shadcn
c1e29824cd feat: add apply command 2026-04-06 23:14:57 +04:00
shadcn
62f6df75f2 Merge pull request #10310 from shadcn-ui/shadcn/create-cls
fix: page cls
2026-04-06 17:51:50 +04:00
shadcn
62bae86e86 fix: page cls 2026-04-06 17:37:56 +04:00
shadcn
aa69fbf85a Merge pull request #10309 from shadcn-ui/shadcn/create-perf
fix: create page perf
2026-04-06 16:56:13 +04:00
shadcn
8d41295f2c fix: create page perf 2026-04-06 16:45:17 +04:00
shadcn
2b053d916d Merge pull request #10285 from vzkiss/add-flowkit-ui-registry
feat(registry): add @flowkit-ui
2026-04-06 15:22:32 +04:00
shadcn
0d1309f322 Merge branch 'main' of github.com:shadcn-ui/ui 2026-04-06 15:15:59 +04:00
shadcn
c26250dcfe docs: changelog updates 2026-04-06 15:15:42 +04:00
vzkiss
07c5c36be8 Merge branch 'main' into add-flowkit-ui-registry 2026-04-06 08:51:51 +02:00
shadcn
21c9cc5246 Merge pull request #10303 from oliviertassinari/keywords
fix: add base-ui keyword to match GitHub topic
2026-04-06 10:41:35 +04:00
shadcn
058960046a Merge pull request #10167 from oliviertassinari/fix-base-ui-use-client-v2
fix: remove unnecessary Base UI use client
2026-04-06 10:41:17 +04:00
Olivier Tassinari
be80c18ea9 fix: add base-ui keyword to match GitHub topic 2026-04-06 00:29:21 +02:00
vzkiss
3c59a0cd95 Merge branch 'main' into add-flowkit-ui-registry 2026-04-05 23:16:10 +02:00
shadcn
26d0228ee9 fix 2026-04-04 13:51:50 +04:00
shadcn
9050646893 chore: rebuild registry 2026-04-04 13:44:23 +04:00
shadcn
3ca09b9647 Merge branch 'main' into fix-base-ui-use-client-v2
# Conflicts:
#	apps/v4/examples/base/button-render.tsx
#	apps/v4/public/r/styles/base-lyra/button.json
#	apps/v4/public/r/styles/base-mira/slider.json
#	apps/v4/public/r/styles/base-nova/button.json
#	apps/v4/public/r/styles/base-vega/button.json
#	apps/v4/styles/base-luma/ui/slider.tsx
#	apps/v4/styles/base-lyra/ui/accordion.tsx
#	apps/v4/styles/base-lyra/ui/slider.tsx
#	apps/v4/styles/base-nova/ui-rtl/accordion.tsx
#	apps/v4/styles/base-nova/ui-rtl/button.tsx
#	apps/v4/styles/base-nova/ui/button.tsx
2026-04-04 13:42:29 +04:00
shadcn
720ccca653 Merge pull request #10242 from Yngesh-Raman-QED42/fix/native-select-option-colors
fix(native-select): use system colors for option and optgroup
2026-04-04 13:33:45 +04:00
shadcn
1e3dff8daa chore: rebuild registry 2026-04-04 13:21:15 +04:00
shadcn
c116b325ab Merge branch 'main' into fix/native-select-option-colors 2026-04-04 13:11:50 +04:00
shadcn
5b266d3fc9 Merge pull request #10229 from MKSinghDev/patch-1
Add @mksingh to the directory registry
2026-04-04 13:10:25 +04:00
shadcn
6095e6272d Merge pull request #10272 from vinihvc/feat/add-shark-ui-registry
Add @shark to open source registry index
2026-04-04 13:00:14 +04:00
shadcn
f3fc5a62f2 Merge pull request #10241 from glsee/gamifykit-patch-2
chore: update GamifyKit logo
2026-04-04 12:55:24 +04:00
shadcn
ef7507cc9a Merge pull request #10263 from ridemountainpig/add-flightcn-registry
feature: add @flightcn registry
2026-04-04 12:52:27 +04:00
vzkiss
16b7bea50d feat(registry): build @flowkit-ui to registries.json 2026-04-04 01:36:16 +02:00
vzkiss
ccc4caad9c feat(registry): add @flowkit-ui to directory.json 2026-04-04 01:13:04 +02:00
Mukesh Singh
ba2c4fc586 added @mksingh in public registries 2026-04-03 09:39:54 +05:30
Mukesh Singh
bb5afb2df1 Merge branch 'shadcn-ui:main' into patch-1 2026-04-02 21:07:47 -07:00
Vinicius Vicentini
53f45f5f6f running pnpm registry:build 2026-04-02 15:43:11 -06:00
Vinicius Vicentini
990040691c Update directory.json 2026-04-02 12:44:52 -07:00
Vinicius Vicentini
83857679cb Rename registry entry from '@shark-ui' to '@shark' 2026-04-02 12:40:37 -07:00
Vinicius Vicentini
61989da8ec chore(registry): add @shark-ui to open source registry index
Adds Shark UI (Ark UI + Tailwind) to directory.json and regenerates
public/r/registries.json per registry index workflow.

Homepage: https://shark.vini.one
Registry template: https://shark.vini.one/r/{name}.json
Source: https://github.com/vinihvc/shark-ui

Made-with: Cursor
2026-04-02 13:32:45 -06:00
shadcn
768d8a808f Merge pull request #10268 from shadcn-ui/claude/update-schema-luma-atRnG 2026-04-02 18:47:23 +04:00
Claude
95479a06bb Add radix-luma and base-luma styles to schema.json
https://claude.ai/code/session_01UBbkLbn8ihvnnzw62FpBax
2026-04-02 10:32:47 +00:00
ridemountainpig
4289d5fe02 feature: add @flightcn registry 2026-04-02 09:14:21 +08:00
shadcn
5a6702845d feat: adjust slider for mira 2026-04-01 05:14:28 +04:00
github-actions[bot]
ebf2192d98 chore(release): version packages (#10247)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-31 22:27:14 +04:00
shadcn
44c09a19b0 feat: luma (#10246)
* feat: init style-luma

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* chore: changeset

* fix

* fix
2026-03-31 22:24:48 +04:00
shadcn
4101ec98af fix: colors 2026-03-31 11:53:03 +04:00
Yngesh Raman
a7c3300d7a fix(native-select): use system colors for option and optgroup 2026-03-31 13:11:40 +05:30
Kaiden See (Github-verified)
b50acc9d21 chore: update GamifyKit logo 2026-03-31 15:37:54 +08:00
shadcn
fc76a9ada2 fix: customizer 2026-03-31 04:55:01 +04:00
Johurul Haque
d6b4bf8ddc Add @tailgrids to registry directory (#10062)
* feat: add @tailgrids to the registry directory.

* chore: run registry:build script
2026-03-30 16:55:32 +04:00
Mukesh Singh
2c334c3c2d Add @mksingh to the registry
#10228 
Added new registry entry for @mksingh with details and logo.
2026-03-29 03:19:27 -07:00
shadcn
d3de6aa760 refactor: clean up unused files (#10227)
* refactor: clean up unused files

* fix
2026-03-29 12:04:18 +04:00
shadcn
23b2ac4dcf refactor: create page (#10212)
* refactor: create page

* fix
2026-03-29 11:17:39 +04:00
shadcn
e56c476105 fix: debug e2es (#10204)
* fix

* fix

* fix

* fix

* fix

* fix

* Revert "fix"

This reverts commit 98cbe82048.

* fix
2026-03-27 19:37:47 +04:00
Olivier Tassinari
0c25e712e1 pnpm registry:build 2026-03-25 11:55:07 +01:00
Olivier Tassinari
4f421aba65 fix: remove unnecessary Base UI use client 2026-03-25 01:35:03 +01:00
Talha Mujahid
b57e192965 Update URL for @shadcn-editor in registries.json 2026-02-25 08:01:05 +05:00
1328 changed files with 69628 additions and 34950 deletions

View File

@@ -63,11 +63,7 @@ export default function IndexPage() {
</Button>
</PageActions>
</PageHeader>
<PageNav className="hidden md:flex">
<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-wrapper flex-1 pb-6">
<div className="container overflow-hidden">
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
<Image

View File

@@ -1,7 +1,7 @@
"use client"
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 {
Picker,
PickerContent,
@@ -9,8 +9,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function MenuAccentPicker({
isMobile,

View File

@@ -12,7 +12,7 @@ import {
CommandItem,
CommandList,
} 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"

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { useMounted } from "@/hooks/use-mounted"
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 {
Picker,
PickerContent,
@@ -12,8 +12,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function BaseColorPicker({
isMobile,

View File

@@ -10,8 +10,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function BasePicker({
isMobile,

View File

@@ -8,7 +8,7 @@ import {
getThemesForBaseColor,
type ChartColorName,
} from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import {
Picker,
PickerContent,
@@ -17,8 +17,8 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function ChartColorPicker({
isMobile,

View File

@@ -5,11 +5,12 @@ import * as React from "react"
import { cn } from "@/lib/utils"
import { copyToClipboardWithMeta } from "@/components/copy-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>) {
const presetCode = usePresetCode()
const [hasCopied, setHasCopied] = React.useState(false)
const label = hasCopied ? "Copied" : `--preset ${presetCode}`
React.useEffect(() => {
if (hasCopied) {
@@ -32,12 +33,13 @@ export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
<Button
variant="outline"
onClick={handleCopy}
title={label}
className={cn(
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
className
)}
>
<span>{hasCopied ? "Copied" : `--preset ${presetCode}`}</span>
<span className="block min-w-0 truncate">{label}</span>
</Button>
)
}

View File

@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
import dynamic from "next/dynamic"
import { type RegistryItem } from "shadcn/schema"
import { useIsMobile } from "@/hooks/use-mobile"
@@ -12,24 +13,31 @@ import {
CardHeader,
} from "@/styles/base-nova/ui/card"
import { FieldGroup, FieldSeparator } from "@/styles/base-nova/ui/field"
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
import { ActionMenu } from "@/app/(create)/components/action-menu"
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
import { BasePicker } from "@/app/(create)/components/base-picker"
import { ChartColorPicker } from "@/app/(create)/components/chart-color-picker"
import { CopyPreset } from "@/app/(create)/components/copy-preset"
import { FontPicker } from "@/app/(create)/components/font-picker"
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
import { MainMenu } from "@/app/(create)/components/main-menu"
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
import { RandomButton } from "@/app/(create)/components/random-button"
import { ResetDialog } from "@/app/(create)/components/reset-button"
import { StylePicker } from "@/app/(create)/components/style-picker"
import { ThemePicker } from "@/app/(create)/components/theme-picker"
import { V0Button } from "@/app/(create)/components/v0-button"
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { MenuAccentPicker } from "@/app/(app)/create/components/accent-picker"
import { ActionMenu } from "@/app/(app)/create/components/action-menu"
import { BaseColorPicker } from "@/app/(app)/create/components/base-color-picker"
import { BasePicker } from "@/app/(app)/create/components/base-picker"
import { ChartColorPicker } from "@/app/(app)/create/components/chart-color-picker"
import { CopyPreset } from "@/app/(app)/create/components/copy-preset"
import { FontPicker } from "@/app/(app)/create/components/font-picker"
import { IconLibraryPicker } from "@/app/(app)/create/components/icon-library-picker"
import { MainMenu } from "@/app/(app)/create/components/main-menu"
import { MenuColorPicker } from "@/app/(app)/create/components/menu-picker"
import { OpenPreset } from "@/app/(app)/create/components/open-preset"
import { RadiusPicker } from "@/app/(app)/create/components/radius-picker"
import { RandomButton } from "@/app/(app)/create/components/random-button"
import { ResetDialog } from "@/app/(app)/create/components/reset-button"
import { StylePicker } from "@/app/(app)/create/components/style-picker"
import { ThemePicker } from "@/app/(app)/create/components/theme-picker"
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/create/lib/fonts"
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({
itemsByBase,
@@ -56,7 +64,6 @@ export function Customizer({
</CardHeader>
<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">
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
<StylePicker
styles={STYLES}
isMobile={isMobile}
@@ -91,14 +98,22 @@ export function Customizer({
<FieldSeparator className="hidden md:block" />
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
</FieldGroup>
</CardContent>
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:**:[button,a]:w-full">
<CopyPreset className="flex-1 md:flex-none" />
<RandomButton className="flex-1 md:flex-none" />
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:rounded-b-none md:**:[button,a]:w-full">
<CopyPreset className="min-w-0 flex-1 md:flex-none" />
<OpenPreset
className="max-w-20 min-w-0 flex-1 sm:max-w-none md:flex-none"
label={isMobile ? "Open" : "Open Preset"}
/>
<RandomButton className="max-w-20 min-w-0 flex-1 sm:max-w-none md:flex-none" />
<ActionMenu itemsByBase={itemsByBase} />
<ResetDialog />
</CardFooter>
<CardFooter className="-mt-3 hidden min-w-0 gap-2 md:flex md:flex-col md:**:[button,a]:w-full">
<ProjectForm />
</CardFooter>
</Card>
)
}

View File

@@ -7,12 +7,12 @@ import {
DEFAULT_CONFIG,
type DesignSystemConfig,
} from "@/registry/config"
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
import { FONTS } from "@/app/(create)/lib/fonts"
import { useIframeMessageListener } from "@/app/(app)/create/hooks/use-iframe-sync"
import { FONTS } from "@/app/(app)/create/lib/fonts"
import {
useDesignSystemSearchParams,
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 MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const

View File

@@ -2,7 +2,7 @@
import * as React from "react"
import { LockButton } from "@/app/(create)/components/lock-button"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import {
Picker,
PickerContent,
@@ -12,12 +12,12 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { FONTS } from "@/app/(create)/lib/fonts"
} from "@/app/(app)/create/components/picker"
import { FONTS } from "@/app/(app)/create/lib/fonts"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/lib/search-params"
type FontPickerOption = {
name: string
@@ -97,7 +97,7 @@ export function FontPicker({
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<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}
</div>
</div>

View File

@@ -5,7 +5,7 @@ import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
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 REDO_FORWARD_TYPE = "redo-forward"

View File

@@ -3,7 +3,7 @@
import * as React from "react"
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 {
Picker,
PickerContent,
@@ -11,8 +11,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
const logos = {
lucide: (

View 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>
)
}

View File

@@ -21,8 +21,8 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/styles/base-nova/ui/sidebar"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
const cachedGroupedItems = React.cache(
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {

View File

@@ -7,7 +7,10 @@ import {
import { HugeiconsIcon } from "@hugeicons/react"
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({
param,

View File

@@ -14,12 +14,13 @@ import {
PickerSeparator,
PickerShortcut,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useActionMenuTrigger } from "@/app/(create)/hooks/use-action-menu"
import { useHistory } from "@/app/(create)/hooks/use-history"
import { useRandom } from "@/app/(create)/hooks/use-random"
import { useReset } from "@/app/(create)/hooks/use-reset"
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
} from "@/app/(app)/create/components/picker"
import { useActionMenuTrigger } from "@/app/(app)/create/hooks/use-action-menu"
import { useHistory } from "@/app/(app)/create/hooks/use-history"
import { useOpenPresetTrigger } from "@/app/(app)/create/hooks/use-open-preset"
import { useRandom } from "@/app/(app)/create/hooks/use-random"
import { useReset } from "@/app/(app)/create/hooks/use-reset"
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
@@ -27,6 +28,7 @@ export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
const [isMac, setIsMac] = React.useState(false)
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
const { openActionMenu } = useActionMenuTrigger()
const { openPreset } = useOpenPresetTrigger()
const { randomize } = useRandom()
const { toggleTheme } = useThemeToggle()
const { setShowResetDialog } = useReset()
@@ -55,6 +57,9 @@ export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
Navigate...
<PickerShortcut>{isMac ? "⌘P" : "Ctrl+P"}</PickerShortcut>
</PickerItem>
<PickerItem onClick={openPreset}>
Open Preset... <PickerShortcut>O</PickerShortcut>
</PickerItem>
<PickerItem onClick={randomize}>
Shuffle <PickerShortcut>R</PickerShortcut>
</PickerItem>

View File

@@ -7,7 +7,7 @@ import { useTheme } from "next-themes"
import { useMounted } from "@/hooks/use-mounted"
import { type MenuColorValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import {
Picker,
PickerContent,
@@ -17,11 +17,11 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
} from "@/app/(app)/create/components/picker"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/lib/search-params"
type ColorChoice = "default" | "inverted"
type SurfaceChoice = "solid" | "translucent"

View File

@@ -5,7 +5,7 @@ import Script from "next/script"
import { cn } from "@/lib/utils"
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"

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import Script from "next/script"
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
import { Button } from "@/styles/base-nova/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/styles/base-nova/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-nova/ui/drawer"
import { Field, FieldContent, FieldLabel } from "@/styles/base-nova/ui/field"
import { Input } from "@/styles/base-nova/ui/input"
import {
OPEN_PRESET_FORWARD_TYPE,
useOpenPreset,
} from "@/app/(app)/create/hooks/use-open-preset"
import { parsePresetInput } from "@/app/(app)/create/lib/parse-preset-input"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
const PRESET_EXAMPLE = "b2D0wqNxT"
const PRESET_TITLE = "Open Preset"
const PRESET_DESCRIPTION = "Paste a preset code to load a saved configuration."
export function OpenPreset({
className,
label = "Open Preset",
}: React.ComponentProps<typeof Button> & {
label?: string
}) {
const [input, setInput] = React.useState("")
const [, setParams] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const { open, setOpen } = useOpenPreset()
const nextPreset = React.useMemo(() => parsePresetInput(input), [input])
const isInvalid = input.trim().length > 0 && nextPreset === null
const handleOpenChange = React.useCallback(
(nextOpen: boolean) => {
setOpen(nextOpen)
if (!nextOpen) {
setInput("")
}
},
[setOpen]
)
const handleSubmit = React.useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (!nextPreset) {
return
}
setParams({ preset: nextPreset })
handleOpenChange(false)
},
[handleOpenChange, nextPreset, setParams]
)
const triggerClassName = cn(
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
className
)
const desktopTrigger = (
<Button variant="outline" className={triggerClassName} />
)
const fields = (
<Field data-invalid={isInvalid || undefined}>
<FieldLabel htmlFor="preset-code" className="sr-only">
Preset code
</FieldLabel>
<FieldContent>
<Input
id="preset-code"
value={input}
onChange={(event) => setInput(event.target.value)}
placeholder={`${PRESET_EXAMPLE} or --preset ${PRESET_EXAMPLE}`}
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
aria-invalid={isInvalid}
className="h-10 md:h-8"
/>
</FieldContent>
</Field>
)
if (isMobile) {
return (
<Drawer open={open} onOpenChange={handleOpenChange}>
<DrawerTrigger asChild>
<Button variant="outline" className={triggerClassName}>
{label}
</Button>
</DrawerTrigger>
<DrawerContent className="dark rounded-t-2xl!">
<DrawerHeader>
<DrawerTitle className="text-xl">{PRESET_TITLE}</DrawerTitle>
<DrawerDescription>{PRESET_DESCRIPTION}</DrawerDescription>
</DrawerHeader>
<form onSubmit={handleSubmit}>
<div className="px-4 py-2">{fields}</div>
<DrawerFooter>
<Button type="submit" className="h-10" disabled={!nextPreset}>
Open
</Button>
<DrawerClose asChild>
<Button variant="outline" type="button" className="h-10">
Cancel
</Button>
</DrawerClose>
</DrawerFooter>
</form>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger render={desktopTrigger}>{label}</DialogTrigger>
<DialogContent className="dark">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>{PRESET_TITLE}</DialogTitle>
<DialogDescription>{PRESET_DESCRIPTION}</DialogDescription>
</DialogHeader>
<div className="py-4">{fields}</div>
<DialogFooter>
<DialogClose render={<Button variant="outline" type="button" />}>
Cancel
</DialogClose>
<Button type="submit" disabled={!nextPreset}>
Open
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
export function OpenPresetScript() {
return (
<Script
id="open-preset-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward O key.
document.addEventListener('keydown', function(e) {
if (e.key === 'o' && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return;
}
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: '${OPEN_PRESET_FORWARD_TYPE}',
key: e.key
}, '*');
}
}
});
})();
`,
}}
/>
)
}

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
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) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
@@ -19,7 +19,7 @@ function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
<MenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
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
)}
{...props}

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { useRouter } from "next/navigation"
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() {
const router = useRouter()

View File

@@ -10,8 +10,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function PresetPicker({
presets,
@@ -112,13 +112,6 @@ export function PresetPicker({
closeOnClick={isMobile}
>
<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}
</div>
</PickerRadioItem>

View 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>
)
}

View File

@@ -0,0 +1,166 @@
"use client"
import * as React from "react"
import { CMD_K_FORWARD_TYPE } from "@/app/(app)/create/components/action-menu"
import {
REDO_FORWARD_TYPE,
UNDO_FORWARD_TYPE,
} from "@/app/(app)/create/components/history-buttons"
import { DARK_MODE_FORWARD_TYPE } from "@/app/(app)/create/components/mode-switcher"
import { PreviewSwitcher } from "@/app/(app)/create/components/preview-switcher"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(app)/create/components/random-button"
import { sendToIframe } from "@/app/(app)/create/hooks/use-iframe-sync"
import { OPEN_PRESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-open-preset"
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
import {
serializeDesignSystemSearchParams,
useDesignSystemSearchParams,
} from "@/app/(app)/create/lib/search-params"
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
export function Preview() {
const [params] = useDesignSystemSearchParams()
const iframeRef = React.useRef<HTMLIFrameElement>(null)
React.useEffect(() => {
const iframe = iframeRef.current
if (!iframe) {
return
}
const sendParams = () => {
sendToIframe(iframe, "design-system-params", params)
}
if (iframe.contentWindow) {
sendParams()
}
iframe.addEventListener("load", sendParams)
return () => {
iframe.removeEventListener("load", sendParams)
}
}, [params])
React.useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const iframeWindow = iframeRef.current?.contentWindow
if (
!iframeWindow ||
event.origin !== window.location.origin ||
event.source !== iframeWindow ||
!event.data ||
typeof event.data !== "object"
) {
return
}
const type = event.data.type
if (type === CMD_K_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "k",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === RANDOMIZE_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "r",
bubbles: true,
cancelable: true,
})
)
} else if (type === OPEN_PRESET_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "o",
bubbles: true,
cancelable: true,
})
)
} else if (type === UNDO_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "z",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === REDO_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "z",
shiftKey: true,
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === RESET_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "R",
shiftKey: true,
bubbles: true,
cancelable: true,
})
)
} else if (type === DARK_MODE_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "d",
bubbles: true,
cancelable: true,
})
)
}
}
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
}, [])
const iframeSrc = React.useMemo(() => {
// The iframe src needs to include the serialized design system params
// for the initial load, but not be reactive to them as it would cause
// full-iframe reloads on every param change (flashes & loss of state).
// Further updates of the search params will be sent to the iframe
// via a postMessage channel, for it to sync its own history onto the host's.
return serializeDesignSystemSearchParams(
`/preview/${params.base}/${params.item}`,
params
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.base, params.item])
return (
<div className="relative flex flex-1 flex-col justify-center overflow-hidden rounded-2xl ring ring-foreground/10 md:ring-muted dark:ring-foreground/10">
<div className="relative z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden">
<div className="absolute inset-0 bg-muted dark:bg-muted/30" />
<iframe
key={params.base + params.item}
ref={iframeRef}
src={iframeSrc}
className="z-10 size-full flex-1"
title="Preview"
/>
</div>
<PreviewSwitcher />
</div>
)
}

View File

@@ -36,17 +36,17 @@ import {
TabsList,
TabsTrigger,
} 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 {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/lib/search-params"
import {
getFramework,
getTemplateValue,
NO_MONOREPO_FRAMEWORKS,
TEMPLATES,
} from "@/app/(create)/lib/templates"
} from "@/app/(app)/create/lib/templates"
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>'

View File

@@ -3,7 +3,7 @@
import * as React from "react"
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 {
Picker,
PickerContent,
@@ -12,8 +12,8 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function RadiusPicker({
isMobile,

View File

@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { Button } from "@/styles/base-nova/ui/button"
import { useRandom } from "@/app/(create)/hooks/use-random"
import { RESET_FORWARD_TYPE } from "@/app/(create)/hooks/use-reset"
import { useRandom } from "@/app/(app)/create/hooks/use-random"
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
@@ -28,7 +28,7 @@ export function RandomButton({
)}
{...props}
>
<span className="w-full text-center font-medium">Shuffle</span>
<span className="w-full truncate text-center font-medium">Shuffle</span>
</Button>
)
}

View File

@@ -10,7 +10,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} 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() {
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()

View File

@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/styles/base-nova/ui/button"
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function ShareButton() {
const [params] = useDesignSystemSearchParams()

View File

@@ -3,7 +3,7 @@
import * as React from "react"
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 {
Picker,
PickerContent,
@@ -11,8 +11,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function StylePicker({
styles,

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { useMounted } from "@/hooks/use-mounted"
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 {
Picker,
PickerContent,
@@ -13,8 +13,8 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
export function ThemePicker({
themes,

View File

@@ -8,7 +8,7 @@ import { useMounted } from "@/hooks/use-mounted"
import { Icons } from "@/components/icons"
import { Button } from "@/styles/base-nova/ui/button"
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 }) {
const [params] = useDesignSystemSearchParams()

View File

@@ -4,8 +4,8 @@ import * as React from "react"
import { type RegistryItem } from "shadcn/schema"
import useSWR from "swr"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"

View File

@@ -1,7 +1,7 @@
"use client"
import { getPresetCode } from "@/app/(create)/lib/preset-code"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
// Returns the canonical preset code derived from the current search params.
export function usePresetCode() {

View File

@@ -1,7 +1,8 @@
"use client"
import * as React from "react"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { Suspense } from "react"
import { useRouter, useSearchParams } from "next/navigation"
type HistoryContextValue = {
canGoBack: boolean
@@ -12,12 +13,28 @@ type HistoryContextValue = {
const HistoryContext = React.createContext<HistoryContextValue | null>(null)
export function HistoryProvider({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
// Reads useSearchParams() in its own Suspense boundary so the
// provider never blanks out children while search params resolve.
function PresetSync({
onPresetChange,
}: {
onPresetChange: (preset: string) => void
}) {
const searchParams = useSearchParams()
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 indexRef = 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 [maxIndex, setMaxIndex] = React.useState(0)
const onPresetChange = React.useCallback((nextPreset: string) => {
setPreset(nextPreset)
}, [])
React.useEffect(() => {
if (isNavigatingRef.current) {
isNavigatingRef.current = false
@@ -67,9 +88,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
} else {
params.delete("preset")
}
const pathname = window.location.pathname
const query = params.toString()
router.replace(query ? `${pathname}?${query}` : pathname)
}, [pathname, router])
}, [router])
const goForward = React.useCallback(() => {
if (indexRef.current >= maxIndexRef.current) {
@@ -88,9 +110,10 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
} else {
params.delete("preset")
}
const pathname = window.location.pathname
const query = params.toString()
router.replace(query ? `${pathname}?${query}` : pathname)
}, [pathname, router])
}, [router])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
@@ -133,7 +156,14 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
[canGoBack, canGoForward, goBack, goForward]
)
return <HistoryContext value={value}>{children}</HistoryContext>
return (
<HistoryContext value={value}>
<Suspense>
<PresetSync onPresetChange={onPresetChange} />
</Suspense>
{children}
</HistoryContext>
)
}
export function useHistory() {

View File

@@ -2,7 +2,7 @@
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: "design-system-params"

View File

@@ -24,10 +24,15 @@ const LocksContext = React.createContext<LocksContextValue | null>(null)
export function LocksProvider({ children }: { children: React.ReactNode }) {
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(
(param: LockableParam) => locks.has(param),
[locks]
(param: LockableParam) => locksRef.current.has(param),
[]
)
const toggleLock = React.useCallback((param: LockableParam) => {

View File

@@ -0,0 +1,81 @@
"use client"
import * as React from "react"
import useSWR from "swr"
const OPEN_PRESET_KEY = "create:open-preset-open"
export const OPEN_PRESET_FORWARD_TYPE = "open-preset-forward"
function isEditableTarget(target: EventTarget | null) {
return (
(target instanceof HTMLElement && target.isContentEditable) ||
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement
)
}
export function useOpenPreset() {
const { data: open = false, mutate: setOpenData } = useSWR<boolean>(
OPEN_PRESET_KEY,
{
fallbackData: false,
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
}
)
const handleOpenChange = React.useCallback(
(nextOpen: boolean) => {
void setOpenData(nextOpen, { revalidate: false })
},
[setOpenData]
)
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (
e.key === "o" &&
!e.shiftKey &&
!e.metaKey &&
!e.ctrlKey &&
!e.altKey
) {
if (isEditableTarget(e.target)) {
return
}
e.preventDefault()
void setOpenData(true, { revalidate: false })
}
}
document.addEventListener("keydown", down)
return () => {
document.removeEventListener("keydown", down)
}
}, [setOpenData])
return {
open,
setOpen: handleOpenChange,
}
}
export function useOpenPresetTrigger() {
const { mutate: setOpenData } = useSWR<boolean>(OPEN_PRESET_KEY, {
fallbackData: false,
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
})
const openPreset = React.useCallback(() => {
void setOpenData(true, { revalidate: false })
}, [setOpenData])
return {
openPreset,
}
}

View File

@@ -12,17 +12,17 @@ import {
STYLES,
type FontHeadingValue,
} from "@/registry/config"
import { useLocks } from "@/app/(create)/hooks/use-locks"
import { FONTS } from "@/app/(create)/lib/fonts"
import { useLocks } from "@/app/(app)/create/hooks/use-locks"
import { FONTS } from "@/app/(app)/create/lib/fonts"
import {
applyBias,
RANDOMIZE_BIASES,
type RandomizeContext,
} from "@/app/(create)/lib/randomize-biases"
} from "@/app/(app)/create/lib/randomize-biases"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
} from "@/app/(app)/create/lib/search-params"
function randomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)]

View File

@@ -3,8 +3,8 @@
import * as React from "react"
import useSWR from "swr"
import { DEFAULT_CONFIG } from "@/registry/config"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { DEFAULT_CONFIG, PRESETS } from "@/registry/config"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
const RESET_DIALOG_KEY = "create:reset-dialog-open"
export const RESET_FORWARD_TYPE = "reset-forward"
@@ -20,22 +20,27 @@ export function useReset() {
})
const reset = React.useCallback(() => {
const preset =
PRESETS.find(
(preset) => preset.base === params.base && preset.style === params.style
) ?? DEFAULT_CONFIG
setParams({
base: params.base,
style: DEFAULT_CONFIG.style,
baseColor: DEFAULT_CONFIG.baseColor,
theme: DEFAULT_CONFIG.theme,
chartColor: DEFAULT_CONFIG.chartColor,
iconLibrary: DEFAULT_CONFIG.iconLibrary,
font: DEFAULT_CONFIG.font,
fontHeading: DEFAULT_CONFIG.fontHeading,
menuAccent: DEFAULT_CONFIG.menuAccent,
menuColor: DEFAULT_CONFIG.menuColor,
radius: DEFAULT_CONFIG.radius,
style: params.style,
baseColor: preset.baseColor,
theme: preset.theme,
chartColor: preset.chartColor,
iconLibrary: preset.iconLibrary,
font: preset.font,
fontHeading: preset.fontHeading,
menuAccent: preset.menuAccent,
menuColor: preset.menuColor,
radius: preset.radius,
template: DEFAULT_CONFIG.template,
item: params.item,
})
}, [setParams, params.base, params.item])
}, [setParams, params.base, params.style, params.item])
const handleShowResetDialogChange = React.useCallback(
(open: boolean) => {

View File

@@ -1,7 +1,7 @@
import { Suspense } from "react"
import { HistoryProvider } from "@/app/(create)/hooks/use-history"
import { LocksProvider } from "@/app/(create)/hooks/use-locks"
import { HistoryProvider } from "@/app/(app)/create/hooks/use-history"
import { LocksProvider } from "@/app/(app)/create/hooks/use-locks"
export default function CreateLayout({
children,

View File

@@ -6,7 +6,7 @@ import { BASES, getThemesForBaseColor, type BaseName } from "@/registry/config"
import {
ALLOWED_ITEM_TYPES,
EXCLUDED_ITEMS,
} from "@/app/(create)/lib/constants"
} from "@/app/(app)/create/lib/constants"
export async function getItemsForBase(base: BaseName) {
const { Index } = await import("@/registry/bases/__index__")

View 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]

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest"
import { parsePresetInput } from "./parse-preset-input"
describe("parsePresetInput", () => {
it("accepts a raw preset code", () => {
expect(parsePresetInput("b0")).toBe("b0")
})
it("accepts a --preset flag", () => {
expect(parsePresetInput(" --preset b0 ")).toBe("b0")
})
it("rejects invalid preset input", () => {
expect(parsePresetInput("open sesame")).toBeNull()
})
})

View File

@@ -0,0 +1,15 @@
import { isPresetCode } from "shadcn/preset"
const PRESET_FLAG_PATTERN = /^--preset\b\s+(.+)$/i
export function parsePresetInput(value: string) {
const input = value.trim()
if (!input) {
return null
}
const preset = input.match(PRESET_FLAG_PATTERN)?.[1]?.trim() ?? input
return isPresetCode(preset) ? preset : null
}

View 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
}

View File

@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
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 () => {
return await vi.importActual("shadcn/schema")

View File

@@ -1,13 +1,21 @@
import { Suspense } from "react"
import { type Metadata } from "next"
import dynamic from "next/dynamic"
import { siteConfig } from "@/lib/config"
import { absoluteUrl } from "@/lib/utils"
import { SiteHeader } from "@/components/site-header"
import { Customizer } from "@/app/(create)/components/customizer"
import { PresetHandler } from "@/app/(create)/components/preset-handler"
import { Preview } from "@/app/(create)/components/preview"
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
import { getAllItems } from "@/app/(create)/lib/api"
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
import { Customizer } from "@/app/(app)/create/components/customizer"
import { PresetHandler } from "@/app/(app)/create/components/preset-handler"
import { Preview } from "@/app/(app)/create/components/preview"
import { getAllItems } from "@/app/(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 = {
title: "New Project",
@@ -38,24 +46,29 @@ export const metadata: Metadata = {
},
}
export default async function CreatePage() {
const itemsByBase = await getAllItems()
export default function CreatePage() {
return (
<div
data-slot="layout"
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
<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)]">
<div
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"
>
<Preview />
<Customizer itemsByBase={itemsByBase} />
<PresetHandler />
<WelcomeDialog />
</main>
<Suspense
fallback={
<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)" />
}
>
<CustomizerLoader />
</Suspense>
</div>
<PresetHandler />
<WelcomeDialog />
</div>
)
}
async function CustomizerLoader() {
const itemsByBase = await getAllItems()
return <Customizer itemsByBase={itemsByBase} />
}

View File

@@ -4,6 +4,7 @@ import { mdxComponents } from "@/mdx-components"
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
import { findNeighbour } from "fumadocs-core/page-tree"
import { replaceComponentsList } from "@/lib/llm"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
import { DocsBaseSwitcher } from "@/components/docs-base-switcher"
@@ -83,7 +84,7 @@ export default async function Page(props: {
const neighbours = isChangelog
? { previous: null, next: null }
: findNeighbour(source.pageTree, page.url)
const raw = await page.data.getText("raw")
const raw = replaceComponentsList(await page.data.getText("raw"))
return (
<div

View File

@@ -10,6 +10,8 @@ import { Button } from "@/styles/radix-nova/ui/button"
export const revalidate = false
export const dynamic = "force-static"
const NUMBER_OF_LATEST_PAGES = 2
export function generateMetadata() {
return {
title: "Changelog",
@@ -34,8 +36,8 @@ export function generateMetadata() {
export default function ChangelogPage() {
const pages = getChangelogPages()
const latestPages = pages.slice(0, 5)
const olderPages = pages.slice(5)
const latestPages = pages.slice(0, NUMBER_OF_LATEST_PAGES)
const olderPages = pages.slice(NUMBER_OF_LATEST_PAGES)
return (
<div
@@ -44,7 +46,7 @@ export default function ChangelogPage() {
>
<div className="flex min-w-0 flex-1 flex-col">
<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 items-center justify-between">
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">

View File

@@ -5,10 +5,10 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div
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 />
<main className="flex flex-1 flex-col">{children}</main>
<main className="flex min-h-0 flex-1 flex-col">{children}</main>
<SiteFooter />
</div>
)

View File

@@ -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>
)
}

View File

@@ -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>
</>
)
}

View File

@@ -1,75 +1,3 @@
"use client"
import { lazy, Suspense } from "react"
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>
)
}
export { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"

View File

@@ -1,152 +0,0 @@
"use client"
import * as React from "react"
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/action-menu"
import {
REDO_FORWARD_TYPE,
UNDO_FORWARD_TYPE,
} from "@/app/(create)/components/history-buttons"
import { DARK_MODE_FORWARD_TYPE } from "@/app/(create)/components/mode-switcher"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
import { RESET_FORWARD_TYPE } from "@/app/(create)/hooks/use-reset"
import {
serializeDesignSystemSearchParams,
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
// Hoisted — only uses module-level constants, no component state. (rendering-hoist-jsx)
function handleMessage(event: MessageEvent) {
if (
typeof window === "undefined" ||
event.origin !== window.location.origin
) {
return
}
const type = event.data.type
if (type === CMD_K_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "k",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === RANDOMIZE_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "r",
bubbles: true,
cancelable: true,
})
)
} else if (type === UNDO_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "z",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === REDO_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "z",
shiftKey: true,
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === RESET_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "R",
shiftKey: true,
bubbles: true,
cancelable: true,
})
)
} else if (type === DARK_MODE_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "d",
bubbles: true,
cancelable: true,
})
)
}
}
export function Preview() {
const [params] = useDesignSystemSearchParams()
const iframeRef = React.useRef<HTMLIFrameElement>(null)
React.useEffect(() => {
const iframe = iframeRef.current
if (!iframe) {
return
}
const sendParams = () => {
sendToIframe(iframe, "design-system-params", params)
}
if (iframe.contentWindow) {
sendParams()
}
iframe.addEventListener("load", sendParams)
return () => {
iframe.removeEventListener("load", sendParams)
}
}, [params])
React.useEffect(() => {
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
}, [])
const iframeSrc = React.useMemo(() => {
// The iframe src needs to include the serialized design system params
// for the initial load, but not be reactive to them as it would cause
// full-iframe reloads on every param change (flashes & loss of state).
// Further updates of the search params will be sent to the iframe
// via a postMessage channel, for it to sync its own history onto the host's.
return serializeDesignSystemSearchParams(
`/preview/${params.base}/${params.item}`,
params
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.base, params.item])
return (
<div className="relative flex flex-1 flex-col justify-center overflow-hidden rounded-2xl ring ring-foreground/10 md:ring-muted dark:ring-foreground/10">
<div className="relative z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden">
<div className="absolute inset-0 bg-muted dark:bg-muted/30" />
<iframe
key={params.base + params.item}
ref={iframeRef}
src={iframeSrc}
className="z-10 size-full flex-1"
title="Preview"
/>
</div>
</div>
)
}

View File

@@ -4,7 +4,7 @@ import {
designSystemConfigSchema,
type DesignSystemConfig,
} 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.
export function parseDesignSystemConfig(searchParams: URLSearchParams) {

View File

@@ -4,8 +4,8 @@ import { isPresetCode } from "shadcn/preset"
import { registryItemSchema } from "shadcn/schema"
import { buildRegistryBase } from "@/registry/config"
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
import { getPresetCode } from "@/app/(create)/lib/preset-code"
export async function GET(request: NextRequest) {
try {

View File

@@ -2,9 +2,9 @@ import { after, NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
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 { getPresetCode } from "@/app/(create)/lib/preset-code"
import { buildV0Payload } from "@/app/(create)/lib/v0"
export async function GET(request: NextRequest) {
try {

View File

@@ -1,233 +1 @@
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]
export * from "@/app/(app)/create/lib/fonts"

View File

@@ -1,309 +1 @@
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/(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
}
export * from "@/app/(app)/create/lib/search-params"

View File

@@ -6,17 +6,18 @@ import { siteConfig } from "@/lib/config"
import { absoluteUrl } from "@/lib/utils"
import { TailwindIndicator } from "@/components/tailwind-indicator"
import { BASES, type Base, type BaseName } from "@/registry/config"
import { ActionMenuScript } from "@/app/(create)/components/action-menu"
import { DesignSystemProvider } from "@/app/(create)/components/design-system-provider"
import { HistoryScript } from "@/app/(create)/components/history-buttons"
import { DarkModeScript } from "@/app/(create)/components/mode-switcher"
import { PreviewStyle } from "@/app/(create)/components/preview-style"
import { RandomizeScript } from "@/app/(create)/components/random-button"
import { ActionMenuScript } from "@/app/(app)/create/components/action-menu"
import { DesignSystemProvider } from "@/app/(app)/create/components/design-system-provider"
import { HistoryScript } from "@/app/(app)/create/components/history-buttons"
import { DarkModeScript } from "@/app/(app)/create/components/mode-switcher"
import { OpenPresetScript } from "@/app/(app)/create/components/open-preset"
import { PreviewStyle } from "@/app/(app)/create/components/preview-style"
import { RandomizeScript } from "@/app/(app)/create/components/random-button"
import {
getBaseComponent,
getBaseItem,
getItemsForBase,
} from "@/app/(create)/lib/api"
} from "@/app/(app)/create/lib/api"
export const revalidate = false
export const dynamic = "force-static"
@@ -139,6 +140,7 @@ export default async function BlockPage({
<PreventScrollOnFocusScript />
<PreviewStyle />
<ActionMenuScript />
<OpenPresetScript />
<RandomizeScript />
<HistoryScript />
<DarkModeScript />

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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"
}
]

Some files were not shown because too many files have changed in this diff Show More