diff --git a/.changeset/gateway-authority-default.md b/.changeset/gateway-authority-default.md new file mode 100644 index 0000000..ecca068 --- /dev/null +++ b/.changeset/gateway-authority-default.md @@ -0,0 +1,12 @@ +--- +"@etus/bhono": minor +"@etus/create-bhono": minor +--- + +Gateway-as-authority is now the default authorization model for scaffolded apps. + +`@etus/auth` bumped to **0.8.0**: generated apps derive permissions from the gateway (RBAC ∪ access_grants) via `scopeMap`, keeping the BFF model (no user tokens stored). Admin is expressed as a scope (`bhono:admin → ['*']`) gated with `requirePermission` — the 0.8.0 release removed the Phase 2 role-derivation (a local role persisted from gateway scopes that could not be revoked) after an adversarial review, so authorization flows scope → permission and revocations propagate within the cache TTL. + +Off by default so a freshly generated app boots before its gateway resource exists. Opt in per app with `ETUS_GATEWAY_AUTHORITY=true` + `ETUS_RESOURCE_ID` + `ETUS_INTEGRATION_KEY` after registering the app as a `web_app` resource in the gateway (see `docs/SETUP-GUIDE.md`). + +Also: sessions moved to D1 (`createSqlSessionStore`, dev-login D1-only), the frontend migrated to the Seven design system (`@etus/seven-react`), and the CLI `--version` drift (stale `0.1.5`) was corrected. diff --git a/.dev.vars.example b/.dev.vars.example index b9f85c4..613651b 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -11,6 +11,14 @@ ETUS_CLIENT_SECRET=your-client-secret-from-npx-etus-auth-init ETUS_ALLOWED_DOMAINS=yourdomain.com,anotherdomain.com ETUS_ADMIN_EMAILS=admin@yourdomain.com +# Gateway-as-authority (@etus/auth v0.7.0) — OFF by default. Turn on once your app +# is registered as a web_app resource in the gateway and you hold an integration +# key (see docs/SETUP-GUIDE.md → "Gateway-as-authority onboarding"). The key is a +# secret — keep it in .dev.vars locally and `wrangler secret put` in deployments. +# ETUS_GATEWAY_AUTHORITY=true +# ETUS_RESOURCE_ID=yourapp.example.com +# ETUS_INTEGRATION_KEY=ag_app_yourapp_xxx.yyy + # SendGrid SENDGRID_API_KEY=your-sendgrid-api-key SENDGRID_FROM_EMAIL=noreply@yourdomain.com diff --git a/.env.example b/.env.example index 539e2cf..a6aff2c 100644 --- a/.env.example +++ b/.env.example @@ -15,9 +15,19 @@ ETUS_CLIENT_SECRET=your-client-secret-from-npx-etus-auth-init ETUS_ALLOWED_DOMAINS=yourdomain.com,anotherdomain.com # Comma-separated emails that always receive the 'admin' role. # @etus/auth requires at least one. The template ships a generic placeholder — -# replace it with your real product admin(s) before deploying. +# replace it with your real product admin(s) before deploying. With +# gateway-as-authority on, this is only the day-0 bootstrap — the real admin role +# is then granted by the gateway (see ETUS_GATEWAY_AUTHORITY below). ETUS_ADMIN_EMAILS=admin@yourdomain.com +# Gateway-as-authority (@etus/auth v0.7.0) — OFF by default. Set to "true" once +# the app is registered as a web_app resource in the gateway and you have an +# integration key (scope app.grants.read). See docs/SETUP-GUIDE.md. The key is a +# secret — `wrangler secret put ETUS_INTEGRATION_KEY` in deployed environments. +# ETUS_GATEWAY_AUTHORITY=true +# ETUS_RESOURCE_ID=yourapp.example.com +# ETUS_INTEGRATION_KEY=ag_app_yourapp_xxx.yyy + # SendGrid (for invitations) SENDGRID_API_KEY=your-sendgrid-api-key SENDGRID_FROM_EMAIL=noreply@yourdomain.com diff --git a/config/wrangler.json b/config/wrangler.json index 53a7126..44973cb 100644 --- a/config/wrangler.json +++ b/config/wrangler.json @@ -43,7 +43,9 @@ "ETUS_GATEWAY": "https://ag.etus.io", "ETUS_CLIENT_ID": "boilerplate-hono", "ETUS_ALLOWED_DOMAINS": "brius.com.br,etus.com.br", - "ETUS_ADMIN_EMAILS": "aa@brius.com.br" + "ETUS_ADMIN_EMAILS": "aa@brius.com.br", + "ETUS_GATEWAY_AUTHORITY": "false", + "ETUS_RESOURCE_ID": "" }, "env": { "staging": { @@ -74,7 +76,9 @@ "ETUS_GATEWAY": "https://ag.etus.io", "ETUS_CLIENT_ID": "boilerplate-hono", "ETUS_ALLOWED_DOMAINS": "brius.com.br,etus.com.br", - "ETUS_ADMIN_EMAILS": "aa@brius.com.br" + "ETUS_ADMIN_EMAILS": "aa@brius.com.br", + "ETUS_GATEWAY_AUTHORITY": "false", + "ETUS_RESOURCE_ID": "" } } } diff --git a/docs/SETUP-GUIDE.md b/docs/SETUP-GUIDE.md index 8b964da..f2817b6 100644 --- a/docs/SETUP-GUIDE.md +++ b/docs/SETUP-GUIDE.md @@ -148,7 +148,57 @@ SENDGRID_FROM_EMAIL=noreply@example.com **Importante:** `ETUS_ADMIN_EMAILS` deve ter pelo menos um email. Esses emails recebem `role='admin'` no callback OAuth e conseguem acessar `/auth/admin/*` -e `/audit/logs`. +e `/audit/logs`. Com **gateway-as-authority** ligado (abaixo), esse allowlist +vira apenas o bootstrap do dia 0 — o papel admin real passa a vir do gateway. + +--- + +### Gateway-as-authority (opcional, @etus/auth v0.7.0) + +Por padrão o app decide as permissões localmente (`PERMISSIONS_MATRIX`). Para +tornar o **gateway a fonte de autoridade** — ou seja, derivar as permissões do +que o gateway resolveu para o usuário neste app (`RBAC ∪ access_grants`) — +ative `ETUS_GATEWAY_AUTHORITY`. O app continua BFF (não guarda tokens do +usuário); ele consulta o gateway com a **integration key do próprio app**. + +Onboarding (uma vez, por app): + +1. **Registrar o app como resource `web_app` no gateway** (um admin do gateway, + no console). Declare o vocabulário de scopes igual às **chaves do `SCOPE_MAP`** + em `src/server/auth/matrix.ts` (o template usa `bhono:admin|editor|viewer` — + renomeie para o prefixo do seu app). ⚠️ Há **duas** referências a esse + vocabulário: as chaves do `SCOPE_MAP` **e** `adminScopes` em + `src/server/auth/setup.ts` — renomeie as duas em lockstep, senão a promoção a + admin via gateway quebra silenciosamente. +2. **Provisionar uma integration key** bound a esse resource, com o scope + **`app.grants.read`**. Copie o segredo `ag_app__…` (mostrado uma vez). +3. **Configurar o app:** + - `ETUS_GATEWAY_AUTHORITY=true` + - `ETUS_RESOURCE_ID=` (var, em `config/wrangler.json`) + - `ETUS_INTEGRATION_KEY=` — **secret**: + `wrangler secret put ETUS_INTEGRATION_KEY --config config/wrangler.json` + em produção/staging. Nunca commitar. +4. **Conceder roles/grants aos usuários no gateway** que resolvam para `bhono:*`. + O app passa a ler isso no login (e revalida a cada `ttlSeconds`). + +> **Dev local (importante):** `pnpm dev` usa o `@cloudflare/vite-plugin` com +> `configPath: ./config/wrangler.json`, que lê as variáveis de +> `config/wrangler.json` e os secrets de **`config/.dev.vars`** — NÃO do +> `.dev.vars` na raiz do projeto. Para testar gateway-as-authority localmente, +> ponha `ETUS_GATEWAY_AUTHORITY`/`ETUS_RESOURCE_ID` nas `vars` de +> `config/wrangler.json` e `ETUS_INTEGRATION_KEY` em `config/.dev.vars` +> (gitignored). Variáveis postas no `.dev.vars` da raiz são silenciosamente +> ignoradas (o app cai no default `ETUS_GATEWAY_AUTHORITY=false`). + +Verificação: logar → o middleware popula `authPermissions` a partir do +`SCOPE_MAP` → `requirePermission(...)` autoriza. Se **todo** request der 403, +cheque que a key tem `app.grants.read` e que o vocabulário do resource bate com +as chaves do `SCOPE_MAP`. Disponibilidade: uma negação explícita do gateway +bloqueia; uma indisponibilidade transitória serve o cache (não derruba o app). + +> O binding KV `SESSIONS` em `config/wrangler.json` não é mais usado para sessão +> (v0.6.0+ usa D1 via `createSqlSessionStore`); pode ser removido se nenhum outro +> código depender dele. --- diff --git a/package.json b/package.json index 6d5e9ae..4b0c1fe 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,10 @@ "sync:template:check": "./scripts/sync-template.sh && git diff --exit-code packages/bhono-app/templates/" }, "dependencies": { - "@etus/auth": "^0.5.0", + "@etus/auth": "^0.8.0", + "@etus/seven-react": "0.1.0-beta.3", + "@etus/tokens": "0.4.0-beta.2", + "@etus/ui": "0.4.0-beta.2", "@hono/swagger-ui": "^0.6.1", "@hono/zod-openapi": "^1.4.0", "@hookform/resolvers": "^5.2.2", diff --git a/packages/bhono-app/src/cli.ts b/packages/bhono-app/src/cli.ts index fd4fb05..0ece912 100644 --- a/packages/bhono-app/src/cli.ts +++ b/packages/bhono-app/src/cli.ts @@ -26,7 +26,7 @@ function buildProgram(): Command { return new Command() .name('bhono-app') .description('Create a new project from the Etus boilerplate') - .version('0.1.5') + .version('0.3.0') .argument('', 'Name of the project') .option('-d, --domain ', 'Production domain') .option('-m, --modules ', 'Comma-separated modules to include') diff --git a/packages/bhono-app/templates/base/.dev.vars.example b/packages/bhono-app/templates/base/.dev.vars.example index b9f85c4..613651b 100644 --- a/packages/bhono-app/templates/base/.dev.vars.example +++ b/packages/bhono-app/templates/base/.dev.vars.example @@ -11,6 +11,14 @@ ETUS_CLIENT_SECRET=your-client-secret-from-npx-etus-auth-init ETUS_ALLOWED_DOMAINS=yourdomain.com,anotherdomain.com ETUS_ADMIN_EMAILS=admin@yourdomain.com +# Gateway-as-authority (@etus/auth v0.7.0) — OFF by default. Turn on once your app +# is registered as a web_app resource in the gateway and you hold an integration +# key (see docs/SETUP-GUIDE.md → "Gateway-as-authority onboarding"). The key is a +# secret — keep it in .dev.vars locally and `wrangler secret put` in deployments. +# ETUS_GATEWAY_AUTHORITY=true +# ETUS_RESOURCE_ID=yourapp.example.com +# ETUS_INTEGRATION_KEY=ag_app_yourapp_xxx.yyy + # SendGrid SENDGRID_API_KEY=your-sendgrid-api-key SENDGRID_FROM_EMAIL=noreply@yourdomain.com diff --git a/packages/bhono-app/templates/base/.env.example b/packages/bhono-app/templates/base/.env.example index 539e2cf..a6aff2c 100644 --- a/packages/bhono-app/templates/base/.env.example +++ b/packages/bhono-app/templates/base/.env.example @@ -15,9 +15,19 @@ ETUS_CLIENT_SECRET=your-client-secret-from-npx-etus-auth-init ETUS_ALLOWED_DOMAINS=yourdomain.com,anotherdomain.com # Comma-separated emails that always receive the 'admin' role. # @etus/auth requires at least one. The template ships a generic placeholder — -# replace it with your real product admin(s) before deploying. +# replace it with your real product admin(s) before deploying. With +# gateway-as-authority on, this is only the day-0 bootstrap — the real admin role +# is then granted by the gateway (see ETUS_GATEWAY_AUTHORITY below). ETUS_ADMIN_EMAILS=admin@yourdomain.com +# Gateway-as-authority (@etus/auth v0.7.0) — OFF by default. Set to "true" once +# the app is registered as a web_app resource in the gateway and you have an +# integration key (scope app.grants.read). See docs/SETUP-GUIDE.md. The key is a +# secret — `wrangler secret put ETUS_INTEGRATION_KEY` in deployed environments. +# ETUS_GATEWAY_AUTHORITY=true +# ETUS_RESOURCE_ID=yourapp.example.com +# ETUS_INTEGRATION_KEY=ag_app_yourapp_xxx.yyy + # SendGrid (for invitations) SENDGRID_API_KEY=your-sendgrid-api-key SENDGRID_FROM_EMAIL=noreply@yourdomain.com diff --git a/packages/bhono-app/templates/base/config/wrangler.json b/packages/bhono-app/templates/base/config/wrangler.json index e9c7b8c..b037669 100644 --- a/packages/bhono-app/templates/base/config/wrangler.json +++ b/packages/bhono-app/templates/base/config/wrangler.json @@ -45,7 +45,9 @@ "ETUS_GATEWAY": "https://ag.etus.io", "ETUS_CLIENT_ID": "boilerplate-hono", "ETUS_ALLOWED_DOMAINS": "brius.com.br,etus.com.br", - "ETUS_ADMIN_EMAILS": "admin@etus.com.br" + "ETUS_ADMIN_EMAILS": "admin@etus.com.br", + "ETUS_GATEWAY_AUTHORITY": "false", + "ETUS_RESOURCE_ID": "" }, "env": { "staging": { @@ -76,7 +78,9 @@ "ETUS_GATEWAY": "https://ag.etus.io", "ETUS_CLIENT_ID": "boilerplate-hono", "ETUS_ALLOWED_DOMAINS": "brius.com.br,etus.com.br", - "ETUS_ADMIN_EMAILS": "admin@etus.com.br" + "ETUS_ADMIN_EMAILS": "admin@etus.com.br", + "ETUS_GATEWAY_AUTHORITY": "false", + "ETUS_RESOURCE_ID": "" } } } diff --git a/packages/bhono-app/templates/base/docs/SETUP-GUIDE.md b/packages/bhono-app/templates/base/docs/SETUP-GUIDE.md index 8b964da..f2817b6 100644 --- a/packages/bhono-app/templates/base/docs/SETUP-GUIDE.md +++ b/packages/bhono-app/templates/base/docs/SETUP-GUIDE.md @@ -148,7 +148,57 @@ SENDGRID_FROM_EMAIL=noreply@example.com **Importante:** `ETUS_ADMIN_EMAILS` deve ter pelo menos um email. Esses emails recebem `role='admin'` no callback OAuth e conseguem acessar `/auth/admin/*` -e `/audit/logs`. +e `/audit/logs`. Com **gateway-as-authority** ligado (abaixo), esse allowlist +vira apenas o bootstrap do dia 0 — o papel admin real passa a vir do gateway. + +--- + +### Gateway-as-authority (opcional, @etus/auth v0.7.0) + +Por padrão o app decide as permissões localmente (`PERMISSIONS_MATRIX`). Para +tornar o **gateway a fonte de autoridade** — ou seja, derivar as permissões do +que o gateway resolveu para o usuário neste app (`RBAC ∪ access_grants`) — +ative `ETUS_GATEWAY_AUTHORITY`. O app continua BFF (não guarda tokens do +usuário); ele consulta o gateway com a **integration key do próprio app**. + +Onboarding (uma vez, por app): + +1. **Registrar o app como resource `web_app` no gateway** (um admin do gateway, + no console). Declare o vocabulário de scopes igual às **chaves do `SCOPE_MAP`** + em `src/server/auth/matrix.ts` (o template usa `bhono:admin|editor|viewer` — + renomeie para o prefixo do seu app). ⚠️ Há **duas** referências a esse + vocabulário: as chaves do `SCOPE_MAP` **e** `adminScopes` em + `src/server/auth/setup.ts` — renomeie as duas em lockstep, senão a promoção a + admin via gateway quebra silenciosamente. +2. **Provisionar uma integration key** bound a esse resource, com o scope + **`app.grants.read`**. Copie o segredo `ag_app__…` (mostrado uma vez). +3. **Configurar o app:** + - `ETUS_GATEWAY_AUTHORITY=true` + - `ETUS_RESOURCE_ID=` (var, em `config/wrangler.json`) + - `ETUS_INTEGRATION_KEY=` — **secret**: + `wrangler secret put ETUS_INTEGRATION_KEY --config config/wrangler.json` + em produção/staging. Nunca commitar. +4. **Conceder roles/grants aos usuários no gateway** que resolvam para `bhono:*`. + O app passa a ler isso no login (e revalida a cada `ttlSeconds`). + +> **Dev local (importante):** `pnpm dev` usa o `@cloudflare/vite-plugin` com +> `configPath: ./config/wrangler.json`, que lê as variáveis de +> `config/wrangler.json` e os secrets de **`config/.dev.vars`** — NÃO do +> `.dev.vars` na raiz do projeto. Para testar gateway-as-authority localmente, +> ponha `ETUS_GATEWAY_AUTHORITY`/`ETUS_RESOURCE_ID` nas `vars` de +> `config/wrangler.json` e `ETUS_INTEGRATION_KEY` em `config/.dev.vars` +> (gitignored). Variáveis postas no `.dev.vars` da raiz são silenciosamente +> ignoradas (o app cai no default `ETUS_GATEWAY_AUTHORITY=false`). + +Verificação: logar → o middleware popula `authPermissions` a partir do +`SCOPE_MAP` → `requirePermission(...)` autoriza. Se **todo** request der 403, +cheque que a key tem `app.grants.read` e que o vocabulário do resource bate com +as chaves do `SCOPE_MAP`. Disponibilidade: uma negação explícita do gateway +bloqueia; uma indisponibilidade transitória serve o cache (não derruba o app). + +> O binding KV `SESSIONS` em `config/wrangler.json` não é mais usado para sessão +> (v0.6.0+ usa D1 via `createSqlSessionStore`); pode ser removido se nenhum outro +> código depender dele. --- diff --git a/packages/bhono-app/templates/base/package.json b/packages/bhono-app/templates/base/package.json index a42e788..5a77985 100644 --- a/packages/bhono-app/templates/base/package.json +++ b/packages/bhono-app/templates/base/package.json @@ -44,7 +44,10 @@ "sync:template:check": "./scripts/sync-template.sh && git diff --exit-code packages/bhono-app/templates/" }, "dependencies": { - "@etus/auth": "^0.5.0", + "@etus/auth": "^0.8.0", + "@etus/seven-react": "0.1.0-beta.3", + "@etus/tokens": "0.4.0-beta.2", + "@etus/ui": "0.4.0-beta.2", "@hono/swagger-ui": "^0.6.1", "@hono/zod-openapi": "^1.4.0", "@hookform/resolvers": "^5.2.2", diff --git a/packages/bhono-app/templates/base/src/client/components/sidebar.tsx b/packages/bhono-app/templates/base/src/client/components/sidebar.tsx index cdaeeae..35c9cd6 100644 --- a/packages/bhono-app/templates/base/src/client/components/sidebar.tsx +++ b/packages/bhono-app/templates/base/src/client/components/sidebar.tsx @@ -1,8 +1,7 @@ import { Link, useLocation } from '@tanstack/react-router' import { useState, useEffect } from 'react' import { cn } from '@/lib/utils' -import { Button } from '@/components/ui/button' -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Avatar, AvatarFallback, AvatarImage, Button } from '@etus/seven-react' import { Separator } from '@/components/ui/separator' import { Icons } from '@/components/icons' import { useAuth } from '@/hooks/use-auth' diff --git a/packages/bhono-app/templates/base/src/client/components/ui/avatar.tsx b/packages/bhono-app/templates/base/src/client/components/ui/avatar.tsx deleted file mode 100644 index 7b1d1b8..0000000 --- a/packages/bhono-app/templates/base/src/client/components/ui/avatar.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { createContext, use, useState } from "react" -import type { HTMLAttributes, ImgHTMLAttributes, Ref } from "react" - -import { cn } from "@/lib/utils" - -interface AvatarContextValue { - imageLoaded: boolean - setImageLoaded: (loaded: boolean) => void -} - -const AvatarContext = createContext(undefined) - -function Avatar({ - className, - ref, - ...props -}: HTMLAttributes & { ref?: Ref }) { - const [imageLoaded, setImageLoaded] = useState(false) - - return ( - -
- - ) -} - -function AvatarImage({ - className, - src, - onLoad, - onError, - ref, - ...props -}: ImgHTMLAttributes & { ref?: Ref }) { - const context = use(AvatarContext) - // Track previous src to detect changes - const [prevSrc, setPrevSrc] = useState(src) - const [hasError, setHasError] = useState(false) - - // Reset error state when src changes (using derived state pattern) - if (src !== prevSrc) { - setPrevSrc(src) - setHasError(false) - context?.setImageLoaded(false) - } - - if (!src || hasError) { - return null - } - - return ( - { - context?.setImageLoaded(true) - onLoad?.(e) - }} - onError={(e) => { - setHasError(true) - context?.setImageLoaded(false) - onError?.(e) - }} - {...props} - /> - ) -} - -function AvatarFallback({ - className, - ref, - ...props -}: HTMLAttributes & { ref?: Ref }) { - const context = use(AvatarContext) - - // Always show fallback if no context (standalone usage) or image not loaded - if (context?.imageLoaded) { - return null - } - - return ( -
- ) -} - -export { Avatar, AvatarFallback, AvatarImage } diff --git a/packages/bhono-app/templates/base/src/client/components/ui/badge.tsx b/packages/bhono-app/templates/base/src/client/components/ui/badge.tsx deleted file mode 100644 index 25b8e9e..0000000 --- a/packages/bhono-app/templates/base/src/client/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { VariantProps } from "class-variance-authority" - -import { cva } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-fast focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow-xs hover:bg-green-500", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-gray-200 dark:hover:bg-gray-700", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow-xs hover:bg-red-600", - outline: "text-foreground border-border", - // Status variants using Seven Design System - success: - "border-transparent bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300", - warning: - "border-transparent bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300", - error: - "border-transparent bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300", - info: - "border-transparent bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/packages/bhono-app/templates/base/src/client/components/ui/button.tsx b/packages/bhono-app/templates/base/src/client/components/ui/button.tsx deleted file mode 100644 index e452153..0000000 --- a/packages/bhono-app/templates/base/src/client/components/ui/button.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import type { VariantProps } from "class-variance-authority" -import type { ButtonHTMLAttributes, Ref } from "react" - -import { Slot } from "@radix-ui/react-slot" -import { cva } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.98]", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-sm hover:bg-green-500 hover:shadow-md", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-red-600 hover:shadow-md", - outline: - "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground hover:border-gray-300 dark:hover:border-gray-600", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-gray-200 dark:hover:bg-gray-700", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline hover:text-green-600", - // Status variants - success: - "bg-green-500 text-white shadow-sm hover:bg-green-600 hover:shadow-md", - warning: - "bg-yellow-500 text-gray-900 shadow-sm hover:bg-yellow-600 hover:shadow-md", - info: - "bg-blue-500 text-white shadow-sm hover:bg-blue-600 hover:shadow-md", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - xl: "h-12 rounded-lg px-10 text-base", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export interface ButtonProps - extends ButtonHTMLAttributes, - VariantProps { - asChild?: boolean - ref?: Ref -} - -function Button({ - className, - variant, - size, - asChild = false, - ref, - ...props -}: ButtonProps) { - const Comp = asChild ? Slot : "button" - return ( - - ) -} - -export { Button, buttonVariants } diff --git a/packages/bhono-app/templates/base/src/client/components/ui/card.tsx b/packages/bhono-app/templates/base/src/client/components/ui/card.tsx deleted file mode 100644 index 43d5d55..0000000 --- a/packages/bhono-app/templates/base/src/client/components/ui/card.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import type React from "react" - -import { cn } from "@/lib/utils" - -function Card({ - className, - ref, - ...props -}: React.HTMLAttributes & { ref?: React.Ref }) { - return ( -
- ) -} - -function CardHeader({ - className, - ref, - ...props -}: React.HTMLAttributes & { ref?: React.Ref }) { - return ( -
- ) -} - -function CardTitle({ - className, - ref, - ...props -}: React.HTMLAttributes & { ref?: React.Ref }) { - return ( -

- ) -} - -function CardDescription({ - className, - ref, - ...props -}: React.HTMLAttributes & { ref?: React.Ref }) { - return ( -
- ) -} - -function CardContent({ - className, - ref, - ...props -}: React.HTMLAttributes & { ref?: React.Ref }) { - return
-} - -function CardFooter({ - className, - ref, - ...props -}: React.HTMLAttributes & { ref?: React.Ref }) { - return ( -
- ) -} - -export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } diff --git a/packages/bhono-app/templates/base/src/client/components/ui/dialog.tsx b/packages/bhono-app/templates/base/src/client/components/ui/dialog.tsx deleted file mode 100644 index d7953ea..0000000 --- a/packages/bhono-app/templates/base/src/client/components/ui/dialog.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import type React from "react" - -import * as DialogPrimitive from "@radix-ui/react-dialog" - -import { Icons } from "@/components/icons" -import { cn } from "@/lib/utils" - -const Dialog = DialogPrimitive.Root - -const DialogTrigger = DialogPrimitive.Trigger - -const DialogPortal = DialogPrimitive.Portal - -const DialogClose = DialogPrimitive.Close - -function DialogOverlay({ - className, - ref, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.Ref> -}) { - return ( - - ) -} - -function DialogContent({ - className, - children, - ref, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.Ref> -}) { - return ( - - - - {children} - - - Close - - - - ) -} - -function DialogHeader({ - className, - ...props -}: React.HTMLAttributes) { - return ( -
- ) -} - -function DialogFooter({ - className, - ...props -}: React.HTMLAttributes) { - return ( -
- ) -} - -function DialogTitle({ - className, - ref, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.Ref> -}) { - return ( - - ) -} - -function DialogDescription({ - className, - ref, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.Ref> -}) { - return ( - - ) -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -} diff --git a/packages/bhono-app/templates/base/src/client/components/ui/error-fallback.tsx b/packages/bhono-app/templates/base/src/client/components/ui/error-fallback.tsx index 4875c85..0fdd348 100644 --- a/packages/bhono-app/templates/base/src/client/components/ui/error-fallback.tsx +++ b/packages/bhono-app/templates/base/src/client/components/ui/error-fallback.tsx @@ -1,7 +1,6 @@ // src/client/components/ui/error-fallback.tsx import { AlertTriangle, RefreshCw, Home, ArrowLeft } from 'lucide-react' -import { Button } from './button' - +import { Button } from '@etus/seven-react' interface ErrorFallbackProps { error?: Error title?: string diff --git a/packages/bhono-app/templates/base/src/client/components/ui/form.tsx b/packages/bhono-app/templates/base/src/client/components/ui/form.tsx deleted file mode 100644 index b9f0f5f..0000000 --- a/packages/bhono-app/templates/base/src/client/components/ui/form.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { createContext, use, useId } from "react" -import type { - ControllerProps, - FieldPath, - FieldValues, -} from "react-hook-form" -import type { HTMLAttributes, LabelHTMLAttributes, Ref } from "react" -import { - Controller, - FormProvider, - useFormContext, -} from "react-hook-form" - -import { Label } from "@/components/ui/label" -import { cn } from "@/lib/utils" - -const Form = FormProvider - -interface FormFieldContextValue< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath -> { - name: TName -} - -const FormFieldContext = createContext( - {} as FormFieldContextValue -) - -function FormField< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath ->({ - ...props -}: ControllerProps) { - return ( - - - - ) -} - -function useFormField() { - const fieldContext = use(FormFieldContext) - const itemContext = use(FormItemContext) - const { getFieldState, formState } = useFormContext() - - const fieldState = getFieldState(fieldContext.name, formState) - const { id } = itemContext - - return { - id, - name: fieldContext.name, - formItemId: `${id}-form-item`, - formDescriptionId: `${id}-form-item-description`, - formMessageId: `${id}-form-item-message`, - ...fieldState, - } -} - -interface FormItemContextValue { - id: string -} - -const FormItemContext = createContext( - {} as FormItemContextValue -) - -function FormItem({ - className, - ref, - ...props -}: HTMLAttributes & { ref?: Ref }) { - const id = useId() - - return ( - -
- - ) -} - -function FormLabel({ - className, - ref, - ...props -}: LabelHTMLAttributes & { ref?: Ref }) { - const { error, formItemId } = useFormField() - - return ( -