diff --git a/README.md b/README.md index a1cc89b..27dfdce 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ npx @hype-stack/cli mcp install

Deploy the entire stack with one command

- + npx @hype-stack/cli deploy: services provisioned, environment variables set, migrations applied, backend, frontend and admin live

@@ -54,7 +54,7 @@ npx @hype-stack/cli deploy

Frontend, admin, backend, Postgres, cache and storage, live on Railway or Fly.io.
-Mobile builds through EAS. The extension publishes to Chrome, Edge and Firefox. Deploy docs +Mobile builds through EAS. The extension publishes to Chrome, Edge and Firefox. Deploy docs

  @@ -122,14 +122,14 @@ npx @hype-stack/cli community # Scaffold, validate and install packs anyone wr npx @hype-stack/cli mcp install # Give your AI editor the same commands as tools ``` -No lock-in. No runtime dependency. Just code in your repo. Every command is documented in the [CLI docs](https://www.hype-stack.dev/docs/cli). +No lock-in. No runtime dependency. Just code in your repo. Every command is documented in the [CLI docs](https://www.hype-stack.dev/docs/cli?utm_source=hype-stack&utm_medium=referral&utm_campaign=readme).   ## Use Feature Packs. Or Build Your Own.

- + Use feature packs or build your own: npx @hype-stack/cli compose writes 56 files into your repo across backend, frontend and admin

@@ -147,10 +147,10 @@ A pack covers every layer at once: routes, Prisma models, webhook handlers, fron | **Cookie Consent** | Banner and preferences. Free |

-Browse packs · -Adding a pack · -Write your own pack · -Manifest reference +Browse packs · +Adding a pack · +Write your own pack · +Manifest reference

> 🔑 Premium packs need a license. Run `npx @hype-stack/cli login` to install the ones your organization owns. The base template, the Basic layout and the Cookie Consent pack are open source forever. @@ -168,62 +168,62 @@ npx @hype-stack/cli template open-calendar
- + Better Studio
-Better Studio
+Better Studio
Auth, billing and an app shell
- + Open Calendar
-Open Calendar
+Open Calendar
Scheduling and calendar
- + Aether
-Aether
+Aether
AI agent chat
- + Vault
-Vault
+Vault
Workflow automation
- + Mind Map
-Mind Map
+Mind Map
Collaborative whiteboard
- + Indie Hacker Portfolio
-Indie Hacker Portfolio
+Indie Hacker Portfolio
Portfolio with live revenue and a newsletter

-All templates +All templates

  @@ -247,19 +247,19 @@ npx @hype-stack/cli template open-calendar ## Built for AI Agents

- + npx @hype-stack/cli mcp install: 11 MCP tools for Cursor, Claude Code, Windsurf and Copilot

-The codebase follows a [vertical architecture](https://tkdodo.eu/blog/the-vertical-codebase): each feature owns its routes, UI, data access, types and tests. `create` writes rules for the editor you pick, and the CLI runs as an MCP server so your agent can search the catalog, plan an install, add packs and set up the project. [MCP docs](https://www.hype-stack.dev/docs/cli/mcp) +The codebase follows a [vertical architecture](https://tkdodo.eu/blog/the-vertical-codebase): each feature owns its routes, UI, data access, types and tests. `create` writes rules for the editor you pick, and the CLI runs as an MCP server so your agent can search the catalog, plan an install, add packs and set up the project. [MCP docs](https://www.hype-stack.dev/docs/cli/mcp?utm_source=hype-stack&utm_medium=referral&utm_campaign=readme)   ## One Backend, Five Clients

- + Web, desktop, admin, mobile and extension apps sharing one typed Hono backend

@@ -323,7 +323,7 @@ pnpm typecheck # Full type checking pnpm test # Run all tests (backend: pnpm test:setup first) ``` -Full setup, environment variables and troubleshooting live in the [docs](https://www.hype-stack.dev/docs). +Full setup, environment variables and troubleshooting live in the [docs](https://www.hype-stack.dev/docs?utm_source=hype-stack&utm_medium=referral&utm_campaign=readme).   diff --git a/apps/admin/src/components/ui/button-group.tsx b/apps/admin/src/components/ui/button-group.tsx new file mode 100644 index 0000000..03b3380 --- /dev/null +++ b/apps/admin/src/components/ui/button-group.tsx @@ -0,0 +1,81 @@ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; + +/** + * The template's default SelectTrigger is h-10 to match Input, while the default Button + * is h-9. Inside a group they must share a height, so default-size triggers are pulled + * down to the button height here. + */ +const buttonGroupVariants = cva( + "flex w-fit items-stretch [&>[data-slot=select-trigger][data-size=default]]:h-9 has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", + { + variants: { + orientation: { + horizontal: + "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", + vertical: + "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", + }, + }, + defaultVariants: { + orientation: "horizontal", + }, + }, +); + +function ButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function ButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean; +}) { + const Comp = asChild ? Slot : "div"; + + return ( + + ); +} + +function ButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants }; diff --git a/apps/admin/src/components/ui/empty.tsx b/apps/admin/src/components/ui/empty.tsx new file mode 100644 index 0000000..0a8b73c --- /dev/null +++ b/apps/admin/src/components/ui/empty.tsx @@ -0,0 +1,86 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Empty({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +const emptyMediaVariants = cva( + "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-transparent", + icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function EmptyMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +

a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", + className, + )} + {...props} + /> + ); +} + +function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +

+ ); +} + +export { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle }; diff --git a/apps/admin/src/components/ui/input-group.tsx b/apps/admin/src/components/ui/input-group.tsx new file mode 100644 index 0000000..51b5f47 --- /dev/null +++ b/apps/admin/src/components/ui/input-group.tsx @@ -0,0 +1,156 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/utils"; + +/** + * Ported from shadcn. The template's Input wraps its `` in a div, so the + * selectors below target descendant inputs (`[&_input]`) instead of direct children, + * and InputGroupInput styles the control through `inputClassName`. + */ +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
textarea]:h-auto", + + // Variants based on alignment. + "has-[>[data-align=inline-start]]:[&_input]:pl-2", + "has-[>[data-align=inline-end]]:[&_input]:pr-2", + "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&_input]:pb-3", + "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&_input]:pt-3", + + // Focus state. + "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]", + + // Error state. + "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", + + className, + )} + {...props} + /> + ); +} + +const inputGroupAddonVariants = cva( + "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]", + "inline-end": "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", + "block-start": + "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3", + "block-end": "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3", + }, + }, + defaultVariants: { + align: "inline-start", + }, + }, +); + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + // The addon behaves like a label: clicking it moves focus into the control. Keyboard + // users reach the control directly with Tab, so a key handler here would add nothing. + return ( + // oxlint-disable-next-line jsx-a11y/click-events-have-key-events +
{ + if ((e.target as HTMLElement).closest("button")) { + return; + } + e.currentTarget.parentElement?.querySelector("input")?.focus(); + }} + {...props} + /> + ); +} + +const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", + sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5", + "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0", + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, + }, + defaultVariants: { + size: "xs", + }, +}); + +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size"> & VariantProps) { + return ( +