From 04ea8768e9bdcc6ed86c98d77dcffb16294b8e73 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:27:33 +0500 Subject: [PATCH 1/5] feat(apps): scaffold @objectstack/studio app package (ADR-0048, boot-neutral) ADR-0048 "one app per package": Studio becomes its own ObjectStack package (`com.objectstack.studio`, namespace `studio`) so `/apps/` resolves unambiguously instead of being lost inside plugin-auth's multi-app manifest. This commit is intentionally boot-neutral: - New package `packages/apps/studio` with a thin `StudioAppPlugin` that registers the Studio manifest; STUDIO_APP is still imported from platform-objects (transitional) and the package is NOT yet wired into the dev/serve plugin set. - Adds `packages/apps/*` to the pnpm workspace globs and `@objectstack/studio` to the changeset fixed group. Boot path (dev-plugin/serve.ts) and plugin-auth's manifest are untouched, so `os dev`/`os serve` behaviour is unchanged. The boot-path switch (wire the new package, drop the app from plugin-auth, move the AppSchema in) lands separately so it can be verified against a live boot. Builds clean (tsup CJS/ESM/DTS). Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/adr-0048-split-studio-app.md | 16 ++++++ .changeset/config.json | 1 + packages/apps/studio/package.json | 52 +++++++++++++++++++ packages/apps/studio/src/index.ts | 69 +++++++++++++++++++++++++ packages/apps/studio/tsconfig.json | 11 ++++ packages/apps/studio/tsup.config.ts | 15 ++++++ pnpm-lock.yaml | 22 ++++++++ pnpm-workspace.yaml | 1 + 8 files changed, 187 insertions(+) create mode 100644 .changeset/adr-0048-split-studio-app.md create mode 100644 packages/apps/studio/package.json create mode 100644 packages/apps/studio/src/index.ts create mode 100644 packages/apps/studio/tsconfig.json create mode 100644 packages/apps/studio/tsup.config.ts 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..545dc377c9 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -24,6 +24,7 @@ "@objectstack/observability", "@objectstack/formula", "@objectstack/platform-objects", + "@objectstack/studio", "@objectstack/runtime", "@objectstack/rest", "@objectstack/driver-memory", diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json new file mode 100644 index 0000000000..ce1a63f6c2 --- /dev/null +++ b/packages/apps/studio/package.json @@ -0,0 +1,52 @@ +{ + "name": "@objectstack/studio", + "version": "9.3.0", + "license": "Apache-2.0", + "description": "ObjectStack Studio — the metadata builder app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@objectstack/platform-objects": "workspace:*", + "@objectstack/spec": "workspace:*" + }, + "devDependencies": { + "@types/node": "^25.9.2", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.8" + }, + "keywords": [ + "objectstack", + "app", + "studio" + ], + "author": "ObjectStack", + "repository": { + "type": "git", + "url": "https://github.com/objectstack-ai/framework.git", + "directory": "packages/apps/studio" + }, + "homepage": "https://objectstack.ai/docs", + "bugs": "https://github.com/objectstack-ai/framework/issues", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "README.md" + ], + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/apps/studio/src/index.ts b/packages/apps/studio/src/index.ts new file mode 100644 index 0000000000..a4db3e0e6e --- /dev/null +++ b/packages/apps/studio/src/index.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `@objectstack/studio` — the Studio metadata-builder app as its own ObjectStack + * package (ADR-0048: one app per package). + * + * Studio is a console navigation-shell app (an `AppSchema`), not a kernel + * extension — but it still needs a thin registration entry to publish its + * manifest into the running kernel, mirroring how `@objectstack/plugin-auth` + * registers its own manifest. Giving it a distinct package id + * (`com.objectstack.studio`) is what lets `/apps/` resolve to exactly + * this app instead of being ambiguous inside a multi-app package. + * + * NOTE (transitional): the `STUDIO_APP` schema is still imported from + * `@objectstack/platform-objects/apps`; a follow-up moves the definition into + * this package and drops the dependency. This package is intentionally NOT yet + * wired into the dev/serve plugin set — that boot-path switch (and removing the + * app from plugin-auth's manifest) lands separately so it can be verified + * against a live `os dev` boot. + */ + +import { STUDIO_APP } from '@objectstack/platform-objects/apps'; + +export const STUDIO_APP_PACKAGE_ID = 'com.objectstack.studio'; +export const STUDIO_APP_NAMESPACE = 'studio'; +export const STUDIO_APP_VERSION = '9.3.0'; + +/** Manifest header for the Studio app package. */ +export const studioAppManifestHeader = { + id: STUDIO_APP_PACKAGE_ID, + namespace: STUDIO_APP_NAMESPACE, + version: STUDIO_APP_VERSION, + type: 'plugin' as const, + scope: 'system' as const, + name: 'Studio', + description: 'ObjectStack Studio — metadata builder app.', + // Studio navigates platform metadata owned by plugin-auth; declare the + // dependency so it loads after the auth objects are registered. + dependencies: ['com.objectstack.plugin-auth'], +}; + +/** + * Thin plugin that registers the Studio app manifest. Structurally typed + * against the kernel `Plugin` contract (no compile-time kernel dependency), + * exactly like `AuthPlugin`. + */ +export class StudioAppPlugin { + readonly name = STUDIO_APP_PACKAGE_ID; + readonly type = 'standard'; + readonly version = STUDIO_APP_VERSION; + readonly dependencies: string[] = ['com.objectstack.plugin-auth']; + + async init(_ctx: any): Promise { + // No-op: registration happens in start() once the manifest service exists. + } + + async start(ctx: any): Promise { + const manifest = ctx?.getService?.('manifest'); + if (!manifest || typeof manifest.register !== 'function') return; + manifest.register({ ...studioAppManifestHeader, apps: [STUDIO_APP] }); + } +} + +/** Convenience factory mirroring the rest of the plugin ecosystem. */ +export function createStudioAppPlugin(): StudioAppPlugin { + return new StudioAppPlugin(); +} + +export { STUDIO_APP }; diff --git a/packages/apps/studio/tsconfig.json b/packages/apps/studio/tsconfig.json new file mode 100644 index 0000000000..fc7524c1f0 --- /dev/null +++ b/packages/apps/studio/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/apps/studio/tsup.config.ts b/packages/apps/studio/tsup.config.ts new file mode 100644 index 0000000000..dde3395982 --- /dev/null +++ b/packages/apps/studio/tsup.config.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + index: 'src/index.ts', + }, + format: ['cjs', 'esm'], + dts: !process.env.OS_SKIP_DTS, + clean: true, + sourcemap: true, + splitting: false, + treeshake: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4971c5d5d..48f1bc5920 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -450,6 +450,28 @@ importers: specifier: ^4.1.8 version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.2)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/apps/studio: + dependencies: + '@objectstack/platform-objects': + specifier: workspace:* + version: link:../../platform-objects + '@objectstack/spec': + specifier: workspace:* + version: link:../../spec + devDependencies: + '@types/node': + specifier: ^25.9.2 + version: 25.9.2 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.8 + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.2)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/cli: dependencies: '@ai-sdk/anthropic': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 375fe6a482..472b8234ab 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - packages/* + - packages/apps/* - packages/plugins/* - packages/triggers/* - packages/services/* From 82b5dd57c3936e8567d74e668eae664f84799faf Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:29:29 +0500 Subject: [PATCH 2/5] feat(apps): scaffold @objectstack/setup app package (ADR-0048, boot-neutral) Mirrors @objectstack/studio: Setup becomes its own package (`com.objectstack.setup`, namespace `setup`) carrying SETUP_APP and its baseline SETUP_NAV_CONTRIBUTIONS. Boot-neutral (transitional import from platform-objects; not yet wired into dev/serve). Added to the changeset fixed group. Builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/adr-0048-split-setup-app.md | 12 ++++ .changeset/config.json | 1 + packages/apps/setup/package.json | 52 +++++++++++++++++ packages/apps/setup/src/index.ts | 66 ++++++++++++++++++++++ packages/apps/setup/tsconfig.json | 11 ++++ packages/apps/setup/tsup.config.ts | 15 +++++ pnpm-lock.yaml | 78 +++++++++++++++++++++++++- 7 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 .changeset/adr-0048-split-setup-app.md create mode 100644 packages/apps/setup/package.json create mode 100644 packages/apps/setup/src/index.ts create mode 100644 packages/apps/setup/tsconfig.json create mode 100644 packages/apps/setup/tsup.config.ts 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/config.json b/.changeset/config.json index 545dc377c9..f8d741691a 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -25,6 +25,7 @@ "@objectstack/formula", "@objectstack/platform-objects", "@objectstack/studio", + "@objectstack/setup", "@objectstack/runtime", "@objectstack/rest", "@objectstack/driver-memory", diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json new file mode 100644 index 0000000000..d69d024ae6 --- /dev/null +++ b/packages/apps/setup/package.json @@ -0,0 +1,52 @@ +{ + "name": "@objectstack/setup", + "version": "9.3.0", + "license": "Apache-2.0", + "description": "ObjectStack Setup — the platform administration app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@objectstack/platform-objects": "workspace:*", + "@objectstack/spec": "workspace:*" + }, + "devDependencies": { + "@types/node": "^25.9.2", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.8" + }, + "keywords": [ + "objectstack", + "app", + "setup" + ], + "author": "ObjectStack", + "repository": { + "type": "git", + "url": "https://github.com/objectstack-ai/framework.git", + "directory": "packages/apps/setup" + }, + "homepage": "https://objectstack.ai/docs", + "bugs": "https://github.com/objectstack-ai/framework/issues", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "README.md" + ], + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/apps/setup/src/index.ts b/packages/apps/setup/src/index.ts new file mode 100644 index 0000000000..348b76952f --- /dev/null +++ b/packages/apps/setup/src/index.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `@objectstack/setup` — the Setup platform-administration app as its own + * ObjectStack package (ADR-0048: one app per package). + * + * Like `@objectstack/studio`, Setup is a console navigation-shell app + * (`AppSchema`) given a distinct package id (`com.objectstack.setup`) so + * `/apps/` resolves to exactly this app. Setup also ships the + * baseline navigation contributions other packages extend into the Setup app. + * + * NOTE (transitional): `SETUP_APP` / `SETUP_NAV_CONTRIBUTIONS` are still + * imported from `@objectstack/platform-objects/apps`, and this package is NOT + * yet wired into the dev/serve plugin set — that boot-path switch lands + * separately so it can be verified against a live `os dev` boot. + */ + +import { SETUP_APP, SETUP_NAV_CONTRIBUTIONS } from '@objectstack/platform-objects/apps'; + +export const SETUP_APP_PACKAGE_ID = 'com.objectstack.setup'; +export const SETUP_APP_NAMESPACE = 'setup'; +export const SETUP_APP_VERSION = '9.3.0'; + +/** Manifest header for the Setup app package. */ +export const setupAppManifestHeader = { + id: SETUP_APP_PACKAGE_ID, + namespace: SETUP_APP_NAMESPACE, + version: SETUP_APP_VERSION, + type: 'plugin' as const, + scope: 'system' as const, + name: 'Setup', + description: 'ObjectStack Setup — platform administration app.', + dependencies: ['com.objectstack.plugin-auth'], +}; + +/** + * Thin plugin that registers the Setup app manifest (app + its baseline nav + * contributions). Structurally typed against the kernel `Plugin` contract. + */ +export class SetupAppPlugin { + readonly name = SETUP_APP_PACKAGE_ID; + readonly type = 'standard'; + readonly version = SETUP_APP_VERSION; + readonly dependencies: string[] = ['com.objectstack.plugin-auth']; + + async init(_ctx: any): Promise { + // No-op: registration happens in start() once the manifest service exists. + } + + async start(ctx: any): Promise { + const manifest = ctx?.getService?.('manifest'); + if (!manifest || typeof manifest.register !== 'function') return; + manifest.register({ + ...setupAppManifestHeader, + apps: [SETUP_APP], + navigationContributions: SETUP_NAV_CONTRIBUTIONS, + }); + } +} + +/** Convenience factory mirroring the rest of the plugin ecosystem. */ +export function createSetupAppPlugin(): SetupAppPlugin { + return new SetupAppPlugin(); +} + +export { SETUP_APP, SETUP_NAV_CONTRIBUTIONS }; diff --git a/packages/apps/setup/tsconfig.json b/packages/apps/setup/tsconfig.json new file mode 100644 index 0000000000..fc7524c1f0 --- /dev/null +++ b/packages/apps/setup/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/apps/setup/tsup.config.ts b/packages/apps/setup/tsup.config.ts new file mode 100644 index 0000000000..dde3395982 --- /dev/null +++ b/packages/apps/setup/tsup.config.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + index: 'src/index.ts', + }, + format: ['cjs', 'esm'], + dts: !process.env.OS_SKIP_DTS, + clean: true, + sourcemap: true, + splitting: false, + treeshake: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48f1bc5920..08b2183f82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -450,7 +450,7 @@ importers: specifier: ^4.1.8 version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.2)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) - packages/apps/studio: + packages/apps/setup: dependencies: '@objectstack/platform-objects': specifier: workspace:* @@ -472,6 +472,28 @@ importers: specifier: ^4.1.8 version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.2)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/apps/studio: + dependencies: + '@objectstack/platform-objects': + specifier: workspace:* + version: link:../../platform-objects + '@objectstack/spec': + specifier: workspace:* + version: link:../../spec + devDependencies: + '@types/node': + specifier: ^25.9.2 + version: 25.9.2 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.8 + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.2)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/cli: dependencies: '@ai-sdk/anthropic': @@ -12251,6 +12273,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.14.6(@types/node@25.9.2)(typescript@6.0.3) + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + '@vitest/mocker@4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.8 @@ -16631,6 +16662,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.2 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.22.4 + yaml: 2.9.0 + vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 @@ -16650,6 +16696,36 @@ snapshots: optionalDependencies: vite: 8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.2)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 25.9.2 + '@vitest/coverage-v8': 4.1.8(vitest@4.1.8) + happy-dom: 20.10.2 + transitivePeerDependencies: + - msw + vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.2)(@vitest/coverage-v8@4.1.8)(happy-dom@20.10.2)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.8 From 2707e3078be4083670ad04eb580b36e0471dd8e4 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:33:53 +0500 Subject: [PATCH 3/5] feat(apps): reclaim @objectstack/account for console Account app; drop dead SPA (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 package (`com.objectstack.account`, namespace `account`). Boot-neutral skeleton (mirrors studio/setup): thin AccountAppPlugin, transitional import from platform-objects, not yet wired into dev/serve. CLI's vestigial workspace dep now resolves to the reclaimed package (no code imported the old SPA). Builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/adr-0048-split-account-app.md | 12 + apps/account/CHANGELOG.md | 295 ---- apps/account/index.html | 63 - apps/account/mocks/node-polyfills.ts | 104 -- apps/account/package.json | 65 - apps/account/postcss.config.js | 6 - apps/account/src/App.tsx | 14 - .../src/components/account-sidebar.tsx | 256 ---- .../src/components/auth/auth-shell.tsx | 101 -- .../src/components/auth/legal-links.tsx | 90 -- .../auth/social-sign-in-buttons.tsx | 134 -- .../components/auth/transition-overlay.tsx | 38 - apps/account/src/components/locale-toggle.tsx | 64 - .../src/components/organization-switcher.tsx | 147 -- apps/account/src/components/page-header.tsx | 75 - apps/account/src/components/theme-toggle.tsx | 73 - apps/account/src/components/top-bar.tsx | 137 -- apps/account/src/components/ui/avatar.tsx | 52 - apps/account/src/components/ui/badge.tsx | 38 - apps/account/src/components/ui/breadcrumb.tsx | 153 -- apps/account/src/components/ui/button.tsx | 59 - apps/account/src/components/ui/card.tsx | 81 - apps/account/src/components/ui/checkbox.tsx | 32 - .../account/src/components/ui/collapsible.tsx | 11 - apps/account/src/components/ui/dialog.tsx | 124 -- .../src/components/ui/dropdown-menu.tsx | 202 --- apps/account/src/components/ui/input.tsx | 26 - apps/account/src/components/ui/label.tsx | 28 - apps/account/src/components/ui/resizable.tsx | 47 - .../account/src/components/ui/scroll-area.tsx | 48 - .../src/components/ui/select-native.tsx | 38 - apps/account/src/components/ui/select.tsx | 161 -- apps/account/src/components/ui/separator.tsx | 28 - apps/account/src/components/ui/sheet.tsx | 142 -- apps/account/src/components/ui/sidebar.tsx | 752 ---------- apps/account/src/components/ui/skeleton.tsx | 17 - apps/account/src/components/ui/switch.tsx | 31 - apps/account/src/components/ui/table.tsx | 121 -- apps/account/src/components/ui/tabs.tsx | 57 - apps/account/src/components/ui/textarea.tsx | 25 - apps/account/src/components/ui/toast.tsx | 129 -- apps/account/src/components/ui/toaster.tsx | 35 - apps/account/src/components/ui/tooltip.tsx | 32 - apps/account/src/components/user-menu.tsx | 117 -- apps/account/src/hooks/use-mobile.tsx | 21 - apps/account/src/hooks/use-toast.ts | 193 --- .../account/src/hooks/useOAuthApplications.ts | 162 -- .../account/src/hooks/useObjectStackClient.ts | 21 - .../src/hooks/useOrganizationMembers.ts | 257 ---- apps/account/src/hooks/useSession.ts | 470 ------ apps/account/src/i18n/index.ts | 37 - apps/account/src/i18n/locales/en.json | 624 -------- apps/account/src/i18n/locales/zh-CN.json | 624 -------- apps/account/src/index.css | 222 --- apps/account/src/lib/config.ts | 21 - apps/account/src/lib/user-agent.ts | 80 - apps/account/src/lib/utils.ts | 8 - apps/account/src/main.tsx | 68 - apps/account/src/routeTree.gen.ts | 671 --------- apps/account/src/router.ts | 40 - apps/account/src/routes/__root.tsx | 163 -- .../accept-invitation.$invitationId.tsx | 102 -- apps/account/src/routes/account.index.tsx | 283 ---- .../src/routes/account.invitations.tsx | 134 -- .../src/routes/account.linked-accounts.tsx | 196 --- .../account.oauth-applications.$clientId.tsx | 220 --- .../account.oauth-applications.index.tsx | 184 --- .../routes/account.oauth-applications.new.tsx | 272 ---- apps/account/src/routes/account.profile.tsx | 170 --- apps/account/src/routes/account.security.tsx | 297 ---- apps/account/src/routes/account.sessions.tsx | 239 --- apps/account/src/routes/account.tsx | 27 - .../account/src/routes/account.two-factor.tsx | 379 ----- apps/account/src/routes/auth.device.tsx | 186 --- apps/account/src/routes/forgot-password.tsx | 107 -- apps/account/src/routes/index.tsx | 11 - apps/account/src/routes/login.tsx | 328 ---- apps/account/src/routes/oauth.consent.tsx | 182 --- .../routes/organizations.$orgId.general.tsx | 336 ----- .../src/routes/organizations.$orgId.index.tsx | 14 - .../routes/organizations.$orgId.members.tsx | 498 ------ .../src/routes/organizations.$orgId.teams.tsx | 370 ----- .../src/routes/organizations.$orgId.tsx | 39 - .../src/routes/organizations.index.tsx | 154 -- apps/account/src/routes/organizations.new.tsx | 144 -- apps/account/src/routes/register.tsx | 309 ---- apps/account/src/routes/reset-password.tsx | 118 -- apps/account/src/routes/setup.tsx | 200 --- .../src/routes/verify-email-prompt.tsx | 188 --- apps/account/src/routes/verify-email.tsx | 96 -- apps/account/src/vite-env.d.ts | 1 - apps/account/tsconfig.json | 30 - apps/account/tsconfig.node.json | 10 - apps/account/vite.config.ts | 69 - packages/apps/account/package.json | 52 + packages/apps/account/src/index.ts | 66 + packages/apps/account/tsconfig.json | 11 + packages/apps/account/tsup.config.ts | 15 + pnpm-lock.yaml | 1330 +---------------- 99 files changed, 231 insertions(+), 15078 deletions(-) create mode 100644 .changeset/adr-0048-split-account-app.md delete mode 100644 apps/account/CHANGELOG.md delete mode 100644 apps/account/index.html delete mode 100644 apps/account/mocks/node-polyfills.ts delete mode 100644 apps/account/package.json delete mode 100644 apps/account/postcss.config.js delete mode 100644 apps/account/src/App.tsx delete mode 100644 apps/account/src/components/account-sidebar.tsx delete mode 100644 apps/account/src/components/auth/auth-shell.tsx delete mode 100644 apps/account/src/components/auth/legal-links.tsx delete mode 100644 apps/account/src/components/auth/social-sign-in-buttons.tsx delete mode 100644 apps/account/src/components/auth/transition-overlay.tsx delete mode 100644 apps/account/src/components/locale-toggle.tsx delete mode 100644 apps/account/src/components/organization-switcher.tsx delete mode 100644 apps/account/src/components/page-header.tsx delete mode 100644 apps/account/src/components/theme-toggle.tsx delete mode 100644 apps/account/src/components/top-bar.tsx delete mode 100644 apps/account/src/components/ui/avatar.tsx delete mode 100644 apps/account/src/components/ui/badge.tsx delete mode 100644 apps/account/src/components/ui/breadcrumb.tsx delete mode 100644 apps/account/src/components/ui/button.tsx delete mode 100644 apps/account/src/components/ui/card.tsx delete mode 100644 apps/account/src/components/ui/checkbox.tsx delete mode 100644 apps/account/src/components/ui/collapsible.tsx delete mode 100644 apps/account/src/components/ui/dialog.tsx delete mode 100644 apps/account/src/components/ui/dropdown-menu.tsx delete mode 100644 apps/account/src/components/ui/input.tsx delete mode 100644 apps/account/src/components/ui/label.tsx delete mode 100644 apps/account/src/components/ui/resizable.tsx delete mode 100644 apps/account/src/components/ui/scroll-area.tsx delete mode 100644 apps/account/src/components/ui/select-native.tsx delete mode 100644 apps/account/src/components/ui/select.tsx delete mode 100644 apps/account/src/components/ui/separator.tsx delete mode 100644 apps/account/src/components/ui/sheet.tsx delete mode 100644 apps/account/src/components/ui/sidebar.tsx delete mode 100644 apps/account/src/components/ui/skeleton.tsx delete mode 100644 apps/account/src/components/ui/switch.tsx delete mode 100644 apps/account/src/components/ui/table.tsx delete mode 100644 apps/account/src/components/ui/tabs.tsx delete mode 100644 apps/account/src/components/ui/textarea.tsx delete mode 100644 apps/account/src/components/ui/toast.tsx delete mode 100644 apps/account/src/components/ui/toaster.tsx delete mode 100644 apps/account/src/components/ui/tooltip.tsx delete mode 100644 apps/account/src/components/user-menu.tsx delete mode 100644 apps/account/src/hooks/use-mobile.tsx delete mode 100644 apps/account/src/hooks/use-toast.ts delete mode 100644 apps/account/src/hooks/useOAuthApplications.ts delete mode 100644 apps/account/src/hooks/useObjectStackClient.ts delete mode 100644 apps/account/src/hooks/useOrganizationMembers.ts delete mode 100644 apps/account/src/hooks/useSession.ts delete mode 100644 apps/account/src/i18n/index.ts delete mode 100644 apps/account/src/i18n/locales/en.json delete mode 100644 apps/account/src/i18n/locales/zh-CN.json delete mode 100644 apps/account/src/index.css delete mode 100644 apps/account/src/lib/config.ts delete mode 100644 apps/account/src/lib/user-agent.ts delete mode 100644 apps/account/src/lib/utils.ts delete mode 100644 apps/account/src/main.tsx delete mode 100644 apps/account/src/routeTree.gen.ts delete mode 100644 apps/account/src/router.ts delete mode 100644 apps/account/src/routes/__root.tsx delete mode 100644 apps/account/src/routes/accept-invitation.$invitationId.tsx delete mode 100644 apps/account/src/routes/account.index.tsx delete mode 100644 apps/account/src/routes/account.invitations.tsx delete mode 100644 apps/account/src/routes/account.linked-accounts.tsx delete mode 100644 apps/account/src/routes/account.oauth-applications.$clientId.tsx delete mode 100644 apps/account/src/routes/account.oauth-applications.index.tsx delete mode 100644 apps/account/src/routes/account.oauth-applications.new.tsx delete mode 100644 apps/account/src/routes/account.profile.tsx delete mode 100644 apps/account/src/routes/account.security.tsx delete mode 100644 apps/account/src/routes/account.sessions.tsx delete mode 100644 apps/account/src/routes/account.tsx delete mode 100644 apps/account/src/routes/account.two-factor.tsx delete mode 100644 apps/account/src/routes/auth.device.tsx delete mode 100644 apps/account/src/routes/forgot-password.tsx delete mode 100644 apps/account/src/routes/index.tsx delete mode 100644 apps/account/src/routes/login.tsx delete mode 100644 apps/account/src/routes/oauth.consent.tsx delete mode 100644 apps/account/src/routes/organizations.$orgId.general.tsx delete mode 100644 apps/account/src/routes/organizations.$orgId.index.tsx delete mode 100644 apps/account/src/routes/organizations.$orgId.members.tsx delete mode 100644 apps/account/src/routes/organizations.$orgId.teams.tsx delete mode 100644 apps/account/src/routes/organizations.$orgId.tsx delete mode 100644 apps/account/src/routes/organizations.index.tsx delete mode 100644 apps/account/src/routes/organizations.new.tsx delete mode 100644 apps/account/src/routes/register.tsx delete mode 100644 apps/account/src/routes/reset-password.tsx delete mode 100644 apps/account/src/routes/setup.tsx delete mode 100644 apps/account/src/routes/verify-email-prompt.tsx delete mode 100644 apps/account/src/routes/verify-email.tsx delete mode 100644 apps/account/src/vite-env.d.ts delete mode 100644 apps/account/tsconfig.json delete mode 100644 apps/account/tsconfig.node.json delete mode 100644 apps/account/vite.config.ts create mode 100644 packages/apps/account/package.json create mode 100644 packages/apps/account/src/index.ts create mode 100644 packages/apps/account/tsconfig.json create mode 100644 packages/apps/account/tsup.config.ts 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/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) => ( -