diff --git a/.changeset/adr-0048-split-account-app.md b/.changeset/adr-0048-split-account-app.md new file mode 100644 index 0000000000..c4d62a1d1f --- /dev/null +++ b/.changeset/adr-0048-split-account-app.md @@ -0,0 +1,12 @@ +--- +"@objectstack/account": minor +--- + +feat(apps): reclaim `@objectstack/account` for the console Account app (ADR-0048) + +Removes the deprecated standalone account-portal SPA (`apps/account`) and +reclaims the `@objectstack/account` name for the console Account app as its own +ObjectStack package (`com.objectstack.account`, namespace `account`) per +ADR-0048 "one app per package". Boot-neutral skeleton (transitional import from +platform-objects; not yet wired into the dev/serve plugin set — that switch +lands in a follow-up verified against a live `os dev` boot). diff --git a/.changeset/adr-0048-split-setup-app.md b/.changeset/adr-0048-split-setup-app.md new file mode 100644 index 0000000000..fdfd43a5e3 --- /dev/null +++ b/.changeset/adr-0048-split-setup-app.md @@ -0,0 +1,12 @@ +--- +"@objectstack/setup": minor +--- + +feat(apps): extract Setup into its own `@objectstack/setup` app package (ADR-0048) + +ADR-0048 "one app per package": Setup gets a distinct package id +(`com.objectstack.setup`) and namespace (`setup`), carrying both `SETUP_APP` and +its baseline `SETUP_NAV_CONTRIBUTIONS`, so `/apps/` resolves +unambiguously. Boot-neutral skeleton (transitional import from platform-objects; +not yet wired into the dev/serve plugin set — that switch lands in a follow-up +verified against a live `os dev` boot). diff --git a/.changeset/adr-0048-split-studio-app.md b/.changeset/adr-0048-split-studio-app.md new file mode 100644 index 0000000000..6e7662111e --- /dev/null +++ b/.changeset/adr-0048-split-studio-app.md @@ -0,0 +1,16 @@ +--- +"@objectstack/studio": minor +--- + +feat(apps): extract Studio into its own `@objectstack/studio` app package (ADR-0048) + +ADR-0048 "one app per package": Studio gets a distinct package id +(`com.objectstack.studio`) and namespace (`studio`) so `/apps/` +resolves unambiguously instead of being lost inside a multi-app package. + +This change adds the package skeleton (`packages/apps/studio`) with a thin +registration plugin. Transitional: `STUDIO_APP` is still imported from +`@objectstack/platform-objects/apps`, and the package is not yet wired into the +dev/serve plugin set — that boot-path switch (and dropping the app from +`plugin-auth`'s manifest) lands in a follow-up so it can be verified against a +live `os dev` boot. diff --git a/.changeset/config.json b/.changeset/config.json index 272622b711..f8d741691a 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -24,6 +24,8 @@ "@objectstack/observability", "@objectstack/formula", "@objectstack/platform-objects", + "@objectstack/studio", + "@objectstack/setup", "@objectstack/runtime", "@objectstack/rest", "@objectstack/driver-memory", diff --git a/apps/account/CHANGELOG.md b/apps/account/CHANGELOG.md deleted file mode 100644 index 63e5861e6d..0000000000 --- a/apps/account/CHANGELOG.md +++ /dev/null @@ -1,295 +0,0 @@ -# @objectstack/account - -## 9.3.0 - -## 9.2.0 - -## 9.1.0 - -## 9.0.1 - -## 9.0.0 - -## 8.0.1 - -## 8.0.0 - -## 7.9.0 - -## 7.8.0 - -## 7.7.0 - -## 7.6.0 - -## 7.5.0 - -## 7.4.1 - -## 7.4.0 - -## 7.3.0 - -## 7.2.1 - -## 7.2.0 - -## 7.1.0 - -### Patch Changes - -- 89771d4: Account: fix post-login redirect to `/organizations/new` in single-tenant - mode when the freshly-authenticated user has no organization yet. - - The auth-redirect effect in `login.tsx` / `register.tsx` decided where to - send the user as soon as `session` populated, but `features` (from - `GET /api/v1/auth/config`) could still be `null` for a beat. The check - `features?.multiOrgEnabled === false` then evaluated to `false`, so the - "no orgs" branch fell through to `navigate({ to: '/organizations/new' })` - — exactly the page that's gated off in single-tenant deployments. The - wizard then bounced to `/organizations` (empty list), producing a jarring - "create organization is not supported" flash. - - Both routes now wait for `features` to resolve before deciding the no-org - redirect target, so single-tenant logins land on `/` (or their original - `redirect=` target) directly. - -## 7.0.0 - -### Patch Changes - -- 39a23c5: fix -- dc72172: Fix: post-signup landing flow - - Single-tenant deployments (`OS_MULTI_ORG_ENABLED=false` / - `OS_MULTI_TENANT=false`) no longer bounce a fresh user to the - `/organizations` picker or the `/organizations/new` wizard — both are - nonsensical when org creation is server-side-gated and the user is - expected to land in the one default org. Registration now hands the - user straight to the platform home (or the original `?redirect=`). - - Multi-tenant deployments still route no-org users into the create-org - wizard, but the wizard now renders as a fullscreen `AuthShell` dialog - (matching `/login` and `/register`) instead of the authenticated - account chrome (TopBar + Sidebar). Showing a sidebar full of - organization-scoped settings to a user who does not yet have an - organization was contradictory and confusing — the create-org step - should feel like a follow-on to register, not a settings panel. After - successful creation, the wizard also honors `?redirect=` and falls - back to `/` instead of dropping the user back into org settings. - - Verified end-to-end in browser for both tenancy modes with two fresh - deployments (single-tenant on :3003 with `OS_MULTI_ORG_ENABLED=false`, - multi-tenant on :3004 with `OS_MULTI_TENANT=true`). - -## 6.9.0 - -## 6.8.1 - -## 6.8.0 - -### Patch Changes - -- 99866d8: Fix forgot-password page returning 404. better-auth's password reset - endpoint is `/api/v1/auth/request-password-reset` (not the legacy - `forget-password`); the account UI now calls the correct path so - the reset email actually dispatches. - -## 6.7.1 - -### Patch Changes - -- 87c4d19: Move all 26 runtime `dependencies` to `devDependencies`. `@objectstack/account` is a pure-SPA package — `files: ['dist']` only ships the Vite-bundled `index.html` + `assets/*.js` + `assets/*.css`. The browser never imports `react`, `@radix-ui/*`, `@tanstack/react-router`, `@objectstack/client`, etc. directly — they are all already bundled into the dist JS. - - **Impact** (measured via `npm install @objectstack/account` in a fresh project): - - | | Before | After | - | ---------------------------- | ---------- | -------- | - | node_modules packages pulled | **33** | **1** | - | on-disk size | **129 MB** | **2 MB** | - | install time | ~4 min | seconds | - - Tarball itself is unchanged (still ~400KB, 7 files in `package/dist/` + `package.json` + `LICENSE`). Vite build still produces the same `dist/` (`pnpm --filter @objectstack/account build` verified — 2103 modules → 1.29 MB bundle). - -## 6.7.0 - -### Patch Changes - -- Updated dependencies [430067b] -- Updated dependencies [4f9e9d4] - - @objectstack/spec@6.7.0 - - @objectstack/client@6.7.0 - - @objectstack/client-react@6.7.0 - -## 6.6.0 - -### Patch Changes - -- Updated dependencies [a49cfc2] - - @objectstack/spec@6.6.0 - - @objectstack/client@6.6.0 - - @objectstack/client-react@6.6.0 - -## 6.5.1 - -### Patch Changes - -- @objectstack/client@6.5.1 -- @objectstack/spec@6.5.1 -- @objectstack/client-react@6.5.1 - -## 6.5.0 - -### Patch Changes - -- @objectstack/spec@6.5.0 -- @objectstack/client@6.5.0 -- @objectstack/client-react@6.5.0 - -## 6.4.0 - -### Minor Changes - -- 15fc484: Upgrade `@object-ui/*` packages to **v6.0**. - - - `@objectstack/cli`: `@object-ui/console` and `@object-ui/studio` from `^5.4.2` → `^6.0.0` — bundled Studio + Console assets now ship the v6 UI shell (new design language, refreshed sidebar, redesigned record header). - - `@objectstack/account`: `@object-ui/i18n` from `^5.4.2` → `^6.0.0` — i18n runtime now matches the v6 console/studio API. - - Root devDependency `@object-ui/console` from `^5.4.2` → `^6.0.0` so workspace scripts and the docs build pick up v6. - - `create-objectstack`: `tar` from `^7.4.3` → `^7.5.15` (security + perf fixes when unpacking remote templates). - - **Heads-up for consumers:** `@object-ui/*` v6 is a major release of the bundled UI; pages rendered through the CLI's `studio` / `console` mounts may look different from v5. The protocol surface is unchanged. - -### Patch Changes - -- Updated dependencies [f8651cc] -- Updated dependencies [f8651cc] -- Updated dependencies [0bf6f9a] - - @objectstack/spec@6.4.0 - - @objectstack/client@6.4.0 - - @objectstack/client-react@6.4.0 - -## 6.3.0 - -### Patch Changes - -- @objectstack/spec@6.3.0 -- @objectstack/client@6.3.0 -- @objectstack/client-react@6.3.0 - -## 6.2.0 - -### Patch Changes - -- Updated dependencies [b4c74a9] - - @objectstack/spec@6.2.0 - - @objectstack/client@6.2.0 - - @objectstack/client-react@6.2.0 - -## 6.1.1 - -### Patch Changes - -- @objectstack/spec@6.1.1 -- @objectstack/client@6.1.1 -- @objectstack/client-react@6.1.1 - -## 6.1.0 - -### Patch Changes - -- Updated dependencies [93c0589] - - @objectstack/spec@6.1.0 - - @objectstack/client@6.1.0 - - @objectstack/client-react@6.1.0 - -## 6.0.0 - -### Patch Changes - -- Updated dependencies [629a716] -- Updated dependencies [dbc4f7d] -- Updated dependencies [944f187] - - @objectstack/spec@6.0.0 - - @objectstack/client@6.0.0 - - @objectstack/client-react@6.0.0 - -## 5.2.0 - -### Patch Changes - -- Updated dependencies [bab2b20] -- Updated dependencies [fa011d8] -- Updated dependencies [b806f58] - - @objectstack/spec@5.2.0 - - @objectstack/client@5.2.0 - - @objectstack/client-react@5.2.0 - -## 5.1.0 - -### Patch Changes - -- Updated dependencies [75f4ee6] -- Updated dependencies [823d559] - - @objectstack/spec@5.1.0 - - @objectstack/client@5.1.0 - - @objectstack/client-react@5.1.0 - -## 5.0.0 - -### Major Changes - -- bb32755: Publish `@objectstack/account` and `@objectstack/console` to npm (major release). - - Previously both apps were marked `private: true`, which prevented `changeset publish` - from releasing them. The CLI (`@objectstack/cli`) resolves these packages from - `node_modules/@objectstack/{account,console,studio}` to serve their built `dist` - assets, so third-party projects could not consume them via `pnpm add`. - - - Removed `private: true` from `apps/account` and `apps/console`. - - Added `publishConfig.access: public` to `account`, `console`, and `studio` for - scoped-package publish safety. - -### Patch Changes - -- Updated dependencies [2f9073a] - - @objectstack/spec@5.0.0 - - @objectstack/client@5.0.0 - - @objectstack/client-react@5.0.0 - -## 4.2.0 - -### Patch Changes - -- Updated dependencies [2869891] - - @objectstack/spec@4.2.0 - - @objectstack/client@4.2.0 - - @objectstack/client-react@4.2.0 - -## 4.1.1 - -### Patch Changes - -- Updated dependencies [5326c6b] - - @objectstack/client@4.1.1 - - @objectstack/client-react@4.1.1 - - @objectstack/spec@4.1.1 - -## 4.0.6 - -### Patch Changes - -- f41466a: Trim the first-run `/setup` page. Removed the optional teammate-invite section, the manually-edited org slug field, the large shield banner and the footer note. The form is now 4 fields (name + org name on one row, then email + password) with concise copy — the new owner can invite teammates from the dashboard after first login. -- Updated dependencies [2108c30] -- Updated dependencies [23db640] - - @objectstack/spec@4.1.0 - - @objectstack/client@4.1.0 - - @objectstack/client-react@4.1.0 - -## 0.1.1 - -### Patch Changes - -- Updated dependencies [15e0df6] - - @objectstack/spec@4.0.5 - - @objectstack/client@4.0.5 - - @objectstack/client-react@4.0.5 diff --git a/apps/account/index.html b/apps/account/index.html deleted file mode 100644 index c0f82a7518..0000000000 --- a/apps/account/index.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - ObjectOS Account - - - - -
- -
- - - diff --git a/apps/account/mocks/node-polyfills.ts b/apps/account/mocks/node-polyfills.ts deleted file mode 100644 index c02500a4ac..0000000000 --- a/apps/account/mocks/node-polyfills.ts +++ /dev/null @@ -1,104 +0,0 @@ -export class EventEmitter { - on() { return this; } - off() { return this; } - emit() { return true; } - once() { return this; } - addListener() { return this; } - removeListener() { return this; } - removeAllListeners() { return this; } -} -export default { EventEmitter }; - -export class Stream extends EventEmitter { - pipe() { return this; } -} -export class Readable extends Stream {} -export class Writable extends Stream {} -export class Transform extends Stream {} - -export class StringDecoder { - write() { return ''; } - end() { return ''; } -} - -export const promises = { - readFile: async () => '', - writeFile: async () => {}, - stat: async () => ({ isDirectory: () => false, isFile: () => false }), - mkdir: async () => {}, - rm: async () => {}, - access: async () => { throw new Error('ENOENT: no such file or directory (polyfill)'); }, -}; - -// os polyfills -export const type = () => 'Browser'; -export const platform = () => 'browser'; -export const release = () => '1.0.0'; -export const tmpdir = () => '/tmp'; -export const homedir = () => '/'; -export const endianness = () => 'LE'; -export const arch = () => 'javascript'; -export const EOL = '\n'; - -export const readFileSync = () => ''; -export const writeFileSync = () => {}; -export const statSync = () => ({ isDirectory: () => false, isFile: () => false, isSymbolicLink: () => false }); -export const lstatSync = () => ({ isDirectory: () => false, isFile: () => false, isSymbolicLink: () => false }); -export const existsSync = () => false; -export const join = (...args) => args.join('/'); -export const resolve = (...args) => args.join('/'); -export const dirname = (path) => path; -export const basename = (path) => path; -export const extname = (path) => ''; -export const sep = '/'; -export const relative = () => ''; -export const isAbsolute = () => false; -export const normalize = (p: string) => p; - -export const posix = {}; -export const win32 = {}; - -// fs/promises and other missing exports -export const open = async () => ({ close: async () => {} }); -export class Stats { - isDirectory() { return false; } - isFile() { return false; } - isSymbolicLink() { return false; } -} -export const watchFile = () => {}; -export const unwatchFile = () => {}; - -export const fileURLToPath = () => ''; -export const pathToFileURL = () => ''; - -export const readdir = () => {}; -export const readdirSync = () => []; -export const readlink = () => {}; -export const readlinkSync = () => ''; -export const realpath = async () => ''; -export const realpathSync = () => ''; -realpathSync.native = () => ''; - -export const constants = {}; -export const lstat = async () => ({ isDirectory: () => false, isFile: () => false, isSymbolicLink: () => false }); -export const stat = async () => ({ isDirectory: () => false, isFile: () => false, isSymbolicLink: () => false }); -export const access = async () => { throw new Error('ENOENT: no such file or directory (polyfill)'); }; -export const accessSync = () => { throw new Error('ENOENT: no such file or directory (polyfill)'); }; -export const mkdir = () => {}; -export const mkdirSync = () => {}; -export const rmdir = () => {}; -export const rmdirSync = () => {}; -export const unlink = () => {}; -export const unlinkSync = () => {}; -export const copyFile = () => {}; -export const copyFileSync = () => {}; -export const createReadStream = () => new Readable(); -export const createWriteStream = () => new Writable(); -export const watch = () => new EventEmitter(); - -export const readFile = async () => ''; -export const writeFile = async () => {}; -export const rename = async () => {}; -export const renameSync = () => {}; -export const createHash = () => ({ update: () => ({ digest: () => '' }) }); -export const randomUUID = () => '00000000-0000-0000-0000-000000000000'; diff --git a/apps/account/package.json b/apps/account/package.json deleted file mode 100644 index 70387c8d38..0000000000 --- a/apps/account/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "@objectstack/account", - "version": "9.3.0", - "description": "ObjectStack Account - End-user account & organization self-service portal", - "license": "Apache-2.0", - "type": "module", - "files": [ - "dist" - ], - "publishConfig": { - "access": "public" - }, - "scripts": { - "dev": "vite", - "build": "vite build", - "typecheck": "tsc --noEmit", - "test": "vitest run --passWithNoTests", - "test:watch": "vitest", - "preview": "vite preview" - }, - "devDependencies": { - "@object-ui/i18n": "^6.2.3", - "@objectstack/client": "workspace:*", - "@objectstack/client-react": "workspace:*", - "@objectstack/spec": "workspace:*", - "@radix-ui/react-avatar": "^1.1.12", - "@radix-ui/react-checkbox": "^1.3.4", - "@radix-ui/react-collapsible": "^1.1.13", - "@radix-ui/react-dialog": "^1.1.16", - "@radix-ui/react-dropdown-menu": "^2.1.17", - "@radix-ui/react-label": "^2.1.9", - "@radix-ui/react-scroll-area": "^1.2.11", - "@radix-ui/react-select": "^2.3.0", - "@radix-ui/react-separator": "^1.1.9", - "@radix-ui/react-slot": "^1.2.5", - "@radix-ui/react-switch": "^1.3.0", - "@radix-ui/react-tabs": "^1.1.14", - "@radix-ui/react-toast": "^1.2.16", - "@radix-ui/react-tooltip": "^1.2.9", - "@tailwindcss/postcss": "^4.3.0", - "@tanstack/react-router": "^1.170.15", - "@tanstack/router-plugin": "^1.168.18", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.2", - "@vitest/coverage-v8": "^4.1.8", - "autoprefixer": "^10.5.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "happy-dom": "^20.10.2", - "lucide-react": "^1.17.0", - "postcss": "^8.5.15", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-resizable-panels": "^4.11.2", - "tailwind-merge": "^3.6.0", - "tailwindcss": "^4.3.0", - "typescript": "^6.0.3", - "vite": "^8.0.16", - "vitest": "^4.1.8" - } -} diff --git a/apps/account/postcss.config.js b/apps/account/postcss.config.js deleted file mode 100644 index 1c8784688c..0000000000 --- a/apps/account/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - '@tailwindcss/postcss': {}, - autoprefixer: {}, - }, -} diff --git a/apps/account/src/App.tsx b/apps/account/src/App.tsx deleted file mode 100644 index 18bb418246..0000000000 --- a/apps/account/src/App.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * App Component - * - * Main application wrapper that provides the TanStack Router instance. - */ - -import { RouterProvider } from '@tanstack/react-router'; -import { router } from './router'; - -export default function App() { - return ; -} diff --git a/apps/account/src/components/account-sidebar.tsx b/apps/account/src/components/account-sidebar.tsx deleted file mode 100644 index 138f5c0fc5..0000000000 --- a/apps/account/src/components/account-sidebar.tsx +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * AccountSidebar — global left navigation for the Account portal. - * - * Three semantic groups: - * - * Account - * ├─ Profile - * ├─ Security - * ├─ Sessions - * └─ Two-Factor - * - * Organization - * ├─ Overview (/organizations) - * ├─ General (/organizations/:id/general — only when an org is active) - * └─ Members (/organizations/:id/members — only when an org is active) - * - * Developer - * └─ OAuth Apps (/account/oauth-applications) - * - * The active org's name is intentionally NOT used as a group label — - * the top-bar OrganizationSwitcher already shows it prominently. When - * collapsed to icon-only the labels hide automatically. - */ - -import { Link, useLocation } from '@tanstack/react-router'; -import { - Building2, - Home, - Inbox, - KeyRound, - Link2, - Monitor, - PanelLeft, - Settings, - Shield, - ShieldCheck, - User, - Users, - Users2, -} from 'lucide-react'; -import { useObjectTranslation } from '@object-ui/i18n'; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarSeparator, - useSidebar, -} from '@/components/ui/sidebar'; -import { useOrganizations } from '@/hooks/useSession'; - -interface NavItem { - to: - | '/account' - | '/account/profile' - | '/account/security' - | '/account/sessions' - | '/account/two-factor' - | '/account/invitations' - | '/account/linked-accounts' - | '/account/oauth-applications' - | '/organizations'; - label: string; - icon: React.ComponentType<{ className?: string }>; - exact?: boolean; -} - -const ACCOUNT_ITEMS: NavItem[] = [ - { to: '/account', label: 'home', icon: Home, exact: true }, - { to: '/account/profile', label: 'profile', icon: User }, - { to: '/account/security', label: 'security', icon: Shield }, - { to: '/account/sessions', label: 'sessions', icon: Monitor }, - { to: '/account/two-factor', label: 'twoFactor', icon: ShieldCheck }, - { to: '/account/invitations', label: 'invitations', icon: Inbox }, - { to: '/account/linked-accounts', label: 'linkedAccounts', icon: Link2 }, -]; - -/** - * Shared classes added to every SidebarMenuButton in the Account portal: - * - left accent bar that fades in on active - * - icon scale-up on hover (and gradient tint on active) - * - smoother transition - */ -const NAV_BUTTON_CLASSES = [ - 'group/nav relative transition-all duration-150', - 'before:absolute before:left-0 before:top-1/2 before:h-5 before:w-[3px] before:-translate-y-1/2 before:rounded-r-full before:bg-brand-gradient before:opacity-0 before:transition-opacity', - 'data-[active=true]:before:opacity-100', - 'data-[active=true]:bg-sidebar-accent/80 data-[active=true]:text-sidebar-accent-foreground', - '[&>svg]:transition-transform [&>svg]:duration-150 hover:[&>svg]:scale-110', - 'data-[active=true]:[&>svg]:text-primary', -].join(' '); - -export function AccountSidebar() { - const { t } = useObjectTranslation(); - const { pathname } = useLocation(); - const { organizations } = useOrganizations(); - - // /account is the home dashboard now; sub-routes match by prefix and the - // `exact` flag is honoured for the Home item so it doesn't light up on - // every sub-route. - const normalised = pathname; - - // Detect /organizations//* — used to surface the org-scoped sub-items. - const orgMatch = pathname.match(/^\/organizations\/([^/]+)(?:\/.*)?$/); - const activeOrgId = - orgMatch && organizations.some((o) => o.id === orgMatch[1]) ? orgMatch[1] : null; - - return ( - - - - {t('sidebar.groups.account')} - - - {ACCOUNT_ITEMS.map((item) => { - const Icon = item.icon; - const isActive = item.exact - ? normalised === item.to || normalised === `${item.to}/` - : normalised === item.to || normalised.startsWith(`${item.to}/`); - return ( - - - - - {t(`sidebar.items.${item.label}`)} - - - - ); - })} - - - - - - - - {t('sidebar.groups.organization')} - - - - - - - {t('sidebar.items.overview')} - - - - {activeOrgId && ( - <> - - - - - {t('sidebar.items.general')} - - - - - - - - {t('sidebar.items.members')} - - - - - - - - {t('sidebar.items.teams')} - - - - - )} - - - - - - - - {t('sidebar.groups.developer')} - - - - - - - {t('sidebar.items.oauthApps')} - - - - - - - - - - - - - - - - ); -} - -function CollapseButton() { - const { t } = useObjectTranslation(); - const { state, toggleSidebar } = useSidebar(); - const collapsed = state === 'collapsed'; - const label = collapsed ? t('sidebar.expand') : t('sidebar.collapse'); - return ( - - - - ); -} diff --git a/apps/account/src/components/auth/auth-shell.tsx b/apps/account/src/components/auth/auth-shell.tsx deleted file mode 100644 index e356476eae..0000000000 --- a/apps/account/src/components/auth/auth-shell.tsx +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * AuthShell — minimal shared layout for every unauthenticated page - * (login, register, forgot-password, reset-password, verify-email, …). - * - * ┌─────────────────────────────────────────┐ - * │ │ - * │ centred form card │ - * │ │ - * └─────────────────────────────────────────┘ - * - * Deliberately brand-agnostic. ObjectStack is a developer tool — operators - * use it to build their own customer-facing apps, and pinning a vendor - * wordmark / marketing copy on the login screen of every downstream - * deployment is wrong. The shell only shows a small host pill when the - * current origin looks like a tenant subdomain, so a user bouncing through - * an SSO redirect can still tell which workspace they're signing in to. - * - * The `headline` and `subline` props are kept for backwards-compat but are - * intentionally unused; the previous split-panel ad copy is gone. Existing - * call sites still compile without changes. - */ - -import * as React from 'react'; -import { useObjectTranslation } from '@object-ui/i18n'; -import { cn } from '@/lib/utils'; - -export interface AuthShellProps { - /** Form / interactive content rendered in the centred column. */ - children: React.ReactNode; - /** - * @deprecated Brand panel was removed for a generic, white-label auth - * surface. Prop is kept so existing pages still compile. - */ - headline?: React.ReactNode; - /** - * @deprecated Brand panel was removed for a generic, white-label auth - * surface. Prop is kept so existing pages still compile. - */ - subline?: React.ReactNode; - /** Optional max-width on the form container (default `sm`). */ - formWidth?: 'sm' | 'md'; -} - -/** - * Hosts that look like a canonical cloud control-plane (e.g. `cloud.*`) - * — when the page is rendered on one of these we hide the host pill - * because the URL bar already identifies the workspace. - */ -function isCanonicalCloudHost(host: string): boolean { - const bare = host.split(':')[0]!.toLowerCase(); - return /^cloud\./.test(bare); -} - -function currentHost(): string | null { - if (typeof window === 'undefined') return null; - const host = window.location.host; - return host || null; -} - -export function AuthShell({ - children, - // headline/subline are intentionally ignored — see file header. - formWidth = 'sm', -}: AuthShellProps) { - const { t } = useObjectTranslation(); - const widthCls = formWidth === 'md' ? 'max-w-md' : 'max-w-sm'; - - const host = currentHost(); - const showHostPill = !!host && !isCanonicalCloudHost(host); - - React.useEffect(() => { - if (typeof document === 'undefined' || !host) return; - const original = document.title; - if (showHostPill) document.title = host; - return () => { - document.title = original; - }; - }, [host, showHostPill]); - - return ( -
-
- {showHostPill ? ( -
- - {host} - -
- ) : null} - {children} -
-
- ); -} diff --git a/apps/account/src/components/auth/legal-links.tsx b/apps/account/src/components/auth/legal-links.tsx deleted file mode 100644 index 9a30a49458..0000000000 --- a/apps/account/src/components/auth/legal-links.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import type { MouseEvent } from 'react'; -import { useObjectTranslation } from '@object-ui/i18n'; - -export interface LegalLinksProps { - /** URL to the deployment's Terms of Service. Hidden when undefined / empty. */ - termsUrl?: string; - /** URL to the deployment's Privacy Policy. Hidden when undefined / empty. */ - privacyUrl?: string; -} - -/** - * Open a legal document in a popup window. We deliberately avoid - * navigating the auth tab (which would lose half-typed credentials) - * and avoid the default new-tab behaviour so the document is visually - * scoped to "this is a side-trip, come back to the form". Falls back - * to a normal navigation if the popup is blocked. - */ -function openLegalPopup(event: MouseEvent, url: string): void { - // Honour cmd/ctrl/middle-click — power users may want a real tab. - if (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1) { - return; - } - event.preventDefault(); - const width = Math.min(960, window.screen.availWidth - 80); - const height = Math.min(720, window.screen.availHeight - 80); - const left = Math.max(0, Math.round((window.screen.availWidth - width) / 2)); - const top = Math.max(0, Math.round((window.screen.availHeight - height) / 2)); - const features = [ - 'popup=yes', - `width=${width}`, - `height=${height}`, - `left=${left}`, - `top=${top}`, - 'resizable=yes', - 'scrollbars=yes', - 'noopener=yes', - 'noreferrer=yes', - ].join(','); - const win = window.open(url, 'objectstack-legal', features); - if (!win) { - // Popup blocked — fall back to a normal new-tab navigation. - window.open(url, '_blank', 'noopener,noreferrer'); - } -} - -/** - * Fine-print legal links shown beneath the login / register cards. - * - * URLs come from `GET /api/v1/auth/config` → `features.termsUrl` / - * `features.privacyUrl`, which the server resolves from env vars - * `OS_TERMS_URL` / `OS_PRIVACY_URL` (defaulting to the public - * ObjectStack pages). Clicking a link opens the document in a centred - * popup window so the user doesn't lose their place in the auth form. - */ -export function LegalLinks({ termsUrl, privacyUrl }: LegalLinksProps): React.ReactElement | null { - const { t } = useObjectTranslation('account'); - const hasTerms = typeof termsUrl === 'string' && termsUrl.length > 0; - const hasPrivacy = typeof privacyUrl === 'string' && privacyUrl.length > 0; - if (!hasTerms && !hasPrivacy) return null; - return ( -

- {t('legal.agreementPrefix')}{' '} - {hasTerms ? ( - openLegalPopup(event, termsUrl!)} - > - {t('legal.termsOfService')} - - ) : null} - {hasTerms && hasPrivacy ? <> {t('legal.and')} : null} - {hasPrivacy ? ( - openLegalPopup(event, privacyUrl!)} - > - {t('legal.privacyPolicy')} - - ) : null} - . -

