shapes-ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.github/workflows/pr-preview.yml +9 -2
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +13 -0
  4. package/content/components/accordion.mdx +13 -0
  5. package/content/components/alert-dialog.mdx +34 -0
  6. package/content/components/autocomplete.mdx +62 -0
  7. package/content/components/avatar.mdx +11 -0
  8. package/content/components/button.mdx +8 -0
  9. package/content/components/checkbox.mdx +11 -0
  10. package/content/components/collapsible.mdx +11 -0
  11. package/content/components/combobox.mdx +33 -0
  12. package/content/components/context-menu.mdx +29 -0
  13. package/content/components/dialog.mdx +33 -0
  14. package/content/components/drawer.mdx +38 -0
  15. package/content/components/field.mdx +21 -0
  16. package/content/components/fieldset.mdx +10 -0
  17. package/content/components/form.mdx +8 -0
  18. package/content/components/input.mdx +4 -0
  19. package/content/components/menu.mdx +27 -0
  20. package/content/components/menubar.mdx +31 -0
  21. package/content/components/meter.mdx +14 -0
  22. package/content/components/navigation-menu.mdx +28 -0
  23. package/content/components/number-field.mdx +25 -0
  24. package/content/components/popover.mdx +22 -0
  25. package/content/components/preview-card.mdx +14 -2
  26. package/content/components/progress.mdx +15 -1
  27. package/content/components/radio.mdx +11 -0
  28. package/content/components/scroll-area.mdx +23 -0
  29. package/content/components/select.mdx +27 -0
  30. package/content/components/separator.mdx +29 -0
  31. package/content/components/slider.mdx +4 -0
  32. package/content/components/switch.mdx +4 -0
  33. package/content/components/tabs.mdx +15 -0
  34. package/content/components/toast.mdx +10 -0
  35. package/content/components/toggle-group.mdx +37 -0
  36. package/content/components/toggle.mdx +12 -0
  37. package/content/components/toolbar.mdx +22 -0
  38. package/content/components/tooltip.mdx +13 -0
  39. package/content/docs/installation.mdx +30 -0
  40. package/content-collections.ts +65 -1
  41. package/dist/cli.js +947 -101
  42. package/examples/__index.tsx +136 -68
  43. package/examples/autocomplete-align.tsx +39 -0
  44. package/examples/autocomplete-controlled.tsx +44 -0
  45. package/examples/autocomplete-groups.tsx +65 -0
  46. package/examples/autocomplete-no-clear.tsx +40 -0
  47. package/examples/avatar-demo.tsx +3 -3
  48. package/examples/input-group-with-button.tsx +1 -1
  49. package/examples/separator-demo.tsx +13 -0
  50. package/examples/separator-horizontal.tsx +18 -0
  51. package/package.json +19 -18
  52. package/public/base-ui.svg +1 -0
  53. package/src/assets/base-ui.svg +1 -0
  54. package/src/commands/add.ts +79 -38
  55. package/src/commands/cli.ts +50 -3
  56. package/src/commands/create.ts +262 -0
  57. package/src/commands/init.ts +45 -12
  58. package/src/commands/palette.ts +55 -0
  59. package/src/components/docs/layout/footer.tsx +2 -2
  60. package/src/components/docs/layout/header.tsx +7 -9
  61. package/src/components/docs/layout/mobile-menu.tsx +0 -1
  62. package/src/components/docs/layout/nav-list.tsx +2 -2
  63. package/src/components/docs/layout/page-header.tsx +52 -7
  64. package/src/components/docs/layout/split-layout.tsx +9 -10
  65. package/src/components/docs/layout/table-of-content.tsx +145 -0
  66. package/src/components/docs/markdown/components.tsx +142 -29
  67. package/src/components/docs/markdown/copy-button.tsx +41 -0
  68. package/src/components/docs/markdown/installation-block.tsx +5 -24
  69. package/src/components/docs/markdown/render-preview.tsx +1 -1
  70. package/src/components/ui/button-group.tsx +1 -1
  71. package/src/components/ui/scroll-area.tsx +11 -2
  72. package/src/lib/docs-headings.ts +72 -0
  73. package/src/routeTree.gen.ts +60 -3
  74. package/src/routes/__root.tsx +2 -2
  75. package/src/routes/components.$slug.tsx +20 -4
  76. package/src/routes/docs.$slug.tsx +78 -0
  77. package/src/routes/docs.tsx +15 -0
  78. package/src/styles/styles.css +1 -1
  79. package/src/utils/cli-utils.ts +8 -8
  80. package/src/utils/dependency-installer.ts +30 -0
  81. package/src/utils/package-manager.ts +4 -4
  82. package/src/utils/palette.ts +666 -0
  83. package/src/utils/schema.ts +6 -0
