Merge branch 'main' into docker_pinned_20260624
@@ -27,7 +27,6 @@ node_modules/
|
||||
**/node_modules/
|
||||
frontend/node_modules/
|
||||
frontend/editor/dist/
|
||||
frontend/dist-portal/
|
||||
frontend/editor/playwright-report/
|
||||
.npm/
|
||||
.yarn/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
# ============================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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=
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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[] };
|
||||
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 253 B After Width: | Height: | Size: 253 B |
|
Before Width: | Height: | Size: 253 B After Width: | Height: | Size: 253 B |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 717 KiB After Width: | Height: | Size: 717 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 426 B |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -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";
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="sui-panelhdr">
|
||||
<div className="sui-panelhdr__bar">
|
||||
<span className="sui-panelhdr__icon">{icon}</span>
|
||||
<span className="sui-panelhdr__label">{title}</span>
|
||||
</div>
|
||||
{onClose && (
|
||||
<ActionIcon
|
||||
className="sui-panelhdr__close"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
radius="xl"
|
||||
size="md"
|
||||
onClick={onClose}
|
||||
aria-label={closeLabel}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 18 }} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -112,12 +112,14 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
|
||||
// falling back to the backend version
|
||||
const currentVersion = appVersion ?? config?.appVersion ?? null;
|
||||
|
||||
// Check for updates on mount
|
||||
// Check for updates on mount — skipped when the update UI is hidden (SaaS
|
||||
// build, managed-disabled desktop) so no external update call ever fires.
|
||||
useEffect(() => {
|
||||
if (hideUpdateSection) return;
|
||||
if (currentVersion) {
|
||||
checkForUpdate();
|
||||
}
|
||||
}, [currentVersion, config?.machineType]);
|
||||
}, [currentVersion, config?.machineType, hideUpdateSection]);
|
||||
|
||||
const checkForUpdate = async () => {
|
||||
if (!currentVersion) return;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 ? (
|
||||
<PanelHeader
|
||||
<ToolPanelHeader
|
||||
icon={
|
||||
<ToolIcon
|
||||
icon={activeTool.icon}
|
||||
|
||||
@@ -50,6 +50,10 @@ function findViteEnvVars(srcDir: string): Set<string> {
|
||||
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),
|
||||
]);
|
||||
|
||||
@@ -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/<folder>/* 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/<folder>/* 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 = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string>): 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<string>();
|
||||
@@ -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 [];
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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([]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@app/*": ["../../src/core/*"],
|
||||
"@shared/*": ["../../../shared/*"]
|
||||
"@app/*": ["../../src/core/*"]
|
||||
}
|
||||
},
|
||||
"include": ["../global.d.ts", "../*.js", "../*.ts", "../*.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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import CoreGeneralSection from "@core/components/shared/config/configSections/GeneralSection";
|
||||
import { DefaultAppSettings } from "@app/components/shared/config/configSections/DefaultAppSettings";
|
||||
import { useDesktopInstall } from "@app/hooks/useDesktopInstall";
|
||||
import { useSaaSMode } from "@app/hooks/useSaaSMode";
|
||||
import {
|
||||
desktopUpdateService,
|
||||
type UpdateMode,
|
||||
@@ -22,6 +23,9 @@ import {
|
||||
const GeneralSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const install = useDesktopInstall();
|
||||
// In SaaS connection mode the cloud owns app versioning — hide the update
|
||||
// section (which also stops the core auto-check from firing).
|
||||
const isSaaSMode = useSaaSMode();
|
||||
const [updateModeInfo, setUpdateModeInfo] = useState<UpdateModeInfo>({
|
||||
mode: "prompt",
|
||||
locked: false,
|
||||
@@ -93,7 +97,8 @@ const GeneralSection: React.FC = () => {
|
||||
)}
|
||||
<CoreGeneralSection
|
||||
hideUpdateSection={
|
||||
updateModeInfo.mode === "disabled" && updateModeInfo.locked
|
||||
isSaaSMode ||
|
||||
(updateModeInfo.mode === "disabled" && updateModeInfo.locked)
|
||||
}
|
||||
desktopInstall={{
|
||||
state: install.state,
|
||||
|
||||
@@ -9,6 +9,7 @@ const getVersionMock = vi.fn();
|
||||
const getUpdateModeMock = vi.fn();
|
||||
const canInstallUpdatesMock = vi.fn();
|
||||
const getUpdateSummaryMock = vi.fn();
|
||||
const getCurrentModeMock = vi.fn();
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: (cmd: string, args?: unknown) => invokeMock(cmd, args),
|
||||
@@ -36,6 +37,12 @@ vi.mock("@app/services/desktopUpdateService", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@app/services/connectionModeService", () => ({
|
||||
connectionModeService: {
|
||||
getCurrentMode: () => getCurrentModeMock(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { useDesktopUpdatePopup } from "@app/hooks/useDesktopUpdatePopup";
|
||||
|
||||
/** Flush pending microtasks so awaited promises settle. */
|
||||
@@ -74,8 +81,11 @@ describe("useDesktopUpdatePopup — auto mode", () => {
|
||||
getUpdateModeMock.mockReset();
|
||||
canInstallUpdatesMock.mockReset();
|
||||
getUpdateSummaryMock.mockReset();
|
||||
getCurrentModeMock.mockReset();
|
||||
|
||||
// Defaults: auto mode, update available, install permitted.
|
||||
// Defaults: local (non-SaaS) connection, auto mode, update available,
|
||||
// install permitted.
|
||||
getCurrentModeMock.mockResolvedValue("local");
|
||||
getUpdateModeMock.mockResolvedValue("auto");
|
||||
getVersionMock.mockResolvedValue("1.0.0");
|
||||
getUpdateSummaryMock.mockResolvedValue({ latest_version: "2.0.0" });
|
||||
@@ -189,4 +199,18 @@ describe("useDesktopUpdatePopup — auto mode", () => {
|
||||
expect(invocations).toContain("download_and_install_update");
|
||||
expect(invocations).toContain("restart_app");
|
||||
});
|
||||
|
||||
it("skips the update check entirely in SaaS connection mode", async () => {
|
||||
// In SaaS mode the cloud owns versioning — the self-hosted update check
|
||||
// must never run: no mode lookup, no external summary fetch, no install.
|
||||
getCurrentModeMock.mockResolvedValue("saas");
|
||||
|
||||
await runStartup();
|
||||
|
||||
expect(getUpdateModeMock).not.toHaveBeenCalled();
|
||||
expect(getUpdateSummaryMock).not.toHaveBeenCalled();
|
||||
const invocations = invokeMock.mock.calls.map((c) => c[0]);
|
||||
expect(invocations).not.toContain("download_and_install_update");
|
||||
expect(invocations).not.toContain("restart_app");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
desktopUpdateService,
|
||||
type CanInstallResult,
|
||||
} from "@app/services/desktopUpdateService";
|
||||
import { connectionModeService } from "@app/services/connectionModeService";
|
||||
|
||||
const SNOOZE_KEY = "stirling-pdf-updater:snoozedUntil";
|
||||
const STARTUP_DELAY_MS = 15_000;
|
||||
@@ -72,6 +73,10 @@ export function useDesktopUpdatePopup() {
|
||||
hasChecked.current = true;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
// In SaaS connection mode the cloud owns app versioning — the self-hosted
|
||||
// update check + popup must never run (no external call, no modal).
|
||||
if ((await connectionModeService.getCurrentMode()) === "saas") return;
|
||||
|
||||
let mode: Awaited<ReturnType<typeof desktopUpdateService.getUpdateMode>> =
|
||||
"prompt";
|
||||
try {
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
@@ -10,8 +10,7 @@
|
||||
],
|
||||
"@cloud/*": ["../../src/cloud/*"],
|
||||
"@proprietary/*": ["../../src/proprietary/*"],
|
||||
"@core/*": ["../../src/core/*"],
|
||||
"@shared/*": ["../../../shared/*"]
|
||||
"@core/*": ["../../src/core/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -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
|
||||
* <BrowserRouter> is the one and only router; the portal's routes are relative
|
||||
* to the /portal mount (see ViewRouter).
|
||||
*/
|
||||
export function PortalApp() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<PortalMantineProvider>
|
||||
<AuthProvider mode="spring">
|
||||
<LinkProvider initialState="unlinked">
|
||||
{/* TierProvider sits INSIDE LinkProvider so it can derive the tier
|
||||
{/* Scopes base.css to the portal so it doesn't restyle the host editor. */}
|
||||
<div className="portal-scope">
|
||||
<AuthProvider mode="spring">
|
||||
<LinkProvider initialState="unlinked">
|
||||
{/* TierProvider sits INSIDE LinkProvider so it can derive the tier
|
||||
from the real link/subscription state when MSW mocks are off. */}
|
||||
<TierProvider initialTier="pro">
|
||||
<BrowserRouter basename={basename}>
|
||||
<TierProvider initialTier="pro">
|
||||
<UIProvider>
|
||||
<GlobalShortcuts />
|
||||
<AuthGate>
|
||||
@@ -148,10 +152,10 @@ export function App() {
|
||||
</AccountLinkProvider>
|
||||
</AuthGate>
|
||||
</UIProvider>
|
||||
</BrowserRouter>
|
||||
</TierProvider>
|
||||
</LinkProvider>
|
||||
</AuthProvider>
|
||||
</TierProvider>
|
||||
</LinkProvider>
|
||||
</AuthProvider>
|
||||
</div>
|
||||
</PortalMantineProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
@@ -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:
|
||||
|
||||
@@ -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 (
|
||||
<Routes>
|
||||
<Route index element={<Home />} />
|
||||
<Route path={rel(VIEW_PATHS.users)} element={<Users />} />
|
||||
<Route path={rel(VIEW_PATHS.pipelines)} element={<Pipelines />} />
|
||||
<Route path={rel(VIEW_PATHS.sources)} element={<Sources />} />
|
||||
<Route
|
||||
path={rel(VIEW_PATHS["agent-builder"])}
|
||||
element={<AgentBuilder />}
|
||||
/>
|
||||
<Route path={rel(VIEW_PATHS.policies)} element={<Policies />} />
|
||||
<Route path={rel(VIEW_PATHS.documents)} element={<Documents />} />
|
||||
<Route path={rel(VIEW_PATHS.components)} element={<Components />} />
|
||||
<Route path={rel(VIEW_PATHS.editor)} element={<EditorAdmin />} />
|
||||
<Route
|
||||
path={rel(VIEW_PATHS.infrastructure)}
|
||||
element={<Infrastructure />}
|
||||
/>
|
||||
<Route path={rel(VIEW_PATHS.usage)} element={<Usage />} />
|
||||
<Route path={rel(VIEW_PATHS.procurement)} element={<Procurement />} />
|
||||
<Route path={rel(VIEW_PATHS.docs)} element={<DeveloperDocs />} />
|
||||
{/* Account-link is now a Settings panel; redirect legacy bookmarks home. */}
|
||||
<Route
|
||||
path="account-link"
|
||||
element={<Navigate to={toPortalPath(VIEW_PATHS.home)} replace />}
|
||||
/>
|
||||
{/* Settings is a modal overlay, not a route (see AppShell + UIContext). */}
|
||||
{/* Unknown paths land on Home. */}
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to={toPortalPath(VIEW_PATHS.home)} replace />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
@@ -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<Wallet> {
|
||||
return apiClient.saas.json<Wallet>("/api/v1/payg/wallet");
|
||||