- ); -} - diff --git a/apps/account/src/components/auth/social-sign-in-buttons.tsx b/apps/account/src/components/auth/social-sign-in-buttons.tsx deleted file mode 100644 index 28c663f115..0000000000 --- a/apps/account/src/components/auth/social-sign-in-buttons.tsx +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { useClient } from '@objectstack/client-react'; -import { useObjectTranslation } from '@object-ui/i18n'; -import { Button } from '@/components/ui/button'; - -interface SocialProvider { - id: string; - name: string; - enabled: boolean; - type?: 'social' | 'oidc'; -} - -interface Props { - mode: 'sign-in' | 'sign-up'; - redirect?: string; -} - -export function SocialSignInButtons({ mode, redirect }: Props) { - const { t } = useObjectTranslation(); - const client = useClient() as any; - const [providers, setProviders] = useState([]); - const [loading, setLoading] = useState(true); - // Tracks which provider button was clicked. We keep it set until the - // browser actually navigates away — the full-page overlay below relies - // on it so the user gets unmistakable feedback during the (sometimes - // multi-second) cross-origin redirect to the IdP. On a slow link there - // was previously no visual hint that the click registered, and users - // would click the button repeatedly thinking it was broken. - const [redirectingTo, setRedirectingTo] = useState(null); - - useEffect(() => { - if (!client?.auth?.getConfig) return; - client.auth.getConfig() - .then((res: any) => { - const list: SocialProvider[] = res?.socialProviders ?? res?.data?.socialProviders ?? []; - setProviders(list.filter((p) => p.enabled)); - }) - .catch((err: unknown) => { - console.warn('[SocialSignInButtons] failed to load auth config', err); - }) - .finally(() => setLoading(false)); - }, [client]); - - if (loading || providers.length === 0) return null; - - const label = - mode === 'sign-in' ? t('auth.social.continueWith') : t('auth.social.signUpWith'); - - const base = window.location.origin + import.meta.env.BASE_URL; - const loginUrl = base + 'login' + (redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''); - - const handleProviderClick = (p: SocialProvider) => { - if (redirectingTo) return; - setRedirectingTo(p); - // signInWithProvider triggers a full-page navigation. We deliberately - // do NOT clear `redirectingTo` on success — the overlay stays up until - // the browser unloads this document, which is the most reliable signal - // that "the click is doing something". On failure we clear it so the - // user can retry / fall back to email login. - Promise.resolve( - client.auth.signInWithProvider(p.id, { - callbackURL: loginUrl, - errorCallbackURL: loginUrl, - type: p.type ?? 'social', - }), - ).catch((err: unknown) => { - console.warn('[SocialSignInButtons] signInWithProvider failed', err); - setRedirectingTo(null); - }); - }; - - return ( -
- {providers.map((p) => { - const isRedirecting = redirectingTo?.id === p.id; - return ( - - ); - })} -
-
- -
-
- {t('auth.social.orContinueEmail')} -
-
- {redirectingTo && typeof document !== 'undefined' - ? createPortal( -
-
-
- - {t('auth.social.redirectingTo', { - provider: redirectingTo.name, - defaultValue: `Redirecting to ${redirectingTo.name}…`, - })} - -
-
, - document.body, - ) - : null} -
- ); -} diff --git a/apps/account/src/components/auth/transition-overlay.tsx b/apps/account/src/components/auth/transition-overlay.tsx deleted file mode 100644 index ef7c275de4..0000000000 --- a/apps/account/src/components/auth/transition-overlay.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * TransitionOverlay — full-page semi-transparent backdrop with a spinner - * and a short status message. Used whenever we kick off a navigation - * that will unload the current document (SSO bounce, logout, post-login - * redirect to another SPA) so the user gets unmistakable feedback that - * the click did fire, especially on slow networks where the bare button - * would look broken. - * - * Rendered via a portal into `document.body` so it floats above every - * Card / Dialog without z-index gymnastics. - */ - -import * as React from 'react'; -import { createPortal } from 'react-dom'; - -export interface TransitionOverlayProps { - /** Status text shown next to the spinner. */ - message: string; -} - -export function TransitionOverlay({ message }: TransitionOverlayProps) { - if (typeof document === 'undefined') return null; - return createPortal( -
-
-
- {message} -
-
, - document.body, - ); -} diff --git a/apps/account/src/components/locale-toggle.tsx b/apps/account/src/components/locale-toggle.tsx deleted file mode 100644 index d90bc1d173..0000000000 --- a/apps/account/src/components/locale-toggle.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * LocaleToggle — language switcher button in the top bar. - * - * Mirrors the ThemeToggle pattern. Persists the selected language to - * localStorage under `account.lang` so the choice survives reloads. - */ - -import { Languages } from 'lucide-react'; -import { useObjectTranslation } from '@object-ui/i18n'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { SUPPORTED_LANGUAGES, type SupportedLanguage } from '@/i18n'; - -const LABELS: Record = { - en: 'English', - 'zh-CN': '中文', -}; - -export function LocaleToggle() { - const { t, language, changeLanguage } = useObjectTranslation(); - - async function handleSelect(lang: SupportedLanguage) { - await changeLanguage(lang); - try { - localStorage.setItem('account.lang', lang); - } catch { - /* ignore storage errors */ - } - } - - return ( - - - - - - {SUPPORTED_LANGUAGES.map((lang) => ( - void handleSelect(lang)} - className={lang === language ? 'font-medium' : undefined} - > - {LABELS[lang]} - - ))} - - - ); -} diff --git a/apps/account/src/components/organization-switcher.tsx b/apps/account/src/components/organization-switcher.tsx deleted file mode 100644 index e6ab135d65..0000000000 --- a/apps/account/src/components/organization-switcher.tsx +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * OrganizationSwitcher — Account portal top-bar dropdown for switching - * between the user's organizations. Mirrors Studio's switcher. - * - * When the user is currently viewing `/organizations//
`, switching to - * another org navigates to the equivalent section on the new org so the - * URL stays in sync with the active org. - */ - -import { useMemo, useState } from 'react'; -import { useLocation, useNavigate } from '@tanstack/react-router'; -import { Check, ChevronsUpDown, Plus } from 'lucide-react'; -import { useObjectTranslation } from '@object-ui/i18n'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Button } from '@/components/ui/button'; -import { useOrganizations, useSession } from '@/hooks/useSession'; -import { toast } from '@/hooks/use-toast'; - -export function OrganizationSwitcher() { - const { t } = useObjectTranslation(); - const navigate = useNavigate(); - const location = useLocation(); - const { organizations, loading, reload } = useOrganizations(); - const { session, setActiveOrganization } = useSession(); - const [open, setOpen] = useState(false); - const [switching, setSwitching] = useState(false); - - const activeId = session?.activeOrganizationId ?? undefined; - const active = useMemo( - () => organizations.find((o) => o.id === activeId) ?? null, - [organizations, activeId], - ); - - const handleSelect = async (id: string) => { - setOpen(false); - if (id === activeId) { - // No-op switch — but still ensure we land on the org page. - navigate({ to: '/organizations/$orgId/general', params: { orgId: id } }); - return; - } - setSwitching(true); - try { - await setActiveOrganization(id); - await reload(); - toast({ title: t('organizations.general.switched') }); - - // Mirror the current section onto the new org when applicable. - const m = location.pathname.match(/^\/organizations\/[^/]+\/(general|members)\/?$/); - const section = m ? (m[1] as 'general' | 'members') : 'general'; - navigate({ - to: section === 'members' ? '/organizations/$orgId/members' : '/organizations/$orgId/general', - params: { orgId: id }, - }); - } catch (err) { - toast({ - title: t('organizations.switchFailed'), - description: (err as Error).message, - variant: 'destructive', - }); - } finally { - setSwitching(false); - } - }; - - return ( - - - - - - - {t('organizations.title')} - - {organizations.length === 0 && !loading && ( -
- {t('organizations.emptyTitle')} -
- )} - {organizations.map((org) => ( - { - e.preventDefault(); - handleSelect(org.id); - }} - className="flex items-center gap-2" - > -
-
{org.name}
- {org.slug && ( - - {org.slug} - - )} -
- {org.id === activeId && } -
- ))} - - { - e.preventDefault(); - setOpen(false); - navigate({ to: '/organizations/new' }); - }} - className="gap-2 text-sm" - > - - {t('organizationSwitcher.new')} - - { - e.preventDefault(); - setOpen(false); - navigate({ to: '/organizations' }); - }} - className="gap-2 text-sm text-muted-foreground" - > - {t('organizationSwitcher.manage')} - -
-
- ); -} diff --git a/apps/account/src/components/page-header.tsx b/apps/account/src/components/page-header.tsx deleted file mode 100644 index c8a589e608..0000000000 --- a/apps/account/src/components/page-header.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * PageHeader — consistent hero strip at the top of every authenticated - * Account page. - * - * ┌─────────────────────────────────────────────────────────────────┐ - * │ (icon) Title [action slot] │ - * │ Description / supporting copy │ - * └─────────────────────────────────────────────────────────────────┘ - * - * The whole block sits on a faint brand-tinted gradient + soft border so the - * header reads as a single "frame" without dominating the content below. - * `icon` is optional; when present it gets a glassy gradient tile. - */ - -import * as React from 'react'; -import { cn } from '@/lib/utils'; - -export interface PageHeaderProps - extends Omit, 'title'> { - title: React.ReactNode; - description?: React.ReactNode; - icon?: React.ComponentType<{ className?: string }>; - actions?: React.ReactNode; -} - -export function PageHeader({ - title, - description, - icon: Icon, - actions, - className, - ...rest -}: PageHeaderProps) { - return ( -
-
-
-
- {Icon ? ( -
- -
- ) : null} -
-

- {title} -

- {description ? ( -

{description}

- ) : null} -
-
- {actions ? ( -
{actions}
- ) : null} -
-
- ); -} diff --git a/apps/account/src/components/theme-toggle.tsx b/apps/account/src/components/theme-toggle.tsx deleted file mode 100644 index f63f8ec4e6..0000000000 --- a/apps/account/src/components/theme-toggle.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { Moon, Sun } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { useObjectTranslation } from '@object-ui/i18n'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; - -type Theme = 'light' | 'dark' | 'system'; - -function applyTheme(t: 'light' | 'dark') { - const root = document.documentElement; - if (t === 'dark') root.classList.add('dark'); - else root.classList.remove('dark'); -} - -export function ThemeToggle() { - const { t } = useObjectTranslation(); - const [, setTheme] = useState('light'); - - useEffect(() => { - const stored = localStorage.getItem('theme') as Theme | null; - if (stored) { - setTheme(stored); - if (stored === 'system') { - applyTheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - } else { - applyTheme(stored); - } - } else { - setTheme('system'); - applyTheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - } - }, []); - - function handleSetTheme(t: Theme) { - setTheme(t); - localStorage.setItem('theme', t); - if (t === 'system') { - applyTheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - } else { - applyTheme(t); - } - } - - return ( - - - - - - handleSetTheme('light')}> - - {t('common.light')} - - handleSetTheme('dark')}> - - {t('common.dark')} - - handleSetTheme('system')}>{t('common.system')} - - - ); -} diff --git a/apps/account/src/components/top-bar.tsx b/apps/account/src/components/top-bar.tsx deleted file mode 100644 index 76275848ff..0000000000 --- a/apps/account/src/components/top-bar.tsx +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * TopBar — global chrome rendered at the top of every authenticated Account - * route. Mirrors the Studio TopBar layout: - * - * - Left: brand → mobile sidebar toggle → breadcrumb (inferred from URL) - * - Right: locale toggle + theme toggle + UserMenu (avatar dropdown w/ sign-out) - */ - -import { useLocation, useParams } from '@tanstack/react-router'; -import { useMemo } from 'react'; -import { ChevronRight, UserCircle2 } from 'lucide-react'; -import { useObjectTranslation } from '@object-ui/i18n'; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from '@/components/ui/breadcrumb'; -import { Separator } from '@/components/ui/separator'; -import { SidebarTrigger } from '@/components/ui/sidebar'; -import { LocaleToggle } from '@/components/locale-toggle'; -import { ThemeToggle } from '@/components/theme-toggle'; -import { UserMenu } from '@/components/user-menu'; -import { OrganizationSwitcher } from '@/components/organization-switcher'; - -function AccountBrand({ label }: { label: string }) { - return ( - - - - ); -} - -function SlashDivider() { - return ( - - ); -} - -export function TopBar() { - const { t } = useObjectTranslation(); - const location = useLocation(); - const params = useParams({ strict: false }) as { orgId?: string; invitationId?: string }; - - const breadcrumbs = useMemo>(() => { - const p = location.pathname; - const tb = (k: string) => t(`topBar.breadcrumb.${k}`); - if (p.startsWith('/account')) { - const items: Array<{ label: string }> = [{ label: tb('account') }]; - if (p === '/account/profile') items.push({ label: tb('profile') }); - else if (p === '/account/security') items.push({ label: tb('security') }); - else if (p === '/account/sessions') items.push({ label: tb('sessions') }); - else if (p === '/account/two-factor') items.push({ label: tb('twoFactor') }); - else if (p === '/account/oauth-applications') items.push({ label: tb('oauthApps') }); - else if (p === '/account/oauth-applications/new') { - items.push({ label: tb('oauthApps') }, { label: tb('new') }); - } else if (p.startsWith('/account/oauth-applications/')) { - items.push({ label: tb('oauthApps') }); - } - return items; - } - if (p === '/organizations/new') return [{ label: tb('organizations') }, { label: tb('new') }]; - if (params.orgId) { - const tail = p.endsWith('/general') - ? tb('general') - : p.endsWith('/members') - ? tb('members') - : tb('settings'); - return [{ label: tb('organizations') }, { label: tail }]; - } - if (p === '/organizations' || p.startsWith('/organizations/')) return [{ label: tb('organizations') }]; - if (p.startsWith('/accept-invitation/')) return [{ label: tb('acceptInvitation') }]; - if (p.startsWith('/auth/device')) return [{ label: tb('deviceAuthorization') }]; - return [{ label: tb('account') }]; - }, [location.pathname, params.orgId, t]); - - return ( -
- {/* Left: brand + mobile trigger + breadcrumb */} -
-
- -
- - - {t('topBar.brand')} - -
- - - - - - {breadcrumbs.map((item, index) => ( -
- {index > 0 && } - - {item.label} - -
- ))} -
-
-
- {/* Mobile: just show last crumb */} -
- {breadcrumbs.length > 0 && ( - - {breadcrumbs[breadcrumbs.length - 1].label} - - )} -
-
- - {/* Right: locale + theme + user */} -
-
- -
-
- -
- -
-
- ); -} diff --git a/apps/account/src/components/ui/avatar.tsx b/apps/account/src/components/ui/avatar.tsx deleted file mode 100644 index 7287b62b27..0000000000 --- a/apps/account/src/components/ui/avatar.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -"use client" - -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" - -import { cn } from "@/lib/utils" - -const Avatar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -Avatar.displayName = AvatarPrimitive.Root.displayName - -const AvatarImage = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName - -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName - -export { Avatar, AvatarImage, AvatarFallback } diff --git a/apps/account/src/components/ui/badge.tsx b/apps/account/src/components/ui/badge.tsx deleted file mode 100644 index 2e06a20a30..0000000000 --- a/apps/account/src/components/ui/badge.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/apps/account/src/components/ui/breadcrumb.tsx b/apps/account/src/components/ui/breadcrumb.tsx deleted file mode 100644 index d531a98f24..0000000000 --- a/apps/account/src/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" -import * as React from "react" - -import { cn } from "@/lib/utils" - -const breadcrumbVariants = cva( - "text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5" -) - -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & - VariantProps ->(({ className, ...props }, ref) => ( -