@@ -1,7 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { confirm, intro, note, outro, select, text, spinner } from "@clack/prompts";
4
- import { execa } from "execa";
3
+ import { confirm, intro, note, outro, select, text } from "@clack/prompts";
5
4
  import figlet from "figlet";
6
5
  import fs from "fs-extra";
7
6
  import gradient from "gradient-string";
@@ -15,7 +14,12 @@ import {
15
14
  isViteProject,
16
15
  readPackageJson,
17
16
  } from "@/utils/cli-utils";
18
- import { getInstallCommand } from "@/utils/package-manager";
17
+ import { installDependencies } from "@/utils/dependency-installer";
18
+ import {
19
+ type ContrastMode,
20
+ getDefaultBrandPaletteOptions,
21
+ writePaletteTokens,
22
+ } from "@/utils/palette";
19
23
  import { type Config } from "@/utils/schema";
20
24
 
21
25
  const BASE_DEPS = [
@@ -28,15 +32,10 @@ const BASE_DEPS = [
28
32
  ];
29
33
 
30
34
  async function installDeps(deps: string[], dev = false) {
31
- if (!deps.length) return;
32
- const spin = spinner();
33
- const label = dev ? "Installing dev dependencies" : "Installing dependencies";
34
- spin.start(label);
35
-
36
- const [command, ...args] = await getInstallCommand(deps, dev);
37
- await execa(command, args);
38
-
39
- spin.stop(pc.green("Dependencies installed"));
35
+ await installDependencies(deps, {
36
+ dev,
37
+ successMessage: pc.green("Dependencies installed"),
38
+ });
40
39
  }
41
40
 
42
41
  async function ensureTailwindStyles(cssPath: string) {
@@ -81,7 +80,30 @@ export async function initCommand() {
81
80
  options: [
82
81
  { label: "Default", value: "default" },
83
82
  { label: "Brutalist", value: "brutalist" },
83
+ { label: "Minimal", value: "minimal" },
84
+ ],
85
+ }),
86
+ );
87
+
88
+ const palette = exitIfCancelled(
89
+ await select({
90
+ message: "Pick a brand palette",
91
+ options: getDefaultBrandPaletteOptions().map((name) => ({
92
+ label: name[0].toUpperCase() + name.slice(1),
93
+ value: name,
94
+ })),
95
+ initialValue: "blue",
96
+ }),
97
+ );
98
+
99
+ const contrastMode = exitIfCancelled(
100
+ await select({
101
+ message: "Choose foreground contrast strategy",
102
+ options: [
103
+ { label: "Deterministic shades", value: "deterministic" },
104
+ { label: "Dynamic contrast", value: "dynamic" },
84
105
  ],
106
+ initialValue: "deterministic",
85
107
  }),
86
108
  );
87
109
 
