diff --git a/.dockerignore b/.dockerignore index 33be4b6ee1..492480e3a0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -27,7 +27,6 @@ node_modules/ **/node_modules/ frontend/node_modules/ frontend/editor/dist/ -frontend/dist-portal/ frontend/editor/playwright-report/ .npm/ .yarn/ diff --git a/.gitleaksignore b/.gitleaksignore index 783a098f68..089de4fedb 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -15,10 +15,10 @@ testing/compose/validate-mcp-test.sh:curl-auth-header:92 testing/compose/validate-mcp-test.sh:curl-auth-header:116 # Storybook example showing curl with a fake Bearer token placeholder (sk_live_a3f8...). -frontend/shared/components/CodeBlock.stories.tsx:curl-auth-header:4 +frontend/editor/src/proprietary/ui/CodeBlock.stories.tsx:curl-auth-header:5 # Truncated placeholder API key in portal docs example (sk_live_8f2c...e10) - not a real secret. -frontend/portal/src/components/docs/GettingStartedSection.tsx:generic-api-key:31 +frontend/editor/src/portal/components/docs/GettingStartedSection.tsx:generic-api-key:30 # False positive: generic-api-key matches the Java type name "X509Certificate" # in a method signature (CreateSignatureBase.resolveSignatureAlgorithm) - not a secret. diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml index 3ad0486759..52277b5b04 100644 --- a/.taskfiles/frontend.yml +++ b/.taskfiles/frontend.yml @@ -128,37 +128,6 @@ tasks: - task: dev:_run vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } - dev:portal: - desc: "Start developer portal dev server" - ignore_error: true - deps: [install] - vars: - PORT: '{{.PORT | default "5173"}}' - BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}' - EDITOR_URL: '{{.EDITOR_URL | default ""}}' - OPEN: '{{.OPEN | default ""}}' - SUBPATH: '{{.SUBPATH | default ""}}' - MOCKS: '{{.MOCKS | default ""}}' - env: - BACKEND_URL: '{{.BACKEND_URL}}' - cmds: - - '{{if .SUBPATH}}RUN_SUBPATH={{.SUBPATH}} {{end}}{{if .MOCKS}}VITE_PORTAL_MOCKS={{.MOCKS}} {{end}}{{if .EDITOR_URL}}VITE_EDITOR_URL={{.EDITOR_URL}} {{end}}npx vite portal --port {{.PORT}}{{if .OPEN}} --open{{end}}' - - dev:portal:proxy:serve: - internal: true - vars: - PORT: '{{.PORT | default "3000"}}' - BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}' - EDITOR_DEV_URL: '{{.EDITOR_DEV_URL | default ""}}' - PORTAL_DEV_URL: '{{.PORTAL_DEV_URL | default ""}}' - env: - PORT: '{{.PORT}}' - BACKEND_URL: '{{.BACKEND_URL}}' - EDITOR_DEV_URL: '{{.EDITOR_DEV_URL}}' - PORTAL_DEV_URL: '{{.PORTAL_DEV_URL}}' - cmds: - - npx tsx scripts/dev-origin-proxy.ts - # ============================================================ # Build # ============================================================ @@ -205,29 +174,6 @@ tasks: cmds: - npx vite build editor --mode prototypes - build:portal: - desc: "Build developer portal" - deps: [install] - vars: - SUBPATH: '{{.SUBPATH | default ""}}' - cmds: - - '{{if .SUBPATH}}RUN_SUBPATH={{.SUBPATH}} {{end}}npx vite build portal' - - preview:portal:proxy: - desc: "Build + serve editor + portal behind one origin (prod-like auth testing)" - deps: [prepare] - vars: - PORT: '{{.PORT | default "3000"}}' - BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}' - env: - PORT: '{{.PORT}}' - BACKEND_URL: '{{.BACKEND_URL}}' - cmds: - - task: build:proprietary - vars: { PREVIEW: '1' } - - task: build:portal - vars: { SUBPATH: portal } - - npx tsx scripts/dev-origin-proxy.ts storybook: desc: "Start Storybook dev server" @@ -263,8 +209,8 @@ tasks: deps: [install] cmds: # Globs so dpdm walks the whole tree. dpdm expands the braces itself, so this is - # shell-agnostic. Covers editor, portal, and the shared design system. - - npx dpdm "editor/src/**/*.{ts,tsx}" "portal/src/**/*.{ts,tsx}" "shared/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1 + # shell-agnostic. Covers the whole editor tree, including the portal layer. + - npx dpdm "editor/src/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1 lint:fix: desc: "Auto-fix lint issues" @@ -345,8 +291,6 @@ tasks: desc: "Typecheck scripts" deps: [prepare] cmds: - - task: typecheck:_run - vars: { PROJECT: scripts/tsconfig.json } - task: typecheck:_run vars: { PROJECT: editor/scripts/tsconfig.json } @@ -362,14 +306,7 @@ tasks: deps: [install] cmds: - task: typecheck:_run - vars: { PROJECT: portal/tsconfig.json } - - typecheck:shared: - desc: "Typecheck the shared design system" - deps: [install] - cmds: - - task: typecheck:_run - vars: { PROJECT: shared/tsconfig.json } + vars: { PROJECT: editor/src/portal/tsconfig.json } typecheck:all: desc: "Typecheck all build variants" @@ -382,7 +319,6 @@ tasks: - task: typecheck:scripts - task: typecheck:prototypes - task: typecheck:portal - - task: typecheck:shared # ============================================================ # Quality Gate @@ -411,7 +347,6 @@ tasks: - task: lint - task: format:check - task: build - - task: build:portal - task: test - task: storybook:build @@ -423,7 +358,6 @@ tasks: desc: "Run tests" cmds: - task: test:editor - - task: test:portal test:editor: desc: "Run editor tests" @@ -431,12 +365,6 @@ tasks: cmds: - npx vitest run --root editor - test:portal: - desc: "Run portal tests" - deps: [prepare] - cmds: - - npx vitest run --root portal - test:watch: desc: "Run tests in watch mode" deps: [prepare] @@ -481,7 +409,7 @@ tasks: clean: desc: "Clean build artifacts and caches" cmds: - - cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist, dist-portal + - cmd: powershell rm -Recurse -Force -ErrorAction SilentlyContinue node_modules/.vite, editor/dist, dist platforms: [windows] - - cmd: rm -rf node_modules/.vite editor/dist dist dist-portal + - cmd: rm -rf node_modules/.vite editor/dist dist platforms: [linux, darwin] diff --git a/AGENTS.md b/AGENTS.md index 44647b0063..9afdad5937 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,7 +139,8 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie #### Environment Variables - All `VITE_*` variables must be declared in the appropriate committed env file: - - `frontend/editor/.env` — core, proprietary, and shared vars + - `frontend/editor/.env` — core and shared vars (base, loaded in every mode) + - `frontend/editor/.env.proprietary` — proprietary-only vars, e.g. the admin portal's SaaS/account-link keys (layered on top of `.env` in proprietary mode) - `frontend/editor/.env.saas` — SaaS-only vars (layered on top of `.env` in SaaS mode) - `frontend/editor/.env.desktop` — desktop (Tauri)-only vars (layered on top of `.env` in desktop mode) - These files are committed to Git and must not contain private keys diff --git a/LICENSE b/LICENSE index 2b2f7fc085..971c9d0b16 100644 --- a/LICENSE +++ b/LICENSE @@ -20,8 +20,8 @@ if that directory exists, is licensed under the license defined in "frontend/edi if that directory exists, is licensed under the license defined in "frontend/editor/src/cloud/LICENSE". * All content that resides under the "frontend/editor/src/prototypes/" directory of this repository, if that directory exists, is licensed under the license defined in "frontend/editor/src/prototypes/LICENSE". -* All content that resides under the "frontend/portal/" directory of this repository, -if that directory exists, is licensed under the license defined in "frontend/portal/LICENSE". +* All content that resides under the "frontend/editor/src/portal/" directory of this repository, +if that directory exists, is licensed under the license defined in "frontend/editor/src/portal/LICENSE". * Content outside of the above mentioned directories or restrictions above is available under the MIT License as defined below. diff --git a/Taskfile.yml b/Taskfile.yml index 34c7c8bc28..26895723d0 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -79,86 +79,12 @@ tasks: OPEN: "true" dev:portal: - desc: "Start backend + developer portal concurrently on free ports" + desc: "Start backend + editor; the portal is an admin route at /portal" vars: PORTS: sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}' BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}' - PORTAL_PORT: '{{index (splitList "\n" .PORTS) 1}}' - deps: - - task: backend:dev - vars: - PORT: '{{.BACKEND_PORT}}' - SECURITY_ENABLELOGIN: "true" - POLICIES_ENABLED: "true" - - task: frontend:dev:portal - vars: - PORT: '{{.PORTAL_PORT}}' - BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - MOCKS: 'false' - OPEN: "true" - - dev:portal:all: - desc: "Start backend + developer portal + editor concurrently on free ports" - vars: - PORTS: - sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5174{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5174{{end}}' - BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}' - PORTAL_PORT: '{{index (splitList "\n" .PORTS) 1}}' - EDITOR_PORT: '{{index (splitList "\n" .PORTS) 2}}' - deps: - - task: backend:dev - vars: - PORT: '{{.BACKEND_PORT}}' - SECURITY_ENABLELOGIN: "true" - POLICIES_ENABLED: "true" - - task: frontend:dev:portal - vars: - PORT: '{{.PORTAL_PORT}}' - BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - # Point the portal's "Editor" app switcher at the editor we spawn here. - EDITOR_URL: 'http://localhost:{{.EDITOR_PORT}}/' - MOCKS: 'false' - OPEN: "true" - - task: frontend:dev - vars: - PORT: '{{.EDITOR_PORT}}' - BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - - dev:portal:all:saas: - desc: "Start SaaS backend + developer portal + editor concurrently on free ports" - vars: - PORTS: - sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5174{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5174{{end}}' - BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}' - PORTAL_PORT: '{{index (splitList "\n" .PORTS) 1}}' - EDITOR_PORT: '{{index (splitList "\n" .PORTS) 2}}' - deps: - - task: backend:dev:saas - vars: - PORT: '{{.BACKEND_PORT}}' - POLICIES_ENABLED: "true" - - task: frontend:dev:portal - vars: - PORT: '{{.PORTAL_PORT}}' - BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - EDITOR_URL: 'http://localhost:{{.EDITOR_PORT}}/' - MOCKS: 'false' - OPEN: "true" - - task: frontend:dev - vars: - PORT: '{{.EDITOR_PORT}}' - BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - - dev:portal:proxy: - desc: "Editor + portal on ONE origin + backend via live dev servers (shared-token login)" - vars: - PORTS: - sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 3000 5173 5174{{else}}{{.FIND_FREE_PORT_SH}} 8080 3000 5173 5174{{end}}' - BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}' - PROXY_PORT: '{{index (splitList "\n" .PORTS) 1}}' - EDITOR_PORT: '{{index (splitList "\n" .PORTS) 2}}' - PORTAL_PORT: '{{index (splitList "\n" .PORTS) 3}}' + EDITOR_PORT: '{{index (splitList "\n" .PORTS) 1}}' deps: - task: backend:dev vars: @@ -169,18 +95,7 @@ tasks: vars: PORT: '{{.EDITOR_PORT}}' BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - - task: frontend:dev:portal - vars: - PORT: '{{.PORTAL_PORT}}' - BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - SUBPATH: portal - MOCKS: 'false' - - task: frontend:dev:portal:proxy:serve - vars: - PORT: '{{.PROXY_PORT}}' - BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - EDITOR_DEV_URL: 'http://localhost:{{.EDITOR_PORT}}' - PORTAL_DEV_URL: 'http://localhost:{{.PORTAL_PORT}}' + OPEN: "true" dev:saas: desc: "Start SaaS backend + frontend concurrently on free ports" @@ -228,24 +143,6 @@ tasks: - task: backend:build - task: frontend:build - preview:portal:proxy: - desc: "Build + serve editor + portal on ONE origin + backend (prod-like auth test)" - vars: - PORTS: - sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 3000{{else}}{{.FIND_FREE_PORT_SH}} 8080 3000{{end}}' - BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}' - PROXY_PORT: '{{index (splitList "\n" .PORTS) 1}}' - deps: - - task: backend:dev - vars: - PORT: '{{.BACKEND_PORT}}' - SECURITY_ENABLELOGIN: "true" - POLICIES_ENABLED: "true" - - task: frontend:preview:portal:proxy - vars: - PORT: '{{.PROXY_PORT}}' - BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' - # ============================================================ # Test # ============================================================ diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/access/model/ResourceType.java b/app/proprietary/src/main/java/stirling/software/proprietary/access/model/ResourceType.java index 9d0108db0f..4b32d6ae6a 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/access/model/ResourceType.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/access/model/ResourceType.java @@ -2,7 +2,8 @@ package stirling.software.proprietary.access.model; /** Types of resources whose access can be gated by {@link ResourceGrant}. */ public enum ResourceType { - // The admin portal / processor (frontend/portal). Singleton resource (empty resourceId). + // The admin portal / processor (frontend/editor/src/portal). Singleton resource (empty + // resourceId). PORTAL, // A stored S3/MCP/API integration configuration. INTEGRATION_CONFIG diff --git a/frontend/.gitignore b/frontend/.gitignore index 3a5de11c57..e07dce196a 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -11,7 +11,6 @@ # production /build /dist -/dist-portal /storybook-static /editor/build @@ -27,8 +26,8 @@ # Root .gitignore ignores all .env* - whitelist only our committed ones, anchored # to their app so a stray top-level frontend/.env stays ignored (Storybook's SaaS # mock env is injected via .storybook/main.ts, not a file). -!/portal/.env !/editor/.env +!/editor/.env.proprietary !/editor/.env.desktop !/editor/.env.saas diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 63d7130f47..25caf7cd10 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -1,5 +1,4 @@ dist/ -dist-portal/ editor/dist/ # Tauri/Cargo build output (binary assets named *.js etc. confuse Prettier). # Match nested target/ dirs too - provisioner/ and thumbnail-handler/ each @@ -9,7 +8,7 @@ editor/src-tauri/gen/ node_modules/ editor/public/vendor/ # Auto-generated by MSW (`msw init`); regenerated verbatim, not hand-formatted. -portal/public/mockServiceWorker.js +editor/public/mockServiceWorker.js # Auto-generated OG/social-preview metadata (scripts/generate-og-metadata.mjs); regenerated verbatim. editor/public/og-metadata.json editor/src/core/data/ogImageMap.json diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index c016a5c37d..4d4aadc8f8 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -6,16 +6,13 @@ import tsconfigPaths from "vite-tsconfig-paths"; * Storybook 9 ships essentials, interactions, and docs as built-ins, so the * addon list is just the extras we want: theme switching + a11y auditing. * - * Story files live next to their components in shared/, portal/src/, and - * editor/src/ — the design system is shared by BOTH apps, so both surface - * their stories here. MDX docs pages live in portal/src/docs/. + * Story files live next to their components under editor/src/ (which includes + * the portal layer at editor/src/portal/). MDX docs pages live in + * editor/src/portal/docs/. */ const config: StorybookConfig = { stories: [ - "../portal/src/**/*.mdx", - "../portal/src/**/*.stories.@(ts|tsx)", - "../shared/**/*.mdx", - "../shared/**/*.stories.@(ts|tsx)", + "../editor/src/portal/**/*.mdx", "../editor/src/**/*.stories.@(ts|tsx)", ], addons: ["@storybook/addon-themes", "@storybook/addon-a11y"], @@ -26,17 +23,21 @@ const config: StorybookConfig = { typescript: { reactDocgen: "react-docgen-typescript", }, - // Serve the MSW worker file from portal/public so Storybook can intercept - // network calls the same way the dev portal does. - staticDirs: ["../portal/public"], + // Serve the MSW worker file from the portal's public dir so Storybook can + // intercept network calls the same way the dev portal does. + staticDirs: ["../editor/public"], viteFinal: async (config) => { - // Wire @portal/* and @shared/* aliases directly on the Storybook bundler so - // portal story imports resolve without needing the portal's vite config. + // Wire the @portal/* alias directly on the Storybook bundler so portal + // story imports resolve without needing the portal's vite config. config.resolve = config.resolve ?? {}; config.resolve.alias = { ...(config.resolve.alias ?? {}), - "@portal": resolve(__dirname, "../portal/src"), - "@shared": resolve(__dirname, "../shared"), + "@portal": resolve(__dirname, "../editor/src/portal"), + // Direct layer aliases so .storybook config files (preview.tsx), which sit + // outside src/ and so aren't covered by tsconfigPaths, can import layer + // modules (e.g. the auth supabase client that moved into proprietary). + "@proprietary": resolve(__dirname, "../editor/src/proprietary"), + "@core": resolve(__dirname, "../editor/src/core"), }; // Editor stories import via @app/* (proprietary→core fallback), @core/* and // @proprietary/*. Resolve them exactly the way the editor's own build does — diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index 1f9c935fe6..d086613f44 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -19,11 +19,11 @@ import { ThemeProvider } from "@portal/contexts/ThemeContext"; import { UIProvider } from "@portal/contexts/UIContext"; import { mantineTheme } from "@portal/theme/mantineTheme"; import { handlers } from "@portal/mocks/handlers"; -import { configureSupabase } from "@shared/auth/supabase/supabaseClient"; +import { configureSupabase } from "@proprietary/auth/supabase/supabaseClient"; import "@mantine/core/styles.css"; -import "@shared/tokens/tokens.css"; -import "@shared/tokens/base.css"; +import "@core/tokens/tokens.css"; +import "@core/tokens/base.css"; // Start MSW once. Storybook runs in a browser so this uses the service worker. initialize({ onUnhandledRequest: "bypass" }, handlers); diff --git a/frontend/editor/.env.proprietary b/frontend/editor/.env.proprietary new file mode 100644 index 0000000000..ed5498ba3a --- /dev/null +++ b/frontend/editor/.env.proprietary @@ -0,0 +1,32 @@ +############################################################################### +# Proprietary-build environment variables, layered on top of `.env` when Vite +# runs in `--mode proprietary` (so the core/OSS build never sees them). These +# back the admin portal mounted at /portal. Committed to Git, so no private keys; +# machine-specific overrides go in the uncommitted .env.proprietary.local / +# .env.local. (VITE_STRIPE_PUBLISHABLE_KEY lives in the base `.env` as it is +# shared across builds.) +############################################################################### + +# Where the portal's "Editor" switcher / non-admin redirect points. "/" is +# correct: the editor serves the portal at /portal on the same origin. In dev, +# override in .env.local with your running editor's URL (e.g. +# VITE_EDITOR_URL=http://localhost:5173/). +VITE_EDITOR_URL=/ + +# Force the portal's MSW mocks on ("true") or off ("false"). Empty = default +# (on in dev, off in production builds). Set "false" to run against the real +# backend. +VITE_PORTAL_MOCKS= + +# Hosted SaaS Supabase project for the self-hosted portal's IN-APP account +# linking (both values are public). Set per deploy; absent -> the account-link +# UI shows a "configure" state. For local e2e, point these at the SaaS Supabase +# project the local backend links against. +VITE_SAAS_SUPABASE_URL= +VITE_SAAS_SUPABASE_ANON_KEY= + +# Hosted SaaS Java backend base URL (e.g. https://api.stirlingpdf.com). Used for +# ATTENDED portal -> SaaS reads (wallet, billing, plans, checkout) with the +# admin's Supabase JWT. Distinct from the local backend (reached same-origin via +# the editor's vite proxy). Absent -> wallet/billing surfaces stay on the mock. +VITE_SAAS_API_URL= diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 7f81e56f3b..d07d8379da 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -6048,6 +6048,1857 @@ stepOf = "Step {{step}} of {{total}}" toolChainDesc = "Configure the tools this policy runs on each document." typesSelected = "{{count}} types selected" +[portal.accountLink.card] +billingNote = "Unattended processing bills against your org wallet." +eyebrow = "Account link" +linkButton = "Link your Stirling account" +linked = "Linked" +linkedAs = "Linked as {{name}}." +linkedGeneric = "This instance is linked." +notLinked = "Not linked" +title = "Link this org to its Stirling account" +unlink = "Unlink" + +[portal.accountLink.card.error] +title = "Couldn't link" + +[portal.accountLink.card.loginNotConfigured] +after = "to enable account linking against the hosted Stirling account. In dev you can simulate sign-in from the link dialog." +before = "Set" +title = "SaaS login not configured" + +[portal.accountLink.gate] +action = "Link account" +description = "Link this org's Stirling account to use billable features." +title = "Link to unlock" +titleFeature = "Link to unlock {{feature}}" + +[portal.accountLink.instances] +active = "Active" +revoke = "Revoke" +revoked = "Revoked" +unnamed = "Unnamed instance" + +[portal.accountLink.instances.columns] +instance = "Instance" +lastSeen = "Last seen" +linked = "Linked" +status = "Status" + +[portal.accountLink.instances.empty] +description = "Link this org's account, then register your self-hosted instances to see them here." +title = "No linked instances" + +[portal.accountLink.instances.time] +daysAgo_one = "{{count}}d ago" +daysAgo_other = "{{count}}d ago" +hoursAgo_one = "{{count}}h ago" +hoursAgo_other = "{{count}}h ago" +justNow = "just now" +minutesAgo_one = "{{count}}m ago" +minutesAgo_other = "{{count}}m ago" +never = "never" + +[portal.accountLink.modal] +linkSubtitle = "Sign in to the account this server should bill against." +linkTitle = "Link your Stirling account" +reauthSubtitle = "Your session expired — sign back in to your Stirling account. Your instance stays linked." +reauthTitle = "Sign in again" +simulateSignIn = "Simulate sign-in (dev)" + +[portal.accountLink.modal.loginNotConfigured] +after = "to enable in-app linking against the hosted Stirling account." +and = "and" +before = "Set" +title = "SaaS login not configured" + +[portal.accountLink.panel] +instancesSub = "Every self-hosted instance registered to this org. Revoke a credential to immediately cut off its unattended access." +instancesTitle = "Linked instances" +revokeError = "Couldn't revoke instance" +sub = "Link this self-hosted org to its Stirling account so unattended processing bills against your org wallet." + +[portal.accountLink.panel.loadError] +forbidden = "Only the team owner can view the org's linked instances." +generic = "Couldn't load the team's linked instances. Try again in a moment." +title = "Couldn't load linked instances" + +[portal.accountLink.state] +free = "Editor plan" +subscribed = "Processor plan" +unlinked = "Not linked" + +[portal.agentBuilder] +bootstrapFromDocument = "Bootstrap from document" +sectionsAriaLabel = "Agent builder sections" +selectorAriaLabel = "Agents" +subtitle = "Design, test and ship the AI agents that classify, extract from and route your documents. Define scenarios, fence tool access, run a golden set, and publish a version." +title = "Agent Builder" + +[portal.agentBuilder.bootstrap] +cancel = "Cancel" +dropzoneText = "Choose a sample document (PDF or image)" +lead = "Drop a sample document and we'll propose scenarios and an extraction schema you can refine. Nothing is published until you review it." +submit = "Bootstrap agent" +subtitle = "Seed a new agent from one representative file" +title = "Bootstrap from a document" + +[portal.agentBuilder.empty] +description = "Bootstrap an agent from a sample document to seed its scenarios and extraction schema, then refine and publish." +title = "No agents yet" + +[portal.agentBuilder.evals] +casesPassing = "Cases passing" +columnCase = "Eval case" +columnLatency = "Latency" +columnResult = "Result" +fail = "fail" +goldenSetPassRate = "Golden-set pass rate" +latencyMs = "{{ms}} ms" +notRun = "not run" +pass = "pass" +passRate = "Pass rate" +runEvals = "Run evals" + +[portal.agentBuilder.evals.empty] +description = "Evals turn your scenarios into a repeatable golden set. Upgrade to capture pass-rate over time and gate publishes on it." +title = "No golden set yet" + +[portal.agentBuilder.kpi] +acrossGoldenSets = "across golden sets" +activeAgents = "Active agents" +avgPassRate = "Avg eval pass rate" +fleetWide = "fleet-wide" +latestPublished = "Latest published" +scenarios = "Scenarios" +testCases = "test cases" +totalDescription_one = "{{count}} total" +totalDescription_other = "{{count}} total" + +[portal.agentBuilder.scenarios] +add = "Add" +addScenario = "Add scenario" +enable = "Enable" +expectationLabel = "Expected behaviour" +expectationPlaceholder = "What the agent should do" +inEval = "in eval" +mute = "Mute" +muted = "muted" +nameLabel = "Name" +namePlaceholder = "e.g. Compliance escalation" + +[portal.agentBuilder.tabs] +evals = "Evals" +scenarios = "Scenarios" +tools = "Tools" +versions = "Versions" + +[portal.agentBuilder.tools] +broadAccess = "Broad access" +deniedHint = "Selected tools are blocked. Everything else stays callable." +deniedTools = "Denied tools" +governanceGate = "Tool governance is available on the Enterprise plan." +restricted = "Restricted" +restrictedAccess = "Restricted tool access" +restrictedDescription = "Allow every tool except the ones you deny below." + +[portal.agentBuilder.versions] +current = "current" +historyGate = "Full version history and rollback are available on the Enterprise plan." +publish = "Publish" +rollBack = "Roll back" + +[portal.assistant] +close = "Close assistant" +error = "Couldn't reach the assistant." +errorWithDetail = "Couldn't reach the assistant: {{detail}}" +inputAriaLabel = "Ask the assistant" +inputPlaceholder = "Ask about Stirling…" +open = "Open assistant" +send = "Send" +title = "Assistant" +tryAsking = "Try asking" +typing = "Typing" + +[portal.auth] +loading = "Loading" +redirectingToEditor = "Redirecting to the editor..." + +[portal.billing.checkout] +noClientSecret = "Edge function returned no client_secret." +subtitle = "Add a card to keep going past your free Editor-plan grant. Stripe handles the rest." +title = "Turn on the Processor plan" + +[portal.billing.checkout.error] +title = "Couldn't start checkout" + +[portal.billing.checkout.notConfigured] +bodyAfter = "in the portal env to enable in-app checkout." +bodyBefore = "Set" +title = "Stripe not configured" + +[portal.billing.enterpriseUpsell] +cta = "Build your Enterprise quote" +description = "Committed volume discounts, air-gapped deployment, custom MSA and security reviews, and 3rd-party distributor partnerships." +eyebrow = "Volume discount · 1M+ PDFs" +title = "Stirling Enterprise" + +[portal.billing.freeEditors] +activeThisMonth = "Active this month" +cost = "Cost" +editorsDeployed = "Editors deployed" +inviteTeammates = "Invite teammates" +pdfsEdited = "PDFs edited" +previewBadge = "Preview · sample data" +subtitle = "Deploy anywhere, for your whole team." +title = "Free PDF Editors" + +[portal.billing.freePlan] +checkoutErrorTitle = "Couldn't start checkout" +currentPlan = "Current plan" +freeForever = "Free forever" +noTeamResolved = "No team is resolved on your wallet yet — refresh and try again." +ownerOnly = "Only the team owner can switch on the Processor plan." +planName = "Editor" +ssoIncluded = "SSO included" +switchOnProcessor = "Switch on the Processor →" +unlimitedUsers = "Unlimited users" + +[portal.billing.invoices] +columnAmount = "Amount" +columnDate = "Date" +columnDescription = "Description" +columnPdfsProcessed = "PDFs processed" +columnStatus = "Status" +descriptionFallback = "Invoice" +downloadAriaLabel = "Download invoice {{number}} as PDF" +emptyDescription = "Once your team subscribes and the first cycle closes, your invoices appear here." +emptyTitle = "No invoices yet" +fetchLimitNote_one = "Showing your {{count}} most recent invoices. Older invoices are in the Stripe portal." +fetchLimitNote_other = "Showing your {{count}} most recent invoices. Older invoices are in the Stripe portal." +loadError = "Couldn't load invoices: {{error}}" +pdfLink = "PDF ↓" +showAll_one = "Show all {{count}}" +showAll_other = "Show all {{count}}" +showFewer_one = "Show fewer (top {{count}})" +showFewer_other = "Show fewer (top {{count}})" +showMostRecent_one = "Show {{count}} most recent" +showMostRecent_other = "Show {{count}} most recent" +title = "Invoice history" +viewAriaLabel = "View invoice {{number}} in Stripe" +viewLink = "View ↗" + +[portal.billing.linkPrompt] +cta = "Link Stirling account" +description = "Manual PDF editing — view, sign, merge, split, watermark, compress, convert, manual OCR — is always free, linked or not. Link to claim 500 free PDFs of metered processing (automation, AI, and the API); when you need more, turn on the Processor plan and only pay for what you use." +title = "Link your Stirling account" + +[portal.billing.paymentMethod] +billedMonthly = "Billed monthly" +cardEnding = "{{brand}} ending {{last4}}" +cardFallback = "Card" +expiresBilledMonthly = "Expires {{expiry}} · billed monthly" +eyebrow = "Payment method" +managedSub = "Your card and billing details are kept securely in Stripe's customer portal." +managedTitle = "Managed in Stripe" +update = "Update" + +[portal.billing.pdfsProcessed] +emptyPeriod = "No metered processing yet this period." +eyebrow = "PDFs processed this period" +legendValue_one = "{{formatted}} PDFs" +legendValue_other = "{{formatted}} PDFs" +segbarAriaLabel = "Metered PDFs split by category" +segmentAgentsDesc = "AI agent actions" +segmentAgentsLabel = "Agents" +segmentApiDesc = "Direct API requests" +segmentApiLabel = "API" +segmentAutomationDesc = "Automations & pipelines" +segmentAutomationLabel = "Automation" +unit = "metered PDFs" + +[portal.billing.spendLimit] +adjustLimit = "Adjust limit" +cancel = "Cancel" +capControlNote = "Changes apply immediately — raise or lower the ceiling any time." +capSuffix = "/ month" +capSuffixWithDocs = "/ month · ≈ {{documents}} documents" +displaySub = "You're only billed for what you process automatically — never past the ceiling." +editTitle = "Set your monthly ceiling" +eyebrow = "Spend limit" +guardrailBody = "a hard ceiling — you're never billed past it. At the cap, metered processing pauses (unlimited PDF editing keeps working) until you raise it or the cycle resets. Nothing is lost." +guardrailLabel = "Your guardrail:" +noCap = "no cap" +pctUsed = "{{pct}}% used" +remaining = "{{amount}} remaining" +save = "Save limit" +saveError = "Couldn't save limit" +thisPeriodUncapped = "{{amount}} this period · uncapped" +usedThisMonth = "{{amount}} used this month" +useSuggested = "Use suggested · {{amount}} / month" + +[portal.billing.spendLimit.projection] +body_one = "At {{rate}}/day you reach the cap in ~{{count}} day (~{{monthEnd}} month-end). Suggested limit ~{{suggested}}." +body_other = "At {{rate}}/day you reach the cap in ~{{count}} days (~{{monthEnd}} month-end). Suggested limit ~{{suggested}}." +label = "Projected to exceed." + +[portal.billing.spendThisMonth] +eyebrow = "Spend this month" +processed_one = "{{formattedCount}} PDF processed." +processed_other = "{{formattedCount}} PDFs processed." +processedWithRate_one = "{{formattedCount}} PDF processed, at {{rate}} each." +processedWithRate_other = "{{formattedCount}} PDFs processed, at {{rate}} each." + +[portal.billing.subscribedPlan.capWarn] +approachingBody = "Raise it now so automated processing never pauses." +approachingTitle = "You're at {{pct}}% of your monthly spend limit" +raiseLimit = "Raise limit" +reachedBody = "Metered processing is paused until you raise the limit or the cycle resets. Unlimited PDF editing keeps working." +reachedTitle = "Monthly spend limit reached" + +[portal.billing.subscribedPlan.portalError] +title = "Couldn't open Stripe portal" + +[portal.billing.walletMeter] +capSuffix_one = "of {{allowance}} free PDFs used" +capSuffix_other = "of {{allowance}} free PDFs used" +eyebrow = "Processor trial" +statusLabel_one = "{{remaining}} left" +statusLabel_other = "{{remaining}} left" +sub = "Use the PDF Editor for free. Pay to process PDFs automatically." +title_one = "Process {{allowance}} PDFs free" +title_other = "Process {{allowance}} PDFs free" +titleWithRate_one = "Process {{allowance}} PDFs free, then {{rate}}/PDF" +titleWithRate_other = "Process {{allowance}} PDFs free, then {{rate}}/PDF" + +[portal.catalogue.card] +lockedAriaLabel = "Locked" +openAriaLabel = "Open {{name}} component" + +[portal.catalogue.detail] +addToProject = "Add to project" +ariaLabel = "Component detail" +tabsAriaLabel = "Component detail sections" +upgradeToUnlock = "Upgrade to unlock" + +[portal.catalogue.detail.code] +install = "Install" +usage = "Usage" + +[portal.catalogue.detail.locked] +description = "{{name}} is included from the {{tier}} plan. Upgrade to embed it." +title = "Not available on your plan" + +[portal.catalogue.detail.preview] +badge = "Live preview" +note = "Interactive sandbox renders here" + +[portal.catalogue.detail.pricing] +note = "Metered per {{unit}}. Usage beyond the monthly free quota is billed to your account and itemised under Usage & Billing." + +[portal.catalogue.detail.stats] +billedOn = "Billed on" +embeds30d = "Embeds (30d)" +freeQuota = "Free quota" +freeQuotaValue = "{{amount}} / mo" +maturity = "Maturity" +none = "None" +perAction = "Per action" +price = "Price" + +[portal.catalogue.detail.tabs] +code = "Code" +overview = "Overview" +pricing = "Pricing" +props = "Props / API" + +[portal.catalogue.props] +optional = "optional" +required = "required" + +[portal.catalogue.props.columns] +description = "Description" +name = "Prop" +required = "Required" +type = "Type" + +[portal.catalogue.summary] +componentsGa = "Components GA" +componentSpendMtd = "Component spend (MTD)" +embedsThisMonth = "Embeds this month" +inBeta = "In beta" + +[portal.common] +inviteMember = "Invite member" + +[portal.componentsView] +subtitle = "Embeddable SDK widgets you drop into your own app — a viewer, an e-sign flow, an AI review panel. Each is metered per action. Click a card for install, usage and props." +title = "Components" + +[portal.componentsView.empty] +description = "The component catalogue could not be loaded. Try again shortly." +title = "No components available" + +[portal.componentsView.lockedBanner] +description = "GA components are available on Pay-as-you-go; a few Beta components are enterprise-only. Locked cards show an upgrade nudge." +title = "Some components need a paid plan" + +[portal.docs.authentication] +codeCaption = "every request" +eyebrow = "GETTING STARTED" +lead = "All requests authenticate with a bearer token. Keys are scoped per environment and never expire unless rotated." +liveKey = "Production keys — billed, rate-limited per your plan." +testKey = "Sandbox keys — free, return synthetic fixtures." +title = "Authentication" + +[portal.docs.components] +codeCaption = "embed the viewer" +eyebrow = "COMPONENTS" +lead = "Embeddable UI for review queues and document inspection. Bring your own styles or use the shipped theme." +title = "Drop-in viewers" + +[portal.docs.endpoints] +eyebrow = "API REFERENCE" +fieldCount_one = "{{count}} field" +fieldCount_other = "{{count}} fields" +filterAll = "All" +filterAriaLabel = "Filter endpoints by vertical" +lead = "Every document type is a typed endpoint. POST a file, receive schema-validated JSON. Filter by vertical below." +title = "Endpoints" + +[portal.docs.errors] +codeCaption = "422 Unprocessable Entity" +eyebrow = "API REFERENCE" +lead = "Errors return a stable machine-readable code plus a human message. The 4xx body always includes a request_id for support." +title = "Errors" + +[portal.docs.nav] +ariaLabel = "Documentation" + +[portal.docs.nav.empty] +description = "The documentation index could not be loaded." +title = "Docs unavailable" + +[portal.docs.quickstart] +eyebrow = "GETTING STARTED" +lead = "Send your first document to a typed endpoint and get structured JSON back in three steps. No model training, no prompt engineering." +title = "Quickstart" + +[portal.docs.quickstart.callout] +bodyAfterLink = "for copy-paste recipes." +bodyBeforeLink = "wire the same call into a pipeline to chain validation, redaction, and delivery — or expose it to an agent over MCP. See" +label = "Next:" +link = "Playbooks" + +[portal.docs.quickstart.step1] +body = "Create a scoped key from the Infrastructure tab. Keys carry rate limits and an optional IP allowlist. Export it into your shell:" +title = "Issue an API key" + +[portal.docs.quickstart.step2] +body = "POST a file to any typed endpoint. The endpoint determines the schema you get back — here, the invoice extractor." +snippetCaption = "extract an invoice" +title = "Send a document" + +[portal.docs.quickstart.step3] +body = "Every response is validated against the endpoint schema, with a confidence score and per-field provenance." +codeCaption = "200 OK" +title = "Read the structured result" + +[portal.docs.rateLimits] +burst = "Burst" +codeCaption = "429 Too Many Requests" +concurrency = "Concurrency" +eyebrow = "GETTING STARTED" +lead = "Limits scale with your plan. A 429 response includes a Retry-After header; the SDKs back off automatically." +requestsPerMinute = "Requests / minute" +title = "Rate limits & quotas" + +[portal.docs.recipes] +cloneButton = "Clone recipe" +eyebrow = "PLAYBOOKS" +lead = "End-to-end patterns that chain sources, operations, and destinations. Each maps to a pipeline you can clone." +title = "Recipes" + +[portal.docs.sdks] +eyebrow = "SDKS" +lead = "First-party clients with typed responses, automatic retries, and streaming uploads. All track the same endpoint catalogue." +title = "Official SDKs" + +[portal.docs.sdks.status] +beta = "Beta" +deprecated = "Deprecated" + +[portal.docs.skills] +eyebrow = "SKILLS" +lead = "Bundled, named capabilities your agent invokes as a single tool. Each skill is a deterministic op chain with evals attached." +title = "Agent skills" + +[portal.docs.webhooks] +codeCaption = "document.processed" +eyebrow = "API REFERENCE" +lead = "Subscribe to document.processed, pipeline.completed, and quota.threshold events. Payloads are signed with HMAC-SHA256." +title = "Webhooks" + +[portal.docs.webhooks.callout] +afterHelper = "helper." +beforeHelper = "header against your signing secret before trusting a payload. SDKs ship a" +beforeSignature = "Verify the" + +[portal.documents] +subtitle = "Review and approve documents moving through your pipelines." +title = "Documents" + +[portal.documents.audit] +empty = "No events recorded yet." + +[portal.documents.drawer] +sectionsAriaLabel = "Document detail sections" + +[portal.documents.drawer.tabs] +audit = "Audit" +extractions = "Extractions" +overview = "Overview" + +[portal.documents.elevation] +requestAccess = "Request access" + +[portal.documents.elevation.active] +description = "Temporary grant — access is logged and time-boxed." +descriptionFourEyes = "Temporary grant — a peer reviewer was notified (four-eyes)." +title = "Access expires in {{time}}" + +[portal.documents.elevation.gated] +description = "Content is gated by zero-standing-access. Requesting starts a time-boxed grant." +descriptionFourEyes = "Content is gated by zero-standing-access. Requesting starts a time-boxed grant and notifies a peer reviewer (four-eyes)." +title = "Sensitive document" + +[portal.documents.extractions] +empty = "No fields were extracted from this document." +masked = "Extracted fields are hidden. Request timed access to view this document's content." + +[portal.documents.extractions.columns] +confidence = "Confidence" +field = "Field" +value = "Value" + +[portal.documents.filters] +all = "All" +archived = "Archived" +ariaLabel = "Filter documents by status" +needsReview = "Needs review" +processed = "Processed" + +[portal.documents.overview] +confidence = "Confidence" +fieldsExtracted = "Fields extracted" +received = "Received" +source = "Source" +status = "Status" +type = "Type" + +[portal.documents.queue.empty] +description = "As sources feed documents into your pipelines they'll appear here for review." +title = "No documents in the queue" + +[portal.documents.summary] +avgConfidence = "Avg confidence" +inQueue = "In queue" +needsReview = "Needs review" +processedToday = "Processed today" + +[portal.documents.table] +empty = "No documents match this filter." +sensitiveLabel = "Sensitive" +sensitiveTitle = "Sensitive — access required" + +[portal.documents.table.columns] +confidence = "Confidence" +fields = "Fields" +name = "Name" +source = "Source" +status = "Status" +time = "Time" +type = "Type" + +[portal.editorAdmin] +subtitle = "Deploy the Stirling PDF Editor, pair self-hosted instances to your org, and operate the running fleet — targets, health, credentials, and offline activation in one place." +title = "Editor deployment" + +[portal.editorAdmin.health.columns] +activeUsers = "Active users" +host = "Host" +lastSeen = "Last seen" +region = "Region" +status = "Status" +version = "Version" + +[portal.editorAdmin.health.empty] +description = "Deploy a target and pair it to see live instance health here." +title = "No instances reporting" + +[portal.editorAdmin.offlineActivation] +enterpriseTag = "Enterprise" +generateButton = "Generate offline bundle" +lockCopy = "Offline and on-prem activation is part of Enterprise." +subtitle = "Generate a signed activation bundle for an offline or on-prem install with no outbound network path. Transfer it to the instance and apply it during first-run setup." +talkToSales = "Talk to sales" +title = "Air-gapped activation" + +[portal.editorAdmin.offlineActivation.readyBanner] +description = "{{file}} is signed and ready to transfer. It activates one instance and expires in 14 days." +title = "Bundle ready" + +[portal.editorAdmin.pairing] +generated = "Generated ✓" +generateNewCode = "Generate new code" +lockCopy = "IaC provisioning is part of Enterprise." +rotate = "Rotate" +talkToSales = "Talk to sales" + +[portal.editorAdmin.sections.health] +sub = "Every Editor instance reporting in — version, region, status, last seen, and active users." +title = "Instance health" + +[portal.editorAdmin.sections.pairing] +sub = "Connect a self-hosted editor to this org. Generate a token, hand off a short code, or wire it through IaC." +title = "Pairing" + +[portal.editorAdmin.sections.targets] +sub = "Where the Editor runs. Copy a snippet to stand up a self-hosted instance, or use Managed Cloud with no ops." +title = "Deployment targets" + +[portal.editorAdmin.serviceToken] +currentToken = "Current token" +lastRotated = "Last rotated" +rotateButton = "Rotate service token" +subtitle = "Instances authenticate to the org with this credential. Rotate it on a schedule or immediately after a suspected leak." +title = "Service token" + +[portal.editorAdmin.serviceToken.rotatedBanner] +description = "A new token was issued. Update each self-hosted instance's STIRLING_SERVICE_TOKEN within the 24h grace window or they'll drop offline." +title = "Rotate running instances" + +[portal.editorAdmin.targets] +instanceCount_one = "{{count}} instance" +instanceCount_other = "{{count}} instances" +talkToSales = "Talk to sales" +upgradePlan = "Upgrade plan" + +[portal.editorAdmin.targets.lock] +enterprise = "On-prem and Kubernetes self-hosting are part of Enterprise." +paid = "Self-hosting with Docker and Kubernetes unlocks on a paid plan." + +[portal.editorAdmin.targets.state] +available = "Available" +locked = "Locked" +running = "Running" + +[portal.errorBoundary] +description = "This view hit an unexpected error. Try again, or pick another section from the sidebar." +retry = "Try again" +title = "Something went wrong on this page" + +[portal.forkWizard] +subtitle = "Clone a proven workflow and tune it — every template ships the same four-stage backbone." +title = "Fork a starter pipeline" + +[portal.forkWizard.action] +cancel = "Cancel" +deploy = "Deploy pipeline" +pickAnother = "Pick another" + +[portal.forkWizard.status] +building = "Building…" +ready = "Ready to deploy" + +[portal.home.chart.empty] +description = "Once documents are processed, your 30-day usage appears here." +title = "No usage yet" + +[portal.home.kpis.enterprise] +docs30d = "Docs / 30d" +evalPassRate = "Eval pass rate" +p95Latency = "P95 latency" +slaUptime = "SLA uptime (30d)" + +[portal.home.kpis.free] +agents = "Agents" +docsProcessed = "Docs processed" +docsProcessedDescription = "Free plan cap" +operations = "Operations" +pipelines = "Pipelines" + +[portal.home.kpis.pro] +agentsActive = "Agents active" +docs30d = "Docs / 30d" +evalPassRate = "Eval pass rate" +pipelines = "Pipelines" + +[portal.home.onboarding] +progress = "{{done}} / {{total}} done" +runAgain = "Run again" +start = "Start" +subtitle = "Four steps to a production-shaped Stirling project." +title = "Get to value" + +[portal.home.onboarding.empty] +description = "Onboarding tasks will appear here once your workspace is set up." +title = "No onboarding steps yet" + +[portal.home.productGrid] +ariaLabel = "Process PDFs at scale" + +[portal.home.productGrid.agents] +blurb = "Wire your agent via MCP, REST, or tool definitions. Deterministic operations, scenarios, evals." +cta = "Connect an agent" +title = "Agents" + +[portal.home.productGrid.pipelines] +badge = "Hero" +blurb = "Compose document workflows from typed operations. Upload a sample to get suggestions or start blank." +cta = "Build a pipeline" +title = "Pipelines" + +[portal.home.productGrid.sources] +blurb = "Attach pipelines where PDFs already live — S3, agents, SharePoint, webhooks, batch, email." +cta = "Connect a source" +title = "Sources" + +[portal.home.quickActions] +subtitle = "Top tasks for today" +title = "Quick actions" + +[portal.home.quickActions.buildPipeline] +blurb = "3-step composer over the typed op library" +title = "Build a pipeline" + +[portal.home.quickActions.connectSource] +blurb = "S3, agents, webhooks, watched folders" +title = "Connect a source" + +[portal.home.quickActions.issueApiKey] +blurb = "Scoped key with rate limits and IP allowlist" +title = "Issue an API key" + +[portal.home.quickActions.tryOp] +blurb = "Drop a sample, pick an op, see the JSON" +title = "Try a PDF operation" + +[portal.home.regions] +subtitle = "Real-time status for every deployed Stirling region." +title = "Region health" + +[portal.home.regions.empty] +description = "Once a region is deployed, its health appears here." +title = "No regions yet" + +[portal.infrastructure] +manageEditorDeployment = "Manage Editor deployment" +sectionsAriaLabel = "Infrastructure sections" +subtitle = "Deployments, credentials, security posture, storage, and the audit trail for your Stirling workspace." +title = "Infrastructure" + +[portal.infrastructure.apiKeys] +createKey = "Create key" +heading = "API keys" +subheading = "Scoped credentials with per-key rate limits, permissions, and IP allowlists." + +[portal.infrastructure.apiKeys.card] +allowedIps = "Allowed IPs" +anyIp = "Any IP (no allowlist)" +created = "Created" +lastUsed = "Last used" +permissions = "Permissions" +rateLimit = "Rate limit" +rateLimitValue = "{{value}} req/min" +usageMonth = "Usage this month" +usageToday = "Usage today" + +[portal.infrastructure.apiKeys.empty] +description = "Create a scoped key to start calling the Stirling API." +title = "No API keys yet" + +[portal.infrastructure.audit] +filterAriaLabel = "Filter audit events by category" +heading = "Audit logs" +latencyValue = "{{value}} ms" +noEventsInCategory = "No events in this category." +subheading = "Every authentication, configuration, and processing event across your workspace." + +[portal.infrastructure.audit.columns] +actor = "Actor" +event = "Event" +latency = "Latency" +status = "Status" +target = "Target" +timestamp = "Timestamp" + +[portal.infrastructure.audit.empty] +description = "Workspace activity will appear here as it happens." +title = "No audit events" + +[portal.infrastructure.audit.filters] +all = "All" +auth = "Auth" +config = "Config" +elevation = "Elevation" +processing = "Processing" +security = "Security" + +[portal.infrastructure.audit.metrics] +config = "Config" +elevation = "Elevation" +processing = "Processing" +totalEvents = "Total events · 24h" + +[portal.infrastructure.createKey] +cancel = "Cancel" +createKey = "Create key" +done = "Done" +ipAllowlistHelper = "Comma-separated CIDR ranges. Leave blank to allow any IP." +ipAllowlistLabel = "IP allowlist" +keyNameLabel = "Key name" +keyNamePlaceholder = "e.g. Production · ingest" +permissionsLabel = "Permissions" +secretKeyCaption = "Secret key" +secretWarning = "Store this in a secrets manager. Stirling only ever stores a hash — there is no way to recover it later." +subtitle = "Scope the key to the minimum it needs. You can rotate or revoke at any time." +subtitleCreated = "Copy this secret now — it won't be shown again." +title = "Create API key" +titleCreated = "Key created" + +[portal.infrastructure.deployments] +msValue = "{{value}} ms" +throughputValue = "{{value}}/min" + +[portal.infrastructure.deployments.deployColumns] +deployedBy = "Deployed by" +environment = "Environment" +product = "Product" +status = "Status" +version = "Version" +when = "When" + +[portal.infrastructure.deployments.recent] +heading = "Recent deployments" +subheading = "The latest rollouts across products and environments." + +[portal.infrastructure.deployments.regionColumns] +instances = "Instances" +latency = "Latency" +load = "Load" +p99 = "P99" +region = "Region" +status = "Status" +throughput = "Throughput" +uptime = "Uptime" +version = "Version" + +[portal.infrastructure.deployments.regions] +heading = "Regions" +subheading = "Live health for every deployed Stirling region — latency, load, and rollout version." + +[portal.infrastructure.deployments.regions.empty] +description = "Deployed regions appear here once your workspace is provisioned." +title = "No regions deployed" + +[portal.infrastructure.models] +heading = "Models" +msValue = "{{value}} ms" +subheading = "The model catalogue and routing that powers document processing across your workspace." + +[portal.infrastructure.models.byom] +description = "Register an on-prem or self-hosted model and pin it to a region for data-residency-bound processing." +title = "Bring your own model" + +[portal.infrastructure.models.catalogue] +heading = "Catalogue" +sub = "Managed models available to your workspace, with live latency and cost." +subEnterprise = "Managed, bring-your-own, and on-prem models — with per-region pinning available." + +[portal.infrastructure.models.catalogue.empty] +description = "Models in your workspace's catalogue appear here." +title = "No models available" + +[portal.infrastructure.models.columns] +cost = "Cost" +latency = "Latency" +load = "Load" +model = "Model" +status = "Status" +type = "Type" +version = "Version" + +[portal.infrastructure.models.metrics] +activeModels = "Active models" +avgLatency = "Avg latency" +included = "Included" +monthlySpend = "Monthly model spend" + +[portal.infrastructure.models.routing] +empty = "No routing rules configured." +heading = "Routing rules" +sub = "Which model handles each operation. The default applies when no narrower rule matches." +subLocked = "Route operations to specific models — available on paid plans." + +[portal.infrastructure.models.routing.lockedBanner] +description = "Upgrade to Pro to control which model handles each operation and document type." +title = "Model routing is a paid feature" + +[portal.infrastructure.models.routingColumns] +default = "Default" +docType = "Document type" +modelForAria = "Model for {{operation}}" +operation = "Operation" +routedTo = "Routed to" + +[portal.infrastructure.security.access.byok] +description = "Supply a key from your own KMS. Stirling encrypts with it but can still read." +label = "Bring your own key (BYOK)" + +[portal.infrastructure.security.access.hyok] +description = "Keys never leave your KMS. Stirling holds only ciphertext." +label = "Hold your own key (HYOK)" + +[portal.infrastructure.security.access.stirling] +description = "Stirling manages encryption keys. Simplest — zero key ops on your side." +label = "Stirling-held keys" + +[portal.infrastructure.security.accessPolicy] +heading = "Document access policy" +subheading = "Controls who can decrypt processed documents at rest." + +[portal.infrastructure.security.attestations] +heading = "Compliance attestations" +noReport = "No report available" +subheading = "Framework-by-framework audit posture, with reports available on attested controls." +viewReport = "View report →" + +[portal.infrastructure.security.compliance] +heading = "Compliance" +subheading = "Attestations and certifications covering the Stirling platform." + +[portal.infrastructure.security.empty] +description = "Your workspace's security configuration will appear here." +title = "Security posture unavailable" + +[portal.infrastructure.security.hyokBanner] +description = "With HYOK, encryption keys never leave your KMS. Stirling stores and processes only ciphertext you can revoke at any time." +title = "Stirling cannot decrypt your documents" + +[portal.infrastructure.security.ipAllowlist] +empty = "No IP ranges configured — all IPs allowed." +heading = "IP allowlist" +sub = "API access is restricted to these CIDR ranges." +subLocked = "Restrict API access to known IP ranges — available on paid plans." + +[portal.infrastructure.security.ipAllowlist.lockedBanner] +description = "Upgrade to Pro to restrict API access to specific networks." +title = "IP allowlisting is a paid feature" + +[portal.infrastructure.security.ipColumns] +added = "Added" +addedBy = "Added by" +cidr = "CIDR" +label = "Label" + +[portal.infrastructure.security.keyManagement] +algorithm = "Algorithm" +heading = "Encryption key management" +keyId = "Key identifier" +lastRotated = "Last rotated" +rotateKey = "Rotate key" +rotationPolicy = "Rotation policy" +subheading = "Custody of the keys that encrypt documents at rest — who can decrypt, and how keys rotate." + +[portal.infrastructure.security.managedBanner] +description = "Bring-your-own-key (BYOK) and hold-your-own-key (HYOK) custody are available on Enterprise. Upgrade to supply keys from your own KMS." +title = "Keys are managed by Stirling on your plan" + +[portal.infrastructure.security.residency.apac] +description = "ap-southeast-1" +label = "Asia Pacific" + +[portal.infrastructure.security.residency.eu] +description = "eu-west-1 · GDPR data boundary" +label = "European Union" + +[portal.infrastructure.security.residency.us] +description = "us-east-1 · us-west-2" +label = "United States" + +[portal.infrastructure.security.residencyHeader] +heading = "Data residency" +subheading = "Where documents are stored and processed." + +[portal.infrastructure.storage] +gbValue = "{{value}} GB" +percentUsed = "{{value}} used" + +[portal.infrastructure.storage.empty] +description = "Connected storage and usage appear here." +title = "No storage configured" + +[portal.infrastructure.storage.lifecycle] +active = "Active" +activeRange = "0–{{value}}d" +archived = "Archived" +coldStorage = "cold storage" +deleted = "Deleted" +never = "never" +purged = "purged" + +[portal.infrastructure.storage.providers] +connect = "Connect" +connected = "Connected" +heading = "Connected providers" +subheading = "Where processed artifacts are written." + +[portal.infrastructure.storage.retention] +heading = "Retention" +subheading = "How long artifacts are kept before lifecycle deletion." +windowLabel = "Default retention window" + +[portal.infrastructure.storage.retentionOption] +days_one = "{{count}} day" +days_other = "{{count}} days" +never = "Never delete" + +[portal.infrastructure.storage.totalUsage] +heading = "Total usage" +progressLabel = "Storage used" +subheading = "Storage consumed across all connected providers." + +[portal.infrastructure.tabs] +apiKeys = "API Keys" +audit = "Audit Logs" +deployments = "Deployments" +models = "Models" +security = "Security" +storage = "Storage" + +[portal.mocks.label] +off = "Mocks OFF" +on = "Mocks ON" + +[portal.mocks.tooltip] +off = "Mock data OFF — fetch calls go to the real network. Click to re-enable mocks (reloads the page)." +on = "Mock data ON — fetch calls are intercepted by MSW. Click to switch to the real network (reloads the page)." + +[portal.nav] +agent-builder = "Agent Builder" +components = "Components" +docs = "Developer Docs" +documents = "Documents" +editor = "Editor" +home = "Home" +infrastructure = "Infrastructure" +pipelines = "Pipelines" +policies = "Policies" +procurement = "Procurement" +settings = "Settings" +sources = "Sources" +usage = "Usage & Billing" +users = "Users" + +[portal.notifications] +markAllRead = "Mark all read" +title = "Notifications" +viewAll = "View all" + +[portal.notifications.ariaLabel] +none = "Notifications, no unread" +unread_one = "Notifications, {{count}} unread" +unread_other = "Notifications, {{count}} unread" + +[portal.notifications.count] +allRead = "all read" +loading = "loading" +new_one = "{{count}} new" +new_other = "{{count}} new" + +[portal.notifications.empty] +description = "No new notifications." +title = "You're all caught up" + +[portal.opRunner] +durationMs = "{{ms}} ms" +featuredOps = "Featured ops" +subtitle = "Drop a sample, pick an op, see what Stirling returns." +title = "Try a PDF operation" + +[portal.opRunner.action] +close = "Close" +openBuilder = "Open the pipeline builder" +run = "Run operation" +runAgain = "Run again" +running = "Running…" + +[portal.opRunner.drop] +hint = "or use a sample document." +pickAnother = "Pick another sample" +replaceHint = "Drop again or pick another sample to replace." +title = "Drop a PDF here" +useSample = "Use a sample" + +[portal.opRunner.empty] +description = "Once operations are published, they'll show up here." +title = "No featured ops yet" + +[portal.opRunner.error] +title = "The operation didn't complete" +unknown = "Unknown error" + +[portal.opRunner.hint] +aSample = "a sample" +press = "Press" +ready = "Ready" +toInvoke = "to invoke" + +[portal.opRunner.hint.runOn] +before = "Run" +middle = "on" + +[portal.opRunner.running] +title = "Running {{label}}…" + +[portal.opRunner.status] +completed = "Completed" +failed = "Failed" + +[portal.pipelines] +subtitle = "Every automated document pipeline on the backend: an ordered chain of operations over a set of sources, run on a trigger. Click a row for its steps and sources." +title = "Pipelines" + +[portal.pipelines.actions] +newPipeline = "New pipeline" + +[portal.pipelines.composer] +cancel = "Cancel" +chainEmpty = "Add operations from the palette below." +create = "Create pipeline" +directory = "Output folder" +directoryHelp = "Absolute path on the server. Must be within the configured allowed folders." +editTitle = "Edit pipeline" +moveDown = "Move down" +moveUp = "Move up" +name = "Name" +namePlaceholder = "e.g. Redaction sweep" +noSources = "No sources connected yet. The pipeline can still run on files supplied to it directly." +operations_one = "Operation ({{count}})" +operations_other = "Operations ({{count}})" +output = "Output" +removeStep = "Remove operation" +save = "Save changes" +scheduleEvery = "Run every" +sources = "Sources" +sourcesLoading = "Loading sources..." +subtitle = "Pick the sources it runs over, chain the operations, then choose when it runs and where output goes." +title = "New pipeline" +trigger = "Trigger" +triggerManual = "Manual only" + +[portal.pipelines.composer.unit] +days = "days" +hours = "hours" +minutes = "minutes" + +[portal.pipelines.delete] +body = "Delete \"{{name}}\"? This can't be undone." +cancel = "Cancel" +confirm = "Delete" +title = "Delete pipeline?" + +[portal.pipelines.detail] +closeAriaLabel = "Close detail" +delete = "Delete pipeline" +edit = "Edit" +noSources = "No sources. Files are supplied directly to each run." +noSteps = "No operations configured." +output = "Output" +pause = "Pause" +resume = "Resume" +run = "Run now" +sources = "Sources" +steps = "Operations" +subtitle = "{{trigger}} · {{status}}" + +[portal.pipelines.empty] +action = "Create a pipeline" +description = "Create your first pipeline: pick the sources it runs over, chain the operations, and choose where output goes." +title = "No pipelines yet" + +[portal.pipelines.kpi] +active = "Active" +paused = "Paused" +total = "Pipelines" + +[portal.pipelines.output] +folder = "Write to folder" +inline = "Return files" + +[portal.pipelines.run] +completed_one = "Run completed." +completed_other = "All {{count}} runs completed." +empty = "Nothing to run: the sources had no documents to process." +failed = "Run failed: {{error}}" +running = "Run started; still in progress." + +[portal.pipelines.status] +active = "Active" +paused = "Paused" + +[portal.pipelines.table] +name = "Pipeline" +sources = "Sources" +status = "Status" +steps = "Steps" + +[portal.pipelines.trigger] +folder-watch = "Folder watch" +manual = "Manual" +schedule = "Scheduled" + +[portal.policies] +subtitle = "Standing automations that enforce a tool pipeline on every document. Each policy fires on upload or export, runs its tool chain, and saves the enforced version alongside the original." +title = "Policies" + +[portal.policies.card] +comingSoon = "Coming soon" +notSetUp = "Not set up" + +[portal.policies.detail] +enforces = "Enforces" +onEveryExport = "On every export" +onEveryUpload = "On every upload" +outputAsNewFile = "as a new file" +outputAsNewVersion = "as a new version" +recentActivity = "Recent activity" +retry = "Retry" +showLess = "Show less" +showMore = "Show more" +sources = "Sources" + +[portal.policies.detail.actions] +delete = "Delete" +editSettings = "Edit settings" +pause = "Pause" +resume = "Resume" +runNow = "Run now" + +[portal.policies.detail.emptyActivity] +description = "Documents will appear here once this policy runs." +title = "No activity yet" + +[portal.policies.offline] +description = "Your policies are saved and will appear once the connection is restored." +retry = "Retry" +title = "Backend unavailable" + +[portal.policies.stats] +activeFor = "Active" +dataProcessed = "Data processed" +docsEnforced = "Docs enforced" + +[portal.policies.status] +active = "Active" +paused = "Paused" + +[portal.policies.summary.active] +description = "Enforcing on upload/export" +label = "Active policies" + +[portal.policies.summary.categories] +description = "Available to configure" +label = "Categories" + +[portal.policies.summary.docsEnforced] +description = "Across active policies" +label = "Docs enforced" + +[portal.policies.summary.paused] +description = "Configured but not firing" +label = "Paused" + +[portal.policies.wizard.actions] +back = "Back" +cancel = "Cancel" +continue = "Continue" +enablePolicy = "Enable policy" +saveChanges = "Save changes" + +[portal.policies.wizard.docTypes] +allDescription = "Set up an Ingestion (classification) policy to narrow this to specific document types." +allTitle = "All document types" +clear = "Clear" +heading = "Document types" +narrow = "Narrow" +selected_one = "{{count}} selected" +selected_other = "{{count}} selected" + +[portal.policies.wizard.errors] +noTools = "Enable at least one tool in the workflow first." +saveFailed = "Couldn't save the policy. Please try again." + +[portal.policies.wizard.output] +heading = "Output & run" + +[portal.policies.wizard.output.filenameRule] +autoNumber = "Auto-number" +label = "Filename rule" +placeholder = "Text to add (optional)" +prefix = "Prefix" +suffix = "Suffix" + +[portal.policies.wizard.output.outputAs] +label = "Output as" +newFile = "New file" +newVersion = "New version" + +[portal.policies.wizard.output.retries] +delayLabel = "Retry delay (min)" +heading = "Retries" +maxLabel = "Max retries" + +[portal.policies.wizard.output.runOn] +export = "Export" +helper = "When the policy fires: on upload, or before export." +label = "Run on" +upload = "Upload" + +[portal.policies.wizard.settings] +heading = "Settings" + +[portal.policies.wizard.sources] +emptyDescription = "Connect a source on the Sources page first, then attach it to a policy here." +emptyTitle = "No sources available" +heading = "Sources" +loading = "Loading sources…" + +[portal.policies.wizard.tabs] +ariaLabel = "Setup steps" +settings = "Settings" +workflow = "Workflow" + +[portal.policies.wizard.title] +edit = "Edit {{category}} policy" +setUp = "Set up {{category}} policy" + +[portal.policies.wizard.workflow] +description = "The sequence of tools this policy runs on each document. Each tool is a Stirling endpoint; toggle the ones this policy should enforce." + +[portal.policySummary] +activeSummary = "{{active}} / {{total}} active" +noRule = "No rule enforced yet" +subtitle = "Standing automations every document passes through, regardless of which pipeline handles it." +title = "What runs on your PDFs" + +[portal.policySummary.action] +comingSoon = "Coming soon" +configure = "Configure" +setUp = "Set up" + +[portal.policySummary.column] +activeRule = "Active rule" +policy = "Policy" +status = "Status" + +[portal.policySummary.empty] +description = "Once policies are configured, the categories appear here." +title = "No policies yet" + +[portal.policySummary.state] +active = "Active" +off = "Off" +soon = "Soon" + +[portal.processingStatus] +managePlan = "Manage plan" +pdfsThisMonth = "PDFs this month" +progressLabel = "{{used}} of {{cap}} PDFs used this month" +upgrade = "Upgrade" +volumeSuffix = "PDFs processed · last 30 days" + +[portal.procurement] +enterpriseBadge = "Enterprise" +subtitle = "Get your team evaluated, contracted, and onboarded. Every document in one place." +title = "Procurement" + +[portal.procurement.action] +download = "Download" +pay = "Pay now" +request = "Request" +sign = "Review & sign" +upload = "Upload" + +[portal.procurement.docs] +count_one = "{{count}} doc" +count_other = "{{count}} docs" +done = "Done" +here = "You're here" +hide = "Hide" +optional = "Optional" +paidAddon = "Paid add-on" +show = "Show" +subtitle = "Everything you need at each step of the journey, surfaced as the deal moves through it." +supportingSubtitle = "SOC 2, security reviews, tax forms and more, ready when your security or procurement team asks. Some carry a one-time fee." +supportingTitle = "Supporting your evaluation" +title = "Documents" +upcoming = "Upcoming" + +[portal.procurement.journey] +daysLeft_one = "{{count}} day left" +daysLeft_other = "{{count}} days left" +engineerLabel = "Your solutions engineer" +eyebrow = "Your rollout" +live = "You're live on Stirling Enterprise" +nextStep = "Next step: {{action}}" +subtitle = "Your solutions engineer is on every step. One next action at a time; the full checklist is below." +title = "From trial to live, one guided path" +trialTitle = "Enterprise trial" + +[portal.procurement.locked] +description = "Trial keys, committed-volume quotes, the one-signature agreement, payment, and your document ledger all live here once you start an enterprise evaluation." +eyebrow = "Enterprise only" +talkToSales = "Talk to sales" +title = "The procurement track opens with Enterprise" + +[portal.procurement.modal] +cancel = "Cancel" +chooseFile = "Choose file" +downloadBody = "Your download will begin shortly." +downloadCta = "Download" +downloadTitle = "Download" +noFile = "No file selected" +payBody = "Pay your committed contract by card or bank transfer through Stripe. Your workspace provisions as soon as payment clears." +payCta = "Continue to Stripe" +payTitle = "Confirm payment" +requestBodyFree = "We generate this on demand. Confirm and your solutions engineer will send it across shortly." +requestBodyPaid = "This is a paid add-on. Confirm and your solutions engineer will scope it and send the paperwork." +requestCta = "Request" +requestTitle = "Request this document" +signBody = "Opens the Stirling Enterprise Agreement for e-signature: one signature covers the MSA, order form, EULA and DPA. We countersign automatically and you advance to payment." +signCta = "Open for signature" +signTitle = "Review and sign your agreement" +uploadBody = "Send us your PO and we invoice against it on your terms. Drag in the PDF or pick a file below." +uploadCta = "Upload purchase order" +uploadTitle = "Upload your purchase order" + +[portal.procurement.status] +action = "Action needed" +available = "Available" +complete = "Complete" +pending = "Pending" +request = "On request" + +[portal.recentActivity] +title = "Recent activity" +viewAll = "View all" + +[portal.recentActivity.empty] +description = "Pipeline runs, deploys and agent events will appear here." +title = "Nothing here yet" + +[portal.search] +ariaLabel = "Search" +placeholder = "Search Stirling — endpoints, pipelines, docs…" + +[portal.search.empty] +noActionsDescription = "Quick actions will appear here once they're available." +noActionsTitle = "No quick actions" +noMatches = "No matches for \"{{query}}\"" +noMatchesDescription = "Try a different keyword or browse the catalogue." + +[portal.settings] +ariaLabel = "Settings" +cancel = "Cancel" +enterpriseBadge = "Enterprise" +footerNote = "Changes apply to this workspace." +saveChanges = "Save changes" + +[portal.settings.appearance] +themeSub = "Choose how the portal looks on this device." +themeTitle = "Theme" + +[portal.settings.appearance.dark] +hint = "Dim surfaces" +label = "Dark" + +[portal.settings.appearance.light] +hint = "Bright surfaces" +label = "Light" + +[portal.settings.authentication] +sessionTimeout = "Session timeout" +sessionTimeoutHelper = "Members re-authenticate after this idle period." +sub = "Organisation-wide authentication controls." +title = "Sign-in policy" + +[portal.settings.authentication.mfa] +description = "Require every member to complete MFA at sign-in." +label = "Enforce two-factor (MFA)" + +[portal.settings.authentication.scim] +description = "Sync members and roles from your directory." +label = "SCIM provisioning" + +[portal.settings.authentication.sso] +description = "Federate sign-in through your identity provider." +label = "Single sign-on (SAML)" + +[portal.settings.authentication.timeout] +1440 = "24 hours" +240 = "4 hours" +480 = "8 hours" +60 = "1 hour" +720 = "12 hours" + +[portal.settings.earlyAccess] +sub = "Opt into features still in preview." +title = "Preview features" + +[portal.settings.groups] +account = "Account" +admin = "Admin" +workspace = "Workspace" + +[portal.settings.notifications] +sub = "Pick which events reach your inbox." +title = "Email notifications" + +[portal.settings.notifications.pipeline-failures] +description = "A run errors out or a step times out." +label = "Pipeline failures" + +[portal.settings.notifications.pipeline-success] +description = "Every successful pipeline run finishes." +label = "Pipeline completions" + +[portal.settings.notifications.product-updates] +description = "New operations, sources, and release notes." +label = "Product updates" + +[portal.settings.notifications.security-alerts] +description = "New API keys, sign-ins, or permission changes." +label = "Security alerts" + +[portal.settings.notifications.usage-alerts] +description = "You approach a plan limit or rate cap." +label = "Usage & quota alerts" + +[portal.settings.notifications.weekly-digest] +description = "A Monday summary of volume and health." +label = "Weekly digest" + +[portal.settings.profile] +accountFallback = "Account" +changePhoto = "Change photo" +email = "Email" +emailHelper = "Used for sign-in and notification delivery." +emailPlaceholder = "you@company.com" +fullName = "Full name" +namePlaceholder = "Your name" + +[portal.settings.sections] +account-link = "Account link" +appearance = "Appearance" +authentication = "Authentication" +early-access = "Early access" +general = "General" +notifications = "Notifications" +profile = "Profile" +sessions = "Active sessions" + +[portal.settings.sessions] +revoke = "Revoke" +sub = "Devices currently signed in to this account." +thisDevice = "This device" +title = "Active sessions" + +[portal.settings.workspace] +manageBilling = "Manage billing" +nameLabel = "Workspace name" +namePlaceholder = "Workspace name" +plan = "Plan" +regionEnterpriseSuffix = "{{region}} · Enterprise" +regionHelper = "Where documents are processed and stored at rest." +regionLabel = "Data residency region" +seats = "Seats" +seatsUsed = "{{used}} of {{total}} used" + +[portal.shell.header] +accountFallback = "Account" +accountMenu = "Account menu" +darkMode = "Dark mode" +lightMode = "Light mode" +search = "Search" +searchPlaceholder = "Search…" +signOut = "Sign out" +switchToDark = "Switch to dark theme" +switchToLight = "Switch to light theme" + +[portal.shell.sidebar] +appEditor = "Editor" +appProcessor = "Processor" +brandSuffix = "Stirling Processor" +docsCount = "{{docs}} docs" +docsProcessed = "Docs processed" +linkAccount = "Link Stirling account" +planEnterprise = "Enterprise plan" +planProcessor = "Processor plan" +primaryNav = "Primary navigation" +switchApp = "Switch app" + +[portal.sources] +subtitle = "Reusable input connections that feed documents into Stirling. Configure a connection once, then reference it from any number of policies. Click a row for its config and which policies use it." +title = "Sources" + +[portal.sources.actions] +agentBuilder = "Agent Builder" +connectSource = "Connect source" + +[portal.sources.delete] +body = "Delete \"{{name}}\"? This can't be undone. Policies that reference it would need to be updated." +cancel = "Cancel" +confirm = "Delete" +title = "Delete source?" + +[portal.sources.detail] +closeAriaLabel = "Close detail" +delete = "Delete source" +docs24h = "Last 24h" +docs30d = "Last 30 days" +docsTotal = "Total seen" +docsTrend = "Documents over the last 30 days" +documents = "Documents" +edit = "Edit" +notReferenced = "Not referenced by any policy, so it's safe to delete." +pause = "Pause" +resume = "Resume" +subtitle = "{{type}} · {{status}}" +usedBy = "Used by" + +[portal.sources.empty] +description = "Connect a folder (and, soon, cloud storage) so your policies have somewhere to pull documents from." +title = "No sources connected yet" + +[portal.sources.kpi] +inUse = "In use" +total = "Connections" +unused = "Unused" + +[portal.sources.status] +active = "Active" +disabled = "Disabled" +unused = "Unused" + +[portal.sources.table] +source = "Source" +status = "Status" +usedBy = "Policies" + +[portal.sources.types.editor] +label = "Editor" + +[portal.sources.types.folder] +description = "Watch a directory on the server for new documents." +label = "Folder" + +[portal.sources.types.folder.fields.directory] +helperText = "Absolute path Stirling watches for files to process." +label = "Directory path" +placeholder = "/data/incoming" + +[portal.sources.types.folder.fields.mode] +label = "Read mode" + +[portal.sources.types.folder.fields.mode.options] +consume = "Consume: process each file once" +snapshot = "Snapshot: re-read the folder every run" + +[portal.sources.types.unknown] +label = "Source" + +[portal.sources.wizard] +back = "Back" +cancel = "Cancel" +continue = "Continue" +editTitle = "Edit source" +name = "Name" +namePlaceholder = "e.g. Claims intake" +save = "Save changes" +subtitle = "Step {{current}} of {{total}} · {{label}}" +title = "Connect a source" +type = "Type" + +[portal.sources.wizard.steps] +chooseType = "Choose type" +configure = "Configure" +review = "Review & connect" + +[portal.usage] +managePayment = "Manage Payment" +subtitle = "Consumption, invoices, and plan management for every PDF Stirling has billed, in one console." +title = "Usage & billing" + +[portal.usage.error] +loadWallet = "Couldn't load wallet" +openStripePortal = "Couldn't open Stripe portal" +walletUnavailable = "Wallet unavailable: {{status}} {{statusText}}" + +[portal.usage.finalizing] +body = "It can take a few seconds for your subscription to activate. This page updates automatically." +title = "Finalizing your subscription…" + +[portal.usage.sessionExpired] +action = "Sign in again" +body = "Your Stirling account session has expired. Sign in again to view billing — your instance stays linked." +title = "Session expired" + +[portal.usageChart] +defaultLabel = "Docs processed · last 30 days" +delta = "{{pct}}% vs prior 30d" +docsValue = "{{value}} docs" +srAnnounce = "{{date}}: {{value}} docs" + +[portal.useCases] +title = "Popular use cases" +viewAll = "View all pipelines" + +[portal.useCases.items.authenticity] +blurb = "Cryptographic checks at the document boundary — signature validation, tamper detection, signing flows for outbound documents. Trust decisions in the pipeline, not your app code." +cta = "Try authenticity check" +eyebrow = "AUTHENTICITY" +title = "Verify signatures and detect tampering" + +[portal.useCases.items.autoRouting] +blurb = "One classifier reads what arrived — KYC form, invoice, contract, COI — and routes to the right downstream pipeline. No manual triage, no docs in the wrong workflow." +cta = "Build a classifier pipeline" +eyebrow = "AUTO-ROUTING" +title = "Auto-classify and route incoming documents" + +[portal.useCases.items.piiRedaction] +blurb = "Strip sensitive fields before storage, indexing, or LLM processing. Schema-aware, per-field audit, BYOK or HYOK keys. Compliance at the document boundary, not per pipeline." +cta = "See redaction pipelines" +eyebrow = "PII REDACTION" +title = "Redact PII before it leaves your stack" + +[portal.useCases.items.trainingData] +blurb = "Batch-import an archive, redact PII, classify, chunk, and emit ready-to-load JSON for fine-tuning, eval sets, or RAG. Self-completing and replayable." +cta = "Build a training-data pipeline" +eyebrow = "TRAINING DATA" +title = "Turn PDFs into training data" + +[portal.users] +subtitle = "The people in your organization and the access they hold — roles, status and security controls." +title = "Users" + +[portal.users.access] +subtitle = "Seats, authentication and provisioning for your organization." +title = "Access & security" + +[portal.users.access.auth] +title = "Authentication" + +[portal.users.access.auth.requireMfa] +description = "Members must set up a second factor to sign in." +enforced = "Enforced org-wide on this plan." +label = "Require MFA" + +[portal.users.access.auth.shortSessions] +description = "Sign members out after inactivity (currently {{timeout}})." +label = "Short-lived sessions" + +[portal.users.access.scim] +active = "Active" +directory = "Directory" +lastSync = "Last sync" +note = "Members are created, updated and deactivated automatically from your identity provider." +off = "Off" +title = "SCIM provisioning" + +[portal.users.access.seats] +title = "Seats" +unlimited = "Your plan includes unlimited seats." +usedLabel = "{{used}} of {{limit}} seats used" + +[portal.users.access.sso] +connected = "Connected" +domains = "Domains" +manage = "Manage connection" +notConfigured = "Not configured" +provider = "Provider" +title = "SSO / SAML" + +[portal.users.access.upgrade] +action = "Upgrade plan" +title = "Unlock team access controls" + +[portal.users.empty] +description = "Invite your team to start collaborating on documents and pipelines." +title = "No members yet" + +[portal.users.invite] +cancel = "Cancel" +email = "Email" +emailError = "Enter a valid email address" +emailPlaceholder = "teammate@acme.com" +role = "Role" +roleHelper = "Determines what the member can do once they join." +send = "Send invite" +subtitle = "They'll receive an email to join your organization." + +[portal.users.roles] +subtitle = "Every role exists on every plan — what each one can do is fixed across the org." +title = "Roles" + +[portal.users.summary] +members = "Members" +pendingInvites = "Pending invites" +seatsUsed = "Seats used" + +[portal.users.table] +actionsFor = "Actions for {{name}}" +changeRole = "Change role" +lastActive = "Last active" +member = "Member" +remove = "Remove from org" +role = "Role" +status = "Status" +suspend = "Suspend" + +[portal.welcome] +ariaLabel = "Stirling product highlights" +pagination = "Carousel pagination" +slideLabel = "Slide {{number}}: {{title}}" + +[portal.welcome.ornament.editor] +critical = "Critical" +ocrClean = "OCR-clean" +schemaMatch = "schema match 0.97" +signed = "signed" + +[portal.welcome.slides.agents] +eyebrow = "AI Agents" +primary = "Try PDF Processor" +secondary = "View MCP docs" +sub = "Wire your agent via MCP, REST or tool definitions. Deterministic operations and guardrails — test with scenarios and evals before you ship." +title = "PDF Processor for AI Agents" + +[portal.welcome.slides.editor] +eyebrow = "PDF Editor" +primary = "Install PDF Editor" +secondary = "Connect an instance" +sub = "Annotate, sign, redact, and review locally or in the cloud. Brought to the platform as the credibility anchor of the Stirling control plane." +title = "The #1 PDF Editor on GitHub" + +[portal.welcome.slides.platform] +eyebrow = "Platform" +primary = "Try a PDF operation" +secondary = "Get an API key" +sub = "Ingest from agents, APIs and connectors. Run composable pipelines with evals and golden sets. Land in a vault with zero-standing-access controls." +title = "PDF Infrastructure for Developers" + [printFile] title = "Print File" diff --git a/frontend/portal/public/mockServiceWorker.js b/frontend/editor/public/mockServiceWorker.js similarity index 100% rename from frontend/portal/public/mockServiceWorker.js rename to frontend/editor/public/mockServiceWorker.js diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx index f26d40f743..654691c2fc 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx @@ -1,5 +1,5 @@ /** - * Editor cloud adapter over the shared {@code @shared/billing} spend-cap control: + * Editor cloud adapter over the shared {@code @app/billing} spend-cap control: * supplies the i18n copy (the shared control is copy-agnostic) and the editor's * {@code scc-*} styling. The public API (controlled {@code capUsd}/{@code * onChange}, optional {@code onSave}/{@code saveLabel}, {@code note}) is @@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next"; import { DEFAULT_CAP_PRESETS, SpendCapControl as SharedSpendCapControl, -} from "@shared/billing"; +} from "@app/billing"; // eslint-disable-next-line no-restricted-imports import "./SpendCapControl.css"; diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx index 308fc97d32..4c8804027c 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx @@ -7,7 +7,7 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useWallet, type Wallet } from "@app/hooks/useWallet"; -import { currencySymbol, MeterBar, meterState } from "@shared/billing"; +import { currencySymbol, MeterBar, meterState } from "@app/billing"; import "@app/components/shared/config/configSections/Payg.css"; import "@app/components/shared/config/configSections/PaygFree.css"; diff --git a/frontend/editor/src/cloud/hooks/useWallet.ts b/frontend/editor/src/cloud/hooks/useWallet.ts index 5a1b1d21cb..07e862fb8c 100644 --- a/frontend/editor/src/cloud/hooks/useWallet.ts +++ b/frontend/editor/src/cloud/hooks/useWallet.ts @@ -57,10 +57,10 @@ import type { WalletMember, WalletCategoryBreakdown, WalletActivityRow, -} from "@shared/billing"; +} from "@app/billing"; // ─── Public types ─────────────────────────────────────────────────────── -// The wallet contract lives in @shared/billing (shared with the admin portal). +// The wallet contract lives in @app/billing (shared with the admin portal). // Re-exported so existing `@app/hooks/useWallet` importers keep their imports. export type { Wallet, diff --git a/frontend/editor/src/cloud/tsconfig.json b/frontend/editor/src/cloud/tsconfig.json index bf3eedb2e5..2b703ea335 100644 --- a/frontend/editor/src/cloud/tsconfig.json +++ b/frontend/editor/src/cloud/tsconfig.json @@ -7,10 +7,10 @@ "../../src/proprietary/*", "../../src/core/*" ], + "@portal/*": ["../../src/portal/*"], "@cloud/*": ["../../src/cloud/*"], "@proprietary/*": ["../../src/proprietary/*"], - "@core/*": ["../../src/core/*"], - "@shared/*": ["../../../shared/*"] + "@core/*": ["../../src/core/*"] } }, "include": [ diff --git a/frontend/shared/assets/Brand.stories.tsx b/frontend/editor/src/core/assets/Brand.stories.tsx similarity index 75% rename from frontend/shared/assets/Brand.stories.tsx rename to frontend/editor/src/core/assets/Brand.stories.tsx index 87cf9f1257..886e7a023d 100644 --- a/frontend/shared/assets/Brand.stories.tsx +++ b/frontend/editor/src/core/assets/Brand.stories.tsx @@ -2,16 +2,16 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; // Visual catalogue of the shared brand assets (shared/assets/brand) — the single // source of truth for the Stirling logos used across the apps. -import modernMarkDark from "@shared/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg"; -import modernMarkLight from "@shared/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg"; -import modernBlack from "@shared/assets/brand/modern-logo/StirlingPDFLogoBlackText.svg"; -import modernWhite from "@shared/assets/brand/modern-logo/StirlingPDFLogoWhiteText.svg"; -import modernGrey from "@shared/assets/brand/modern-logo/StirlingPDFLogoGreyText.svg"; -import classicMarkDark from "@shared/assets/brand/classic-logo/StirlingPDFLogoNoTextDark.svg"; -import classicMarkLight from "@shared/assets/brand/classic-logo/StirlingPDFLogoNoTextLight.svg"; -import classicBlack from "@shared/assets/brand/classic-logo/StirlingPDFLogoBlackText.svg"; -import classicWhite from "@shared/assets/brand/classic-logo/StirlingPDFLogoWhiteText.svg"; -import classicGrey from "@shared/assets/brand/classic-logo/StirlingPDFLogoGreyText.svg"; +import modernMarkDark from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg"; +import modernMarkLight from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg"; +import modernBlack from "@app/assets/brand/modern-logo/StirlingPDFLogoBlackText.svg"; +import modernWhite from "@app/assets/brand/modern-logo/StirlingPDFLogoWhiteText.svg"; +import modernGrey from "@app/assets/brand/modern-logo/StirlingPDFLogoGreyText.svg"; +import classicMarkDark from "@app/assets/brand/classic-logo/StirlingPDFLogoNoTextDark.svg"; +import classicMarkLight from "@app/assets/brand/classic-logo/StirlingPDFLogoNoTextLight.svg"; +import classicBlack from "@app/assets/brand/classic-logo/StirlingPDFLogoBlackText.svg"; +import classicWhite from "@app/assets/brand/classic-logo/StirlingPDFLogoWhiteText.svg"; +import classicGrey from "@app/assets/brand/classic-logo/StirlingPDFLogoGreyText.svg"; type Asset = { label: string; src: string; onDark?: boolean }; type VariantSet = { variant: string; mark: Asset[]; wordmark: Asset[] }; diff --git a/frontend/shared/assets/brand/classic-logo/Firstpage.png b/frontend/editor/src/core/assets/brand/classic-logo/Firstpage.png similarity index 100% rename from frontend/shared/assets/brand/classic-logo/Firstpage.png rename to frontend/editor/src/core/assets/brand/classic-logo/Firstpage.png diff --git a/frontend/shared/assets/brand/classic-logo/StirlingPDFLogoBlackText.svg b/frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoBlackText.svg similarity index 100% rename from frontend/shared/assets/brand/classic-logo/StirlingPDFLogoBlackText.svg rename to frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoBlackText.svg diff --git a/frontend/shared/assets/brand/classic-logo/StirlingPDFLogoGreyText.svg b/frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoGreyText.svg similarity index 100% rename from frontend/shared/assets/brand/classic-logo/StirlingPDFLogoGreyText.svg rename to frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoGreyText.svg diff --git a/frontend/shared/assets/brand/classic-logo/StirlingPDFLogoNoTextDark.svg b/frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoNoTextDark.svg similarity index 100% rename from frontend/shared/assets/brand/classic-logo/StirlingPDFLogoNoTextDark.svg rename to frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoNoTextDark.svg diff --git a/frontend/shared/assets/brand/classic-logo/StirlingPDFLogoNoTextLight.svg b/frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoNoTextLight.svg similarity index 100% rename from frontend/shared/assets/brand/classic-logo/StirlingPDFLogoNoTextLight.svg rename to frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoNoTextLight.svg diff --git a/frontend/shared/assets/brand/classic-logo/StirlingPDFLogoWhiteText.svg b/frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoWhiteText.svg similarity index 100% rename from frontend/shared/assets/brand/classic-logo/StirlingPDFLogoWhiteText.svg rename to frontend/editor/src/core/assets/brand/classic-logo/StirlingPDFLogoWhiteText.svg diff --git a/frontend/shared/assets/brand/classic-logo/favicon.ico b/frontend/editor/src/core/assets/brand/classic-logo/favicon.ico similarity index 100% rename from frontend/shared/assets/brand/classic-logo/favicon.ico rename to frontend/editor/src/core/assets/brand/classic-logo/favicon.ico diff --git a/frontend/shared/assets/brand/classic-logo/logo-tooltip.svg b/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg similarity index 100% rename from frontend/shared/assets/brand/classic-logo/logo-tooltip.svg rename to frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg diff --git a/frontend/shared/assets/brand/classic-logo/logo192.png b/frontend/editor/src/core/assets/brand/classic-logo/logo192.png similarity index 100% rename from frontend/shared/assets/brand/classic-logo/logo192.png rename to frontend/editor/src/core/assets/brand/classic-logo/logo192.png diff --git a/frontend/shared/assets/brand/classic-logo/logo512.png b/frontend/editor/src/core/assets/brand/classic-logo/logo512.png similarity index 100% rename from frontend/shared/assets/brand/classic-logo/logo512.png rename to frontend/editor/src/core/assets/brand/classic-logo/logo512.png diff --git a/frontend/shared/assets/brand/modern-logo/Firstpage.png b/frontend/editor/src/core/assets/brand/modern-logo/Firstpage.png similarity index 100% rename from frontend/shared/assets/brand/modern-logo/Firstpage.png rename to frontend/editor/src/core/assets/brand/modern-logo/Firstpage.png diff --git a/frontend/shared/assets/brand/modern-logo/LoginDarkModeHeader.svg b/frontend/editor/src/core/assets/brand/modern-logo/LoginDarkModeHeader.svg similarity index 100% rename from frontend/shared/assets/brand/modern-logo/LoginDarkModeHeader.svg rename to frontend/editor/src/core/assets/brand/modern-logo/LoginDarkModeHeader.svg diff --git a/frontend/shared/assets/brand/modern-logo/LoginLightModeHeader.svg b/frontend/editor/src/core/assets/brand/modern-logo/LoginLightModeHeader.svg similarity index 100% rename from frontend/shared/assets/brand/modern-logo/LoginLightModeHeader.svg rename to frontend/editor/src/core/assets/brand/modern-logo/LoginLightModeHeader.svg diff --git a/frontend/shared/assets/brand/modern-logo/StirlingPDFLogoBlackText.svg b/frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoBlackText.svg similarity index 100% rename from frontend/shared/assets/brand/modern-logo/StirlingPDFLogoBlackText.svg rename to frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoBlackText.svg diff --git a/frontend/shared/assets/brand/modern-logo/StirlingPDFLogoGreyText.svg b/frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoGreyText.svg similarity index 100% rename from frontend/shared/assets/brand/modern-logo/StirlingPDFLogoGreyText.svg rename to frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoGreyText.svg diff --git a/frontend/shared/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg b/frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg similarity index 100% rename from frontend/shared/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg rename to frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg diff --git a/frontend/shared/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg b/frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg similarity index 100% rename from frontend/shared/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg rename to frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg diff --git a/frontend/shared/assets/brand/modern-logo/StirlingPDFLogoWhiteText.svg b/frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoWhiteText.svg similarity index 100% rename from frontend/shared/assets/brand/modern-logo/StirlingPDFLogoWhiteText.svg rename to frontend/editor/src/core/assets/brand/modern-logo/StirlingPDFLogoWhiteText.svg diff --git a/frontend/portal/public/favicon.ico b/frontend/editor/src/core/assets/brand/modern-logo/favicon.ico similarity index 100% rename from frontend/portal/public/favicon.ico rename to frontend/editor/src/core/assets/brand/modern-logo/favicon.ico diff --git a/frontend/shared/assets/brand/modern-logo/logo-tooltip.svg b/frontend/editor/src/core/assets/brand/modern-logo/logo-tooltip.svg similarity index 100% rename from frontend/shared/assets/brand/modern-logo/logo-tooltip.svg rename to frontend/editor/src/core/assets/brand/modern-logo/logo-tooltip.svg diff --git a/frontend/shared/assets/brand/modern-logo/logo192.png b/frontend/editor/src/core/assets/brand/modern-logo/logo192.png similarity index 100% rename from frontend/shared/assets/brand/modern-logo/logo192.png rename to frontend/editor/src/core/assets/brand/modern-logo/logo192.png diff --git a/frontend/shared/assets/brand/modern-logo/logo512.png b/frontend/editor/src/core/assets/brand/modern-logo/logo512.png similarity index 100% rename from frontend/shared/assets/brand/modern-logo/logo512.png rename to frontend/editor/src/core/assets/brand/modern-logo/logo512.png diff --git a/frontend/shared/assets/login/AddToPDF.png b/frontend/editor/src/core/assets/login/AddToPDF.png similarity index 100% rename from frontend/shared/assets/login/AddToPDF.png rename to frontend/editor/src/core/assets/login/AddToPDF.png diff --git a/frontend/shared/assets/login/Firstpage.png b/frontend/editor/src/core/assets/login/Firstpage.png similarity index 100% rename from frontend/shared/assets/login/Firstpage.png rename to frontend/editor/src/core/assets/login/Firstpage.png diff --git a/frontend/shared/assets/login/LoginBackgroundPanel.png b/frontend/editor/src/core/assets/login/LoginBackgroundPanel.png similarity index 100% rename from frontend/shared/assets/login/LoginBackgroundPanel.png rename to frontend/editor/src/core/assets/login/LoginBackgroundPanel.png diff --git a/frontend/shared/assets/login/SecurePDF.png b/frontend/editor/src/core/assets/login/SecurePDF.png similarity index 100% rename from frontend/shared/assets/login/SecurePDF.png rename to frontend/editor/src/core/assets/login/SecurePDF.png diff --git a/frontend/shared/assets/login/apple.svg b/frontend/editor/src/core/assets/login/apple.svg similarity index 100% rename from frontend/shared/assets/login/apple.svg rename to frontend/editor/src/core/assets/login/apple.svg diff --git a/frontend/shared/assets/login/authentik.svg b/frontend/editor/src/core/assets/login/authentik.svg similarity index 100% rename from frontend/shared/assets/login/authentik.svg rename to frontend/editor/src/core/assets/login/authentik.svg diff --git a/frontend/shared/assets/login/cloudron.svg b/frontend/editor/src/core/assets/login/cloudron.svg similarity index 100% rename from frontend/shared/assets/login/cloudron.svg rename to frontend/editor/src/core/assets/login/cloudron.svg diff --git a/frontend/shared/assets/login/github.svg b/frontend/editor/src/core/assets/login/github.svg similarity index 100% rename from frontend/shared/assets/login/github.svg rename to frontend/editor/src/core/assets/login/github.svg diff --git a/frontend/shared/assets/login/google.svg b/frontend/editor/src/core/assets/login/google.svg similarity index 100% rename from frontend/shared/assets/login/google.svg rename to frontend/editor/src/core/assets/login/google.svg diff --git a/frontend/shared/assets/login/keycloak.svg b/frontend/editor/src/core/assets/login/keycloak.svg similarity index 100% rename from frontend/shared/assets/login/keycloak.svg rename to frontend/editor/src/core/assets/login/keycloak.svg diff --git a/frontend/shared/assets/login/microsoft.svg b/frontend/editor/src/core/assets/login/microsoft.svg similarity index 100% rename from frontend/shared/assets/login/microsoft.svg rename to frontend/editor/src/core/assets/login/microsoft.svg diff --git a/frontend/shared/assets/login/oidc.svg b/frontend/editor/src/core/assets/login/oidc.svg similarity index 100% rename from frontend/shared/assets/login/oidc.svg rename to frontend/editor/src/core/assets/login/oidc.svg diff --git a/frontend/shared/auth/ui/oauthIcons.ts b/frontend/editor/src/core/auth/ui/oauthIcons.ts similarity index 67% rename from frontend/shared/auth/ui/oauthIcons.ts rename to frontend/editor/src/core/auth/ui/oauthIcons.ts index c588274b92..f0d879786f 100644 --- a/frontend/shared/auth/ui/oauthIcons.ts +++ b/frontend/editor/src/core/auth/ui/oauthIcons.ts @@ -6,14 +6,14 @@ * editor's saas/desktop login buttons, and the config provider list) share one * copy that works in both the editor and the portal bundles. */ -import googleIcon from "@shared/assets/login/google.svg"; -import githubIcon from "@shared/assets/login/github.svg"; -import appleIcon from "@shared/assets/login/apple.svg"; -import microsoftIcon from "@shared/assets/login/microsoft.svg"; -import keycloakIcon from "@shared/assets/login/keycloak.svg"; -import cloudronIcon from "@shared/assets/login/cloudron.svg"; -import authentikIcon from "@shared/assets/login/authentik.svg"; -import oidcIcon from "@shared/assets/login/oidc.svg"; +import googleIcon from "@app/assets/login/google.svg"; +import githubIcon from "@app/assets/login/github.svg"; +import appleIcon from "@app/assets/login/apple.svg"; +import microsoftIcon from "@app/assets/login/microsoft.svg"; +import keycloakIcon from "@app/assets/login/keycloak.svg"; +import cloudronIcon from "@app/assets/login/cloudron.svg"; +import authentikIcon from "@app/assets/login/authentik.svg"; +import oidcIcon from "@app/assets/login/oidc.svg"; /** Generic fallback icon (filename) for unknown providers. */ export const GENERIC_PROVIDER_ICON = "oidc.svg"; diff --git a/frontend/editor/src/core/components/shared/ThemeProvider.tsx b/frontend/editor/src/core/components/shared/ThemeProvider.tsx index 2dda9b8d39..bf15ce187f 100644 --- a/frontend/editor/src/core/components/shared/ThemeProvider.tsx +++ b/frontend/editor/src/core/components/shared/ThemeProvider.tsx @@ -18,8 +18,8 @@ import { getSystemTheme, resolveColorScheme, } from "@app/constants/theme"; -// SUI shared design-system tokens (used by @shared/components); key on `data-theme`. -import "@shared/tokens/tokens.css"; +// SUI shared design-system tokens (used by @app/ui); key on `data-theme`. +import "@app/tokens/tokens.css"; interface ThemeContextType { themeMode: ThemeMode; diff --git a/frontend/editor/src/core/components/shared/ToolPanelHeader.css b/frontend/editor/src/core/components/shared/ToolPanelHeader.css new file mode 100644 index 0000000000..f1d99d1913 --- /dev/null +++ b/frontend/editor/src/core/components/shared/ToolPanelHeader.css @@ -0,0 +1,93 @@ +/* ===================== PanelHeader (core) ===================== */ +/* Header for the active-tool rail. Core-owned so the OSS build has no */ +/* design-system dependency; mirrors the shared rail header treatment so it */ +/* reads well in both light and dark mode (thin border, no heavy fill). */ + +.sui-panelhdr { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 1rem 1rem 0.75rem; + flex-shrink: 0; +} + +.sui-panelhdr__bar { + display: inline-flex; + align-items: center; + gap: 0.5rem; + flex: 1; + min-width: 0; + padding: 0.4rem 0.75rem 0.4rem 0.4rem; + border: 1px solid var(--border-subtle, var(--mantine-color-default-border)); + border-radius: 9999px; + background: var(--mantine-color-body); + text-align: left; + color: inherit; +} + +.sui-panelhdr__icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + border-radius: 9999px; + background: var(--mantine-color-blue-light); + color: var(--mantine-color-blue-filled); + flex-shrink: 0; +} + +.sui-panelhdr__icon svg { + font-size: 1rem; + width: 1rem; + height: 1rem; +} + +/* ToolIcon wraps its glyph in .tool-button-icon with its own margin/transform; + reset them so the glyph sits dead-centre in the circular badge. */ +.sui-panelhdr__icon .tool-button-icon { + margin: 0 !important; + transform: none !important; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.sui-panelhdr__label { + flex: 1; + min-width: 0; + font-size: 0.9rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--mantine-color-text); +} + +/* Dark mode: let the header blend into the rail — just a thin border, no fill — + so it doesn't read as a clashing lighter card on the dark toolbar. */ +[data-mantine-color-scheme="dark"] .sui-panelhdr__bar { + background: transparent; + border-color: var(--border-subtle, var(--mantine-color-default-border)); +} + +[data-mantine-color-scheme="dark"] .sui-panelhdr__icon { + background: color-mix( + in srgb, + var(--mantine-color-blue-filled) 18%, + transparent + ); + color: var(--mantine-color-blue-3, var(--mantine-color-blue-filled)); +} + +/* Dark mode: the subtle gray close button is too dim against the dark rail — + brighten it to a clearly-visible light grey (near-white on hover). */ +[data-mantine-color-scheme="dark"] .sui-panelhdr__close { + color: var(--mantine-color-gray-4); +} + +[data-mantine-color-scheme="dark"] .sui-panelhdr__close:hover { + color: var(--mantine-color-gray-2); +} diff --git a/frontend/editor/src/core/components/shared/ToolPanelHeader.tsx b/frontend/editor/src/core/components/shared/ToolPanelHeader.tsx new file mode 100644 index 0000000000..e34ca4c6c2 --- /dev/null +++ b/frontend/editor/src/core/components/shared/ToolPanelHeader.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from "react"; +import { ActionIcon } from "@mantine/core"; +import CloseIcon from "@mui/icons-material/Close"; +import "@app/components/shared/ToolPanelHeader.css"; + +export interface ToolPanelHeaderProps { + /** Glyph rendered in the tinted circular badge at the header's leading edge. */ + icon: ReactNode; + /** Header title. */ + title: ReactNode; + /** Close (X) handler. The trailing close button renders only when supplied. */ + onClose?: () => void; + /** aria-label for the close button. */ + closeLabel?: string; +} + +/** + * Header for the active-tool rail surface: a tinted icon badge + title in a + * rounded bar with a trailing close button. + * + * Core-owned so the OSS build carries no design-system dependency. It mirrors + * the shared rail header's styling for the simple (non-menu) case the tool panel + * needs; the richer variant (dropdown menu, accents, loading dot) lives in the + * design system for the AI chat and Policies surfaces. + */ +export function ToolPanelHeader({ + icon, + title, + onClose, + closeLabel, +}: ToolPanelHeaderProps) { + return ( +
+
+ {icon} + {title} +
+ {onClose && ( + + + + )} +
+ ); +} diff --git a/frontend/editor/src/core/components/shared/config/configSections/providerDefinitions.ts b/frontend/editor/src/core/components/shared/config/configSections/providerDefinitions.ts index 6b5983822f..72dc675f8f 100644 --- a/frontend/editor/src/core/components/shared/config/configSections/providerDefinitions.ts +++ b/frontend/editor/src/core/components/shared/config/configSections/providerDefinitions.ts @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { oauthIconUrl } from "@shared/auth/ui/oauthIcons"; +import { oauthIconUrl } from "@app/auth/ui/oauthIcons"; export type ProviderType = "oauth2" | "saml2" | "telegram" | "googledrive"; diff --git a/frontend/editor/src/core/components/tools/RightSidebar.tsx b/frontend/editor/src/core/components/tools/RightSidebar.tsx index bfd98d7b32..7e3aa04629 100644 --- a/frontend/editor/src/core/components/tools/RightSidebar.tsx +++ b/frontend/editor/src/core/components/tools/RightSidebar.tsx @@ -19,7 +19,7 @@ import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems"; import { useToolSections } from "@app/hooks/useToolSections"; import type { SubcategoryGroup } from "@app/hooks/useToolSections"; import { ToolIcon } from "@app/components/shared/ToolIcon"; -import { PanelHeader } from "@shared/components/PanelHeader"; +import { ToolPanelHeader } from "@app/components/shared/ToolPanelHeader"; import { Tooltip as AppTooltip } from "@app/components/shared/Tooltip"; import { withViewTransition } from "@app/utils/viewTransition"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; @@ -291,7 +291,7 @@ export default function RightSidebar() { <> {!showPolicies && (activeTool ? ( - { describe("env vars", () => { it("every VITE_ var used in source is present in an example env file", () => { const baseEnv = readFileSync(join(frontendRoot, ".env"), "utf-8"); + const proprietaryEnv = readFileSync( + join(frontendRoot, ".env.proprietary"), + "utf-8", + ); const desktopEnv = readFileSync( join(frontendRoot, ".env.desktop"), "utf-8", @@ -58,6 +62,7 @@ describe("env vars", () => { const declaredKeys = new Set([ ...parseEnvKeys(baseEnv), + ...parseEnvKeys(proprietaryEnv), ...parseEnvKeys(desktopEnv), ...parseEnvKeys(saasEnv), ]); diff --git a/frontend/editor/src/core/hooks/useLogoAssets.test.ts b/frontend/editor/src/core/hooks/useLogoAssets.test.ts index b22701dedd..3d83fb35f4 100644 --- a/frontend/editor/src/core/hooks/useLogoAssets.test.ts +++ b/frontend/editor/src/core/hooks/useLogoAssets.test.ts @@ -10,11 +10,11 @@ import type { LogoVariant } from "@app/services/preferencesService"; */ describe("useLogoAssets - Logo Asset Files", () => { const publicDir = path.resolve(__dirname, "../../../public"); - // Brand logo assets live in the shared design system; the editor's vite - // config copies shared/assets/brand//* into the served root at build - // time (see viteStaticCopy in editor/vite.config.ts), so useLogoAssets can - // keep referencing them by their public-URL path. Validate them at source. - const brandDir = path.resolve(__dirname, "../../../../shared/assets/brand"); + // Brand logo assets live in core; the editor's vite config copies + // core/assets/brand//* into the served root at build time (see + // viteStaticCopy in editor/vite.config.ts), so useLogoAssets can keep + // referencing them by their public-URL path. Validate them at source. + const brandDir = path.resolve(__dirname, "../assets/brand"); // All asset files that useLogoAssets references const requiredAssets = [ diff --git a/frontend/editor/src/core/i18n.ts b/frontend/editor/src/core/i18n.ts index 82b860b467..9663a1c2e2 100644 --- a/frontend/editor/src/core/i18n.ts +++ b/frontend/editor/src/core/i18n.ts @@ -1,7 +1,7 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import LanguageDetector from "i18next-browser-languagedetector"; -import TomlBackend from "@shared/i18n/tomlBackend"; +import TomlBackend from "@app/i18n/tomlBackend"; import { supportedLanguages, rtlLanguages, @@ -10,10 +10,10 @@ import { normalizeLanguageCode, toUnderscoreFormat, toUnderscoreLanguages, -} from "@shared/i18n/languages"; +} from "@app/i18n/languages"; // Language metadata and code helpers are shared with the portal via -// @shared/i18n. Re-export them so existing `@app/i18n` consumers are unchanged. +// @app/i18n. Re-export them so existing `@app/i18n` consumers are unchanged. export { supportedLanguages, rtlLanguages, diff --git a/frontend/editor/src/core/i18n/config.ts b/frontend/editor/src/core/i18n/config.ts index 47e028c1f4..02b79f06b1 100644 --- a/frontend/editor/src/core/i18n/config.ts +++ b/frontend/editor/src/core/i18n/config.ts @@ -1,6 +1,6 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; -import TomlBackend from "@shared/i18n/tomlBackend"; +import TomlBackend from "@app/i18n/tomlBackend"; i18n .use(TomlBackend) diff --git a/frontend/shared/i18n/languages.ts b/frontend/editor/src/core/i18n/languages.ts similarity index 100% rename from frontend/shared/i18n/languages.ts rename to frontend/editor/src/core/i18n/languages.ts diff --git a/frontend/shared/i18n/tomlBackend.ts b/frontend/editor/src/core/i18n/tomlBackend.ts similarity index 100% rename from frontend/shared/i18n/tomlBackend.ts rename to frontend/editor/src/core/i18n/tomlBackend.ts diff --git a/frontend/shared/i18n/translationAudit.ts b/frontend/editor/src/core/i18n/translationAudit.ts similarity index 90% rename from frontend/shared/i18n/translationAudit.ts rename to frontend/editor/src/core/i18n/translationAudit.ts index 50cd1b2c76..e2cc309c72 100644 --- a/frontend/shared/i18n/translationAudit.ts +++ b/frontend/editor/src/core/i18n/translationAudit.ts @@ -19,8 +19,8 @@ import { fileURLToPath } from "url"; import ts from "typescript"; import { parse } from "smol-toml"; -const HERE = path.dirname(fileURLToPath(import.meta.url)); // frontend/shared/i18n -const FRONTEND_ROOT = path.resolve(HERE, "../.."); // frontend +const HERE = path.dirname(fileURLToPath(import.meta.url)); // frontend/editor/src/core/i18n +const FRONTEND_ROOT = path.resolve(HERE, "../../../.."); // frontend /** Repo root, for rendering source paths relative in CI annotations. */ export const REPO_ROOT = path.resolve(FRONTEND_ROOT, ".."); @@ -29,12 +29,6 @@ export interface TranslationProject { name: string; /** Absolute path to the source tree to scan. */ srcRoot: string; - /** - * Extra source trees that also contribute used keys (e.g. the shared layer - * (frontend/shared), whose components (login UI, etc.) are rendered by both - * apps and reference each app's locale keys). - */ - extraRoots?: string[]; /** Absolute path to the en-US source locale. */ localeFile: string; /** Static keys flagged as missing that are genuinely fine (false positives). */ @@ -68,11 +62,6 @@ const PLURAL_SUFFIX_RE = /_(zero|one|two|few|many|other)$/; const front = (rel: string): string => path.join(FRONTEND_ROOT, rel); -// The shared layer is rendered by both apps, so its translation usage counts -// toward each project (or keys used only from shared look unused, and a shared -// component's keys go unvalidated against an app's locale). -const SHARED_SRC = front("shared"); - /** * The projects the i18n suites guard. Each carries its own ignore lists: the * editor exempts a few runtime-assembled key families; the portal starts clean. @@ -80,8 +69,11 @@ const SHARED_SRC = front("shared"); export const I18N_PROJECTS: TranslationProject[] = [ { name: "editor", + // One project over the whole editor tree, including the portal layer. Portal + // strings live under portal.* in the shared locale and are referenced with + // that prefix in source (t("portal.users.title")), so they validate here + // like any other key. srcRoot: front("editor/src"), - extraRoots: [SHARED_SRC], localeFile: front("editor/public/locales/en-US/translation.toml"), ignoredKeyPatterns: [ // SignSettings / SavedSignaturesSection resolve every key as @@ -93,24 +85,14 @@ export const I18N_PROJECTS: TranslationProject[] = [ /^account\./, // [language] direction is read by the i18n layer, never as a UI string. /^language\.direction$/, + // Portal source-type copy is referenced via metadata keys in + // components/sources/sourceTypes.ts (t(field.labelKey)), invisible to the + // static scan. + /^portal\.sources\.types\./, ], minUsedKeys: 100, minLocaleKeys: 100, }, - { - name: "portal", - srcRoot: front("portal/src"), - extraRoots: [SHARED_SRC], - localeFile: front("portal/public/locales/en-US/translation.toml"), - ignoredKeyPatterns: [ - // Source-type copy is referenced via metadata keys in - // components/sources/sourceTypes.ts (t(field.labelKey)), so the static - // scan can't see these as used. - /^sources\.types\./, - ], - minUsedKeys: 20, - minLocaleKeys: 20, - }, ]; // Locale tables are shallow by design; the cap only trips on a pathological @@ -189,11 +171,6 @@ const listSourceFiles = (roots: string[]): string[] => ) .filter((file) => !IGNORED_FILE_PATTERNS.some((re) => re.test(file))); -const projectRoots = (project: TranslationProject): string[] => [ - project.srcRoot, - ...(project.extraRoots ?? []), -]; - const hasPluralCoverage = (key: string, available: Set): boolean => [...available].some( (k) => k.startsWith(`${key}_`) && PLURAL_SUFFIX_RE.test(k), @@ -347,7 +324,7 @@ export function findMissingKeys(project: TranslationProject): { return false; }; - const used = listSourceFiles(projectRoots(project)) + const used = listSourceFiles([project.srcRoot]) .flatMap(extractStaticKeys) .filter(({ key }) => !ignored.has(key)); const missing = used.filter(({ key }) => !resolves(key)); @@ -360,7 +337,7 @@ export function findUnusedKeys(project: TranslationProject): { localeCount: number; } { const localeKeys = Array.from(collectLocaleKeys(project.localeFile)); - const files = listSourceFiles(projectRoots(project)); + const files = listSourceFiles([project.srcRoot]); const source = files.map((file) => fs.readFileSync(file, "utf8")).join("\n"); const shapes = new Set(); diff --git a/frontend/editor/src/core/routes/adminRouteExtensions.tsx b/frontend/editor/src/core/routes/adminRouteExtensions.tsx new file mode 100644 index 0000000000..657d4799a3 --- /dev/null +++ b/frontend/editor/src/core/routes/adminRouteExtensions.tsx @@ -0,0 +1,10 @@ +import type { ReactElement } from "react"; + +/** + * Admin-only route-set contributed by higher layers (the portal). The OSS core + * build ships none, so this stub returns an empty list and the portal chunk is + * never referenced in the core bundle. + */ +export function getAdminRouteExtensions(): ReactElement[] { + return []; +} diff --git a/frontend/editor/src/core/tests/missingTranslations.test.ts b/frontend/editor/src/core/tests/missingTranslations.test.ts index f98022559b..617699e43a 100644 --- a/frontend/editor/src/core/tests/missingTranslations.test.ts +++ b/frontend/editor/src/core/tests/missingTranslations.test.ts @@ -5,10 +5,10 @@ import { I18N_PROJECTS, REPO_ROOT, findMissingKeys, -} from "@shared/i18n/translationAudit"; +} from "@app/i18n/translationAudit"; // One suite per frontend app (editor + portal). The scan logic lives in -// @shared/i18n/translationAudit so both apps share one implementation. +// @app/i18n/translationAudit so both apps share one implementation. describe.each(I18N_PROJECTS)( "Missing translation coverage — $name", (project) => { diff --git a/frontend/editor/src/core/tests/unusedTranslations.test.ts b/frontend/editor/src/core/tests/unusedTranslations.test.ts index 1193c737bf..f0d0fbce93 100644 --- a/frontend/editor/src/core/tests/unusedTranslations.test.ts +++ b/frontend/editor/src/core/tests/unusedTranslations.test.ts @@ -5,10 +5,10 @@ import { I18N_PROJECTS, REPO_ROOT, findUnusedKeys, -} from "@shared/i18n/translationAudit"; +} from "@app/i18n/translationAudit"; // One suite per frontend app (editor + portal). The scan logic lives in -// @shared/i18n/translationAudit so both apps share one implementation; each +// @app/i18n/translationAudit so both apps share one implementation; each // project carries its own ignoredKeyPatterns for runtime-assembled keys. describe.each(I18N_PROJECTS)( "Unused translation coverage — $name", @@ -38,7 +38,7 @@ describe.each(I18N_PROJECTS)( `Found ${unused.length} unused en-US translation key(s). ` + `Remove them from ${localeRelative}, or (if the usage is too ` + `dynamic for the heuristic to spot) add a pattern to this ` + - `project's ignoredKeyPatterns in @shared/i18n/translationAudit.`, + `project's ignoredKeyPatterns in @app/i18n/translationAudit.`, ).toEqual([]); }, ); diff --git a/frontend/shared/tokens/Tokens.stories.tsx b/frontend/editor/src/core/tokens/Tokens.stories.tsx similarity index 100% rename from frontend/shared/tokens/Tokens.stories.tsx rename to frontend/editor/src/core/tokens/Tokens.stories.tsx diff --git a/frontend/shared/tokens/base.css b/frontend/editor/src/core/tokens/base.css similarity index 100% rename from frontend/shared/tokens/base.css rename to frontend/editor/src/core/tokens/base.css diff --git a/frontend/shared/tokens/tokens.css b/frontend/editor/src/core/tokens/tokens.css similarity index 100% rename from frontend/shared/tokens/tokens.css rename to frontend/editor/src/core/tokens/tokens.css diff --git a/frontend/shared/tokens/tokens.ts b/frontend/editor/src/core/tokens/tokens.ts similarity index 100% rename from frontend/shared/tokens/tokens.ts rename to frontend/editor/src/core/tokens/tokens.ts diff --git a/frontend/editor/src/core/tsconfig.json b/frontend/editor/src/core/tsconfig.json index 781d894b6d..08bf588c6a 100644 --- a/frontend/editor/src/core/tsconfig.json +++ b/frontend/editor/src/core/tsconfig.json @@ -2,8 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "paths": { - "@app/*": ["../../src/core/*"], - "@shared/*": ["../../../shared/*"] + "@app/*": ["../../src/core/*"] } }, "include": ["../global.d.ts", "../*.js", "../*.ts", "../*.tsx", "."] diff --git a/frontend/editor/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx b/frontend/editor/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx index ad783e9f76..f5e64f2927 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/DesktopAuthLayout.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import LoginRightCarousel from "@shared/auth/ui/LoginRightCarousel"; +import LoginRightCarousel from "@app/auth/ui/LoginRightCarousel"; import buildLoginSlides from "@app/components/shared/loginSlides"; -import styles from "@shared/auth/ui/AuthShell.module.css"; +import styles from "@app/auth/ui/AuthShell.module.css"; import { useLogoVariant } from "@app/hooks/useLogoVariant"; interface DesktopAuthLayoutProps { diff --git a/frontend/editor/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx b/frontend/editor/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx index c2da65845b..c9813d818e 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { authService, UserInfo } from "@app/services/authService"; import { buildOAuthCallbackHtml } from "@app/utils/oauthCallbackHtml"; -import { oauthIconUrl } from "@shared/auth/ui/oauthIcons"; +import { oauthIconUrl } from "@app/auth/ui/oauthIcons"; import { STIRLING_SAAS_URL } from "@app/constants/connection"; import "@app/components/SetupWizard/desktopOAuth.css"; diff --git a/frontend/editor/src/desktop/components/SetupWizard/SaaSLoginScreen.tsx b/frontend/editor/src/desktop/components/SetupWizard/SaaSLoginScreen.tsx index 7f679052a3..dc346ef9ac 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/SaaSLoginScreen.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/SaaSLoginScreen.tsx @@ -1,13 +1,13 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import LoginHeader from "@app/routes/login/LoginHeader"; -import ErrorMessage from "@shared/auth/ui/ErrorMessage"; -import EmailPasswordForm from "@shared/auth/ui/EmailPasswordForm"; +import ErrorMessage from "@app/auth/ui/ErrorMessage"; +import EmailPasswordForm from "@app/auth/ui/EmailPasswordForm"; import DividerWithText from "@app/components/shared/DividerWithText"; import { DesktopOAuthButtons } from "@app/components/SetupWizard/DesktopOAuthButtons"; import { SelfHostedLink } from "@app/components/SetupWizard/SelfHostedLink"; import { UserInfo } from "@app/services/authService"; -import "@shared/auth/ui/auth.css"; +import "@app/auth/ui/auth.css"; interface SaaSLoginScreenProps { serverUrl: string; diff --git a/frontend/editor/src/desktop/components/SetupWizard/SaaSSignupScreen.tsx b/frontend/editor/src/desktop/components/SetupWizard/SaaSSignupScreen.tsx index bcc7bf88c7..a3765c2c98 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/SaaSSignupScreen.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/SaaSSignupScreen.tsx @@ -1,14 +1,14 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import LoginHeader from "@app/routes/login/LoginHeader"; -import ErrorMessage from "@shared/auth/ui/ErrorMessage"; +import ErrorMessage from "@app/auth/ui/ErrorMessage"; import SignupForm from "@app/routes/signup/SignupForm"; import { useSignupFormValidation, SignupFieldErrors, } from "@app/routes/signup/SignupFormValidation"; import { authService } from "@app/services/authService"; -import "@shared/auth/ui/auth.css"; +import "@app/auth/ui/auth.css"; interface SaaSSignupScreenProps { loading: boolean; diff --git a/frontend/editor/src/desktop/components/SetupWizard/SelfHostedLink.tsx b/frontend/editor/src/desktop/components/SetupWizard/SelfHostedLink.tsx index 739c02516d..8d5c98fe6e 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/SelfHostedLink.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/SelfHostedLink.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import "@shared/auth/ui/auth.css"; +import "@app/auth/ui/auth.css"; interface SelfHostedLinkProps { onClick: () => void; diff --git a/frontend/editor/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx b/frontend/editor/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx index ff2d743809..34a26a8fa0 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/SelfHostedLoginScreen.tsx @@ -2,13 +2,13 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Text } from "@mantine/core"; import LoginHeader from "@app/routes/login/LoginHeader"; -import ErrorMessage from "@shared/auth/ui/ErrorMessage"; -import EmailPasswordForm from "@shared/auth/ui/EmailPasswordForm"; +import ErrorMessage from "@app/auth/ui/ErrorMessage"; +import EmailPasswordForm from "@app/auth/ui/EmailPasswordForm"; import DividerWithText from "@app/components/shared/DividerWithText"; import { DesktopOAuthButtons } from "@app/components/SetupWizard/DesktopOAuthButtons"; import { UserInfo } from "@app/services/authService"; import { SSOProviderConfig } from "@app/services/connectionModeService"; -import "@shared/auth/ui/auth.css"; +import "@app/auth/ui/auth.css"; interface SelfHostedLoginScreenProps { serverUrl: string; diff --git a/frontend/editor/src/desktop/components/SetupWizard/ServerSelectionScreen.tsx b/frontend/editor/src/desktop/components/SetupWizard/ServerSelectionScreen.tsx index 7ddf7b3cb2..c3fc2f3846 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/ServerSelectionScreen.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/ServerSelectionScreen.tsx @@ -1,10 +1,10 @@ import React from "react"; import { useTranslation } from "react-i18next"; import LoginHeader from "@app/routes/login/LoginHeader"; -import ErrorMessage from "@shared/auth/ui/ErrorMessage"; +import ErrorMessage from "@app/auth/ui/ErrorMessage"; import { ServerSelection } from "@app/components/SetupWizard/ServerSelection"; import { ServerConfig } from "@app/services/connectionModeService"; -import "@shared/auth/ui/auth.css"; +import "@app/auth/ui/auth.css"; interface ServerSelectionScreenProps { onSelect: (config: ServerConfig) => void; diff --git a/frontend/editor/src/desktop/components/SetupWizard/index.tsx b/frontend/editor/src/desktop/components/SetupWizard/index.tsx index e218f06e0c..2f6ddaba2b 100644 --- a/frontend/editor/src/desktop/components/SetupWizard/index.tsx +++ b/frontend/editor/src/desktop/components/SetupWizard/index.tsx @@ -19,7 +19,7 @@ import { import { tauriBackendService } from "@app/services/tauriBackendService"; import { STIRLING_SAAS_URL } from "@app/constants/connection"; import { listen } from "@tauri-apps/api/event"; -import "@shared/auth/ui/auth.css"; +import "@app/auth/ui/auth.css"; import { DisabledButtonWithTooltip } from "@app/components/shared/DisabledButtonWithTooltip"; enum SetupStep { diff --git a/frontend/editor/src/desktop/routes/adminRouteExtensions.tsx b/frontend/editor/src/desktop/routes/adminRouteExtensions.tsx new file mode 100644 index 0000000000..afd92050bc --- /dev/null +++ b/frontend/editor/src/desktop/routes/adminRouteExtensions.tsx @@ -0,0 +1,10 @@ +import type { ReactElement } from "react"; + +/** + * Desktop inherits proprietary's App but must NOT ship the portal. Shadowing + * the seam back to empty means the desktop bundle never references PortalApp, + * so the portal chunk is not emitted (and @portal never has to resolve there). + */ +export function getAdminRouteExtensions(): ReactElement[] { + return []; +} diff --git a/frontend/editor/src/desktop/tsconfig.json b/frontend/editor/src/desktop/tsconfig.json index 27f094be66..e7f83893c4 100644 --- a/frontend/editor/src/desktop/tsconfig.json +++ b/frontend/editor/src/desktop/tsconfig.json @@ -10,8 +10,7 @@ ], "@cloud/*": ["../../src/cloud/*"], "@proprietary/*": ["../../src/proprietary/*"], - "@core/*": ["../../src/core/*"], - "@shared/*": ["../../../shared/*"] + "@core/*": ["../../src/core/*"] } }, "include": [ diff --git a/frontend/portal/LICENSE b/frontend/editor/src/portal/LICENSE similarity index 100% rename from frontend/portal/LICENSE rename to frontend/editor/src/portal/LICENSE diff --git a/frontend/portal/MOCKS.md b/frontend/editor/src/portal/MOCKS.md similarity index 100% rename from frontend/portal/MOCKS.md rename to frontend/editor/src/portal/MOCKS.md diff --git a/frontend/portal/src/App.tsx b/frontend/editor/src/portal/PortalApp.tsx similarity index 81% rename from frontend/portal/src/App.tsx rename to frontend/editor/src/portal/PortalApp.tsx index cd1195cde3..1494e23cdf 100644 --- a/frontend/portal/src/App.tsx +++ b/frontend/editor/src/portal/PortalApp.tsx @@ -1,12 +1,12 @@ import { useEffect, type ReactNode } from "react"; -import { BrowserRouter, useLocation } from "react-router-dom"; +import { useLocation } from "react-router-dom"; import { MantineProvider } from "@mantine/core"; -import { AuthProvider } from "@shared/auth"; +import { AuthProvider } from "@app/auth"; import { ErrorBoundary } from "@portal/components/ErrorBoundary"; import { ThemeProvider, useTheme } from "@portal/contexts/ThemeContext"; import { TierProvider } from "@portal/contexts/TierContext"; import { LinkProvider, useLink } from "@portal/contexts/LinkContext"; -import type { SupabaseLoginSession } from "@shared/auth/ui/useSupabaseLogin"; +import type { SupabaseLoginSession } from "@app/auth/ui/useSupabaseLogin"; import { UIProvider, useUI } from "@portal/contexts/UIContext"; import { mantineTheme } from "@portal/theme/mantineTheme"; import { AppShell } from "@portal/components/AppShell"; @@ -21,6 +21,8 @@ import { useAccountLinkContext, } from "@portal/contexts/AccountLinkContext"; import { ViewRouter } from "@portal/ViewRouter"; +// Reset + typography, scoped to .portal-scope below. +import "@portal/theme/base.css"; /** * Binds Mantine's colour scheme to the portal's own ThemeProvider so Mantine @@ -117,22 +119,24 @@ function RoutedContent() { ); } -export function App() { - // Honour the Vite base path so the portal routes correctly when served under a - // subpath (e.g. "/portal" behind the single-origin proxy). BASE_URL is "./" - // for a standalone build, which isn't a valid router basename, so only pass it - // when it's an absolute subpath. - const baseUrl = import.meta.env.BASE_URL; - const basename = baseUrl.startsWith("/") ? baseUrl : undefined; +/** + * The portal, mounted as a route-set under /portal/* inside the editor app (via + * the admin-route seam). It supplies its own providers and its own i18next + * instance (the `portal` namespace), but NOT a router — the editor's + * is the one and only router; the portal's routes are relative + * to the /portal mount (see ViewRouter). + */ +export function PortalApp() { return ( - - - {/* TierProvider sits INSIDE LinkProvider so it can derive the tier + {/* Scopes base.css to the portal so it doesn't restyle the host editor. */} +
+ + + {/* TierProvider sits INSIDE LinkProvider so it can derive the tier from the real link/subscription state when MSW mocks are off. */} - - + @@ -148,10 +152,10 @@ export function App() { - - - - + + + +
); diff --git a/frontend/portal/UI_CONVENTIONS.md b/frontend/editor/src/portal/UI_CONVENTIONS.md similarity index 95% rename from frontend/portal/UI_CONVENTIONS.md rename to frontend/editor/src/portal/UI_CONVENTIONS.md index c4d51b5819..40879e2199 100644 --- a/frontend/portal/UI_CONVENTIONS.md +++ b/frontend/editor/src/portal/UI_CONVENTIONS.md @@ -3,7 +3,7 @@ The portal has two component sources. The rule: > **Simple, presentational, brand-defining UI → our SUI design system -> (`@shared/components`). Complex, stateful, or accessibility-hard widgets → +> (`@app/ui`). Complex, stateful, or accessibility-hard widgets → > Mantine.** Don't reinvent what Mantine already does well; do own the look of > the simple, high-frequency pieces. @@ -13,7 +13,7 @@ follow the same light/dark switch and brand colours as SUI. **The provider is intentional** — it exists precisely so we can drop Mantine widgets in where they earn their keep. -## Use SUI (`@shared/components`) — our own style +## Use SUI (`@app/ui`) — our own style Layout and presentational primitives we want full brand control over and that are cheap to own: diff --git a/frontend/editor/src/portal/ViewRouter.tsx b/frontend/editor/src/portal/ViewRouter.tsx new file mode 100644 index 0000000000..2b09a24507 --- /dev/null +++ b/frontend/editor/src/portal/ViewRouter.tsx @@ -0,0 +1,58 @@ +import { Navigate, Route, Routes } from "react-router-dom"; +import { Home } from "@portal/views/Home"; +import { Users } from "@portal/views/Users"; +import { Documents } from "@portal/views/Documents"; +import { Pipelines } from "@portal/views/Pipelines"; +import { Sources } from "@portal/views/Sources"; +import { AgentBuilder } from "@portal/views/AgentBuilder"; +import { Policies } from "@portal/views/Policies"; +import { Components } from "@portal/views/Components"; +import { EditorAdmin } from "@portal/views/EditorAdmin"; +import { Infrastructure } from "@portal/views/Infrastructure"; +import { Usage } from "@portal/views/Usage"; +import { DeveloperDocs } from "@portal/views/DeveloperDocs"; +import { Procurement } from "@portal/views/Procurement"; +import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext"; + +// The portal mounts as a route-set under /portal/* in the editor app, so these +// child routes are relative to that base: strip the leading slash from the +// logical VIEW_PATHS, and home is the index route. Redirects use toPortalPath +// so they resolve to the portal, not the editor root. +const rel = (viewPath: string) => viewPath.replace(/^\//, ""); + +export function ViewRouter() { + return ( + + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + {/* Account-link is now a Settings panel; redirect legacy bookmarks home. */} + } + /> + {/* Settings is a modal overlay, not a route (see AppShell + UIContext). */} + {/* Unknown paths land on Home. */} + } + /> + + ); +} diff --git a/frontend/portal/src/api/agents.ts b/frontend/editor/src/portal/api/agents.ts similarity index 100% rename from frontend/portal/src/api/agents.ts rename to frontend/editor/src/portal/api/agents.ts diff --git a/frontend/portal/src/api/assistant.ts b/frontend/editor/src/portal/api/assistant.ts similarity index 100% rename from frontend/portal/src/api/assistant.ts rename to frontend/editor/src/portal/api/assistant.ts diff --git a/frontend/portal/src/api/billing.ts b/frontend/editor/src/portal/api/billing.ts similarity index 95% rename from frontend/portal/src/api/billing.ts rename to frontend/editor/src/portal/api/billing.ts index 7c87bb5ca5..fbb8673a26 100644 --- a/frontend/portal/src/api/billing.ts +++ b/frontend/editor/src/portal/api/billing.ts @@ -1,10 +1,10 @@ import { apiClient } from "@portal/api/http"; -import type { Wallet } from "@shared/billing"; +import type { Wallet } from "@app/billing"; /** * Real wallet + billing surface. All calls go to apiClient.saas — the hosted * SaaS Java backend, authed by the admin's Supabase JWT. The wallet contract - * itself lives in {@code @shared/billing} (shared with the editor cloud surface). + * itself lives in {@code @app/billing} (shared with the editor cloud surface). */ // Re-export the shared contract so existing `@portal/api/billing` importers keep working. @@ -15,7 +15,7 @@ export type { WalletMember, WalletCategoryBreakdown, WalletActivityRow, -} from "@shared/billing"; +} from "@app/billing"; export async function fetchWallet(): Promise { return apiClient.saas.json("/api/v1/payg/wallet"); diff --git a/frontend/portal/src/api/docs.ts b/frontend/editor/src/portal/api/docs.ts similarity index 100% rename from frontend/portal/src/api/docs.ts rename to frontend/editor/src/portal/api/docs.ts diff --git a/frontend/portal/src/api/documents.ts b/frontend/editor/src/portal/api/documents.ts similarity index 100% rename from frontend/portal/src/api/documents.ts rename to frontend/editor/src/portal/api/documents.ts diff --git a/frontend/portal/src/api/editorDeploy.ts b/frontend/editor/src/portal/api/editorDeploy.ts similarity index 100% rename from frontend/portal/src/api/editorDeploy.ts rename to frontend/editor/src/portal/api/editorDeploy.ts diff --git a/frontend/portal/src/api/home.ts b/frontend/editor/src/portal/api/home.ts similarity index 100% rename from frontend/portal/src/api/home.ts rename to frontend/editor/src/portal/api/home.ts diff --git a/frontend/portal/src/api/http.test.ts b/frontend/editor/src/portal/api/http.test.ts similarity index 96% rename from frontend/portal/src/api/http.test.ts rename to frontend/editor/src/portal/api/http.test.ts index 085948c77f..031327c0cf 100644 --- a/frontend/portal/src/api/http.test.ts +++ b/frontend/editor/src/portal/api/http.test.ts @@ -12,8 +12,8 @@ const { getSession, getStoredTokenMock } = vi.hoisted(() => ({ getStoredTokenMock: vi.fn(), })); -vi.mock("@shared/auth", () => ({ getStoredToken: getStoredTokenMock })); -vi.mock("@shared/auth/supabase/supabaseClient", () => ({ +vi.mock("@app/auth", () => ({ getStoredToken: getStoredTokenMock })); +vi.mock("@app/auth/supabase/supabaseClient", () => ({ getSupabaseClient: () => ({ auth: { getSession } }), configureSupabase: vi.fn(), })); diff --git a/frontend/portal/src/api/http.ts b/frontend/editor/src/portal/api/http.ts similarity index 97% rename from frontend/portal/src/api/http.ts rename to frontend/editor/src/portal/api/http.ts index a37df3ce81..9f7b28da03 100644 --- a/frontend/portal/src/api/http.ts +++ b/frontend/editor/src/portal/api/http.ts @@ -5,7 +5,7 @@ * * apiClient.local Same-origin (vite proxy → this instance's local * Stirling backend on :8080). Spring admin bearer - * (`stirling_jwt` from @shared/auth) auto-attached. + * (`stirling_jwt` from @app/auth) auto-attached. * USE FOR: actions on this instance — * /api/v1/account-link/{status,link,unlink}, etc. * @@ -39,8 +39,8 @@ * entitlement calls. It never enters the portal — the browser is the human * admin and uses the Supabase JWT for SaaS reads. Don't add it here. */ -import { clearStoredToken, getStoredToken } from "@shared/auth"; -import { getSupabaseClient } from "@shared/auth/supabase/supabaseClient"; +import { clearStoredToken, getStoredToken } from "@app/auth"; +import { getSupabaseClient } from "@app/auth/supabase/supabaseClient"; import { ensureSaasSupabase } from "@portal/auth/saasSupabase"; /** Read the SaaS base URL at call time so tests can stub it via vi.stubEnv. */ diff --git a/frontend/portal/src/api/infrastructure.ts b/frontend/editor/src/portal/api/infrastructure.ts similarity index 100% rename from frontend/portal/src/api/infrastructure.ts rename to frontend/editor/src/portal/api/infrastructure.ts diff --git a/frontend/portal/src/api/link.test.ts b/frontend/editor/src/portal/api/link.test.ts similarity index 98% rename from frontend/portal/src/api/link.test.ts rename to frontend/editor/src/portal/api/link.test.ts index e6d9b12703..2f5260486a 100644 --- a/frontend/portal/src/api/link.test.ts +++ b/frontend/editor/src/portal/api/link.test.ts @@ -20,7 +20,7 @@ const { getSession } = vi.hoisted(() => ({ data: { session: { access_token: "supabase_jwt_test" } }, }), })); -vi.mock("@shared/auth/supabase/supabaseClient", () => ({ +vi.mock("@app/auth/supabase/supabaseClient", () => ({ getSupabaseClient: () => ({ auth: { getSession } }), configureSupabase: vi.fn(), })); diff --git a/frontend/portal/src/api/link.ts b/frontend/editor/src/portal/api/link.ts similarity index 100% rename from frontend/portal/src/api/link.ts rename to frontend/editor/src/portal/api/link.ts diff --git a/frontend/portal/src/api/notifications.ts b/frontend/editor/src/portal/api/notifications.ts similarity index 100% rename from frontend/portal/src/api/notifications.ts rename to frontend/editor/src/portal/api/notifications.ts diff --git a/frontend/portal/src/api/ops.ts b/frontend/editor/src/portal/api/ops.ts similarity index 100% rename from frontend/portal/src/api/ops.ts rename to frontend/editor/src/portal/api/ops.ts diff --git a/frontend/portal/src/api/pipelines.ts b/frontend/editor/src/portal/api/pipelines.ts similarity index 100% rename from frontend/portal/src/api/pipelines.ts rename to frontend/editor/src/portal/api/pipelines.ts diff --git a/frontend/portal/src/api/policies.ts b/frontend/editor/src/portal/api/policies.ts similarity index 95% rename from frontend/portal/src/api/policies.ts rename to frontend/editor/src/portal/api/policies.ts index 70eccd5cfb..e225449a16 100644 --- a/frontend/portal/src/api/policies.ts +++ b/frontend/editor/src/portal/api/policies.ts @@ -11,9 +11,9 @@ */ import { apiClient } from "@portal/api/http"; -import { fromWirePolicy, toWirePolicy } from "@shared/policies/codec"; -import { runsToActivity, runsToStats } from "@shared/policies/runs"; -import type { PolicyDecodedState, WirePolicy } from "@shared/policies/types"; +import { fromWirePolicy, toWirePolicy } from "@app/policies/codec"; +import { runsToActivity, runsToStats } from "@app/policies/runs"; +import type { PolicyDecodedState, WirePolicy } from "@app/policies/types"; import { POLICY_CATEGORIES, POLICY_CONFIG, @@ -25,7 +25,7 @@ import { type PolicyState, type PolicyStatus, } from "@portal/mocks/policies"; -import type { PolicyRunView } from "@shared/policies/types"; +import type { PolicyRunView } from "@app/policies/types"; export type { CatalogueEntry, @@ -58,7 +58,7 @@ export { } from "@portal/mocks/policies"; // Re-export the wire step type under the legacy name components depend on. -export type { WirePipelineStep as PipelineStep } from "@shared/policies/types"; +export type { WirePipelineStep as PipelineStep } from "@app/policies/types"; // ── Client-side catalogue assembly ─────────────────────────────────────────── diff --git a/frontend/portal/src/api/procurement.ts b/frontend/editor/src/portal/api/procurement.ts similarity index 100% rename from frontend/portal/src/api/procurement.ts rename to frontend/editor/src/portal/api/procurement.ts diff --git a/frontend/portal/src/api/sdkComponents.ts b/frontend/editor/src/portal/api/sdkComponents.ts similarity index 100% rename from frontend/portal/src/api/sdkComponents.ts rename to frontend/editor/src/portal/api/sdkComponents.ts diff --git a/frontend/portal/src/api/search.ts b/frontend/editor/src/portal/api/search.ts similarity index 100% rename from frontend/portal/src/api/search.ts rename to frontend/editor/src/portal/api/search.ts diff --git a/frontend/portal/src/api/settings.ts b/frontend/editor/src/portal/api/settings.ts similarity index 100% rename from frontend/portal/src/api/settings.ts rename to frontend/editor/src/portal/api/settings.ts diff --git a/frontend/portal/src/api/sources.ts b/frontend/editor/src/portal/api/sources.ts similarity index 100% rename from frontend/portal/src/api/sources.ts rename to frontend/editor/src/portal/api/sources.ts diff --git a/frontend/portal/src/api/users.ts b/frontend/editor/src/portal/api/users.ts similarity index 100% rename from frontend/portal/src/api/users.ts rename to frontend/editor/src/portal/api/users.ts diff --git a/frontend/portal/src/auth/editorUrl.ts b/frontend/editor/src/portal/auth/editorUrl.ts similarity index 86% rename from frontend/portal/src/auth/editorUrl.ts rename to frontend/editor/src/portal/auth/editorUrl.ts index 2cf77c75b1..43eb466cb7 100644 --- a/frontend/portal/src/auth/editorUrl.ts +++ b/frontend/editor/src/portal/auth/editorUrl.ts @@ -5,6 +5,6 @@ * Sourced from VITE_EDITOR_URL so it's configurable per deploy rather than * hardcoded. The committed default is "/" (production serves the editor at the * root on the same origin as the portal). For dev cross-app navigation to a - * separately-running editor, set VITE_EDITOR_URL in portal/.env.local. + * separately-running editor, set VITE_EDITOR_URL in editor/.env.local. */ export const EDITOR_URL = import.meta.env.VITE_EDITOR_URL; diff --git a/frontend/portal/src/auth/saasSupabase.ts b/frontend/editor/src/portal/auth/saasSupabase.ts similarity index 97% rename from frontend/portal/src/auth/saasSupabase.ts rename to frontend/editor/src/portal/auth/saasSupabase.ts index 25a8a6ed6b..813a2f7cad 100644 --- a/frontend/portal/src/auth/saasSupabase.ts +++ b/frontend/editor/src/portal/auth/saasSupabase.ts @@ -1,7 +1,7 @@ import { configureSupabase, getSupabaseClient, -} from "@shared/auth/supabase/supabaseClient"; +} from "@app/auth/supabase/supabaseClient"; /** * Configures the shared Supabase client against the hosted SaaS project so the diff --git a/frontend/portal/src/auth/saasSupabaseLogin.test.ts b/frontend/editor/src/portal/auth/saasSupabaseLogin.test.ts similarity index 96% rename from frontend/portal/src/auth/saasSupabaseLogin.test.ts rename to frontend/editor/src/portal/auth/saasSupabaseLogin.test.ts index 5517897ec9..f1d991a3c0 100644 --- a/frontend/portal/src/auth/saasSupabaseLogin.test.ts +++ b/frontend/editor/src/portal/auth/saasSupabaseLogin.test.ts @@ -8,11 +8,11 @@ const { signInWithPassword, signInWithOAuth } = vi.hoisted(() => ({ signInWithOAuth: vi.fn(), })); -vi.mock("@shared/auth/supabase/supabaseClient", () => ({ +vi.mock("@app/auth/supabase/supabaseClient", () => ({ getSupabaseClient: () => ({ auth: { signInWithPassword, signInWithOAuth } }), })); -import { useSupabaseLogin } from "@shared/auth/ui/useSupabaseLogin"; +import { useSupabaseLogin } from "@app/auth/ui/useSupabaseLogin"; describe("useSupabaseLogin (in-app account-link login)", () => { beforeEach(() => { diff --git a/frontend/portal/src/billing/sharedBillingFormat.test.ts b/frontend/editor/src/portal/billing/sharedBillingFormat.test.ts similarity index 96% rename from frontend/portal/src/billing/sharedBillingFormat.test.ts rename to frontend/editor/src/portal/billing/sharedBillingFormat.test.ts index 8882df796b..d7ccd22df7 100644 --- a/frontend/portal/src/billing/sharedBillingFormat.test.ts +++ b/frontend/editor/src/portal/billing/sharedBillingFormat.test.ts @@ -5,10 +5,10 @@ import { formatMinor, formatPeriodDate, meterState, -} from "@shared/billing"; +} from "@app/billing"; /** - * Unit tests for the @shared/billing money/meter helpers the portal billing + * Unit tests for the @app/billing money/meter helpers the portal billing * surface (and the editor cloud surface) depend on. docCapForMoney mirrors the * backend's cap→PDF conversion and meterState mirrors the BE warn/degrade bands, * so these invariants matter beyond cosmetics. diff --git a/frontend/portal/src/billing/stripe.test.ts b/frontend/editor/src/portal/billing/stripe.test.ts similarity index 98% rename from frontend/portal/src/billing/stripe.test.ts rename to frontend/editor/src/portal/billing/stripe.test.ts index eeb9e16dcd..53a732fb63 100644 --- a/frontend/portal/src/billing/stripe.test.ts +++ b/frontend/editor/src/portal/billing/stripe.test.ts @@ -11,7 +11,7 @@ const { getClient, invoke } = vi.hoisted(() => ({ })); vi.mock("@portal/auth/saasSupabase", () => ({ ensureSaasSupabase: vi.fn() })); -vi.mock("@shared/auth/supabase/supabaseClient", () => ({ +vi.mock("@app/auth/supabase/supabaseClient", () => ({ getSupabaseClient: () => getClient(), configureSupabase: vi.fn(), })); diff --git a/frontend/portal/src/billing/stripe.ts b/frontend/editor/src/portal/billing/stripe.ts similarity index 98% rename from frontend/portal/src/billing/stripe.ts rename to frontend/editor/src/portal/billing/stripe.ts index 068046ef37..87fa82cf7b 100644 --- a/frontend/portal/src/billing/stripe.ts +++ b/frontend/editor/src/portal/billing/stripe.ts @@ -1,4 +1,4 @@ -import { getSupabaseClient } from "@shared/auth/supabase/supabaseClient"; +import { getSupabaseClient } from "@app/auth/supabase/supabaseClient"; import { ensureSaasSupabase } from "@portal/auth/saasSupabase"; /** diff --git a/frontend/portal/src/components/AppShell.css b/frontend/editor/src/portal/components/AppShell.css similarity index 100% rename from frontend/portal/src/components/AppShell.css rename to frontend/editor/src/portal/components/AppShell.css diff --git a/frontend/portal/src/components/AppShell.tsx b/frontend/editor/src/portal/components/AppShell.tsx similarity index 100% rename from frontend/portal/src/components/AppShell.tsx rename to frontend/editor/src/portal/components/AppShell.tsx diff --git a/frontend/portal/src/components/AssistantButton.css b/frontend/editor/src/portal/components/AssistantButton.css similarity index 100% rename from frontend/portal/src/components/AssistantButton.css rename to frontend/editor/src/portal/components/AssistantButton.css diff --git a/frontend/portal/src/components/AssistantButton.tsx b/frontend/editor/src/portal/components/AssistantButton.tsx similarity index 87% rename from frontend/portal/src/components/AssistantButton.tsx rename to frontend/editor/src/portal/components/AssistantButton.tsx index e88130094b..7f3586c8cc 100644 --- a/frontend/portal/src/components/AssistantButton.tsx +++ b/frontend/editor/src/portal/components/AssistantButton.tsx @@ -11,9 +11,11 @@ export function AssistantButton() { type="button" className={"portal-assistant-btn" + (assistantOpen ? " is-active" : "")} onClick={toggleAssistant} - aria-label={assistantOpen ? t("assistant.close") : t("assistant.open")} + aria-label={ + assistantOpen ? t("portal.assistant.close") : t("portal.assistant.open") + } aria-expanded={assistantOpen} - title={t("assistant.title")} + title={t("portal.assistant.title")} > {assistantOpen ? ( [...prev, failMsg]); } finally { @@ -86,20 +86,20 @@ export function AssistantPanel() {
- {t("assistant.title")} + {t("portal.assistant.title")}
@@ -109,7 +109,7 @@ export function AssistantPanel() { {messages.length === 0 && suggestions && (
- {t("assistant.tryAsking")} + {t("portal.assistant.tryAsking")}
{suggestions.map((s) => ( @@ -140,7 +140,7 @@ export function AssistantPanel() {
@@ -161,8 +161,8 @@ export function AssistantPanel() { ref={inputRef} value={input} onChange={(e) => setInput(e.target.value)} - placeholder={t("assistant.inputPlaceholder")} - aria-label={t("assistant.inputAriaLabel")} + placeholder={t("portal.assistant.inputPlaceholder")} + aria-label={t("portal.assistant.inputAriaLabel")} className="portal-assistant__input" disabled={typing} /> @@ -170,7 +170,7 @@ export function AssistantPanel() { type="submit" className="portal-assistant__send" disabled={!input.trim() || typing} - aria-label={t("assistant.send")} + aria-label={t("portal.assistant.send")} > diff --git a/frontend/portal/src/components/AuthGate.tsx b/frontend/editor/src/portal/components/AuthGate.tsx similarity index 83% rename from frontend/portal/src/components/AuthGate.tsx rename to frontend/editor/src/portal/components/AuthGate.tsx index 6d923abd12..a7f4e58ef5 100644 --- a/frontend/portal/src/components/AuthGate.tsx +++ b/frontend/editor/src/portal/components/AuthGate.tsx @@ -1,7 +1,7 @@ import { type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { RequirePortalAccess } from "@shared/auth"; -import { Spinner } from "@shared/components"; +import { RequirePortalAccess } from "@app/auth"; +import { Spinner } from "@app/ui"; import { LoginScreen } from "@portal/components/LoginScreen"; import { EDITOR_URL } from "@portal/auth/editorUrl"; @@ -36,12 +36,12 @@ export function AuthGate({ children }: { children: ReactNode }) { onForbidden={redirectToEditor} loading={ - + } forbidden={ - {t("auth.redirectingToEditor", "Redirecting to the editor...")} + {t("portal.auth.redirectingToEditor", "Redirecting to the editor...")} } > diff --git a/frontend/portal/src/components/ChatFABWidget.stories.tsx b/frontend/editor/src/portal/components/ChatFABWidget.stories.tsx similarity index 98% rename from frontend/portal/src/components/ChatFABWidget.stories.tsx rename to frontend/editor/src/portal/components/ChatFABWidget.stories.tsx index 1d49dc8e5b..db9b006173 100644 --- a/frontend/portal/src/components/ChatFABWidget.stories.tsx +++ b/frontend/editor/src/portal/components/ChatFABWidget.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { useRef, useState } from "react"; -import { ChatFABButton } from "@shared/components/ChatFABButton"; -import { ChatFABWindow } from "@shared/components/ChatFABWindow"; +import { ChatFABButton } from "@app/ui/ChatFABButton"; +import { ChatFABWindow } from "@app/ui/ChatFABWindow"; /** * Full ChatFAB widget demo — composed from ChatFABButton + ChatFABWindow. diff --git a/frontend/portal/src/components/ErrorBoundary.test.tsx b/frontend/editor/src/portal/components/ErrorBoundary.test.tsx similarity index 81% rename from frontend/portal/src/components/ErrorBoundary.test.tsx rename to frontend/editor/src/portal/components/ErrorBoundary.test.tsx index 91c22effca..4e84344e77 100644 --- a/frontend/portal/src/components/ErrorBoundary.test.tsx +++ b/frontend/editor/src/portal/components/ErrorBoundary.test.tsx @@ -25,8 +25,10 @@ describe("ErrorBoundary", () => { , ); - expect(screen.getByText("errorBoundary.title")).toBeInTheDocument(); - expect(screen.getByText("errorBoundary.description")).toBeInTheDocument(); + expect(screen.getByText("portal.errorBoundary.title")).toBeInTheDocument(); + expect( + screen.getByText("portal.errorBoundary.description"), + ).toBeInTheDocument(); spy.mockRestore(); }); @@ -51,14 +53,16 @@ describe("ErrorBoundary", () => { ); // Fallback is shown. - expect(screen.getByText("errorBoundary.title")).toBeInTheDocument(); + expect(screen.getByText("portal.errorBoundary.title")).toBeInTheDocument(); // Stop throwing, then click retry to clear the boundary's error state. shouldThrow = false; - fireEvent.click(screen.getByText("errorBoundary.retry")); + fireEvent.click(screen.getByText("portal.errorBoundary.retry")); expect(screen.getByText("recovered content")).toBeInTheDocument(); - expect(screen.queryByText("errorBoundary.title")).not.toBeInTheDocument(); + expect( + screen.queryByText("portal.errorBoundary.title"), + ).not.toBeInTheDocument(); spy.mockRestore(); }); }); diff --git a/frontend/portal/src/components/ErrorBoundary.tsx b/frontend/editor/src/portal/components/ErrorBoundary.tsx similarity index 88% rename from frontend/portal/src/components/ErrorBoundary.tsx rename to frontend/editor/src/portal/components/ErrorBoundary.tsx index f97d208a31..4c5dad9b0e 100644 --- a/frontend/portal/src/components/ErrorBoundary.tsx +++ b/frontend/editor/src/portal/components/ErrorBoundary.tsx @@ -1,6 +1,6 @@ import { Component, type ErrorInfo, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { Button, EmptyState } from "@shared/components"; +import { Button, EmptyState } from "@app/ui"; /** * Default fallback for {@link ErrorBoundary}. Split into a function component so @@ -11,9 +11,11 @@ function DefaultErrorFallback({ onRetry }: { onRetry: () => void }) { const { t } = useTranslation(); return ( {t("errorBoundary.retry")}} + title={t("portal.errorBoundary.title")} + description={t("portal.errorBoundary.description")} + actions={ + + } /> ); } diff --git a/frontend/portal/src/components/Header.css b/frontend/editor/src/portal/components/Header.css similarity index 100% rename from frontend/portal/src/components/Header.css rename to frontend/editor/src/portal/components/Header.css diff --git a/frontend/portal/src/components/Header.stories.tsx b/frontend/editor/src/portal/components/Header.stories.tsx similarity index 100% rename from frontend/portal/src/components/Header.stories.tsx rename to frontend/editor/src/portal/components/Header.stories.tsx diff --git a/frontend/portal/src/components/Header.tsx b/frontend/editor/src/portal/components/Header.tsx similarity index 85% rename from frontend/portal/src/components/Header.tsx rename to frontend/editor/src/portal/components/Header.tsx index 8783e9bafc..7194094171 100644 --- a/frontend/portal/src/components/Header.tsx +++ b/frontend/editor/src/portal/components/Header.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; -import { Avatar, Dropdown } from "@shared/components"; -import { useAuth } from "@shared/auth"; +import { Avatar, Dropdown } from "@app/ui"; +import { useAuth } from "@app/auth"; import { useTheme } from "@portal/contexts/ThemeContext"; import { useTier, TIER_INFO, type Tier } from "@portal/contexts/TierContext"; import { useView } from "@portal/contexts/ViewContext"; @@ -25,13 +25,13 @@ function ThemeToggle() { onClick={toggle} aria-label={ theme === "light" - ? t("shell.header.switchToDark") - : t("shell.header.switchToLight") + ? t("portal.shell.header.switchToDark") + : t("portal.shell.header.switchToLight") } title={ theme === "light" - ? t("shell.header.darkMode") - : t("shell.header.lightMode") + ? t("portal.shell.header.darkMode") + : t("portal.shell.header.lightMode") } > {theme === "light" ? : } @@ -84,14 +84,15 @@ function TierSwitcher() { function UserMenu() { const { t } = useTranslation(); const { displayName, signOut } = useAuth(); - const name = displayName ?? t("shell.header.accountFallback", "Account"); + const name = + displayName ?? t("portal.shell.header.accountFallback", "Account"); return ( ); diff --git a/frontend/portal/src/components/NotificationsDropdown.css b/frontend/editor/src/portal/components/NotificationsDropdown.css similarity index 100% rename from frontend/portal/src/components/NotificationsDropdown.css rename to frontend/editor/src/portal/components/NotificationsDropdown.css diff --git a/frontend/portal/src/components/NotificationsDropdown.stories.tsx b/frontend/editor/src/portal/components/NotificationsDropdown.stories.tsx similarity index 100% rename from frontend/portal/src/components/NotificationsDropdown.stories.tsx rename to frontend/editor/src/portal/components/NotificationsDropdown.stories.tsx diff --git a/frontend/portal/src/components/NotificationsDropdown.tsx b/frontend/editor/src/portal/components/NotificationsDropdown.tsx similarity index 85% rename from frontend/portal/src/components/NotificationsDropdown.tsx rename to frontend/editor/src/portal/components/NotificationsDropdown.tsx index c93a2f9f33..4a0d84d5c3 100644 --- a/frontend/portal/src/components/NotificationsDropdown.tsx +++ b/frontend/editor/src/portal/components/NotificationsDropdown.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Dropdown, EmptyState, Skeleton } from "@shared/components"; +import { Dropdown, EmptyState, Skeleton } from "@app/ui"; import { BellIcon } from "@portal/components/icons"; import { useAsync, useSectionFlags } from "@portal/hooks/useAsync"; import { @@ -50,10 +50,10 @@ export function NotificationsDropdown() { className="portal-header__icon-btn portal-header__icon-btn--badge" aria-label={ hasUnread - ? t("notifications.ariaLabel.unread", { + ? t("portal.notifications.ariaLabel.unread", { count: visible.length, }) - : t("notifications.ariaLabel.none") + : t("portal.notifications.ariaLabel.none") } > @@ -65,19 +65,19 @@ export function NotificationsDropdown() {
- {t("notifications.title")} + {t("portal.notifications.title")} {hasUnread ? ( - {t("notifications.count.new", { count: visible.length })} + {t("portal.notifications.count.new", { count: visible.length })} ) : isLoading ? ( - {t("notifications.count.loading")} + {t("portal.notifications.count.loading")} ) : ( - {t("notifications.count.allRead")} + {t("portal.notifications.count.allRead")} )}
@@ -92,8 +92,8 @@ export function NotificationsDropdown() { {isEmpty && ( )} {!isLoading && !isEmpty && ( @@ -121,10 +121,10 @@ export function NotificationsDropdown() { onClick={onMarkAllRead} disabled={!hasUnread} > - {t("notifications.markAllRead")} + {t("portal.notifications.markAllRead")}
diff --git a/frontend/portal/src/components/PipelineForkWizard.css b/frontend/editor/src/portal/components/PipelineForkWizard.css similarity index 100% rename from frontend/portal/src/components/PipelineForkWizard.css rename to frontend/editor/src/portal/components/PipelineForkWizard.css diff --git a/frontend/portal/src/components/PipelineForkWizard.stories.tsx b/frontend/editor/src/portal/components/PipelineForkWizard.stories.tsx similarity index 100% rename from frontend/portal/src/components/PipelineForkWizard.stories.tsx rename to frontend/editor/src/portal/components/PipelineForkWizard.stories.tsx diff --git a/frontend/portal/src/components/PipelineForkWizard.tsx b/frontend/editor/src/portal/components/PipelineForkWizard.tsx similarity index 90% rename from frontend/portal/src/components/PipelineForkWizard.tsx rename to frontend/editor/src/portal/components/PipelineForkWizard.tsx index a357a6bb75..838afda3db 100644 --- a/frontend/portal/src/components/PipelineForkWizard.tsx +++ b/frontend/editor/src/portal/components/PipelineForkWizard.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button, Card, Chip, StatusBadge } from "@shared/components"; +import { Button, Card, Chip, StatusBadge } from "@app/ui"; import { useView } from "@portal/contexts/ViewContext"; import { PIPELINE_STAGES, @@ -70,14 +70,14 @@ export function PipelineForkWizard() {
-

{t("forkWizard.title")}

-

{t("forkWizard.subtitle")}

+

{t("portal.forkWizard.title")}

+

{t("portal.forkWizard.subtitle")}

{phase !== "pick" && template && ( {phase === "ready" - ? t("forkWizard.status.ready") - : t("forkWizard.status.building")} + ? t("portal.forkWizard.status.ready") + : t("portal.forkWizard.status.building")} )}
@@ -141,8 +141,8 @@ export function PipelineForkWizard() {
diff --git a/frontend/portal/src/components/PolicySummary.css b/frontend/editor/src/portal/components/PolicySummary.css similarity index 100% rename from frontend/portal/src/components/PolicySummary.css rename to frontend/editor/src/portal/components/PolicySummary.css diff --git a/frontend/portal/src/components/PolicySummary.stories.tsx b/frontend/editor/src/portal/components/PolicySummary.stories.tsx similarity index 100% rename from frontend/portal/src/components/PolicySummary.stories.tsx rename to frontend/editor/src/portal/components/PolicySummary.stories.tsx diff --git a/frontend/portal/src/components/PolicySummary.tsx b/frontend/editor/src/portal/components/PolicySummary.tsx similarity index 81% rename from frontend/portal/src/components/PolicySummary.tsx rename to frontend/editor/src/portal/components/PolicySummary.tsx index 57199bc8f7..084e7e5b69 100644 --- a/frontend/portal/src/components/PolicySummary.tsx +++ b/frontend/editor/src/portal/components/PolicySummary.tsx @@ -7,7 +7,7 @@ import { StatusBadge, Table, type TableColumn, -} from "@shared/components"; +} from "@app/ui"; import { useView } from "@portal/contexts/ViewContext"; import { useAsync, useSectionFlags } from "@portal/hooks/useAsync"; import { @@ -37,9 +37,9 @@ const STATE_BADGE: Record< RowState, { tone: "success" | "neutral" | "info"; labelKey: string } > = { - active: { tone: "success", labelKey: "policySummary.state.active" }, - off: { tone: "neutral", labelKey: "policySummary.state.off" }, - locked: { tone: "info", labelKey: "policySummary.state.soon" }, + active: { tone: "success", labelKey: "portal.policySummary.state.active" }, + off: { tone: "neutral", labelKey: "portal.policySummary.state.off" }, + locked: { tone: "info", labelKey: "portal.policySummary.state.soon" }, }; function toRow(entry: CatalogueEntry): PolicyRow { @@ -60,7 +60,7 @@ export function PolicySummary() { const columns: TableColumn[] = [ { key: "category", - header: t("policySummary.column.policy"), + header: t("portal.policySummary.column.policy"), render: ({ entry }) => (
@@ -75,7 +75,7 @@ export function PolicySummary() { }, { key: "status", - header: t("policySummary.column.status"), + header: t("portal.policySummary.column.status"), width: "7rem", render: ({ state }) => { const badge = STATE_BADGE[state]; @@ -88,12 +88,12 @@ export function PolicySummary() { }, { key: "rule", - header: t("policySummary.column.activeRule"), + header: t("portal.policySummary.column.activeRule"), render: ({ entry, state }) => ( {state === "active" ? entry.config.summary - : t("policySummary.noRule")} + : t("portal.policySummary.noRule")} ), }, @@ -106,7 +106,7 @@ export function PolicySummary() { if (state === "locked") { return ( ); } @@ -117,8 +117,8 @@ export function PolicySummary() { onClick={goToPolicies} > {state === "active" - ? t("policySummary.action.configure") - : t("policySummary.action.setUp")} + ? t("portal.policySummary.action.configure") + : t("portal.policySummary.action.setUp")} ); }, @@ -128,20 +128,23 @@ export function PolicySummary() { const rows: PolicyRow[] = data?.catalogue?.map(toRow) ?? []; return ( -
+

- {t("policySummary.title")} + {t("portal.policySummary.title")}

- {t("policySummary.subtitle")} + {t("portal.policySummary.subtitle")}

{data?.summary && ( - {t("policySummary.activeSummary", { + {t("portal.policySummary.activeSummary", { active: data.summary.active, total: data.summary.categories, })} @@ -163,8 +166,8 @@ export function PolicySummary() { {isEmpty && ( )} diff --git a/frontend/portal/src/components/PopularUseCases.css b/frontend/editor/src/portal/components/PopularUseCases.css similarity index 100% rename from frontend/portal/src/components/PopularUseCases.css rename to frontend/editor/src/portal/components/PopularUseCases.css diff --git a/frontend/portal/src/components/PopularUseCases.tsx b/frontend/editor/src/portal/components/PopularUseCases.tsx similarity index 79% rename from frontend/portal/src/components/PopularUseCases.tsx rename to frontend/editor/src/portal/components/PopularUseCases.tsx index 6f99f74453..b85912761f 100644 --- a/frontend/portal/src/components/PopularUseCases.tsx +++ b/frontend/editor/src/portal/components/PopularUseCases.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Card, type CardProps } from "@shared/components"; +import { Card, type CardProps } from "@app/ui"; import { useView } from "@portal/contexts/ViewContext"; import "@portal/components/PopularUseCases.css"; @@ -36,15 +36,18 @@ export function PopularUseCases() { const { t } = useTranslation(); const { setActiveView } = useView(); return ( -
+
-

{t("useCases.title")}

+

{t("portal.useCases.title")}

@@ -59,13 +62,13 @@ export function PopularUseCases() { className="portal-usecases__eyebrow" style={{ color: ACCENT_COLOR[uc.accent] }} > - {t(`useCases.items.${uc.key}.eyebrow`)} + {t(`portal.useCases.items.${uc.key}.eyebrow`)}

- {t(`useCases.items.${uc.key}.title`)} + {t(`portal.useCases.items.${uc.key}.title`)}

- {t(`useCases.items.${uc.key}.blurb`)} + {t(`portal.useCases.items.${uc.key}.blurb`)}

))} diff --git a/frontend/portal/src/components/ProcessingStatusStrip.css b/frontend/editor/src/portal/components/ProcessingStatusStrip.css similarity index 100% rename from frontend/portal/src/components/ProcessingStatusStrip.css rename to frontend/editor/src/portal/components/ProcessingStatusStrip.css diff --git a/frontend/portal/src/components/ProcessingStatusStrip.stories.tsx b/frontend/editor/src/portal/components/ProcessingStatusStrip.stories.tsx similarity index 100% rename from frontend/portal/src/components/ProcessingStatusStrip.stories.tsx rename to frontend/editor/src/portal/components/ProcessingStatusStrip.stories.tsx diff --git a/frontend/portal/src/components/ProcessingStatusStrip.tsx b/frontend/editor/src/portal/components/ProcessingStatusStrip.tsx similarity index 89% rename from frontend/portal/src/components/ProcessingStatusStrip.tsx rename to frontend/editor/src/portal/components/ProcessingStatusStrip.tsx index 05adba3e40..9aa3025f98 100644 --- a/frontend/portal/src/components/ProcessingStatusStrip.tsx +++ b/frontend/editor/src/portal/components/ProcessingStatusStrip.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Banner, Button, ProgressBar, Skeleton } from "@shared/components"; +import { Banner, Button, ProgressBar, Skeleton } from "@app/ui"; import { TIER_INFO, useTier } from "@portal/contexts/TierContext"; import { useView } from "@portal/contexts/ViewContext"; import { useAsync } from "@portal/hooks/useAsync"; @@ -60,7 +60,7 @@ export function ProcessingStatusStrip() { variant="outline" onClick={() => setActiveView("usage")} > - {t("processingStatus.upgrade")} + {t("portal.processingStatus.upgrade")} ) : undefined } @@ -68,7 +68,7 @@ export function ProcessingStatusStrip() {
{used.toLocaleString()} / {cap.toLocaleString()}{" "} - {t("processingStatus.pdfsThisMonth")} + {t("portal.processingStatus.pdfsThisMonth")} {Math.round(ratio * 100)}% @@ -77,7 +77,7 @@ export function ProcessingStatusStrip() { ); @@ -99,7 +99,8 @@ export function ProcessingStatusStrip() { · - {volume ?? "—"} {t("processingStatus.volumeSuffix")} + {volume ?? "—"}{" "} + {t("portal.processingStatus.volumeSuffix")}
); diff --git a/frontend/portal/src/components/RecentActivity.css b/frontend/editor/src/portal/components/RecentActivity.css similarity index 100% rename from frontend/portal/src/components/RecentActivity.css rename to frontend/editor/src/portal/components/RecentActivity.css diff --git a/frontend/portal/src/components/RecentActivity.stories.tsx b/frontend/editor/src/portal/components/RecentActivity.stories.tsx similarity index 100% rename from frontend/portal/src/components/RecentActivity.stories.tsx rename to frontend/editor/src/portal/components/RecentActivity.stories.tsx diff --git a/frontend/portal/src/components/RecentActivity.tsx b/frontend/editor/src/portal/components/RecentActivity.tsx similarity index 87% rename from frontend/portal/src/components/RecentActivity.tsx rename to frontend/editor/src/portal/components/RecentActivity.tsx index 024eb2f6ce..8934a5f0bd 100644 --- a/frontend/portal/src/components/RecentActivity.tsx +++ b/frontend/editor/src/portal/components/RecentActivity.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Card, EmptyState, Skeleton, StatusBadge } from "@shared/components"; +import { Card, EmptyState, Skeleton, StatusBadge } from "@app/ui"; import { useAsync, useSectionFlags } from "@portal/hooks/useAsync"; import { fetchRecentActivity, @@ -27,12 +27,14 @@ export function RecentActivity() {
-

{t("recentActivity.title")}

+

+ {t("portal.recentActivity.title")} +

@@ -56,8 +58,8 @@ export function RecentActivity() { {isEmpty && ( )} diff --git a/frontend/portal/src/components/SearchModal.css b/frontend/editor/src/portal/components/SearchModal.css similarity index 100% rename from frontend/portal/src/components/SearchModal.css rename to frontend/editor/src/portal/components/SearchModal.css diff --git a/frontend/portal/src/components/SearchModal.stories.tsx b/frontend/editor/src/portal/components/SearchModal.stories.tsx similarity index 100% rename from frontend/portal/src/components/SearchModal.stories.tsx rename to frontend/editor/src/portal/components/SearchModal.stories.tsx diff --git a/frontend/portal/src/components/SearchModal.tsx b/frontend/editor/src/portal/components/SearchModal.tsx similarity index 88% rename from frontend/portal/src/components/SearchModal.tsx rename to frontend/editor/src/portal/components/SearchModal.tsx index df27a86732..ca66f9365e 100644 --- a/frontend/portal/src/components/SearchModal.tsx +++ b/frontend/editor/src/portal/components/SearchModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { EmptyState, Modal, Skeleton } from "@shared/components"; +import { EmptyState, Modal, Skeleton } from "@app/ui"; import { useUI } from "@portal/contexts/UIContext"; import { useAsync, useSectionFlags } from "@portal/hooks/useAsync"; import { fetchQuickActions, type QuickAction } from "@portal/api/search"; @@ -50,7 +50,7 @@ export function SearchModal() { open={searchOpen} onClose={closeSearch} width="lg" - ariaLabel={t("search.ariaLabel")} + ariaLabel={t("portal.search.ariaLabel")} >
@@ -59,8 +59,8 @@ export function SearchModal() { ref={inputRef} value={query} onChange={(e) => setQuery(e.target.value)} - placeholder={t("search.placeholder")} - aria-label={t("search.ariaLabel")} + placeholder={t("portal.search.placeholder")} + aria-label={t("portal.search.ariaLabel")} className="portal-search__input" autoComplete="off" spellCheck={false} @@ -84,13 +84,13 @@ export function SearchModal() { size="compact" title={ query.trim() - ? t("search.empty.noMatches", { query: query.trim() }) - : t("search.empty.noActionsTitle") + ? t("portal.search.empty.noMatches", { query: query.trim() }) + : t("portal.search.empty.noActionsTitle") } description={ query.trim() - ? t("search.empty.noMatchesDescription") - : t("search.empty.noActionsDescription") + ? t("portal.search.empty.noMatchesDescription") + : t("portal.search.empty.noActionsDescription") } /> )} diff --git a/frontend/portal/src/components/SettingsModal.css b/frontend/editor/src/portal/components/SettingsModal.css similarity index 100% rename from frontend/portal/src/components/SettingsModal.css rename to frontend/editor/src/portal/components/SettingsModal.css diff --git a/frontend/portal/src/components/SettingsModal.tsx b/frontend/editor/src/portal/components/SettingsModal.tsx similarity index 85% rename from frontend/portal/src/components/SettingsModal.tsx rename to frontend/editor/src/portal/components/SettingsModal.tsx index 7cb4ad8c44..aa01c2ba0e 100644 --- a/frontend/portal/src/components/SettingsModal.tsx +++ b/frontend/editor/src/portal/components/SettingsModal.tsx @@ -13,7 +13,7 @@ import { ToggleSwitch, type SelectOption, type SettingsNavSection, -} from "@shared/components"; +} from "@app/ui"; import { useTier, type Tier } from "@portal/contexts/TierContext"; import { useTheme, type Theme } from "@portal/contexts/ThemeContext"; import { useAsync } from "@portal/hooks/useAsync"; @@ -118,56 +118,56 @@ export function SettingsModal({ const navSections = useMemo( () => [ { - title: t("settings.groups.account"), + title: t("portal.settings.groups.account"), items: [ { key: "profile", - label: t("settings.sections.profile"), + label: t("portal.settings.sections.profile"), icon: , }, { key: "appearance", - label: t("settings.sections.appearance"), + label: t("portal.settings.sections.appearance"), icon: , }, { key: "notifications", - label: t("settings.sections.notifications"), + label: t("portal.settings.sections.notifications"), icon: , }, ], }, { - title: t("settings.groups.workspace"), + title: t("portal.settings.groups.workspace"), items: [ { key: "general", - label: t("settings.sections.general"), + label: t("portal.settings.sections.general"), icon: , }, ], }, { - title: t("settings.groups.admin"), + title: t("portal.settings.groups.admin"), items: [ { key: "account-link", - label: t("settings.sections.account-link"), + label: t("portal.settings.sections.account-link"), icon: , }, { key: "authentication", - label: t("settings.sections.authentication"), + label: t("portal.settings.sections.authentication"), icon: , }, { key: "sessions", - label: t("settings.sections.sessions"), + label: t("portal.settings.sections.sessions"), icon: , }, { key: "early-access", - label: t("settings.sections.early-access"), + label: t("portal.settings.sections.early-access"), icon: , }, ], @@ -231,7 +231,9 @@ export function SettingsModal({ value: r.value, label: r.enterpriseOnly && tier !== "enterprise" - ? t("settings.workspace.regionEnterpriseSuffix", { region: r.label }) + ? t("portal.settings.workspace.regionEnterpriseSuffix", { + region: r.label, + }) : r.label, disabled: r.enterpriseOnly && tier !== "enterprise", })); @@ -244,25 +246,25 @@ export function SettingsModal({ open={open} onClose={onClose} width="xl" - ariaLabel={t("settings.ariaLabel")} + ariaLabel={t("portal.settings.ariaLabel")} className="portal-settings" > setSection(k as SettingsSection)} - title={t(`settings.sections.${section}`)} + title={t(`portal.settings.sections.${section}`)} onClose={onClose} footer={ <> - {t("settings.footerNote")} + {t("portal.settings.footerNote")} } @@ -384,13 +386,13 @@ function ProfilePanel({
- {name || t("settings.profile.accountFallback")} + {name || t("portal.settings.profile.accountFallback")} {role && ( {role} @@ -400,27 +402,27 @@ function ProfilePanel({ {email}
- + onName(e.target.value)} - placeholder={t("settings.profile.namePlaceholder")} + placeholder={t("portal.settings.profile.namePlaceholder")} /> onEmail(e.target.value)} - placeholder={t("settings.profile.emailPlaceholder")} + placeholder={t("portal.settings.profile.emailPlaceholder")} />
@@ -444,16 +446,16 @@ function AppearancePanel({

- {t("settings.appearance.themeTitle")} + {t("portal.settings.appearance.themeTitle")}

- {t("settings.appearance.themeSub")} + {t("portal.settings.appearance.themeSub")}

{THEME_OPTIONS.map((opt) => ( ))} @@ -507,10 +511,10 @@ function NotificationsPanel({

- {t("settings.notifications.title")} + {t("portal.settings.notifications.title")}

- {t("settings.notifications.sub")} + {t("portal.settings.notifications.sub")}

@@ -537,8 +541,12 @@ function NotificationsPanel({ return (
- {t(`settings.notifications.${id}.label`)} - {t(`settings.notifications.${id}.description`)} + + {t(`portal.settings.notifications.${id}.label`)} + + + {t(`portal.settings.notifications.${id}.description`)} +
- + onWorkspaceName(e.target.value)} - placeholder={t("settings.workspace.namePlaceholder")} + placeholder={t("portal.settings.workspace.namePlaceholder")} /> ({ value, - label: t(`settings.authentication.timeout.${value}`), + label: t(`portal.settings.authentication.timeout.${value}`), }))} /> @@ -778,10 +790,10 @@ function SessionsPanel({

- {t("settings.sessions.title")} + {t("portal.settings.sessions.title")}

- {t("settings.sessions.sub")} + {t("portal.settings.sessions.sub")}

@@ -795,12 +807,12 @@ function SessionsPanel({
{s.current ? ( - {t("settings.sessions.thisDevice")} + {t("portal.settings.sessions.thisDevice")} ) : ( // TODO(backend): DELETE /v1/settings/sessions/{id} )}
@@ -845,10 +857,10 @@ function EarlyAccessPanel({

- {t("settings.earlyAccess.title")} + {t("portal.settings.earlyAccess.title")}

- {t("settings.earlyAccess.sub")} + {t("portal.settings.earlyAccess.sub")}

@@ -861,7 +873,7 @@ function EarlyAccessPanel({ {f.label} {locked && ( - {t("settings.enterpriseBadge")} + {t("portal.settings.enterpriseBadge")} )} diff --git a/frontend/portal/src/components/Sidebar.css b/frontend/editor/src/portal/components/Sidebar.css similarity index 100% rename from frontend/portal/src/components/Sidebar.css rename to frontend/editor/src/portal/components/Sidebar.css diff --git a/frontend/portal/src/components/Sidebar.stories.tsx b/frontend/editor/src/portal/components/Sidebar.stories.tsx similarity index 100% rename from frontend/portal/src/components/Sidebar.stories.tsx rename to frontend/editor/src/portal/components/Sidebar.stories.tsx diff --git a/frontend/portal/src/components/Sidebar.tsx b/frontend/editor/src/portal/components/Sidebar.tsx similarity index 88% rename from frontend/portal/src/components/Sidebar.tsx rename to frontend/editor/src/portal/components/Sidebar.tsx index ecfa0e7900..d9ced4ae65 100644 --- a/frontend/portal/src/components/Sidebar.tsx +++ b/frontend/editor/src/portal/components/Sidebar.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Dropdown, NavItem } from "@shared/components"; +import { Dropdown, NavItem } from "@app/ui"; import { useView, type ViewId } from "@portal/contexts/ViewContext"; import { useTier } from "@portal/contexts/TierContext"; import { useTheme } from "@portal/contexts/ThemeContext"; @@ -8,8 +8,8 @@ import { useLink } from "@portal/contexts/LinkContext"; import { useAsync } from "@portal/hooks/useAsync"; import { fetchHomeKpis, type KpiEntry } from "@portal/api/home"; import { EDITOR_URL } from "@portal/auth/editorUrl"; -import markLight from "@shared/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg"; -import markDark from "@shared/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg"; +import markLight from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg"; +import markDark from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg"; import { HomeIcon, UsersIcon, @@ -64,7 +64,7 @@ function LinkAccountFooterItem() { return ( } onClick={() => openLinkModal()} /> @@ -93,7 +93,7 @@ function UsageFooter() {
- {t("shell.sidebar.docsProcessed")} + {t("portal.shell.sidebar.docsProcessed")} {docs ?? "—"}
@@ -114,8 +114,8 @@ function UsageFooter() { const planLabel = tier === "pro" - ? t("shell.sidebar.planProcessor", "Processor plan") - : t("shell.sidebar.planEnterprise", "Enterprise plan"); + ? t("portal.shell.sidebar.planProcessor", "Processor plan") + : t("portal.shell.sidebar.planEnterprise", "Enterprise plan"); return (
@@ -125,7 +125,7 @@ function UsageFooter() { {planLabel} - {docs != null ? t("shell.sidebar.docsCount", { docs }) : "—"} + {docs != null ? t("portal.shell.sidebar.docsCount", { docs }) : "—"}
@@ -151,7 +151,7 @@ export function Sidebar() { setActiveView(id as ViewId)} @@ -162,7 +162,7 @@ export function Sidebar() { return (
- {t("opRunner.featuredOps")} + {t("portal.opRunner.featuredOps")}
{opsIsLoading && @@ -270,8 +264,8 @@ export function SingleOpRunner({ {opsIsEmpty && ( )} {ops?.map((op) => ( @@ -307,18 +301,18 @@ export function SingleOpRunner({ {phase === "idle" && selectedOp && (
- {t("opRunner.hint.ready")} + {t("portal.opRunner.hint.ready")}

- {t("opRunner.hint.runOn.before")}{" "} + {t("portal.opRunner.hint.runOn.before")}{" "} {selectedOp.label}{" "} - {t("opRunner.hint.runOn.middle")}{" "} - {sample ?? t("opRunner.hint.aSample")} + {t("portal.opRunner.hint.runOn.middle")}{" "} + {sample ?? t("portal.opRunner.hint.aSample")}

- {selectedOp.blurb}. {t("opRunner.hint.press")}{" "} - {t("opRunner.action.run")}{" "} - {t("opRunner.hint.toInvoke")}{" "} + {selectedOp.blurb}. {t("portal.opRunner.hint.press")}{" "} + {t("portal.opRunner.action.run")}{" "} + {t("portal.opRunner.hint.toInvoke")}{" "} POST {selectedOp.endpoint}.

@@ -328,7 +322,9 @@ export function SingleOpRunner({
- {t("opRunner.running.title", { label: selectedOp.label })} + {t("portal.opRunner.running.title", { + label: selectedOp.label, + })}
POST {selectedOp.endpoint}
@@ -338,11 +334,13 @@ export function SingleOpRunner({
- {t("opRunner.status.completed")} + {t("portal.opRunner.status.completed")} POST {selectedOp.endpoint} - {t("opRunner.durationMs", { ms: runResult.durationMs })} + {t("portal.opRunner.durationMs", { + ms: runResult.durationMs, + })}
@@ -356,10 +354,10 @@ export function SingleOpRunner({
                 className="portal-runner__hint-eyebrow"
                 style={{ color: "var(--color-red)" }}
               >
-                {t("opRunner.status.failed")}
+                {t("portal.opRunner.status.failed")}
               
-

{t("opRunner.error.title")}

-

{errorMsg ?? t("opRunner.error.unknown")}

+

{t("portal.opRunner.error.title")}

+

{errorMsg ?? t("portal.opRunner.error.unknown")}

)}
diff --git a/frontend/portal/src/components/UsageAreaChart.css b/frontend/editor/src/portal/components/UsageAreaChart.css similarity index 100% rename from frontend/portal/src/components/UsageAreaChart.css rename to frontend/editor/src/portal/components/UsageAreaChart.css diff --git a/frontend/portal/src/components/UsageAreaChart.stories.tsx b/frontend/editor/src/portal/components/UsageAreaChart.stories.tsx similarity index 100% rename from frontend/portal/src/components/UsageAreaChart.stories.tsx rename to frontend/editor/src/portal/components/UsageAreaChart.stories.tsx diff --git a/frontend/portal/src/components/UsageAreaChart.tsx b/frontend/editor/src/portal/components/UsageAreaChart.tsx similarity index 97% rename from frontend/portal/src/components/UsageAreaChart.tsx rename to frontend/editor/src/portal/components/UsageAreaChart.tsx index 65fd0e3eec..ff9750d57c 100644 --- a/frontend/portal/src/components/UsageAreaChart.tsx +++ b/frontend/editor/src/portal/components/UsageAreaChart.tsx @@ -35,7 +35,7 @@ export function UsageAreaChart({ deltaPct, }: UsageAreaChartProps) { const { t } = useTranslation(); - const totalLabel = totalLabelProp ?? t("usageChart.defaultLabel"); + const totalLabel = totalLabelProp ?? t("portal.usageChart.defaultLabel"); const [hoverIndex, setHoverIndex] = useState(null); const svgRef = useRef(null); @@ -153,7 +153,7 @@ export function UsageAreaChart({ } > {deltaPct >= 0 ? "↑" : "↓"} - {t("usageChart.delta", { + {t("portal.usageChart.delta", { pct: Math.abs(Math.round(deltaPct * 100)), })}
@@ -265,7 +265,7 @@ export function UsageAreaChart({ })}
- {t("usageChart.docsValue", { + {t("portal.usageChart.docsValue", { value: hovered.raw.value.toLocaleString(), })}
@@ -273,7 +273,7 @@ export function UsageAreaChart({ )}
{hovered - ? t("usageChart.srAnnounce", { + ? t("portal.usageChart.srAnnounce", { date: new Date(hovered.raw.date).toLocaleDateString(undefined, { weekday: "short", month: "short", diff --git a/frontend/portal/src/components/WelcomeCarousel.css b/frontend/editor/src/portal/components/WelcomeCarousel.css similarity index 100% rename from frontend/portal/src/components/WelcomeCarousel.css rename to frontend/editor/src/portal/components/WelcomeCarousel.css diff --git a/frontend/portal/src/components/WelcomeCarousel.stories.tsx b/frontend/editor/src/portal/components/WelcomeCarousel.stories.tsx similarity index 100% rename from frontend/portal/src/components/WelcomeCarousel.stories.tsx rename to frontend/editor/src/portal/components/WelcomeCarousel.stories.tsx diff --git a/frontend/portal/src/components/WelcomeCarousel.tsx b/frontend/editor/src/portal/components/WelcomeCarousel.tsx similarity index 82% rename from frontend/portal/src/components/WelcomeCarousel.tsx rename to frontend/editor/src/portal/components/WelcomeCarousel.tsx index b485eee1a4..d0b01708e6 100644 --- a/frontend/portal/src/components/WelcomeCarousel.tsx +++ b/frontend/editor/src/portal/components/WelcomeCarousel.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { Button, StatusBadge } from "@shared/components"; +import { Button, StatusBadge } from "@app/ui"; import { useView, type ViewId } from "@portal/contexts/ViewContext"; import "@portal/components/WelcomeCarousel.css"; @@ -21,18 +21,18 @@ function EditorOrnament() { return (
- {t("welcome.ornament.editor.critical")} + {t("portal.welcome.ornament.editor.critical")}
Vulnerability Assessment Report
CVE-2026-1847 · 12 pages
- {t("welcome.ornament.editor.signed")} + {t("portal.welcome.ornament.editor.signed")} · - {t("welcome.ornament.editor.ocrClean")} + {t("portal.welcome.ornament.editor.ocrClean")} · - {t("welcome.ornament.editor.schemaMatch")} + {t("portal.welcome.ornament.editor.schemaMatch")}
); @@ -88,9 +88,12 @@ const SLIDES: Slide[] = [ { id: "editor", durationMs: 12000, - primary: { labelKey: "welcome.slides.editor.primary", target: "editor" }, + primary: { + labelKey: "portal.welcome.slides.editor.primary", + target: "editor", + }, secondary: { - labelKey: "welcome.slides.editor.secondary", + labelKey: "portal.welcome.slides.editor.secondary", target: "editor", }, ornament: , @@ -98,9 +101,12 @@ const SLIDES: Slide[] = [ { id: "platform", durationMs: 8000, - primary: { labelKey: "welcome.slides.platform.primary", action: "try-op" }, + primary: { + labelKey: "portal.welcome.slides.platform.primary", + action: "try-op", + }, secondary: { - labelKey: "welcome.slides.platform.secondary", + labelKey: "portal.welcome.slides.platform.secondary", target: "infrastructure", }, ornament: , @@ -108,8 +114,14 @@ const SLIDES: Slide[] = [ { id: "agents", durationMs: 8000, - primary: { labelKey: "welcome.slides.agents.primary", target: "sources" }, - secondary: { labelKey: "welcome.slides.agents.secondary", target: "docs" }, + primary: { + labelKey: "portal.welcome.slides.agents.primary", + target: "sources", + }, + secondary: { + labelKey: "portal.welcome.slides.agents.secondary", + target: "docs", + }, ornament: , }, ]; @@ -157,19 +169,19 @@ export function WelcomeCarousel({ onTryOp }: WelcomeCarouselProps) { setPaused(false); } }} - aria-label={t("welcome.ariaLabel")} + aria-label={t("portal.welcome.ariaLabel")} aria-roledescription="carousel" >
- {t(`welcome.slides.${slide.id}.eyebrow`)} + {t(`portal.welcome.slides.${slide.id}.eyebrow`)}

- {t(`welcome.slides.${slide.id}.title`)} + {t(`portal.welcome.slides.${slide.id}.title`)}

- {t(`welcome.slides.${slide.id}.sub`)} + {t(`portal.welcome.slides.${slide.id}.sub`)}

) : (
)} diff --git a/frontend/portal/src/components/account-link/LinkAccountModal.tsx b/frontend/editor/src/portal/components/account-link/LinkAccountModal.tsx similarity index 76% rename from frontend/portal/src/components/account-link/LinkAccountModal.tsx rename to frontend/editor/src/portal/components/account-link/LinkAccountModal.tsx index 8c23bbcd0f..2b07e12b3a 100644 --- a/frontend/portal/src/components/account-link/LinkAccountModal.tsx +++ b/frontend/editor/src/portal/components/account-link/LinkAccountModal.tsx @@ -1,12 +1,12 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { Banner, Button, Modal } from "@shared/components"; -import SupabaseLoginForm from "@shared/auth/ui/SupabaseLoginForm"; +import { Banner, Button, Modal } from "@app/ui"; +import SupabaseLoginForm from "@app/auth/ui/SupabaseLoginForm"; import { useSupabaseLogin, type SupabaseLoginSession, -} from "@shared/auth/ui/useSupabaseLogin"; -import "@shared/auth/ui/auth-theme.css"; +} from "@app/auth/ui/useSupabaseLogin"; +import "@app/auth/ui/auth-theme.css"; import { ensureSaasSupabase, isSaasSupabaseConfigured, @@ -66,17 +66,20 @@ export function LinkAccountModal({ width="md" title={ reauth - ? t("accountLink.modal.reauthTitle", "Sign in again") - : t("accountLink.modal.linkTitle", "Link your Stirling account") + ? t("portal.accountLink.modal.reauthTitle", "Sign in again") + : t( + "portal.accountLink.modal.linkTitle", + "Link your Stirling account", + ) } subtitle={ reauth ? t( - "accountLink.modal.reauthSubtitle", + "portal.accountLink.modal.reauthSubtitle", "Your session expired — sign back in to your Stirling account. Your instance stays linked.", ) : t( - "accountLink.modal.linkSubtitle", + "portal.accountLink.modal.linkSubtitle", "Sign in to the account this server should bill against.", ) } @@ -88,16 +91,16 @@ export function LinkAccountModal({ - {t("accountLink.modal.loginNotConfigured.before", "Set")}{" "} + {t("portal.accountLink.modal.loginNotConfigured.before", "Set")}{" "} VITE_SAAS_SUPABASE_URL{" "} - {t("accountLink.modal.loginNotConfigured.and", "and")}{" "} + {t("portal.accountLink.modal.loginNotConfigured.and", "and")}{" "} VITE_SAAS_SUPABASE_ANON_KEY{" "} {t( - "accountLink.modal.loginNotConfigured.after", + "portal.accountLink.modal.loginNotConfigured.after", "to enable in-app linking against the hosted Stirling account.", )} @@ -109,7 +112,10 @@ export function LinkAccountModal({ onClose(); }} > - {t("accountLink.modal.simulateSignIn", "Simulate sign-in (dev)")} + {t( + "portal.accountLink.modal.simulateSignIn", + "Simulate sign-in (dev)", + )} )}
diff --git a/frontend/portal/src/components/account-link/LinkGate.stories.tsx b/frontend/editor/src/portal/components/account-link/LinkGate.stories.tsx similarity index 93% rename from frontend/portal/src/components/account-link/LinkGate.stories.tsx rename to frontend/editor/src/portal/components/account-link/LinkGate.stories.tsx index af2ac5c96b..60d9a9962c 100644 --- a/frontend/portal/src/components/account-link/LinkGate.stories.tsx +++ b/frontend/editor/src/portal/components/account-link/LinkGate.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Card } from "@shared/components"; +import { Card } from "@app/ui"; import { LinkGate } from "@portal/components/account-link/LinkGate"; const meta: Meta = { diff --git a/frontend/portal/src/components/account-link/LinkGate.tsx b/frontend/editor/src/portal/components/account-link/LinkGate.tsx similarity index 74% rename from frontend/portal/src/components/account-link/LinkGate.tsx rename to frontend/editor/src/portal/components/account-link/LinkGate.tsx index de56c844e9..d5743d0041 100644 --- a/frontend/portal/src/components/account-link/LinkGate.tsx +++ b/frontend/editor/src/portal/components/account-link/LinkGate.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { Banner, Button } from "@shared/components"; +import { Banner, Button } from "@app/ui"; import { useLink } from "@portal/contexts/LinkContext"; import { useUI } from "@portal/contexts/UIContext"; @@ -29,18 +29,22 @@ export function LinkGate({ children, feature }: Props) { tone="info" title={ feature - ? t("accountLink.gate.titleFeature", "Link to unlock {{feature}}", { - feature, - }) - : t("accountLink.gate.title", "Link to unlock") + ? t( + "portal.accountLink.gate.titleFeature", + "Link to unlock {{feature}}", + { + feature, + }, + ) + : t("portal.accountLink.gate.title", "Link to unlock") } description={t( - "accountLink.gate.description", + "portal.accountLink.gate.description", "Link this org's Stirling account to use billable features.", )} action={ } /> diff --git a/frontend/portal/src/components/account-link/LinkedInstancesTable.stories.tsx b/frontend/editor/src/portal/components/account-link/LinkedInstancesTable.stories.tsx similarity index 100% rename from frontend/portal/src/components/account-link/LinkedInstancesTable.stories.tsx rename to frontend/editor/src/portal/components/account-link/LinkedInstancesTable.stories.tsx diff --git a/frontend/portal/src/components/account-link/LinkedInstancesTable.tsx b/frontend/editor/src/portal/components/account-link/LinkedInstancesTable.tsx similarity index 69% rename from frontend/portal/src/components/account-link/LinkedInstancesTable.tsx rename to frontend/editor/src/portal/components/account-link/LinkedInstancesTable.tsx index a6460016d5..f6839596d3 100644 --- a/frontend/portal/src/components/account-link/LinkedInstancesTable.tsx +++ b/frontend/editor/src/portal/components/account-link/LinkedInstancesTable.tsx @@ -7,7 +7,7 @@ import { StatusBadge, Table, type TableColumn, -} from "@shared/components"; +} from "@app/ui"; import type { LinkedInstanceRow } from "@portal/api/link"; interface Props { @@ -19,20 +19,21 @@ interface Props { } function relativeTime(iso: string | null, t: TFunction): string { - if (!iso) return t("accountLink.instances.time.never", "never"); + if (!iso) return t("portal.accountLink.instances.time.never", "never"); const diffMs = Date.now() - new Date(iso).getTime(); const mins = Math.round(diffMs / 60_000); - if (mins < 1) return t("accountLink.instances.time.justNow", "just now"); + if (mins < 1) + return t("portal.accountLink.instances.time.justNow", "just now"); if (mins < 60) - return t("accountLink.instances.time.minutesAgo", "{{count}}m ago", { + return t("portal.accountLink.instances.time.minutesAgo", "{{count}}m ago", { count: mins, }); const hrs = Math.round(mins / 60); if (hrs < 24) - return t("accountLink.instances.time.hoursAgo", "{{count}}h ago", { + return t("portal.accountLink.instances.time.hoursAgo", "{{count}}h ago", { count: hrs, }); - return t("accountLink.instances.time.daysAgo", "{{count}}d ago", { + return t("portal.accountLink.instances.time.daysAgo", "{{count}}d ago", { count: Math.round(hrs / 24), }); } @@ -47,11 +48,12 @@ export function LinkedInstancesTable({ const cols: TableColumn[] = [ { key: "name", - header: t("accountLink.instances.columns.instance", "Instance"), + header: t("portal.accountLink.instances.columns.instance", "Instance"), render: (i) => (
- {i.name ?? t("accountLink.instances.unnamed", "Unnamed instance")} + {i.name ?? + t("portal.accountLink.instances.unnamed", "Unnamed instance")} {i.deviceId}
@@ -59,21 +61,21 @@ export function LinkedInstancesTable({ }, { key: "status", - header: t("accountLink.instances.columns.status", "Status"), + header: t("portal.accountLink.instances.columns.status", "Status"), render: (i) => i.revoked ? ( - {t("accountLink.instances.revoked", "Revoked")} + {t("portal.accountLink.instances.revoked", "Revoked")} ) : ( - {t("accountLink.instances.active", "Active")} + {t("portal.accountLink.instances.active", "Active")} ), }, { key: "lastSeen", - header: t("accountLink.instances.columns.lastSeen", "Last seen"), + header: t("portal.accountLink.instances.columns.lastSeen", "Last seen"), render: (i) => ( {relativeTime(i.lastSeenAt, t)} @@ -82,7 +84,7 @@ export function LinkedInstancesTable({ }, { key: "created", - header: t("accountLink.instances.columns.linked", "Linked"), + header: t("portal.accountLink.instances.columns.linked", "Linked"), render: (i) => ( {relativeTime(i.createdAt, t)} @@ -102,7 +104,7 @@ export function LinkedInstancesTable({ loading={revokingId === i.instanceId} onClick={() => onRevoke(i)} > - {t("accountLink.instances.revoke", "Revoke")} + {t("portal.accountLink.instances.revoke", "Revoke")} ), }, @@ -113,9 +115,12 @@ export function LinkedInstancesTable({ {instances.length === 0 ? ( diff --git a/frontend/portal/src/components/agent-builder/AgentBuilderPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.stories.tsx similarity index 100% rename from frontend/portal/src/components/agent-builder/AgentBuilderPanel.stories.tsx rename to frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.stories.tsx diff --git a/frontend/portal/src/components/agent-builder/AgentBuilderPanel.tsx b/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.tsx similarity index 87% rename from frontend/portal/src/components/agent-builder/AgentBuilderPanel.tsx rename to frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.tsx index 46b45231b2..20cbcab89c 100644 --- a/frontend/portal/src/components/agent-builder/AgentBuilderPanel.tsx +++ b/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { StatusBadge, Tabs, type TabItem } from "@shared/components"; +import { StatusBadge, Tabs, type TabItem } from "@app/ui"; import { type Agent, AGENT_STATUS_TONE } from "@portal/api/agents"; import { ScenariosPanel } from "@portal/components/agent-builder/ScenariosPanel"; import { ToolsPanel } from "@portal/components/agent-builder/ToolsPanel"; @@ -27,18 +27,18 @@ export function AgentBuilderPanel({ const tabs: TabItem[] = [ { key: "scenarios", - label: t("agentBuilder.tabs.scenarios"), + label: t("portal.agentBuilder.tabs.scenarios"), count: agent.scenarios.length, }, - { key: "tools", label: t("agentBuilder.tabs.tools") }, + { key: "tools", label: t("portal.agentBuilder.tabs.tools") }, { key: "evals", - label: t("agentBuilder.tabs.evals"), + label: t("portal.agentBuilder.tabs.evals"), count: agent.evalsTotal > 0 ? agent.evalsTotal : undefined, }, { key: "versions", - label: t("agentBuilder.tabs.versions"), + label: t("portal.agentBuilder.tabs.versions"), count: agent.versions.length, }, ]; @@ -66,7 +66,7 @@ export function AgentBuilderPanel({ activeKey={tab} onChange={setTab} variant="underline" - ariaLabel={t("agentBuilder.sectionsAriaLabel")} + ariaLabel={t("portal.agentBuilder.sectionsAriaLabel")} /> {tab === "scenarios" && } diff --git a/frontend/portal/src/components/agent-builder/AgentKpiStrip.stories.tsx b/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.stories.tsx similarity index 100% rename from frontend/portal/src/components/agent-builder/AgentKpiStrip.stories.tsx rename to frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.stories.tsx diff --git a/frontend/portal/src/components/agent-builder/AgentKpiStrip.tsx b/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.tsx similarity index 70% rename from frontend/portal/src/components/agent-builder/AgentKpiStrip.tsx rename to frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.tsx index 029d300f29..0d3b729920 100644 --- a/frontend/portal/src/components/agent-builder/AgentKpiStrip.tsx +++ b/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { MetricCard, MetricStrip } from "@shared/components"; +import { MetricCard, MetricStrip } from "@app/ui"; import type { AgentsSummary } from "@portal/api/agents"; interface AgentKpiStripProps { @@ -16,10 +16,10 @@ export function AgentKpiStrip({ summary, loading }: AgentKpiStripProps) { * states; only values flow from the API. */ const kpiLabels = [ - t("agentBuilder.kpi.activeAgents"), - t("agentBuilder.kpi.avgPassRate"), - t("agentBuilder.kpi.scenarios"), - t("agentBuilder.kpi.latestPublished"), + t("portal.agentBuilder.kpi.activeAgents"), + t("portal.agentBuilder.kpi.avgPassRate"), + t("portal.agentBuilder.kpi.scenarios"), + t("portal.agentBuilder.kpi.latestPublished"), ] as const; const values: (string | number)[] = summary @@ -33,10 +33,12 @@ export function AgentKpiStrip({ summary, loading }: AgentKpiStripProps) { const descriptions: (string | undefined)[] = summary ? [ - t("agentBuilder.kpi.totalDescription", { count: summary.totalAgents }), - t("agentBuilder.kpi.acrossGoldenSets"), - t("agentBuilder.kpi.testCases"), - t("agentBuilder.kpi.fleetWide"), + t("portal.agentBuilder.kpi.totalDescription", { + count: summary.totalAgents, + }), + t("portal.agentBuilder.kpi.acrossGoldenSets"), + t("portal.agentBuilder.kpi.testCases"), + t("portal.agentBuilder.kpi.fleetWide"), ] : []; diff --git a/frontend/portal/src/components/agent-builder/AgentSelector.stories.tsx b/frontend/editor/src/portal/components/agent-builder/AgentSelector.stories.tsx similarity index 100% rename from frontend/portal/src/components/agent-builder/AgentSelector.stories.tsx rename to frontend/editor/src/portal/components/agent-builder/AgentSelector.stories.tsx diff --git a/frontend/portal/src/components/agent-builder/AgentSelector.tsx b/frontend/editor/src/portal/components/agent-builder/AgentSelector.tsx similarity index 93% rename from frontend/portal/src/components/agent-builder/AgentSelector.tsx rename to frontend/editor/src/portal/components/agent-builder/AgentSelector.tsx index 73248b8128..f6734721b8 100644 --- a/frontend/portal/src/components/agent-builder/AgentSelector.tsx +++ b/frontend/editor/src/portal/components/agent-builder/AgentSelector.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { type Agent, AGENT_STATUS_TONE } from "@portal/api/agents"; -import { StatusBadge } from "@shared/components"; +import { StatusBadge } from "@app/ui"; import "@portal/views/AgentBuilder.css"; interface AgentSelectorProps { @@ -19,7 +19,7 @@ export function AgentSelector({ return (
} >

- {t("agentBuilder.bootstrap.lead")} + {t("portal.agentBuilder.bootstrap.lead")}

diff --git a/frontend/portal/src/components/agent-builder/EvalsPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/EvalsPanel.stories.tsx similarity index 100% rename from frontend/portal/src/components/agent-builder/EvalsPanel.stories.tsx rename to frontend/editor/src/portal/components/agent-builder/EvalsPanel.stories.tsx diff --git a/frontend/portal/src/components/agent-builder/EvalsPanel.tsx b/frontend/editor/src/portal/components/agent-builder/EvalsPanel.tsx similarity index 73% rename from frontend/portal/src/components/agent-builder/EvalsPanel.tsx rename to frontend/editor/src/portal/components/agent-builder/EvalsPanel.tsx index 651f6c1d69..3f1580652f 100644 --- a/frontend/portal/src/components/agent-builder/EvalsPanel.tsx +++ b/frontend/editor/src/portal/components/agent-builder/EvalsPanel.tsx @@ -7,7 +7,7 @@ import { StatusBadge, Table, type TableColumn, -} from "@shared/components"; +} from "@app/ui"; import type { Agent, EvalCase } from "@portal/api/agents"; import "@portal/views/AgentBuilder.css"; @@ -22,32 +22,32 @@ export function EvalsPanel({ agent }: EvalsPanelProps) { const columns: TableColumn[] = [ { key: "name", - header: t("agentBuilder.evals.columnCase"), + header: t("portal.agentBuilder.evals.columnCase"), render: (c) => c.name, }, { key: "result", - header: t("agentBuilder.evals.columnResult"), + header: t("portal.agentBuilder.evals.columnResult"), render: (c) => c.passing === null ? ( - {t("agentBuilder.evals.notRun")} + {t("portal.agentBuilder.evals.notRun")} ) : ( {c.passing - ? t("agentBuilder.evals.pass") - : t("agentBuilder.evals.fail")} + ? t("portal.agentBuilder.evals.pass") + : t("portal.agentBuilder.evals.fail")} ), }, { key: "latency", - header: t("agentBuilder.evals.columnLatency"), + header: t("portal.agentBuilder.evals.columnLatency"), align: "right", render: (c) => ( - {t("agentBuilder.evals.latencyMs", { ms: c.latencyMs })} + {t("portal.agentBuilder.evals.latencyMs", { ms: c.latencyMs })} ), }, @@ -57,8 +57,8 @@ export function EvalsPanel({ agent }: EvalsPanelProps) { return (
@@ -77,29 +77,29 @@ export function EvalsPanel({ agent }: EvalsPanelProps) {
= 0.95 ? "success" : rate >= 0.8 ? "warning" : "danger"} />
- {t("agentBuilder.evals.goldenSetPassRate")} + {t("portal.agentBuilder.evals.goldenSetPassRate")} {Math.round(rate * 100)}%
= 0.95 ? "var(--color-green)" : "var(--color-amber)"} - label={t("agentBuilder.evals.goldenSetPassRate")} + label={t("portal.agentBuilder.evals.goldenSetPassRate")} />
diff --git a/frontend/portal/src/components/agent-builder/ScenariosPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.stories.tsx similarity index 100% rename from frontend/portal/src/components/agent-builder/ScenariosPanel.stories.tsx rename to frontend/editor/src/portal/components/agent-builder/ScenariosPanel.stories.tsx diff --git a/frontend/portal/src/components/agent-builder/ScenariosPanel.tsx b/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.tsx similarity index 79% rename from frontend/portal/src/components/agent-builder/ScenariosPanel.tsx rename to frontend/editor/src/portal/components/agent-builder/ScenariosPanel.tsx index f355dfa4d0..f4057ea2b6 100644 --- a/frontend/portal/src/components/agent-builder/ScenariosPanel.tsx +++ b/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.tsx @@ -1,12 +1,6 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Button, - Chip, - FormField, - Input, - StatusBadge, -} from "@shared/components"; +import { Button, Chip, FormField, Input, StatusBadge } from "@app/ui"; import type { Agent, Scenario } from "@portal/api/agents"; import "@portal/views/AgentBuilder.css"; @@ -64,8 +58,8 @@ export function ScenariosPanel({ agent }: ScenariosPanelProps) { showDot={false} > {s.enabled - ? t("agentBuilder.scenarios.inEval") - : t("agentBuilder.scenarios.muted")} + ? t("portal.agentBuilder.scenarios.inEval") + : t("portal.agentBuilder.scenarios.muted")}
@@ -78,8 +72,8 @@ export function ScenariosPanel({ agent }: ScenariosPanelProps) { onClick={() => toggleEnabled(s.id)} > {s.enabled - ? t("agentBuilder.scenarios.mute") - : t("agentBuilder.scenarios.enable")} + ? t("portal.agentBuilder.scenarios.mute") + : t("portal.agentBuilder.scenarios.enable")} ))} @@ -87,25 +81,29 @@ export function ScenariosPanel({ agent }: ScenariosPanelProps) {
- {t("agentBuilder.scenarios.addScenario")} + {t("portal.agentBuilder.scenarios.addScenario")}
- + setName(e.target.value)} - placeholder={t("agentBuilder.scenarios.namePlaceholder")} + placeholder={t("portal.agentBuilder.scenarios.namePlaceholder")} /> - + setExpectation(e.target.value)} - placeholder={t("agentBuilder.scenarios.expectationPlaceholder")} + placeholder={t( + "portal.agentBuilder.scenarios.expectationPlaceholder", + )} />
diff --git a/frontend/portal/src/components/agent-builder/ToolsPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/ToolsPanel.stories.tsx similarity index 100% rename from frontend/portal/src/components/agent-builder/ToolsPanel.stories.tsx rename to frontend/editor/src/portal/components/agent-builder/ToolsPanel.stories.tsx diff --git a/frontend/portal/src/components/agent-builder/ToolsPanel.tsx b/frontend/editor/src/portal/components/agent-builder/ToolsPanel.tsx similarity index 83% rename from frontend/portal/src/components/agent-builder/ToolsPanel.tsx rename to frontend/editor/src/portal/components/agent-builder/ToolsPanel.tsx index 81e0f57af4..4dde8ff54d 100644 --- a/frontend/portal/src/components/agent-builder/ToolsPanel.tsx +++ b/frontend/editor/src/portal/components/agent-builder/ToolsPanel.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Chip, ToggleSwitch } from "@shared/components"; +import { Chip, ToggleSwitch } from "@app/ui"; import { type Agent, type ToolMode, TOOL_CATALOGUE } from "@portal/api/agents"; import "@portal/views/AgentBuilder.css"; @@ -40,27 +40,27 @@ export function ToolsPanel({ agent, governanceUnlocked }: ToolsPanelProps) { checked={restricted} onChange={setRestricted} disabled={!governanceUnlocked} - label={t("agentBuilder.tools.restrictedAccess")} + label={t("portal.agentBuilder.tools.restrictedAccess")} description={ governanceUnlocked - ? t("agentBuilder.tools.restrictedDescription") - : t("agentBuilder.tools.governanceGate") + ? t("portal.agentBuilder.tools.restrictedDescription") + : t("portal.agentBuilder.tools.governanceGate") } /> {restricted - ? t("agentBuilder.tools.restricted") - : t("agentBuilder.tools.broadAccess")} + ? t("portal.agentBuilder.tools.restricted") + : t("portal.agentBuilder.tools.broadAccess")} {restricted && (
- {t("agentBuilder.tools.deniedTools")} + {t("portal.agentBuilder.tools.deniedTools")}

- {t("agentBuilder.tools.deniedHint")} + {t("portal.agentBuilder.tools.deniedHint")}

{TOOL_CATALOGUE.map((tool) => { diff --git a/frontend/portal/src/components/agent-builder/VersionsPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/VersionsPanel.stories.tsx similarity index 100% rename from frontend/portal/src/components/agent-builder/VersionsPanel.stories.tsx rename to frontend/editor/src/portal/components/agent-builder/VersionsPanel.stories.tsx diff --git a/frontend/portal/src/components/agent-builder/VersionsPanel.tsx b/frontend/editor/src/portal/components/agent-builder/VersionsPanel.tsx similarity index 91% rename from frontend/portal/src/components/agent-builder/VersionsPanel.tsx rename to frontend/editor/src/portal/components/agent-builder/VersionsPanel.tsx index b05e2fa7fc..88edc7993c 100644 --- a/frontend/portal/src/components/agent-builder/VersionsPanel.tsx +++ b/frontend/editor/src/portal/components/agent-builder/VersionsPanel.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Button, StatusBadge } from "@shared/components"; +import { Button, StatusBadge } from "@app/ui"; import { type Agent, AGENT_STATUS_TONE } from "@portal/api/agents"; import "@portal/views/AgentBuilder.css"; @@ -56,7 +56,7 @@ export function VersionsPanel({ agent, historyUnlocked }: VersionsPanelProps) { {isCurrent && ( - {t("agentBuilder.versions.current")} + {t("portal.agentBuilder.versions.current")} )}
@@ -72,7 +72,7 @@ export function VersionsPanel({ agent, historyUnlocked }: VersionsPanelProps) { variant="outline" onClick={() => publish(v.version)} > - {t("agentBuilder.versions.publish")} + {t("portal.agentBuilder.versions.publish")} )} {v.status === "published" && !isCurrent && ( @@ -81,7 +81,7 @@ export function VersionsPanel({ agent, historyUnlocked }: VersionsPanelProps) { variant="ghost" onClick={() => rollback(v.version)} > - {t("agentBuilder.versions.rollBack")} + {t("portal.agentBuilder.versions.rollBack")} )}
@@ -92,7 +92,7 @@ export function VersionsPanel({ agent, historyUnlocked }: VersionsPanelProps) { {!historyUnlocked && publishedExists && (

- {t("agentBuilder.versions.historyGate")} + {t("portal.agentBuilder.versions.historyGate")}

)} diff --git a/frontend/portal/src/components/billing/EnterpriseUpsell.stories.tsx b/frontend/editor/src/portal/components/billing/EnterpriseUpsell.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/EnterpriseUpsell.stories.tsx rename to frontend/editor/src/portal/components/billing/EnterpriseUpsell.stories.tsx diff --git a/frontend/portal/src/components/billing/EnterpriseUpsell.tsx b/frontend/editor/src/portal/components/billing/EnterpriseUpsell.tsx similarity index 74% rename from frontend/portal/src/components/billing/EnterpriseUpsell.tsx rename to frontend/editor/src/portal/components/billing/EnterpriseUpsell.tsx index 9ad3cbeae7..b303c530ff 100644 --- a/frontend/portal/src/components/billing/EnterpriseUpsell.tsx +++ b/frontend/editor/src/portal/components/billing/EnterpriseUpsell.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Button, Card } from "@shared/components"; +import { Button, Card } from "@app/ui"; interface Props { /** Render without the Card wrapper, to embed inside another card's column. */ @@ -15,23 +15,29 @@ export function EnterpriseUpsell({ bare = false }: Props) { const body = ( <> - {t("billing.enterpriseUpsell.eyebrow", "Volume discount · 1M+ PDFs")} + {t( + "portal.billing.enterpriseUpsell.eyebrow", + "Volume discount · 1M+ PDFs", + )}

- {t("billing.enterpriseUpsell.title", "Stirling Enterprise")} + {t("portal.billing.enterpriseUpsell.title", "Stirling Enterprise")}

{t( - "billing.enterpriseUpsell.description", + "portal.billing.enterpriseUpsell.description", "Committed volume discounts, air-gapped deployment, custom MSA and security reviews, and 3rd-party distributor partnerships.", )}

{/* Destination wired when the enterprise/sales URL is confirmed. */}
diff --git a/frontend/portal/src/components/billing/FreePdfEditorsCard.stories.tsx b/frontend/editor/src/portal/components/billing/FreePdfEditorsCard.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/FreePdfEditorsCard.stories.tsx rename to frontend/editor/src/portal/components/billing/FreePdfEditorsCard.stories.tsx diff --git a/frontend/portal/src/components/billing/FreePdfEditorsCard.tsx b/frontend/editor/src/portal/components/billing/FreePdfEditorsCard.tsx similarity index 75% rename from frontend/portal/src/components/billing/FreePdfEditorsCard.tsx rename to frontend/editor/src/portal/components/billing/FreePdfEditorsCard.tsx index 2744e26d08..77cbdbad5b 100644 --- a/frontend/portal/src/components/billing/FreePdfEditorsCard.tsx +++ b/frontend/editor/src/portal/components/billing/FreePdfEditorsCard.tsx @@ -1,12 +1,6 @@ import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { - Button, - Card, - MetricCard, - MetricStrip, - StatusBadge, -} from "@shared/components"; +import { Button, Card, MetricCard, MetricStrip, StatusBadge } from "@app/ui"; import GroupsIcon from "@mui/icons-material/GroupsRounded"; import PersonAddIcon from "@mui/icons-material/PersonAddAltRounded"; @@ -36,14 +30,17 @@ export function FreePdfEditorsCard() {

- {t("billing.freeEditors.title", "Free PDF Editors")}{" "} + {t("portal.billing.freeEditors.title", "Free PDF Editors")}{" "} - {t("billing.freeEditors.previewBadge", "Preview · sample data")} + {t( + "portal.billing.freeEditors.previewBadge", + "Preview · sample data", + )}

{t( - "billing.freeEditors.subtitle", + "portal.billing.freeEditors.subtitle", "Deploy anywhere, for your whole team.", )}

@@ -51,22 +48,25 @@ export function FreePdfEditorsCard() {
@@ -77,7 +77,7 @@ export function FreePdfEditorsCard() { leadingIcon={} onClick={() => navigate("/users?invite=1")} > - {t("billing.freeEditors.inviteTeammates", "Invite teammates")} + {t("portal.billing.freeEditors.inviteTeammates", "Invite teammates")} diff --git a/frontend/portal/src/components/billing/FreePlanView.stories.tsx b/frontend/editor/src/portal/components/billing/FreePlanView.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/FreePlanView.stories.tsx rename to frontend/editor/src/portal/components/billing/FreePlanView.stories.tsx diff --git a/frontend/portal/src/components/billing/FreePlanView.tsx b/frontend/editor/src/portal/components/billing/FreePlanView.tsx similarity index 83% rename from frontend/portal/src/components/billing/FreePlanView.tsx rename to frontend/editor/src/portal/components/billing/FreePlanView.tsx index 02a5352a43..0a1ae5216a 100644 --- a/frontend/portal/src/components/billing/FreePlanView.tsx +++ b/frontend/editor/src/portal/components/billing/FreePlanView.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Banner, Button, StatusBadge } from "@shared/components"; +import { Banner, Button, StatusBadge } from "@app/ui"; import type { Wallet } from "@portal/api/billing"; import type { SaasCurrency } from "@portal/billing/stripe"; import { WalletMeter } from "@portal/components/billing/WalletMeter"; @@ -37,7 +37,7 @@ export function FreePlanView({ wallet, onSubscribed }: Props) { if (wallet.teamId == null) { setMissingTeam( t( - "billing.freePlan.noTeamResolved", + "portal.billing.freePlan.noTeamResolved", "No team is resolved on your wallet yet — refresh and try again.", ), ); @@ -53,7 +53,10 @@ export function FreePlanView({ wallet, onSubscribed }: Props) { onClick={openCheckout} disabled={wallet.teamId == null} > - {t("billing.freePlan.switchOnProcessor", "Switch on the Processor →")} + {t( + "portal.billing.freePlan.switchOnProcessor", + "Switch on the Processor →", + )} ) : null; @@ -62,20 +65,20 @@ export function FreePlanView({ wallet, onSubscribed }: Props) { {/* Current plan */}
- {t("billing.freePlan.currentPlan", "Current plan")} + {t("portal.billing.freePlan.currentPlan", "Current plan")}

- {t("billing.freePlan.planName", "Editor")} + {t("portal.billing.freePlan.planName", "Editor")}

- {t("billing.freePlan.freeForever", "Free forever")} + {t("portal.billing.freePlan.freeForever", "Free forever")} - {t("billing.freePlan.ssoIncluded", "SSO included")} + {t("portal.billing.freePlan.ssoIncluded", "SSO included")} - {t("billing.freePlan.unlimitedUsers", "Unlimited users")} + {t("portal.billing.freePlan.unlimitedUsers", "Unlimited users")}
@@ -89,7 +92,7 @@ export function FreePlanView({ wallet, onSubscribed }: Props) { @@ -99,7 +102,7 @@ export function FreePlanView({ wallet, onSubscribed }: Props) { {!isLeader && (

{t( - "billing.freePlan.ownerOnly", + "portal.billing.freePlan.ownerOnly", "Only the team owner can switch on the Processor plan.", )}

diff --git a/frontend/portal/src/components/billing/InvoicesList.tsx b/frontend/editor/src/portal/components/billing/InvoicesList.tsx similarity index 82% rename from frontend/portal/src/components/billing/InvoicesList.tsx rename to frontend/editor/src/portal/components/billing/InvoicesList.tsx index 8b13670148..1a9f015f5e 100644 --- a/frontend/portal/src/components/billing/InvoicesList.tsx +++ b/frontend/editor/src/portal/components/billing/InvoicesList.tsx @@ -8,8 +8,8 @@ import { StatusBadge, Table, type TableColumn, -} from "@shared/components"; -import { formatMinor, formatPeriodDate } from "@shared/billing"; +} from "@app/ui"; +import { formatMinor, formatPeriodDate } from "@app/billing"; import { fetchInvoices, type Invoice } from "@portal/api/billing"; const DEFAULT_VISIBLE = 5; @@ -83,13 +83,16 @@ export function InvoicesList() { const columns: TableColumn[] = [ { key: "date", - header: t("billing.invoices.columnDate", "Date"), + header: t("portal.billing.invoices.columnDate", "Date"), render: (inv) => inv.createdAt ? formatPeriodDate(inv.createdAt, { year: true }) : "—", }, { key: "pdfs", - header: t("billing.invoices.columnPdfsProcessed", "PDFs processed"), + header: t( + "portal.billing.invoices.columnPdfsProcessed", + "PDFs processed", + ), align: "right", // Billed units on the invoice's metered line item; "—" when the // line-item table isn't synced into the Stripe mirror. @@ -98,7 +101,7 @@ export function InvoicesList() { }, { key: "amount", - header: t("billing.invoices.columnAmount", "Amount"), + header: t("portal.billing.invoices.columnAmount", "Amount"), align: "right", render: (inv) => inv.totalMinor == null @@ -107,7 +110,7 @@ export function InvoicesList() { }, { key: "status", - header: t("billing.invoices.columnStatus", "Status"), + header: t("portal.billing.invoices.columnStatus", "Status"), render: (inv) => ( {inv.status} @@ -116,11 +119,11 @@ export function InvoicesList() { }, { key: "description", - header: t("billing.invoices.columnDescription", "Description"), + header: t("portal.billing.invoices.columnDescription", "Description"), render: (inv) => ( {inv.description ?? - t("billing.invoices.descriptionFallback", "Invoice")} + t("portal.billing.invoices.descriptionFallback", "Invoice")} ), }, @@ -137,14 +140,14 @@ export function InvoicesList() { target="_blank" rel="noopener noreferrer" aria-label={t( - "billing.invoices.viewAriaLabel", + "portal.billing.invoices.viewAriaLabel", "View invoice {{number}} in Stripe", { number: inv.number ?? inv.id, }, )} > - {t("billing.invoices.viewLink", "View ↗")} + {t("portal.billing.invoices.viewLink", "View ↗")} )} {inv.invoicePdf && ( @@ -154,14 +157,14 @@ export function InvoicesList() { target="_blank" rel="noopener noreferrer" aria-label={t( - "billing.invoices.downloadAriaLabel", + "portal.billing.invoices.downloadAriaLabel", "Download invoice {{number}} as PDF", { number: inv.number ?? inv.id, }, )} > - {t("billing.invoices.pdfLink", "PDF ↓")} + {t("portal.billing.invoices.pdfLink", "PDF ↓")} )} @@ -172,7 +175,7 @@ export function InvoicesList() { return (

- {t("billing.invoices.title", "Invoice history")} + {t("portal.billing.invoices.title", "Invoice history")}

{invoices === null && !error && ( @@ -186,7 +189,7 @@ export function InvoicesList() { {error && (

{t( - "billing.invoices.loadError", + "portal.billing.invoices.loadError", "Couldn't load invoices: {{error}}", { error }, )} @@ -196,9 +199,9 @@ export function InvoicesList() { {invoices !== null && invoices.length === 0 && !error && ( @@ -221,24 +224,28 @@ export function InvoicesList() { > {showAll ? t( - "billing.invoices.showFewer", + "portal.billing.invoices.showFewer", "Show fewer (top {{count}})", { count: DEFAULT_VISIBLE }, ) : atFetchLimit ? t( - "billing.invoices.showMostRecent", + "portal.billing.invoices.showMostRecent", "Show {{count}} most recent", { count: total }, ) - : t("billing.invoices.showAll", "Show all {{count}}", { - count: total, - })} + : t( + "portal.billing.invoices.showAll", + "Show all {{count}}", + { + count: total, + }, + )} {showAll && atFetchLimit && ( {t( - "billing.invoices.fetchLimitNote", + "portal.billing.invoices.fetchLimitNote", "Showing your {{count}} most recent invoices. Older invoices are in the Stripe portal.", { count: FETCH_LIMIT }, )} diff --git a/frontend/portal/src/components/billing/LinkAccountPrompt.stories.tsx b/frontend/editor/src/portal/components/billing/LinkAccountPrompt.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/LinkAccountPrompt.stories.tsx rename to frontend/editor/src/portal/components/billing/LinkAccountPrompt.stories.tsx diff --git a/frontend/portal/src/components/billing/LinkAccountPrompt.tsx b/frontend/editor/src/portal/components/billing/LinkAccountPrompt.tsx similarity index 76% rename from frontend/portal/src/components/billing/LinkAccountPrompt.tsx rename to frontend/editor/src/portal/components/billing/LinkAccountPrompt.tsx index df7429784e..522fcc17c3 100644 --- a/frontend/portal/src/components/billing/LinkAccountPrompt.tsx +++ b/frontend/editor/src/portal/components/billing/LinkAccountPrompt.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Button, Card, EmptyState } from "@shared/components"; +import { Button, Card, EmptyState } from "@app/ui"; import { useUI } from "@portal/contexts/UIContext"; /** @@ -14,14 +14,17 @@ export function LinkAccountPrompt() { openLinkModal()}> - {t("billing.linkPrompt.cta", "Link Stirling account")} + {t("portal.billing.linkPrompt.cta", "Link Stirling account")} } /> diff --git a/frontend/portal/src/components/billing/PaymentMethodCard.stories.tsx b/frontend/editor/src/portal/components/billing/PaymentMethodCard.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/PaymentMethodCard.stories.tsx rename to frontend/editor/src/portal/components/billing/PaymentMethodCard.stories.tsx diff --git a/frontend/portal/src/components/billing/PaymentMethodCard.tsx b/frontend/editor/src/portal/components/billing/PaymentMethodCard.tsx similarity index 79% rename from frontend/portal/src/components/billing/PaymentMethodCard.tsx rename to frontend/editor/src/portal/components/billing/PaymentMethodCard.tsx index ee2a1a4f01..cd3b094a5e 100644 --- a/frontend/portal/src/components/billing/PaymentMethodCard.tsx +++ b/frontend/editor/src/portal/components/billing/PaymentMethodCard.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button, Card } from "@shared/components"; +import { Button, Card } from "@app/ui"; import { fetchPaymentMethod, type PaymentMethod } from "@portal/api/billing"; interface Props { @@ -46,18 +46,18 @@ export function PaymentMethodCard({ onManage, managing }: Props) {

- {t("billing.paymentMethod.eyebrow", "Payment method")} + {t("portal.billing.paymentMethod.eyebrow", "Payment method")} {hasCard ? ( <>

{t( - "billing.paymentMethod.cardEnding", + "portal.billing.paymentMethod.cardEnding", "{{brand}} ending {{last4}}", { brand: titleCase( pm.brand ?? - t("billing.paymentMethod.cardFallback", "Card"), + t("portal.billing.paymentMethod.cardFallback", "Card"), ), last4: pm.last4, }, @@ -66,23 +66,29 @@ export function PaymentMethodCard({ onManage, managing }: Props) {

{pm.expMonth != null && pm.expYear != null ? t( - "billing.paymentMethod.expiresBilledMonthly", + "portal.billing.paymentMethod.expiresBilledMonthly", "Expires {{expiry}} · billed monthly", { expiry: `${String(pm.expMonth).padStart(2, "0")}/${pm.expYear}`, }, ) - : t("billing.paymentMethod.billedMonthly", "Billed monthly")} + : t( + "portal.billing.paymentMethod.billedMonthly", + "Billed monthly", + )}

) : ( <>

- {t("billing.paymentMethod.managedTitle", "Managed in Stripe")} + {t( + "portal.billing.paymentMethod.managedTitle", + "Managed in Stripe", + )}

{t( - "billing.paymentMethod.managedSub", + "portal.billing.paymentMethod.managedSub", "Your card and billing details are kept securely in Stripe's customer portal.", )}

@@ -95,7 +101,7 @@ export function PaymentMethodCard({ onManage, managing }: Props) { loading={managing} onClick={onManage} > - {t("billing.paymentMethod.update", "Update")} + {t("portal.billing.paymentMethod.update", "Update")}
diff --git a/frontend/portal/src/components/billing/PdfsProcessedCard.stories.tsx b/frontend/editor/src/portal/components/billing/PdfsProcessedCard.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/PdfsProcessedCard.stories.tsx rename to frontend/editor/src/portal/components/billing/PdfsProcessedCard.stories.tsx diff --git a/frontend/portal/src/components/billing/PdfsProcessedCard.tsx b/frontend/editor/src/portal/components/billing/PdfsProcessedCard.tsx similarity index 80% rename from frontend/portal/src/components/billing/PdfsProcessedCard.tsx rename to frontend/editor/src/portal/components/billing/PdfsProcessedCard.tsx index ba5ac17ea9..aec101ea83 100644 --- a/frontend/portal/src/components/billing/PdfsProcessedCard.tsx +++ b/frontend/editor/src/portal/components/billing/PdfsProcessedCard.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Card } from "@shared/components"; +import { Card } from "@app/ui"; import type { Wallet, WalletCategoryBreakdown } from "@portal/api/billing"; /** @@ -19,25 +19,25 @@ const SEGMENTS: ReadonlyArray<{ }> = [ { key: "api", - labelKey: "billing.pdfsProcessed.segmentApiLabel", + labelKey: "portal.billing.pdfsProcessed.segmentApiLabel", labelDefault: "API", - descKey: "billing.pdfsProcessed.segmentApiDesc", + descKey: "portal.billing.pdfsProcessed.segmentApiDesc", descDefault: "Direct API requests", cls: "blue", }, { key: "ai", - labelKey: "billing.pdfsProcessed.segmentAgentsLabel", + labelKey: "portal.billing.pdfsProcessed.segmentAgentsLabel", labelDefault: "Agents", - descKey: "billing.pdfsProcessed.segmentAgentsDesc", + descKey: "portal.billing.pdfsProcessed.segmentAgentsDesc", descDefault: "AI agent actions", cls: "purple", }, { key: "automation", - labelKey: "billing.pdfsProcessed.segmentAutomationLabel", + labelKey: "portal.billing.pdfsProcessed.segmentAutomationLabel", labelDefault: "Automation", - descKey: "billing.pdfsProcessed.segmentAutomationDesc", + descKey: "portal.billing.pdfsProcessed.segmentAutomationDesc", descDefault: "Automations & pipelines", cls: "teal", }, @@ -51,14 +51,17 @@ export function PdfsProcessedCard({ wallet }: { wallet: Wallet }) { return ( - {t("billing.pdfsProcessed.eyebrow", "PDFs processed this period")} + {t( + "portal.billing.pdfsProcessed.eyebrow", + "PDFs processed this period", + )}
{wallet.billableUsed.toLocaleString()} - {t("billing.pdfsProcessed.unit", "metered PDFs")} + {t("portal.billing.pdfsProcessed.unit", "metered PDFs")}
@@ -68,7 +71,7 @@ export function PdfsProcessedCard({ wallet }: { wallet: Wallet }) { className="portal-billing__segbar" role="img" aria-label={t( - "billing.pdfsProcessed.segbarAriaLabel", + "portal.billing.pdfsProcessed.segbarAriaLabel", "Metered PDFs split by category", )} > @@ -94,7 +97,7 @@ export function PdfsProcessedCard({ wallet }: { wallet: Wallet }) { {t( - "billing.pdfsProcessed.legendValue", + "portal.billing.pdfsProcessed.legendValue", "{{formatted}} PDFs", { count: b[s.key], @@ -112,7 +115,7 @@ export function PdfsProcessedCard({ wallet }: { wallet: Wallet }) { ) : (

{t( - "billing.pdfsProcessed.emptyPeriod", + "portal.billing.pdfsProcessed.emptyPeriod", "No metered processing yet this period.", )}

diff --git a/frontend/portal/src/components/billing/SpendLimitCard.stories.tsx b/frontend/editor/src/portal/components/billing/SpendLimitCard.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/SpendLimitCard.stories.tsx rename to frontend/editor/src/portal/components/billing/SpendLimitCard.stories.tsx diff --git a/frontend/portal/src/components/billing/SpendLimitCard.tsx b/frontend/editor/src/portal/components/billing/SpendLimitCard.tsx similarity index 84% rename from frontend/portal/src/components/billing/SpendLimitCard.tsx rename to frontend/editor/src/portal/components/billing/SpendLimitCard.tsx index 2593810017..d117f3d9bc 100644 --- a/frontend/portal/src/components/billing/SpendLimitCard.tsx +++ b/frontend/editor/src/portal/components/billing/SpendLimitCard.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Banner, Button, Card } from "@shared/components"; +import { Banner, Button, Card } from "@app/ui"; import { currencySymbol, docCapForMoney, @@ -9,7 +9,7 @@ import { MeterBar, meterState, SpendCapControl as SharedSpendCapControl, -} from "@shared/billing"; +} from "@app/billing"; import type { Wallet } from "@portal/api/billing"; import { updateCap } from "@portal/api/billing"; @@ -127,10 +127,10 @@ export function SpendLimitCard({ return ( - {t("billing.spendLimit.eyebrow", "Spend limit")} + {t("portal.billing.spendLimit.eyebrow", "Spend limit")}

- {t("billing.spendLimit.editTitle", "Set your monthly ceiling")} + {t("portal.billing.spendLimit.editTitle", "Set your monthly ceiling")}

@@ -151,7 +151,7 @@ export function SpendLimitCard({ onClick={() => setDraftCap(proj.suggestedMajor)} > {t( - "billing.spendLimit.useSuggested", + "portal.billing.spendLimit.useSuggested", "Use suggested · {{amount}} / month", { amount: formatMoneyMajor(proj.suggestedMajor, wallet.currency), @@ -162,10 +162,10 @@ export function SpendLimitCard({
- {t("billing.spendLimit.guardrailLabel", "Your guardrail:")} + {t("portal.billing.spendLimit.guardrailLabel", "Your guardrail:")} {" "} {t( - "billing.spendLimit.guardrailBody", + "portal.billing.spendLimit.guardrailBody", "a hard ceiling — you're never billed past it. At the cap, metered processing pauses (unlimited PDF editing keeps working) until you raise it or the cycle resets. Nothing is lost.", )}
@@ -173,7 +173,10 @@ export function SpendLimitCard({ {error && ( {error} @@ -185,10 +188,10 @@ export function SpendLimitCard({ size="sm" onClick={() => onAdjustingChange(false)} > - {t("billing.spendLimit.cancel", "Cancel")} + {t("portal.billing.spendLimit.cancel", "Cancel")}
@@ -209,11 +212,11 @@ export function SpendLimitCard({
- {t("billing.spendLimit.eyebrow", "Spend limit")} + {t("portal.billing.spendLimit.eyebrow", "Spend limit")}

{t( - "billing.spendLimit.displaySub", + "portal.billing.spendLimit.displaySub", "You're only billed for what you process automatically — never past the ceiling.", )}

@@ -224,7 +227,7 @@ export function SpendLimitCard({ size="sm" onClick={() => onAdjustingChange(true)} > - {t("billing.spendLimit.adjustLimit", "Adjust limit")} + {t("portal.billing.spendLimit.adjustLimit", "Adjust limit")} )}
@@ -240,18 +243,18 @@ export function SpendLimitCard({ capActive ? docEstimate != null ? t( - "billing.spendLimit.capSuffixWithDocs", + "portal.billing.spendLimit.capSuffixWithDocs", "/ month · ≈ {{documents}} documents", { documents: docEstimate.toLocaleString(), }, ) - : t("billing.spendLimit.capSuffix", "/ month") - : t("billing.spendLimit.noCap", "no cap") + : t("portal.billing.spendLimit.capSuffix", "/ month") + : t("portal.billing.spendLimit.noCap", "no cap") } statusLabel={ capActive - ? t("billing.spendLimit.pctUsed", "{{pct}}% used", { + ? t("portal.billing.spendLimit.pctUsed", "{{pct}}% used", { pct: Math.round(pct), }) : null @@ -262,7 +265,7 @@ export function SpendLimitCard({ <> {t( - "billing.spendLimit.usedThisMonth", + "portal.billing.spendLimit.usedThisMonth", "{{amount}} used this month", { amount: spentLabel, @@ -270,15 +273,19 @@ export function SpendLimitCard({ )} - {t("billing.spendLimit.remaining", "{{amount}} remaining", { - amount: formatMinor(remainingMinor, wallet.currency), - })} + {t( + "portal.billing.spendLimit.remaining", + "{{amount}} remaining", + { + amount: formatMinor(remainingMinor, wallet.currency), + }, + )} ) : ( {t( - "billing.spendLimit.thisPeriodUncapped", + "portal.billing.spendLimit.thisPeriodUncapped", "{{amount}} this period · uncapped", { amount: spentLabel, @@ -293,10 +300,13 @@ export function SpendLimitCard({ {proj && (

- {t("billing.spendLimit.projection.label", "Projected to exceed.")} + {t( + "portal.billing.spendLimit.projection.label", + "Projected to exceed.", + )} {" "} {t( - "billing.spendLimit.projection.body", + "portal.billing.spendLimit.projection.body", "At {{rate}}/day you reach the cap in ~{{count}} days (~{{monthEnd}} month-end). Suggested limit ~{{suggested}}.", { count: proj.daysToCap, diff --git a/frontend/portal/src/components/billing/SpendThisMonthCard.stories.tsx b/frontend/editor/src/portal/components/billing/SpendThisMonthCard.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/SpendThisMonthCard.stories.tsx rename to frontend/editor/src/portal/components/billing/SpendThisMonthCard.stories.tsx diff --git a/frontend/portal/src/components/billing/SpendThisMonthCard.tsx b/frontend/editor/src/portal/components/billing/SpendThisMonthCard.tsx similarity index 86% rename from frontend/portal/src/components/billing/SpendThisMonthCard.tsx rename to frontend/editor/src/portal/components/billing/SpendThisMonthCard.tsx index 2a8baf2986..d4a969ae0c 100644 --- a/frontend/portal/src/components/billing/SpendThisMonthCard.tsx +++ b/frontend/editor/src/portal/components/billing/SpendThisMonthCard.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; -import { Card } from "@shared/components"; -import { formatMinor } from "@shared/billing"; +import { Card } from "@app/ui"; +import { formatMinor } from "@app/billing"; import type { Wallet } from "@portal/api/billing"; import { EnterpriseUpsell } from "@portal/components/billing/EnterpriseUpsell"; @@ -19,7 +19,7 @@ export function SpendThisMonthCard({ wallet }: { wallet: Wallet }) { return ( - {t("billing.spendThisMonth.eyebrow", "Spend this month")} + {t("portal.billing.spendThisMonth.eyebrow", "Spend this month")}

@@ -29,7 +29,7 @@ export function SpendThisMonthCard({ wallet }: { wallet: Wallet }) {

{rateLabel ? t( - "billing.spendThisMonth.processedWithRate", + "portal.billing.spendThisMonth.processedWithRate", "{{formattedCount}} PDFs processed, at {{rate}} each.", { count: wallet.billableUsed, @@ -38,7 +38,7 @@ export function SpendThisMonthCard({ wallet }: { wallet: Wallet }) { }, ) : t( - "billing.spendThisMonth.processed", + "portal.billing.spendThisMonth.processed", "{{formattedCount}} PDFs processed.", { count: wallet.billableUsed, diff --git a/frontend/portal/src/components/billing/StripeCheckoutModal.tsx b/frontend/editor/src/portal/components/billing/StripeCheckoutModal.tsx similarity index 89% rename from frontend/portal/src/components/billing/StripeCheckoutModal.tsx rename to frontend/editor/src/portal/components/billing/StripeCheckoutModal.tsx index 24d5cbb251..313694baa9 100644 --- a/frontend/portal/src/components/billing/StripeCheckoutModal.tsx +++ b/frontend/editor/src/portal/components/billing/StripeCheckoutModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Banner, Modal, Skeleton } from "@shared/components"; +import { Banner, Modal, Skeleton } from "@app/ui"; import { EmbeddedCheckout, EmbeddedCheckoutProvider, @@ -91,7 +91,7 @@ export function StripeCheckoutModal({ if (!session.clientSecret) { setError( t( - "billing.checkout.noClientSecret", + "portal.billing.checkout.noClientSecret", "Edge function returned no client_secret.", ), ); @@ -120,9 +120,9 @@ export function StripeCheckoutModal({ open={open} onClose={onClose} width="lg" - title={t("billing.checkout.title", "Turn on the Processor plan")} + title={t("portal.billing.checkout.title", "Turn on the Processor plan")} subtitle={t( - "billing.checkout.subtitle", + "portal.billing.checkout.subtitle", "Add a card to keep going past your free Editor-plan grant. Stripe handles the rest.", )} > @@ -130,14 +130,14 @@ export function StripeCheckoutModal({ - {t("billing.checkout.notConfigured.bodyBefore", "Set")}{" "} + {t("portal.billing.checkout.notConfigured.bodyBefore", "Set")}{" "} VITE_STRIPE_PUBLISHABLE_KEY{" "} {t( - "billing.checkout.notConfigured.bodyAfter", + "portal.billing.checkout.notConfigured.bodyAfter", "in the portal env to enable in-app checkout.", )} @@ -145,7 +145,10 @@ export function StripeCheckoutModal({ {publishableKey && error && ( {error} diff --git a/frontend/portal/src/components/billing/SubscribedPlanView.stories.tsx b/frontend/editor/src/portal/components/billing/SubscribedPlanView.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/SubscribedPlanView.stories.tsx rename to frontend/editor/src/portal/components/billing/SubscribedPlanView.stories.tsx diff --git a/frontend/portal/src/components/billing/SubscribedPlanView.tsx b/frontend/editor/src/portal/components/billing/SubscribedPlanView.tsx similarity index 87% rename from frontend/portal/src/components/billing/SubscribedPlanView.tsx rename to frontend/editor/src/portal/components/billing/SubscribedPlanView.tsx index 6ccdbe7e8d..37bf35a54a 100644 --- a/frontend/portal/src/components/billing/SubscribedPlanView.tsx +++ b/frontend/editor/src/portal/components/billing/SubscribedPlanView.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Banner, Button } from "@shared/components"; -import { meterState } from "@shared/billing"; +import { Banner, Button } from "@app/ui"; +import { meterState } from "@app/billing"; import type { Wallet } from "@portal/api/billing"; import { useStripePortal } from "@portal/hooks/useStripePortal"; import { FreePdfEditorsCard } from "@portal/components/billing/FreePdfEditorsCard"; @@ -57,11 +57,11 @@ export function SubscribedPlanView({ wallet, onWalletChange }: Props) { title={ state === "DEGRADED" ? t( - "billing.subscribedPlan.capWarn.reachedTitle", + "portal.billing.subscribedPlan.capWarn.reachedTitle", "Monthly spend limit reached", ) : t( - "billing.subscribedPlan.capWarn.approachingTitle", + "portal.billing.subscribedPlan.capWarn.approachingTitle", "You're at {{pct}}% of your monthly spend limit", { pct: Math.round(pct), @@ -71,18 +71,21 @@ export function SubscribedPlanView({ wallet, onWalletChange }: Props) { action={ isLeader ? ( ) : undefined } > {state === "DEGRADED" ? t( - "billing.subscribedPlan.capWarn.reachedBody", + "portal.billing.subscribedPlan.capWarn.reachedBody", "Metered processing is paused until you raise the limit or the cycle resets. Unlimited PDF editing keeps working.", ) : t( - "billing.subscribedPlan.capWarn.approachingBody", + "portal.billing.subscribedPlan.capWarn.approachingBody", "Raise it now so automated processing never pauses.", )} @@ -110,7 +113,7 @@ export function SubscribedPlanView({ wallet, onWalletChange }: Props) { diff --git a/frontend/portal/src/components/billing/WalletMeter.stories.tsx b/frontend/editor/src/portal/components/billing/WalletMeter.stories.tsx similarity index 100% rename from frontend/portal/src/components/billing/WalletMeter.stories.tsx rename to frontend/editor/src/portal/components/billing/WalletMeter.stories.tsx diff --git a/frontend/portal/src/components/billing/WalletMeter.tsx b/frontend/editor/src/portal/components/billing/WalletMeter.tsx similarity index 78% rename from frontend/portal/src/components/billing/WalletMeter.tsx rename to frontend/editor/src/portal/components/billing/WalletMeter.tsx index c8a2ed81de..1cb7f5794f 100644 --- a/frontend/portal/src/components/billing/WalletMeter.tsx +++ b/frontend/editor/src/portal/components/billing/WalletMeter.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { Card } from "@shared/components"; -import { formatMinor, MeterBar, meterState } from "@shared/billing"; +import { Card } from "@app/ui"; +import { formatMinor, MeterBar, meterState } from "@app/billing"; import type { Wallet } from "@portal/api/billing"; interface Props { @@ -27,7 +27,7 @@ export function WalletMeter({ wallet, action }: Props) { const title = rate != null ? t( - "billing.walletMeter.titleWithRate", + "portal.billing.walletMeter.titleWithRate", "Process {{allowance}} PDFs free, then {{rate}}/PDF", { count: wallet.freeAllowance, @@ -35,22 +35,26 @@ export function WalletMeter({ wallet, action }: Props) { rate: formatMinor(rate, wallet.currency), }, ) - : t("billing.walletMeter.title", "Process {{allowance}} PDFs free", { - count: wallet.freeAllowance, - allowance: wallet.freeAllowance.toLocaleString(), - }); + : t( + "portal.billing.walletMeter.title", + "Process {{allowance}} PDFs free", + { + count: wallet.freeAllowance, + allowance: wallet.freeAllowance.toLocaleString(), + }, + ); return (

- {t("billing.walletMeter.eyebrow", "Processor trial")} + {t("portal.billing.walletMeter.eyebrow", "Processor trial")}

{title}

{t( - "billing.walletMeter.sub", + "portal.billing.walletMeter.sub", "Use the PDF Editor for free. Pay to process PDFs automatically.", )}

@@ -63,7 +67,7 @@ export function WalletMeter({ wallet, action }: Props) { pct={pct} figure={wallet.billableUsed.toLocaleString()} capSuffix={t( - "billing.walletMeter.capSuffix", + "portal.billing.walletMeter.capSuffix", "of {{allowance}} free PDFs used", { count: wallet.freeAllowance, @@ -71,7 +75,7 @@ export function WalletMeter({ wallet, action }: Props) { }, )} statusLabel={t( - "billing.walletMeter.statusLabel", + "portal.billing.walletMeter.statusLabel", "{{remaining}} left", { count: wallet.freeRemaining, diff --git a/frontend/portal/src/components/billing/billing.css b/frontend/editor/src/portal/components/billing/billing.css similarity index 100% rename from frontend/portal/src/components/billing/billing.css rename to frontend/editor/src/portal/components/billing/billing.css diff --git a/frontend/portal/src/components/billing/walletFixtures.ts b/frontend/editor/src/portal/components/billing/walletFixtures.ts similarity index 100% rename from frontend/portal/src/components/billing/walletFixtures.ts rename to frontend/editor/src/portal/components/billing/walletFixtures.ts diff --git a/frontend/portal/src/components/catalogue/ComponentCard.stories.tsx b/frontend/editor/src/portal/components/catalogue/ComponentCard.stories.tsx similarity index 100% rename from frontend/portal/src/components/catalogue/ComponentCard.stories.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentCard.stories.tsx diff --git a/frontend/portal/src/components/catalogue/ComponentCard.tsx b/frontend/editor/src/portal/components/catalogue/ComponentCard.tsx similarity index 90% rename from frontend/portal/src/components/catalogue/ComponentCard.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentCard.tsx index 9c662d5b9b..07d3af75c1 100644 --- a/frontend/portal/src/components/catalogue/ComponentCard.tsx +++ b/frontend/editor/src/portal/components/catalogue/ComponentCard.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Card, Chip, StatusBadge } from "@shared/components"; +import { Card, Chip, StatusBadge } from "@app/ui"; import { type SdkComponent, MATURITY_META, @@ -30,7 +30,9 @@ export function ComponentCard({ className={"portal-components__card" + (unlocked ? "" : " is-locked")} role="button" tabIndex={0} - aria-label={t("catalogue.card.openAriaLabel", { name: component.name })} + aria-label={t("portal.catalogue.card.openAriaLabel", { + name: component.name, + })} onClick={() => onOpen(component)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { @@ -47,7 +49,7 @@ export function ComponentCard({ {!unlocked && ( 🔒 diff --git a/frontend/portal/src/components/catalogue/ComponentDetailModal.stories.tsx b/frontend/editor/src/portal/components/catalogue/ComponentDetailModal.stories.tsx similarity index 100% rename from frontend/portal/src/components/catalogue/ComponentDetailModal.stories.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentDetailModal.stories.tsx diff --git a/frontend/portal/src/components/catalogue/ComponentDetailModal.tsx b/frontend/editor/src/portal/components/catalogue/ComponentDetailModal.tsx similarity index 79% rename from frontend/portal/src/components/catalogue/ComponentDetailModal.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentDetailModal.tsx index 970d2fe392..1959e10b1d 100644 --- a/frontend/portal/src/components/catalogue/ComponentDetailModal.tsx +++ b/frontend/editor/src/portal/components/catalogue/ComponentDetailModal.tsx @@ -9,7 +9,7 @@ import { StatTile, StatusBadge, Tabs, -} from "@shared/components"; +} from "@app/ui"; import { type SdkComponent, MATURITY_META, @@ -49,14 +49,14 @@ export function ComponentDetailModal({ ); } const tabs = TAB_KEYS.map((key) => ({ key, - label: t(`catalogue.detail.tabs.${key}`), + label: t(`portal.catalogue.detail.tabs.${key}`), })); const maturity = MATURITY_META[component.maturity]; @@ -92,7 +92,7 @@ export function ComponentDetailModal({ // publishable key scoped to this component. onClick={() => onClose()} > - {t("catalogue.detail.addToProject")} + {t("portal.catalogue.detail.addToProject")}
) : ( @@ -102,7 +102,7 @@ export function ComponentDetailModal({ // TODO(backend): route to the upgrade / contact-sales flow. onClick={() => onClose()} > - {t("catalogue.detail.upgradeToUnlock")} + {t("portal.catalogue.detail.upgradeToUnlock")} ) } @@ -110,8 +110,8 @@ export function ComponentDetailModal({ {!unlocked && ( here, booting the component against a demo document and the dev's publishable key. */} - {t("catalogue.detail.preview.badge")} + {t("portal.catalogue.detail.preview.badge")} - {t("catalogue.detail.preview.note")} + {t("portal.catalogue.detail.preview.note")}
@@ -136,7 +136,7 @@ export function ComponentDetailModal({ activeKey={tab} onChange={setTab} variant="underline" - ariaLabel={t("catalogue.detail.tabsAriaLabel")} + ariaLabel={t("portal.catalogue.detail.tabsAriaLabel")} />
@@ -154,25 +154,25 @@ export function ComponentDetailModal({
0 - ? t("catalogue.detail.stats.freeQuotaValue", { + ? t("portal.catalogue.detail.stats.freeQuotaValue", { amount: component.pricing.freeQuota.toLocaleString(), }) - : t("catalogue.detail.stats.none") + : t("portal.catalogue.detail.stats.none") } />
@@ -184,12 +184,12 @@ export function ComponentDetailModal({
)} @@ -200,26 +200,26 @@ export function ComponentDetailModal({
0 - ? t("catalogue.detail.stats.freeQuotaValue", { + ? t("portal.catalogue.detail.stats.freeQuotaValue", { amount: component.pricing.freeQuota.toLocaleString(), }) - : t("catalogue.detail.stats.none") + : t("portal.catalogue.detail.stats.none") } />

- {t("catalogue.detail.pricing.note", { + {t("portal.catalogue.detail.pricing.note", { unit: component.pricing.unit, })}

diff --git a/frontend/portal/src/components/catalogue/ComponentGrid.stories.tsx b/frontend/editor/src/portal/components/catalogue/ComponentGrid.stories.tsx similarity index 100% rename from frontend/portal/src/components/catalogue/ComponentGrid.stories.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentGrid.stories.tsx diff --git a/frontend/portal/src/components/catalogue/ComponentGrid.tsx b/frontend/editor/src/portal/components/catalogue/ComponentGrid.tsx similarity index 100% rename from frontend/portal/src/components/catalogue/ComponentGrid.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentGrid.tsx diff --git a/frontend/portal/src/components/catalogue/ComponentPropsTable.stories.tsx b/frontend/editor/src/portal/components/catalogue/ComponentPropsTable.stories.tsx similarity index 100% rename from frontend/portal/src/components/catalogue/ComponentPropsTable.stories.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentPropsTable.stories.tsx diff --git a/frontend/portal/src/components/catalogue/ComponentPropsTable.tsx b/frontend/editor/src/portal/components/catalogue/ComponentPropsTable.tsx similarity index 77% rename from frontend/portal/src/components/catalogue/ComponentPropsTable.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentPropsTable.tsx index 0bb77cb47a..8ff5155b63 100644 --- a/frontend/portal/src/components/catalogue/ComponentPropsTable.tsx +++ b/frontend/editor/src/portal/components/catalogue/ComponentPropsTable.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Chip, Table, type TableColumn } from "@shared/components"; +import { Chip, Table, type TableColumn } from "@app/ui"; import type { ComponentProp } from "@portal/api/sdkComponents"; import "@portal/views/Components.css"; @@ -15,35 +15,35 @@ export function ComponentPropsTable({ props: rows }: ComponentPropsTableProps) { () => [ { key: "name", - header: t("catalogue.props.columns.name"), + header: t("portal.catalogue.props.columns.name"), render: (p) => ( {p.name} ), }, { key: "type", - header: t("catalogue.props.columns.type"), + header: t("portal.catalogue.props.columns.type"), render: (p) => ( {p.type} ), }, { key: "required", - header: t("catalogue.props.columns.required"), + header: t("portal.catalogue.props.columns.required"), render: (p) => p.required ? ( - {t("catalogue.props.required")} + {t("portal.catalogue.props.required")} ) : ( - {t("catalogue.props.optional")} + {t("portal.catalogue.props.optional")} ), }, { key: "description", - header: t("catalogue.props.columns.description"), + header: t("portal.catalogue.props.columns.description"), render: (p) => ( {p.description} ), diff --git a/frontend/portal/src/components/catalogue/ComponentsSummaryStrip.tsx b/frontend/editor/src/portal/components/catalogue/ComponentsSummaryStrip.tsx similarity index 82% rename from frontend/portal/src/components/catalogue/ComponentsSummaryStrip.tsx rename to frontend/editor/src/portal/components/catalogue/ComponentsSummaryStrip.tsx index ec46ad855f..ccb3965a5c 100644 --- a/frontend/portal/src/components/catalogue/ComponentsSummaryStrip.tsx +++ b/frontend/editor/src/portal/components/catalogue/ComponentsSummaryStrip.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { MetricCard, MetricStrip } from "@shared/components"; +import { MetricCard, MetricStrip } from "@app/ui"; import type { ComponentsResponse } from "@portal/api/sdkComponents"; /** @@ -8,10 +8,10 @@ import type { ComponentsResponse } from "@portal/api/sdkComponents"; * Only values flow from the API. */ const KPI_LABEL_KEYS = [ - "catalogue.summary.componentsGa", - "catalogue.summary.inBeta", - "catalogue.summary.embedsThisMonth", - "catalogue.summary.componentSpendMtd", + "portal.catalogue.summary.componentsGa", + "portal.catalogue.summary.inBeta", + "portal.catalogue.summary.embedsThisMonth", + "portal.catalogue.summary.componentSpendMtd", ] as const; interface ComponentsSummaryStripProps { diff --git a/frontend/portal/src/components/docs/AuthenticationSection.stories.tsx b/frontend/editor/src/portal/components/docs/AuthenticationSection.stories.tsx similarity index 100% rename from frontend/portal/src/components/docs/AuthenticationSection.stories.tsx rename to frontend/editor/src/portal/components/docs/AuthenticationSection.stories.tsx diff --git a/frontend/portal/src/components/docs/AuthenticationSection.tsx b/frontend/editor/src/portal/components/docs/AuthenticationSection.tsx similarity index 65% rename from frontend/portal/src/components/docs/AuthenticationSection.tsx rename to frontend/editor/src/portal/components/docs/AuthenticationSection.tsx index 65b1d89599..e15e04592c 100644 --- a/frontend/portal/src/components/docs/AuthenticationSection.tsx +++ b/frontend/editor/src/portal/components/docs/AuthenticationSection.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Chip, CodeBlock } from "@shared/components"; +import { Chip, CodeBlock } from "@app/ui"; import { DocsSection } from "@portal/components/docs/DocsSection"; export function AuthenticationSection() { @@ -7,13 +7,13 @@ export function AuthenticationSection() { return (
@@ -21,13 +21,13 @@ export function AuthenticationSection() { sk_live_ - {t("docs.authentication.liveKey")} + {t("portal.docs.authentication.liveKey")}
sk_test_ - {t("docs.authentication.testKey")} + {t("portal.docs.authentication.testKey")}
diff --git a/frontend/portal/src/components/docs/ComponentsSection.stories.tsx b/frontend/editor/src/portal/components/docs/ComponentsSection.stories.tsx similarity index 100% rename from frontend/portal/src/components/docs/ComponentsSection.stories.tsx rename to frontend/editor/src/portal/components/docs/ComponentsSection.stories.tsx diff --git a/frontend/portal/src/components/docs/ComponentsSection.tsx b/frontend/editor/src/portal/components/docs/ComponentsSection.tsx similarity index 81% rename from frontend/portal/src/components/docs/ComponentsSection.tsx rename to frontend/editor/src/portal/components/docs/ComponentsSection.tsx index 10a2349154..5ad8c4b502 100644 --- a/frontend/portal/src/components/docs/ComponentsSection.tsx +++ b/frontend/editor/src/portal/components/docs/ComponentsSection.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Card, Chip, CodeBlock } from "@shared/components"; +import { Card, Chip, CodeBlock } from "@app/ui"; import type { EmbedComponent } from "@portal/api/docs"; import { DocsSection } from "@portal/components/docs/DocsSection"; @@ -12,9 +12,9 @@ export function ComponentsSection({ return (
{components.map((c) => ( @@ -31,7 +31,7 @@ export function ComponentsSection({
+