@@ -121,9 +143,20 @@ export async function initCommand() {
121
143
  await installDeps(missingBaseDeps, false);
122
144
  await ensureTailwindStyles(cssPath);
123
145
 
146
+ const paletteResult = await writePaletteTokens({
147
+ cssPath,
148
+ paletteName: palette as string,
149
+ neutralPalette: "zinc",
150
+ contrastMode: contrastMode as ContrastMode,
151
+ });
152
+
124
153
  const config: Config = {
125
154
  $schema: "https://shapes-ui.com/schema.json",
126
155
  style: style as Config["style"],
156
+ palette: {
157
+ name: paletteResult.paletteName,
158
+ contrastMode: paletteResult.contrastMode,
159
+ },
127
160
  tailwind: { css: cssPath as string, baseColor: "zinc" },
128
161
  paths: { ui: uiPath as string, lib: "./src/lib" },
129
162
  };
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+
3
+ import { intro, note, outro } from "@clack/prompts";
4
+ import fs from "fs-extra";
5
+ import pc from "picocolors";
6
+
7
+ import { getConfig } from "@/utils/cli-utils";
8
+ import { normalizeContrastMode, writePaletteTokens } from "@/utils/palette";
9
+
10
+ type PaletteSetOptions = {
11
+ cssPath?: string;
12
+ contrastMode?: string;
13
+ refresh?: boolean;
14
+ };
15
+
16
+ export async function paletteSetCommand(name: string, options: PaletteSetOptions = {}) {
17
+ intro(pc.bgCyan(pc.black(" Update Shapes palette ")));
18
+
19
+ const config = await getConfig();
20
+ if (!config) {
21
+ throw new Error("Could not find shapes.json. Run `shapes-ui init` first.");
22
+ }
23
+
24
+ const contrastMode = normalizeContrastMode(options.contrastMode ?? config.palette?.contrastMode);
25
+ const cssPath = options.cssPath ?? config.tailwind.css;
26
+ const neutralPalette = config.tailwind.baseColor;
27
+
28
+ const result = await writePaletteTokens({
29
+ cssPath,
30
+ paletteName: name,
31
+ neutralPalette,
32
+ contrastMode,
33
+ refresh: options.refresh,
34
+ });
35
+
36
+ const updatedConfig = {
37
+ ...config,
38
+ palette: {
39
+ name: result.paletteName,
40
+ contrastMode: result.contrastMode,
41
+ },
42
+ tailwind: {
43
+ ...config.tailwind,
44
+ css: cssPath,
45
+ },
46
+ };
47
+
48
+ await fs.writeJSON(path.join(process.cwd(), "shapes.json"), updatedConfig, { spaces: 2 });
49
+
50
+ note(
51
+ `palette=${result.paletteName} neutral=${result.neutralPalette} contrast=${result.contrastMode} source=${result.source}`,
52
+ "Palette updated",
53
+ );
54
+ outro(pc.green("Shapes UI palette updated."));
55
+ }
@@ -2,8 +2,8 @@ import { Badge } from "@/components/ui/badge";
2
2
 
3
3
  export function Footer() {
4
4
  return (
5
- <footer className=" h-12 border-t">
6
- <div className=" container mx-auto flex h-full items-center border-x px-4">
5
+ <footer className=" z-10 h-12">
6
+ <div className=" container mx-auto flex h-full items-center px-4">
7
7
  <Badge variant={"info"}>Alpha</Badge>
8
8
  </div>
9
9
  </footer>
@@ -1,6 +1,5 @@
1
1
  import { Link } from "@tanstack/react-router";
2
- import { SunMoonIcon } from "lucide-react";
3
-
2
+ import { SunMoonIcon } from "lucide-react";
4
3
 
5
4
  import { DocsButton } from "@/components/docs/docs-button";
6
5
  import { useTheme } from "@/components/docs/theme-provider";
@@ -10,16 +9,16 @@ import { MobileMenu } from "./mobile-menu";
10
9
  export function Header() {
11
10
  const { toggleTheme, theme } = useTheme();
12
11
 
13
-
14
12
  return (
15
- <header className=" sticky top-0 z-10 h-12 border-b bg-background">
16
- <div className=" container mx-auto flex h-full items-center justify-between border-x px-4">
17
- <Link to="/">
13
+ <header className=" sticky top-0 z-10 h-12">
14
+ <div className=" container mx-auto flex h-full items-center justify-between px-4">
15
+ <Link to="/" className="flex items-center gap-2 font-medium font-heading tracking-wide">
18
16
  <img
19
17
  src={theme === "dark" ? "/shps_white.svg" : "/shps_black.svg"}
20
18
  alt="Shapes UI Logo"
21
- className=" size-6"
19
+ className=" size-5"
22
20
  />
21
+ shapes ui
23
22
  </Link>
24
23
 
25
24
  <div className=" mx-auto hidden gap-4 lg:flex">
@@ -31,9 +30,8 @@ export function Header() {
31
30
  <DocsButton onClick={toggleTheme} variant={"ghost"} className={"hidden lg:inline-flex"}>
32
31
  <SunMoonIcon className=" size-4" />
33
32
  </DocsButton>
34
- <MobileMenu />
33
+ <MobileMenu />
35
34
  </div>
36
-
37
35
  </header>
38
36
  );
39
37
  }
@@ -13,7 +13,6 @@ export function MobileMenu() {
13
13
  return <Drawer open={open} onOpenChange={setOpen} >
14
14
  <DrawerTrigger className={'flex md:hidden'} render={<Button size={'xs'} variant={'secondary'} >
15
15
  <MenuIcon />
16
- Navigation
17
16
  </Button>} />
18
17
  <DrawerPopup position="bottom">
19
18
  <nav className="mt-4 flex flex-col gap-2">
@@ -17,9 +17,9 @@ function NavSection({ title, children }: { title: string; children: React.ReactN
17
17
 
18
18
  export function NavSidebar({ className, ...props }: ComponentProps<"nav">) {
19
19
  return (
20
- <nav className={cn("hidden h-full min-h-0 w-64 flex-col p-4 lg:flex", className)} {...props}>
20
+ <nav className={cn("hidden h-full min-h-0 w-64 flex-col p-4 bg-card lg:flex", className)} {...props}>
21
21
  <NavSection title="Components">
22
- <ScrollArea className="h-full">
22
+ <ScrollArea className="h-[98%]">
23
23
  <ul>
24
24
  {[...allComponents]
25
25
  .sort((a, b) => a.title.localeCompare(b.title))
@@ -1,33 +1,78 @@
1
+ import { IconMarkdown } from "@tabler/icons-react";
2
+ import { ChevronDownIcon } from "lucide-react";
1
3
  import { ComponentProps } from "react";
2
4
 
5
+ import BaseUI from "@/assets/base-ui.svg?url";
6
+ import { Button } from "@/components/ui/button";
7
+ import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from "@/components/ui/button-group";
8
+ import { Menu, MenuItem, MenuPopup, MenuTrigger } from "@/components/ui/menu";
3
9
  import { cn } from "@/lib/utils";
4
10
 
5
- import CircuitBoard from "./circuit-board";
6
-
7
11
  export function PageHeader({
8
12
  title,
9
13
  subtitle,
10
14
  children,
15
+ baseUILink,
16
+ markdownLink,
11
17
  className,
12
18
  ...props
13
19
  }: ComponentProps<"div"> & {
14
20
  title: string;
15
21
  subtitle?: string;
22
+ baseUILink?: string;
23
+ markdownLink?: string;
16
24
  }) {
25
+ const handleOpenMarkdown = () => {
26
+ if (!markdownLink) {
27
+ return;
28
+ }
29
+
30
+ window.open(markdownLink, "_blank", "noopener,noreferrer");
31
+ };
32
+
17
33
  return (
18
34
  <div
19
35
  className={cn(
20
- "flex h-32 items-center overflow-hidden border-b bg-background md:h-60",
36
+ "flex items-start justify-between overflow-hidden bg-background max-w-7xl mx-auto p-4 lg:px-0 ",
21
37
  className,
22
38
  )}
23
39
  {...props}
24
40
  >
25
- <div className=" flex flex-col gap-1 p-6">
26
- <h1 className="font-heading text-3xl font-bold xl:text-5xl">{title}</h1>
27
- {subtitle && <p className="text-xs text-muted-foreground md:text-sm">{subtitle}</p>}
41
+ <div className=" flex flex-col">
42
+ <h1 className="font-heading text-2xl font-bold xl:text-3xl">{title}</h1>
43
+ {subtitle && <p className="text-xs w-2/3 text-muted-foreground md:text-sm">{subtitle}</p>}
28
44
  {children}
29
45
  </div>
30
- <CircuitBoard className="mask-rtl ml-auto h-full max-w-70 text-foreground/40" />
46
+
47
+ <div className="flex flex-col">
48
+ <ButtonGroup>
49
+ <ButtonGroupText>
50
+ View
51
+ </ButtonGroupText>
52
+ <ButtonGroupSeparator />
53
+ <Menu>
54
+ <MenuTrigger
55
+ render={
56
+ <Button variant={"secondary"} size={"icon"}>
57
+ <ChevronDownIcon />
58
+ </Button>
59
+ }
60
+ />
61
+ <MenuPopup align="end">
62
+ <MenuItem onClick={handleOpenMarkdown} disabled={!markdownLink}>
63
+ <IconMarkdown />
64
+ Markdown
65
+ </MenuItem>
66
+ {baseUILink && (
67
+ <MenuItem>
68
+ <img className="size-4" alt="Base UI" src={BaseUI} />
69
+ Base UI
70
+ </MenuItem>
71
+ )}
72
+ </MenuPopup>
73
+ </Menu>
74
+ </ButtonGroup>
75
+ </div>
31
76
  </div>
32
77
  );
33
78
  }
@@ -1,24 +1,23 @@
1
- import { ComponentProps } from "react";
1
+ import { ComponentProps, useState } from "react";
2
2
 
3
3
  import { ScrollArea } from "@/components/ui/scroll-area";
4
4
  import { cn } from "@/lib/utils";
5
5
 
6
6
  import { NavSidebar } from "./nav-list";
7
+ import { TableOfContent } from "./table-of-content";
7
8
 
8
- export function SplitLayout({
9
- children,
10
- className,
9
+ export function SplitLayout({ children, className, ...props }: ComponentProps<"div">) {
10
+ const [contentViewport, setContentViewport] = useState<HTMLDivElement | null>(null);
11
11
 
12
- ...props
13
- }: ComponentProps<"div">) {
14
12
  return (
15
- <div className={cn("flex h-full min-h-0 w-full", className)} {...props}>
13
+ <div className={cn("flex h-full min-h-0 w-full border", className)} {...props}>
16
14
  <NavSidebar />
17
- <main className="min-w-0 flex-1">
18
- <ScrollArea className="h-full">
19
- <div className="min-h-full lg:border-l">{children}</div>
15
+ <main className="min-w-0 flex-1 bg-card ">
16
+ <ScrollArea viewportRef={setContentViewport} className="h-full">
17
+ <div className="min-h-full">{children}</div>
20
18
  </ScrollArea>
21
19
  </main>
20
+ <TableOfContent contentViewport={contentViewport} />
22
21
  </div>
23
22
  );
24
23
  }
@@ -0,0 +1,145 @@
1
+ import { Link, useRouterState } from "@tanstack/react-router";
2
+ import { allComponents, allDocs } from "content-collections";
3
+ import { useEffect, useMemo, useState } from "react";
4
+
5
+ import { ScrollArea } from "@/components/ui/scroll-area";
6
+ import type { TocHeading } from "@/lib/docs-headings";
7
+ import { cn } from "@/lib/utils";
8
+
9
+ function getRouteContent(pathname: string): TocHeading[] {
10
+ const [section, slug] = pathname.split("/").filter(Boolean);
11
+
12
+ if (!slug) {
13
+ return [];
14
+ }
15
+
16
+ if (section === "components") {
17
+ const current = allComponents.find((component) => component.slug === slug) as
18
+ | { toc?: TocHeading[] }
19
+ | undefined;
20
+
21
+ return current?.toc ?? [];
22
+ }
23
+
24
+ if (section === "docs") {
25
+ const current = allDocs.find((doc) => doc.slug === slug) as { toc?: TocHeading[] } | undefined;
26
+
27
+ return current?.toc ?? [];
28
+ }
29
+
30
+ return [];
31
+ }
32
+
33
+ type TableOfContentProps = {
34
+ contentViewport: HTMLDivElement | null;
35
+ };
36
+
37
+ export const TableOfContent = ({ contentViewport }: TableOfContentProps) => {
38
+ const pathname = useRouterState({ select: (state) => state.location.pathname });
39
+ const toc = useMemo(() => getRouteContent(pathname), [pathname]);
40
+ const [activeId, setActiveId] = useState<string>(toc[0]?.id ?? "");
41
+
42
+ useEffect(() => {
43
+ if (!toc.length) {
44
+ setActiveId("");
45
+ return;
46
+ }
47
+
48
+ let frameId = 0;
49
+ let warmupInterval: number | undefined;
50
+
51
+ const computeActiveHeading = () => {
52
+ const topOffset = contentViewport ? contentViewport.getBoundingClientRect().top + 96 : 120;
53
+
54
+ const renderedHeadings = toc
55
+ .map((heading) => ({
56
+ id: heading.id,
57
+ element: document.getElementById(heading.id),
58
+ }))
59
+ .filter((item) => item.element);
60
+
61
+ if (!renderedHeadings.length) {
62
+ return;
63
+ }
64
+
65
+ let nextActive = renderedHeadings[0]?.id ?? "";
66
+
67
+ for (const item of renderedHeadings) {
68
+ if ((item.element?.getBoundingClientRect().top ?? Number.POSITIVE_INFINITY) <= topOffset) {
69
+ nextActive = item.id;
70
+ continue;
71
+ }
72
+
73
+ break;
74
+ }
75
+
76
+ setActiveId((prev) => (prev === nextActive ? prev : nextActive));
77
+ };
78
+
79
+ const scheduleActiveUpdate = () => {
80
+ if (frameId) {
81
+ cancelAnimationFrame(frameId);
82
+ }
83
+
84
+ frameId = requestAnimationFrame(() => {
85
+ computeActiveHeading();
86
+ frameId = 0;
87
+ });
88
+ };
89
+
90
+ contentViewport?.addEventListener("scroll", scheduleActiveUpdate, { passive: true });
91
+ window.addEventListener("resize", scheduleActiveUpdate);
92
+
93
+ scheduleActiveUpdate();
94
+ warmupInterval = window.setInterval(scheduleActiveUpdate, 80);
95
+ window.setTimeout(() => {
96
+ if (warmupInterval) {
97
+ window.clearInterval(warmupInterval);
98
+ }
99
+ }, 1500);
100
+
101
+ return () => {
102
+ if (frameId) {
103
+ cancelAnimationFrame(frameId);
104
+ }
105
+
106
+ if (warmupInterval) {
107
+ window.clearInterval(warmupInterval);
108
+ }
109
+
110
+ contentViewport?.removeEventListener("scroll", scheduleActiveUpdate);
111
+ window.removeEventListener("resize", scheduleActiveUpdate);
112
+ };
113
+ }, [contentViewport, pathname, toc]);
114
+
115
+ return (
116
+ <aside className="hidden h-full min-h-0 w-64 flex-col p-4 xl:flex">
117
+ <h4 className="pl-2 font-heading text-xs font-medium text-muted-foreground">On this page</h4>
118
+ <ScrollArea className="mt-2 h-[98%]">
119
+ <ul className="space-y-1">
120
+ {toc.map((heading) => {
121
+ const isActive = activeId === heading.id;
122
+
123
+ return (
124
+ <li key={heading.id}>
125
+ <Link
126
+ to="."
127
+ hash={heading.id}
128
+ style={{ paddingLeft: `${(heading.level - 1) * 10}px` }}
129
+ className={cn(
130
+ "block truncate rounded px-2 py-1.5 text-xs transition-colors",
131
+ isActive
132
+ ? "bg-muted text-foreground"
133
+ : "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
134
+ )}
135
+ >
136
+ {heading.title}
137
+ </Link>
138
+ </li>
139
+ );
140
+ })}
141
+ </ul>
142
+ </ScrollArea>
143
+ </aside>
144
+ );
145
+ };