@k2b/cloud 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/src/access/PermissionEditor.tsx +5 -4
- package/src/access/PrincipalPicker.tsx +2 -1
- package/src/account/EntitySearch.tsx +5 -3
- package/src/ai/code-mode-skill.ts +2 -2
- package/src/ai/store.ts +4 -4
- package/src/api/accounts-entities.ts +3 -1
- package/src/api/app-approval.ts +4 -1
- package/src/browser/live-websocket.ts +75 -10
- package/src/browser/mermaid.ts +202 -0
- package/src/browser/reload.ts +34 -0
- package/src/contracts/shared.ts +10 -0
- package/src/services/account-lifecycle/index.ts +2 -2
- package/src/services/accounts/app.ts +5 -2
- package/src/services/accounts/base-group.ts +29 -0
- package/src/services/accounts/entities.ts +22 -1
- package/src/services/accounts/groups.ts +13 -5
- package/src/services/accounts/identities.ts +7 -3
- package/src/services/accounts/local-groups.ts +12 -12
- package/src/services/app-approval.ts +62 -5
- package/src/services/ipa/groups.ts +3 -0
- package/src/services/ipa/search.ts +1 -0
- package/src/services/logging/index.ts +3 -3
- package/src/services/logging/trace.ts +1 -1
- package/src/services/notifications/batches.ts +1 -1
- package/src/services/notifications/index.ts +4 -4
- package/src/services/service-account-credentials.ts +1 -1
- package/src/services/webauthn.ts +1 -1
- package/src/shared/account-display.ts +14 -0
- package/src/shared/markdown/client.ts +3 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"./browser": "./src/server/api-client.ts",
|
|
32
32
|
"./browser/app-approval": "./src/browser/app-approval.ts",
|
|
33
33
|
"./browser/live": "./src/browser/live-websocket.ts",
|
|
34
|
+
"./browser/mermaid": "./src/browser/mermaid.ts",
|
|
35
|
+
"./browser/reload": "./src/browser/reload.ts",
|
|
34
36
|
"./browser/notifications": "./src/browser/notifications.ts",
|
|
35
37
|
"./browser/resource-clipboard": "./src/browser/resource-clipboard.ts",
|
|
36
38
|
"./browser/commands": "./src/browser/commands.ts",
|
|
@@ -84,8 +86,7 @@
|
|
|
84
86
|
"./styles/global.css": "./src/styles/global.css"
|
|
85
87
|
},
|
|
86
88
|
"scripts": {
|
|
87
|
-
"test": "bun test --isolate --path-ignore-patterns '**/*.behavior.test
|
|
88
|
-
"test:dom": "bun test --isolate --conditions=browser --preload ../ui/test/solid-dom-preload.ts behavior.test.tsx dom.test.tsx",
|
|
89
|
+
"test": "bun test --isolate --path-ignore-patterns '**/*.behavior.test.*'",
|
|
89
90
|
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
|
|
90
91
|
},
|
|
91
92
|
"dependencies": {
|
|
@@ -98,8 +99,8 @@
|
|
|
98
99
|
"@tailwindcss/typography": "0.5.20",
|
|
99
100
|
"@k2b/nessi": "0.12.1",
|
|
100
101
|
"@k2b/ssr": "0.14.0",
|
|
101
|
-
"@k2b/ui": "0.
|
|
102
|
-
"@k2b/stdlib": "0.
|
|
102
|
+
"@k2b/ui": "0.6.0",
|
|
103
|
+
"@k2b/stdlib": "0.26.0",
|
|
103
104
|
"@k2b/sync": "6.5.0",
|
|
104
105
|
"@nats-io/transport-node": "3.4.0",
|
|
105
106
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
"katex": "0.18.7",
|
|
110
111
|
"liquidjs": "10.29.0",
|
|
111
112
|
"marked": "17.0.6",
|
|
112
|
-
"mermaid": "
|
|
113
|
+
"mermaid": "12.0.0",
|
|
113
114
|
"nodemailer": "10.0.10",
|
|
114
115
|
"postcss": "8.5.28",
|
|
115
116
|
"reflect-metadata": "0.2.2",
|
|
@@ -3,6 +3,7 @@ import { IconButton, Placeholder, prompts, SelectChip, Tooltip, useLocale } from
|
|
|
3
3
|
import { createSignal, For, Show } from "solid-js";
|
|
4
4
|
import { CloudAvatar } from "../account/Avatar";
|
|
5
5
|
import type { AccessEntry, PermissionLevel, Principal } from "../contracts/shared";
|
|
6
|
+
import { groupDisplayName } from "../shared/account-display";
|
|
6
7
|
import { accessMessages } from "./messages";
|
|
7
8
|
import PrincipalPicker from "./PrincipalPicker";
|
|
8
9
|
|
|
@@ -112,8 +113,8 @@ const resolveEntryDisplay = (
|
|
|
112
113
|
return defaults[permission] ?? defaults.none;
|
|
113
114
|
};
|
|
114
115
|
|
|
115
|
-
const getEntryDisplayName = (entry: AccessEntry, t: ReturnType<typeof accessMessages.resolve>["t"]): string => {
|
|
116
|
-
if (entry.displayName) return entry.displayName;
|
|
116
|
+
const getEntryDisplayName = (entry: AccessEntry, t: ReturnType<typeof accessMessages.resolve>["t"], locale: string): string => {
|
|
117
|
+
if (entry.displayName) return entry.principal.type === "group" ? groupDisplayName(entry.displayName, locale) : entry.displayName;
|
|
117
118
|
if (entry.principal.type === "authenticated") return t.allUsers;
|
|
118
119
|
if (entry.principal.type === "public") return t.public;
|
|
119
120
|
if (entry.principal.type === "user") return entry.principal.userId;
|
|
@@ -184,7 +185,7 @@ export default function PermissionEditor(props: PermissionEditorProps) {
|
|
|
184
185
|
|
|
185
186
|
const revokeMut = mutation.create<string | null, AccessEntry>({
|
|
186
187
|
mutation: async (entry) => {
|
|
187
|
-
const displayName = getEntryDisplayName(entry, t());
|
|
188
|
+
const displayName = getEntryDisplayName(entry, t(), locale());
|
|
188
189
|
const confirmed = await prompts.confirm(t().removeAccessConfirm({ name: displayName }), {
|
|
189
190
|
title: t().removeAccess,
|
|
190
191
|
variant: "danger",
|
|
@@ -263,7 +264,7 @@ function AccessEntryRow(props: {
|
|
|
263
264
|
}) {
|
|
264
265
|
const locale = useLocale();
|
|
265
266
|
const t = () => accessMessages.resolve([locale()]).t;
|
|
266
|
-
const displayName = () => getEntryDisplayName(props.entry, t());
|
|
267
|
+
const displayName = () => getEntryDisplayName(props.entry, t(), locale());
|
|
267
268
|
const display = () => resolveEntryDisplay(props.entry.permission, props.allowed, t());
|
|
268
269
|
const isInteractive = () =>
|
|
269
270
|
props.canEdit && !props.disabled && !props.singlePicker && props.allowed.some((option) => option.level === props.entry.permission);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Combobox, type ComboboxOption, useLocale } from "@k2b/ui";
|
|
2
2
|
import type { Principal } from "../contracts/shared";
|
|
3
|
+
import { groupDisplayName } from "../shared/account-display";
|
|
3
4
|
import { accessMessages } from "./messages";
|
|
4
5
|
|
|
5
6
|
export const principalKey = (p: Principal) =>
|
|
@@ -66,7 +67,7 @@ export default function PrincipalPicker(props: {
|
|
|
66
67
|
else if (e.kind === "group")
|
|
67
68
|
entries.push({
|
|
68
69
|
principal: { type: "group", groupId: e.group.id },
|
|
69
|
-
label: e.group.name,
|
|
70
|
+
label: groupDisplayName(e.group.name, locale()),
|
|
70
71
|
icon: "ti ti-users-group",
|
|
71
72
|
description: e.group.description ?? undefined,
|
|
72
73
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { timed } from "@k2b/stdlib/solid";
|
|
2
|
-
import { Button, ScrollArea, TextInput } from "@k2b/ui";
|
|
2
|
+
import { Button, ScrollArea, TextInput, useLocale } from "@k2b/ui";
|
|
3
3
|
import type { JSX } from "solid-js";
|
|
4
4
|
import { createSignal, For, Show } from "solid-js";
|
|
5
|
+
import { groupDisplayName } from "../shared/account-display";
|
|
5
6
|
import { CloudAvatar } from "./Avatar";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -108,6 +109,7 @@ type ApiServiceAccount = {
|
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
const EntitySearch = (props: EntitySearchProps) => {
|
|
112
|
+
const locale = useLocale();
|
|
111
113
|
const [search, setSearch] = createSignal("");
|
|
112
114
|
const [users, setUsers] = createSignal<ApiUser[]>([]);
|
|
113
115
|
const [groups, setGroups] = createSignal<ApiGroup[]>([]);
|
|
@@ -270,7 +272,7 @@ const EntitySearch = (props: EntitySearchProps) => {
|
|
|
270
272
|
{(group) => (
|
|
271
273
|
<ResultRow
|
|
272
274
|
icon="ti-users-group"
|
|
273
|
-
title={group.name}
|
|
275
|
+
title={groupDisplayName(group.name, locale())}
|
|
274
276
|
subtitle={group.description ?? undefined}
|
|
275
277
|
disabled={props.disabled}
|
|
276
278
|
onSelect={() =>
|
|
@@ -339,7 +341,7 @@ const ResultRow = (props: {
|
|
|
339
341
|
disabled?: boolean;
|
|
340
342
|
onSelect: () => void;
|
|
341
343
|
}) => (
|
|
342
|
-
<Button onClick={props.onSelect} disabled={props.disabled} variant="ghost" class="w-full
|
|
344
|
+
<Button onClick={props.onSelect} disabled={props.disabled} variant="ghost" align="start" class="w-full">
|
|
343
345
|
<Show
|
|
344
346
|
when={props.avatar}
|
|
345
347
|
fallback={
|
|
@@ -4,7 +4,7 @@ import type { AiSkillTemplate } from "./skills";
|
|
|
4
4
|
|
|
5
5
|
export const ASSISTANT_CODE_MODE_SKILL = {
|
|
6
6
|
"key": "assistant:code-mode",
|
|
7
|
-
"version":
|
|
7
|
+
"version": 52,
|
|
8
8
|
"name": "assistant-code-mode",
|
|
9
9
|
"description": "Inspect and transform unfamiliar data, analyze files, compare results across Cloud apps, or build and improve interactive and agent-only Apps in Assistant Studio. Use for quick code experiments, data analysis, file generation, resource SQL queries and combining discovered Cloud capabilities. For plain arithmetic or date offsets, answer directly or use calculate.",
|
|
10
10
|
"instructions": "# Assistant code mode\n\nChoose the smallest useful result: one-off answer, exported file, or reusable\nStudio App. Apps may expose agent actions, a display-only dashboard, or both.\nPersistence is optional. One-off scripts stay in their chat and cannot be shared. Reuse an\nexisting Cloud feature when it fits. For a\nquick reading of an uploaded PDF or Office document, `read_file` can return\nMarkdown; use code for exact cells, calculations, original PDF text or positions.\n\n## Start from the contract\n\nLoad the needed `code_*` tools individually through `load_tools` and read their\ninput schemas. They are Assistant tools, not capabilities or functions inside\ncode. Discover other Cloud operations before using `capabilities.run`.\n\nRuntime namespaces are globals: no imports or package installation are needed.\nOnly relative imports of the resource's own source files are supported. There is\nno DOM or native network access. Before using a namespace, read its reference\nbelow for signatures, options and return values. Do not invent methods or infer\nan API from a familiar library. For discovered Cloud capabilities and external\nAPIs, obtain their actual contracts separately.\n\nInspect supplied data before joining, filtering or calculating: column names,\ntypes, units, date ranges and missing values. Ask only for decisions or inputs\nthat cannot be established from available evidence. For several real steps,\nkeep a short `todo_write` plan and update it as work changes; skip ceremony for a\nsmall experiment. A failed experiment should change the next hypothesis.\n\n## First file script\n\nPass exact current-chat manifest paths as `code_run.inputPaths`, and this entry\nas `code_run.code` for a small CSV:\n\n```js\nexport default async () => {\n const [input] = await files.list();\n if (!input) throw new Error(\"Select a CSV input.\");\n const rows = await sheet.fromCsv(await files.read(input.name));\n return { rows: rows.length, columns: Object.keys(rows[0] ?? {}), sample: rows.slice(0, 3) };\n};\n```\n\n`input.name` is the full path, such as `/sales.csv`; pass it unchanged to\n`files.read`, which returns a `File`. CSV rows are objects keyed by headers:\n`rows[0]` is already data. Do not drop it. For older Excel CSVs, use\n`sheet.fromCsv(file, {encoding:\"windows-1252\"})`. Inspect actual headings first.\nFor a tiny experiment without files, `export default () => ({answer:42})` suffices.\nEach run has fresh variables. No saved resource or UI is required.\n\n## Reference routing\n\nRead only the rows relevant to the task. Each link describes its own complete\nsupported surface; links within references add related workflows when needed.\n\n| Task / API | Read |\n| --- | --- |\n| Source entry, input/output files, pickers, CSV, IDs | [Runtime and files](/skills/assistant-code-mode/references/runtime.md) |\n| Inspect PDF pages, read PDF text/positions or XLSX/ODS cells, write ODS | [Documents](/skills/assistant-code-mode/references/documents.md) |\n| Generate a PDF, embed attachments, combine invoice HTML and XML | [PDF generation](/skills/assistant-code-mode/references/pdf.md) |\n| Exact amounts, taxes, allocation, localized money | [Money](/skills/assistant-code-mode/references/money.md) |\n| Export DATEV bookings or SEPA transfers | [DATEV and SEPA](/skills/assistant-code-mode/references/finance.md) |\n| Parse a CAMT bank report | [Bank reports](/skills/assistant-code-mode/references/camt.md) |\n| Calculate, create or read electronic invoices/XML/PDF attachments | [Electronic invoices](/skills/assistant-code-mode/references/einvoice.md) |\n| Controls, layouts and dialogs | [UI and dialogs](/skills/assistant-code-mode/references/ui.md), [Analytics UI](/skills/assistant-code-mode/references/analytics.md) |\n| Chart types, series and axes | [Charts](/skills/assistant-code-mode/references/charts.md) |\n| Long processing, progress, cancellation | [Background work](/skills/assistant-code-mode/references/work.md) |\n| Persist JSON or files locally/shared | [Storage](/skills/assistant-code-mode/references/storage.md) |\n| Copy files between stores; list and download Filesv2 beside Grids documents | [File transfers](/skills/assistant-code-mode/references/files.md) |\n| Resource SQL, schema, row CRUD, imports | [Database](/skills/assistant-code-mode/references/database.md) |\n| Generate text, classify data or extract structured fields | [AI calculations](/skills/assistant-code-mode/references/ai.md) |\n| Discovered Cloud queries/actions | [Capability calls](/skills/assistant-code-mode/references/capabilities.md) |\n| External HTTPS and personal secrets | [HTTP and secrets](/skills/assistant-code-mode/references/http.md) |\n| Call a published App action; declare handlers | [App actions](/skills/assistant-code-mode/references/app-actions.md) |\n| Reuse work across chats, create or edit an App | [Source workflow](/skills/assistant-code-mode/references/source-workflow.md) |\n| Publish, restore, copy | [Publishing](/skills/assistant-code-mode/references/publishing.md) |\n| Find recipients or change App/Skill sharing | [Access](/skills/assistant-code-mode/references/access.md) |\n| Inspect, export, clear server data, or delete an App | [Management](/skills/assistant-code-mode/references/management.md) |\n| Execute, inspect, interact, export, stop, diagnose errors | [Run and debug](/skills/assistant-code-mode/references/debugging.md) |\n| Unfamiliar inputs or cross-app investigation | [Investigation](/skills/assistant-code-mode/references/investigation.md) |\n| Complete app starters | [Examples](/skills/assistant-code-mode/references/examples.md) |\n\nFor a new app, read Source workflow and the closest complete example before\nwriting source, plus only the API references it uses. For analytical reports or\ndashboards, also load `assistant-data-analysis` for metrics and source validation.\n\n## Choose the delivery\n\nFor a one-off chart, calculator, or interactive analysis in this conversation,\nuse `code_run({code,inputPaths})`, test the controls, then\n`code_present({runId,title})`. Read [Chat visualizations](/skills/assistant-code-mode/references/chat.md).\nA successful run is visible to the agent only; present it before saying the\nuser can see it. No saved App or chat file is necessary.\n\nUse a Studio App when the user needs an independently accessible, reusable\napplication. Use `files.save`, `code_export`, and `present` when the requested\nresult is a file. These are separate delivery choices.\n\n## Verify and deliver\n\nRun the actual source (the saved revision for Apps) and test relevant controls with IDs returned by\n`code_run`/`code_interact`, including invalid inputs and picker fixtures. Creating,\ncompiling or saving source does not verify behavior. If `work.status` is\n`running`, wait with `code_inspect({runId,waitMs:30000})`; do not restart the job.\nInspect only when the returned snapshot needs more detail. Errors and\n`outputTruncated` are not successful complete results.\n\nFor a CSV, call `await files.save(sheet.toCsv(rows), \"result.csv\")` inside code;\nfor a spreadsheet, `await files.save(await sheet.toOds(sheets), \"result.ods\")`.\nThen call the **tool** `code_export` with the returned `runId` and captured file\nname, and `present` its returned chat path. `files.save` returns no path.\nReuse exported data via its path/version rather than retyping truncated output.\nReconcile row counts, exclusions and totals before reporting findings.\n\nOpen GUI apps with `code_open`. Saving or testing does\nnot replace a user's already-running app. Stop runs no longer needed that retain\nUI, jobs or output files. Never claim an unexecuted result is verified.\n\nAgent execution runs independently of the user's tab. Agent local storage is\ntemporary; shared storage, database writes and external actions are real, even\nin tests. Cancellation and source restore do not undo them. Apps select local\nfiles explicitly; they never gain implicit access to chat attachments. Use\n`code_secret` for credentials, never chat or app controls. Honor normal access\nand approval decisions; availability is not authorization for unrelated actions.",
|
|
@@ -56,7 +56,7 @@ export const ASSISTANT_CODE_MODE_SKILL = {
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"path": "references/einvoice.md",
|
|
59
|
-
"content": "# Electronic invoices\n\n`einvoice` is a global. These methods return Results: inspect `ok`, then use\n`data` or `error: {code,status,message,issues}`. Issue entries contain\n`{code,path,message,line?,column?}`; paths have zero-based row indices.\n\n| Call | Successful `data` |\n| --- | --- |\n| `einvoice.validate(input)` | `Invoice` |\n| `einvoice.calculate(lines)` | `InvoiceCalculation` |\n| `einvoice.serialize(invoice, {format: \"zugferd-2.5-en16931\"})` | `{format, xml: string, bytes: Uint8Array}` |\n| `einvoice.parseXml(xml, options?)` | `ParsedInvoice` |\n| `await einvoice.parsePdf(bytes, options?)` | `ParsedInvoice` |\n\nAll calls except `parsePdf` are synchronous. `parsePdf` takes a `Uint8Array`,\nfor example `new Uint8Array(await file.arrayBuffer())`. It reads embedded XML,\nnot scanned pages or arbitrary visual invoice layouts. For those, use the\n[local PDF text reader](documents.md) or the agent's document/vision tools.\n\nThe supported slice covers EUR CII EN16931 invoices, credit notes and self-billing,\
|
|
59
|
+
"content": "# Electronic invoices\n\n`einvoice` is a global. These methods return Results: inspect `ok`, then use\n`data` or `error: {code,status,message,issues}`. Issue entries contain\n`{code,path,message,line?,column?}`; paths have zero-based row indices.\n\n| Call | Successful `data` |\n| --- | --- |\n| `einvoice.validate(input)` | `Invoice` |\n| `einvoice.calculate(lines)` | `InvoiceCalculation` |\n| `einvoice.serialize(invoice, {format: \"zugferd-2.5-en16931\"})` | `{format, xml: string, bytes: Uint8Array}` |\n| `einvoice.parseXml(xml, options?)` | `ParsedInvoice` |\n| `await einvoice.parsePdf(bytes, options?)` | `ParsedInvoice` |\n| `einvoice.parseXml(xml, {mode: \"incoming\"})` | `ParsedIncomingInvoice` |\n| `await einvoice.parsePdf(bytes, {mode: \"incoming\"})` | `ParsedIncomingInvoice` |\n\nAll calls except `parsePdf` are synchronous. `parsePdf` takes a `Uint8Array`,\nfor example `new Uint8Array(await file.arrayBuffer())`. It reads embedded XML,\nnot scanned pages or arbitrary visual invoice layouts. For those, use the\n[local PDF text reader](documents.md) or the agent's document/vision tools.\n\nThe supported slice covers EUR CII EN16931 invoices, credit notes and self-billing,\nVAT categories S/Z/E/AE/K/G/O, units C62/HUR/DAY/KGM. Generation does not\nsupport UBL, XRechnung, discounts or prepayments. Readers preserve declared totals;\nparsing is not arithmetic verification. Validation is not XSD or Schematron\ncertification. No XSD validator is exposed.\n\n## Complete input and result shapes\n\nType descriptions only; no imports are needed. All fields are required unless\nmarked `?`; unknown fields are rejected.\n\n```ts\ntype Party = {\n name: string; id?: string; vatId: string; // \"\" when the party has no VAT ID\n address: {line1: string; city: string; postalCode: string; countryCode: string};\n};\ntype Tax = {\n taxCategory?: \"S\" | \"Z\" | \"E\" | \"AE\" | \"K\" | \"G\" | \"O\"; // default \"S\"\n taxRate: string; taxExemptionReason?: string; taxExemptionReasonCode?: string;\n};\ntype InvoiceLine = Tax & {\n id: string; name: string; description?: string;\n quantity: string; unitPrice: string; unitCode: \"C62\" | \"HUR\" | \"DAY\" | \"KGM\";\n netAmount?: string;\n};\ntype InvoiceTotals = {\n netAmount: string; taxAmount: string; grossAmount: string; dueAmount: string;\n taxGroups: (Tax & {netAmount: string; taxAmount: string})[];\n};\ntype Invoice = {\n kind: \"invoice\" | \"creditNote\" | \"selfBilling\";\n number: string; invoiceDate: string; serviceDate: string; dueDate: string;\n currency: \"EUR\"; seller: Party & {taxRegistrationId?: string}; buyer: Party;\n deliverToCountryCode?: string; buyerReference: string;\n notes?: string[];\n precedingInvoice?: {number: string; invoiceDate: string};\n payment: {iban: string; accountName: string};\n lines: InvoiceLine[]; totals?: InvoiceTotals;\n};\ntype InvoiceCalculation = InvoiceTotals & {\n lines: (InvoiceLine & {netAmount: string})[];\n};\ntype ParsedInvoice = {\n format: \"zugferd-2.5-en16931\"; profile: string; xml: string;\n invoice: Invoice; filename?: string;\n};\ntype ParseOptions = {maxCharacters?: number; maxElements?: number; maxDepth?: number};\ntype PdfOptions = ParseOptions & {maxPdfBytes?: number};\n```\n\nXML options default to 10 Mi UTF-16 code units, 100,000 elements, depth 64.\nPDF input defaults to 25 MiB. Overrides must be positive safe integers.\nA parser result's business fields are under **`data.invoice`**. Calculated\namounts are directly under **`data.netAmount`**, etc., with no `data.totals` wrapper.\n\n- Dates are real `YYYY-MM-DD` dates; `dueDate` cannot precede `invoiceDate`.\n Credit notes require `precedingInvoice`, whose date cannot be later than the\n credit note; other kinds cannot supply it. Credit-note amounts stay unsigned.\n- Lines: 1–1000, unique IDs. Quantities are positive, prices nonnegative,\n VAT rates at most 100. Decimal strings allow up to four\n fractional digits and no leading zeros. Totals/net amounts require exactly\n two fractional digits; do not convert through JavaScript Number.\n- Country codes: two uppercase letters. `payment.iban` must be valid.\n Required text is nonblank valid XML text. Limits: number/reference/line ID/VAT ID\n 100; names/address line/accountName 200; city 100; postalCode 20;\n line description and each note 4000; at most 100 notes.\n- Category S needs a positive rate; every other category uses `taxRate: \"0\"`\n and zero tax. E/AE/K/G/O need `taxExemptionReason` or a VATEX\n `taxExemptionReasonCode`; S/Z forbid both. O cannot be mixed with other\n categories and requires `vatId: \"\"` for both parties. A seller without a VAT\n ID needs `seller.id` and, outside O, `seller.taxRegistrationId`. AE/K need a\n buyer VAT ID, K/G a seller VAT ID, and K `deliverToCountryCode`.\n- `calculate` rounds each line half up to cents, then VAT per category and rate. It recalculates\n line `netAmount`; `serialize` also rejects supplied line/totals values that\n disagree. Render these calculated amounts in HTML instead of another arithmetic path.\n\n## Minimal supported invoice\n\nUse real business data and an app-owned invoice number. This illustrative\nfixture demonstrates the required fields; it is not a document to issue.\n\n```js\nconst invoice = {\n kind: \"invoice\",\n number: \"EXAMPLE-42\",\n invoiceDate: \"2026-09-15\",\n serviceDate: \"2026-09-15\",\n dueDate: \"2026-09-30\",\n currency: \"EUR\",\n seller: {\n name: \"Example Seller\", vatId: \"DE123456789\",\n address: { line1: \"Street 1\", city: \"Ulm\", postalCode: \"89073\", countryCode: \"DE\" },\n },\n buyer: {\n name: \"Example Buyer\", vatId: \"DE987654321\",\n address: { line1: \"Street 2\", city: \"Berlin\", postalCode: \"10115\", countryCode: \"DE\" },\n },\n buyerReference: \"ORDER-42\",\n payment: { iban: \"DE89370400440532013000\", accountName: \"Example Seller\" },\n lines: [{ id: \"1\", name: \"Service\", quantity: \"2.0000\", unitPrice: \"50.0000\", unitCode: \"HUR\", taxRate: \"19.00\" }],\n};\nconst result = einvoice.serialize(invoice, { format: \"zugferd-2.5-en16931\" });\nif (!result.ok) throw new Error(JSON.stringify(result.error));\nawait files.save(new Blob([result.data.bytes], { type: \"application/xml\" }), \"invoice.xml\");\n```\n\nNever infer a missing VAT identifier, tax category, exemption reason, account\nor business reference merely to satisfy input validation.\n\n## Reading received invoices\n\n`{mode: \"incoming\"}` (plus the same limits) reads a broader separate model:\nalso XRechnung 3.0/2.3 CII, other currencies, discounts, prepayments, all\npayment means and optional references. `data` is\n`{format: \"cii-en16931\", profile, xml, invoice, unmapped, filename?}`.\nAmounts are declared strings, never recalculated; O lines have no `taxRate`.\n`unmapped` lists supplementary XML elements and attributes with their paths; review it before\naccounting. Do not pass this `invoice` to `validate` or `serialize`.\n\nFor an invoice PDF, pass `serialized.data.xml` to\n[`pdf.facturX`](pdf.md) with profile `\"EN 16931\"` and matching HTML.\nNumbering, business mapping, issuance and persistence belong to the app.\n"
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"path": "references/examples.md",
|
package/src/ai/store.ts
CHANGED
|
@@ -1095,10 +1095,10 @@ export const aiConversations: AiConversationService = {
|
|
|
1095
1095
|
const refs = input.refs ?? [];
|
|
1096
1096
|
const rows = await withConversationSearchBackend(query, async (backend) => {
|
|
1097
1097
|
const order = query
|
|
1098
|
-
? sql`${conversationSearchRank(backend, query)} DESC, conversation.pinned_at DESC NULLS LAST, conversation.updated_at DESC, conversation.created_at DESC`
|
|
1098
|
+
? sql`${conversationSearchRank(backend, query)} DESC, conversation.pinned_at DESC NULLS LAST, conversation.updated_at DESC, conversation.created_at DESC, conversation.id DESC`
|
|
1099
1099
|
: input.done === true
|
|
1100
1100
|
? sql`conversation.last_used_at DESC, conversation.id DESC`
|
|
1101
|
-
: sql`conversation.pinned_at DESC NULLS LAST, conversation.updated_at DESC, conversation.created_at DESC`;
|
|
1101
|
+
: sql`conversation.pinned_at DESC NULLS LAST, conversation.updated_at DESC, conversation.created_at DESC, conversation.id DESC`;
|
|
1102
1102
|
return sql<ConversationRow[]>`
|
|
1103
1103
|
SELECT
|
|
1104
1104
|
conversation.*,
|
|
@@ -1198,10 +1198,10 @@ export const aiConversations: AiConversationService = {
|
|
|
1198
1198
|
const { page, perPage, offset } = sanitizePagination(input);
|
|
1199
1199
|
const rows = await withConversationSearchBackend(query, async (backend) => {
|
|
1200
1200
|
const order = query
|
|
1201
|
-
? sql`${conversationSearchRank(backend, query)} DESC, conversation.pinned_at DESC NULLS LAST, conversation.updated_at DESC, conversation.created_at DESC`
|
|
1201
|
+
? sql`${conversationSearchRank(backend, query)} DESC, conversation.pinned_at DESC NULLS LAST, conversation.updated_at DESC, conversation.created_at DESC, conversation.id DESC`
|
|
1202
1202
|
: input.done === true
|
|
1203
1203
|
? sql`conversation.last_used_at DESC, conversation.id DESC`
|
|
1204
|
-
: sql`conversation.pinned_at DESC NULLS LAST, conversation.updated_at DESC, conversation.created_at DESC`;
|
|
1204
|
+
: sql`conversation.pinned_at DESC NULLS LAST, conversation.updated_at DESC, conversation.created_at DESC, conversation.id DESC`;
|
|
1205
1205
|
return sql<ConversationRow[]>`
|
|
1206
1206
|
SELECT
|
|
1207
1207
|
conversation.*,
|
|
@@ -41,6 +41,7 @@ const QuerySchema = z
|
|
|
41
41
|
parent_group_id: z.uuid().optional(),
|
|
42
42
|
managed_by_user_id: z.uuid().optional(),
|
|
43
43
|
recursive: z.enum(["true", "false"]).optional(),
|
|
44
|
+
include_personal: z.enum(["true", "false"]).optional(),
|
|
44
45
|
})
|
|
45
46
|
.refine(
|
|
46
47
|
(value) => {
|
|
@@ -98,7 +99,7 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
|
|
|
98
99
|
tags: ["Accounts"],
|
|
99
100
|
summary: "List mixed users and groups",
|
|
100
101
|
description:
|
|
101
|
-
"List visible users and groups with SQL-backed filtering and pagination. Guest accounts see only themselves and their effective groups; relation filters require a full user account.",
|
|
102
|
+
"List visible users and groups with SQL-backed filtering and pagination. Guest accounts see only themselves and their effective groups; relation filters require a full user account. Personal Linux groups (a user's private primary group, flagged with `personalOwner`) are left out unless `include_personal=true`; exact `group_ids` lookups and relation filters always return them.",
|
|
102
103
|
...requiresAuth,
|
|
103
104
|
responses: {
|
|
104
105
|
200: jsonResponse(EntitiesListResponseSchema, "Paginated mixed entity list"),
|
|
@@ -159,6 +160,7 @@ export const createAccountsEntitiesRoutes = (dependencies: AccountsEntitiesRoute
|
|
|
159
160
|
parentGroupId: query.parent_group_id,
|
|
160
161
|
managedByUserId: query.managed_by_user_id,
|
|
161
162
|
recursive: query.recursive === "true",
|
|
163
|
+
includePersonal: query.include_personal === "true",
|
|
162
164
|
});
|
|
163
165
|
|
|
164
166
|
return respond(c, {
|
package/src/api/app-approval.ts
CHANGED
|
@@ -113,7 +113,10 @@ export const createAppApprovalRoutes = (service: Service = appApproval) => {
|
|
|
113
113
|
)
|
|
114
114
|
.post("/login/status", v("json", AppLoginReferenceSchema), async (c) => {
|
|
115
115
|
const input = c.req.valid("json");
|
|
116
|
-
|
|
116
|
+
// Hold a pending request for one poll interval: the browser learns a decision at once,
|
|
117
|
+
// keeps its request cadence, and stays below Bun's 10-second idle timeout.
|
|
118
|
+
const hold = { ms: APP_APPROVAL_LIMITS.pollSeconds * 1000, signal: c.req.raw.signal };
|
|
119
|
+
return c.json(await service.browserStatus(input.requestId, input.browserSecret, hold));
|
|
117
120
|
})
|
|
118
121
|
.post(
|
|
119
122
|
"/login/complete",
|
|
@@ -52,6 +52,16 @@ const DEFAULT_RECONNECT = {
|
|
|
52
52
|
jitterMs: 250,
|
|
53
53
|
} as const;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Deadline for one opening handshake. The handshake is a single HTTP upgrade
|
|
57
|
+
* round trip (the gateway accepts it before reaching the application), so a
|
|
58
|
+
* healthy attempt finishes far sooner. Browsers can otherwise leave a socket in
|
|
59
|
+
* `CONNECTING` indefinitely after sleep or a network change. Matching the
|
|
60
|
+
* longest default reconnect delay means a stalled attempt never blocks recovery
|
|
61
|
+
* for longer than an ordinary retry.
|
|
62
|
+
*/
|
|
63
|
+
const CONNECT_TIMEOUT_MS = DEFAULT_RECONNECT.maxDelayMs;
|
|
64
|
+
|
|
55
65
|
const defaultCloseError = ({ code, reason }: LiveWebSocketClose): LiveWebSocketError | null => {
|
|
56
66
|
if (code === 1008) return { code: reason || "access_denied", message: "Live access changed or expired." };
|
|
57
67
|
if (code === 1011) return { code: reason || "internal_error", message: "Live updates failed." };
|
|
@@ -72,7 +82,8 @@ const socketUrl = (raw: string): string => {
|
|
|
72
82
|
*
|
|
73
83
|
* The app owns its wire protocol, validation, permissions, and domain updates.
|
|
74
84
|
* This helper owns only browser transport lifecycle: one socket, visibility,
|
|
75
|
-
* reconnect backoff,
|
|
85
|
+
* reconnect backoff, connection deadlines, recovery when the tab or network
|
|
86
|
+
* returns, applied-cursor resume, terminal closes, and disposal.
|
|
76
87
|
*/
|
|
77
88
|
export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMessage>): LiveWebSocket => {
|
|
78
89
|
const activity = options.activity ?? "visible";
|
|
@@ -81,6 +92,8 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
81
92
|
|
|
82
93
|
let socket: WebSocket | null = null;
|
|
83
94
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
95
|
+
let connectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
96
|
+
let connectStartedAt = 0;
|
|
84
97
|
let reconnectAttempt = 0;
|
|
85
98
|
let lastAppliedCursor = options.initialCursor ?? null;
|
|
86
99
|
let started = false;
|
|
@@ -120,9 +133,15 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
120
133
|
}
|
|
121
134
|
};
|
|
122
135
|
|
|
136
|
+
const clearConnectDeadline = () => {
|
|
137
|
+
if (connectTimer !== null) clearTimeout(connectTimer);
|
|
138
|
+
connectTimer = null;
|
|
139
|
+
};
|
|
140
|
+
|
|
123
141
|
const closeSocket = (code = 1000, reason = "") => {
|
|
124
142
|
const current = socket;
|
|
125
143
|
socket = null;
|
|
144
|
+
clearConnectDeadline();
|
|
126
145
|
if (!current || current.readyState > WebSocket.OPEN) return;
|
|
127
146
|
try {
|
|
128
147
|
current.close(code, reason);
|
|
@@ -135,6 +154,7 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
135
154
|
if (terminated) return;
|
|
136
155
|
terminated = true;
|
|
137
156
|
clearReconnect();
|
|
157
|
+
detachRecovery();
|
|
138
158
|
closeSocket(close.code, close.reason);
|
|
139
159
|
setStatus("closed");
|
|
140
160
|
if (!fatalSent) {
|
|
@@ -165,6 +185,13 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
165
185
|
}, delay);
|
|
166
186
|
};
|
|
167
187
|
|
|
188
|
+
/** Gives up on an attempt that is still connecting and schedules a retry. */
|
|
189
|
+
const abandonAttempt = (attempt: WebSocket) => {
|
|
190
|
+
if (socket !== attempt) return;
|
|
191
|
+
closeSocket();
|
|
192
|
+
scheduleReconnect();
|
|
193
|
+
};
|
|
194
|
+
|
|
168
195
|
const openSocket = () => {
|
|
169
196
|
if (disposed || terminated || !started || socket) return;
|
|
170
197
|
if (!browserAvailable()) {
|
|
@@ -174,7 +201,7 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
174
201
|
if (!isActive()) return;
|
|
175
202
|
|
|
176
203
|
let next: WebSocket;
|
|
177
|
-
setStatus(reconnectAttempt > 0 ? "reconnecting" : "connecting");
|
|
204
|
+
setStatus(reconnectAttempt > 0 || status === "reconnecting" ? "reconnecting" : "connecting");
|
|
178
205
|
try {
|
|
179
206
|
next = new WebSocket(socketUrl(typeof options.url === "function" ? options.url() : options.url));
|
|
180
207
|
} catch (error) {
|
|
@@ -182,9 +209,15 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
182
209
|
return;
|
|
183
210
|
}
|
|
184
211
|
socket = next;
|
|
212
|
+
connectStartedAt = Date.now();
|
|
213
|
+
connectTimer = setTimeout(() => {
|
|
214
|
+
connectTimer = null;
|
|
215
|
+
if (next.readyState === WebSocket.CONNECTING) abandonAttempt(next);
|
|
216
|
+
}, CONNECT_TIMEOUT_MS);
|
|
185
217
|
|
|
186
218
|
next.onopen = () => {
|
|
187
219
|
if (socket !== next || disposed || terminated) return;
|
|
220
|
+
clearConnectDeadline();
|
|
188
221
|
try {
|
|
189
222
|
if (!send(options.subscribe(lastAppliedCursor))) throw new Error("Live WebSocket subscription could not be sent");
|
|
190
223
|
setStatus("open");
|
|
@@ -213,6 +246,7 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
213
246
|
next.onclose = (event) => {
|
|
214
247
|
if (socket !== next) return;
|
|
215
248
|
socket = null;
|
|
249
|
+
clearConnectDeadline();
|
|
216
250
|
if (disposed || terminated) return;
|
|
217
251
|
const closeError = classifyClose({ code: event.code, reason: event.reason.trim() });
|
|
218
252
|
if (closeError) fatal(closeError, { code: event.code, reason: event.reason });
|
|
@@ -224,22 +258,55 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
224
258
|
next.close();
|
|
225
259
|
} catch {
|
|
226
260
|
if (socket === next) {
|
|
227
|
-
|
|
261
|
+
closeSocket();
|
|
228
262
|
scheduleReconnect();
|
|
229
263
|
}
|
|
230
264
|
}
|
|
231
265
|
};
|
|
232
266
|
};
|
|
233
267
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Reconnects now when the tab or network returns. Open and closing sockets
|
|
270
|
+
* are left alone; a closing socket's close event still decides between retry
|
|
271
|
+
* and a terminal error. A handshake within its deadline is kept unless the
|
|
272
|
+
* network just returned, because it began on the network that was gone. A
|
|
273
|
+
* stalled handshake or a pending backoff is replaced by an immediate attempt
|
|
274
|
+
* whose backoff starts over.
|
|
275
|
+
*/
|
|
276
|
+
const recover = (event: Event) => {
|
|
277
|
+
if (disposed || terminated || !started || !isActive()) return;
|
|
278
|
+
if (socket) {
|
|
279
|
+
if (socket.readyState !== WebSocket.CONNECTING) return;
|
|
280
|
+
if (event.type !== "online" && Date.now() - connectStartedAt < CONNECT_TIMEOUT_MS) return;
|
|
281
|
+
closeSocket();
|
|
282
|
+
}
|
|
283
|
+
clearReconnect();
|
|
284
|
+
reconnectAttempt = 0;
|
|
285
|
+
openSocket();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const syncVisibility = (event: Event) => {
|
|
289
|
+
if (isActive()) recover(event);
|
|
290
|
+
else if (activity === "visible") {
|
|
237
291
|
clearReconnect();
|
|
238
292
|
closeSocket(1000, "inactive");
|
|
239
293
|
setStatus("paused");
|
|
240
294
|
}
|
|
241
295
|
};
|
|
242
296
|
|
|
297
|
+
const attachRecovery = () => {
|
|
298
|
+
document.addEventListener("visibilitychange", syncVisibility);
|
|
299
|
+
window.addEventListener("online", recover);
|
|
300
|
+
window.addEventListener("focus", recover);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const detachRecovery = () => {
|
|
304
|
+
if (!started || !browserAvailable()) return;
|
|
305
|
+
document.removeEventListener("visibilitychange", syncVisibility);
|
|
306
|
+
window.removeEventListener("online", recover);
|
|
307
|
+
window.removeEventListener("focus", recover);
|
|
308
|
+
};
|
|
309
|
+
|
|
243
310
|
return {
|
|
244
311
|
connect: () => {
|
|
245
312
|
if (started || disposed || terminated) return;
|
|
@@ -248,7 +315,7 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
248
315
|
fatal({ code: "unsupported", message: "Live WebSockets are not available in this browser" });
|
|
249
316
|
return;
|
|
250
317
|
}
|
|
251
|
-
|
|
318
|
+
attachRecovery();
|
|
252
319
|
openSocket();
|
|
253
320
|
},
|
|
254
321
|
markApplied,
|
|
@@ -258,9 +325,7 @@ export const createLiveWebSocket = <TMessage>(options: LiveWebSocketOptions<TMes
|
|
|
258
325
|
if (disposed) return;
|
|
259
326
|
disposed = true;
|
|
260
327
|
clearReconnect();
|
|
261
|
-
|
|
262
|
-
document.removeEventListener("visibilitychange", syncVisibility);
|
|
263
|
-
}
|
|
328
|
+
detachRecovery();
|
|
264
329
|
closeSocket();
|
|
265
330
|
setStatus("closed");
|
|
266
331
|
},
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { MermaidConfig } from "mermaid";
|
|
2
|
+
|
|
3
|
+
/** Series colors shared by pie slices and mindmap branches. */
|
|
4
|
+
const lightSeries = [
|
|
5
|
+
"#dbeafe",
|
|
6
|
+
"#d1fae5",
|
|
7
|
+
"#fef3c7",
|
|
8
|
+
"#ede9fe",
|
|
9
|
+
"#ffe4e6",
|
|
10
|
+
"#cffafe",
|
|
11
|
+
"#ecfccb",
|
|
12
|
+
"#ffedd4",
|
|
13
|
+
"#e0f2fe",
|
|
14
|
+
"#ccfbf1",
|
|
15
|
+
"#fae8ff",
|
|
16
|
+
"#fef9c3",
|
|
17
|
+
];
|
|
18
|
+
const darkSeries = [
|
|
19
|
+
"#1e3a8a",
|
|
20
|
+
"#065f46",
|
|
21
|
+
"#78350f",
|
|
22
|
+
"#4c1d95",
|
|
23
|
+
"#881337",
|
|
24
|
+
"#155e75",
|
|
25
|
+
"#3f6212",
|
|
26
|
+
"#7c2d12",
|
|
27
|
+
"#075985",
|
|
28
|
+
"#115e59",
|
|
29
|
+
"#86198f",
|
|
30
|
+
"#713f12",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Mermaid derives colors from hex values, so these mirror the Cloud tokens
|
|
34
|
+
// (zinc ink, --ui-surface*, --ui-border*) instead of reading CSS variables.
|
|
35
|
+
const light = {
|
|
36
|
+
darkMode: false,
|
|
37
|
+
background: "#ffffff",
|
|
38
|
+
textColor: "#27272a",
|
|
39
|
+
titleColor: "#18181b",
|
|
40
|
+
lineColor: "#71717a",
|
|
41
|
+
primaryColor: "#f4f4f5",
|
|
42
|
+
primaryTextColor: "#18181b",
|
|
43
|
+
primaryBorderColor: "#a1a1aa",
|
|
44
|
+
secondaryColor: "#eff6ff",
|
|
45
|
+
secondaryTextColor: "#18181b",
|
|
46
|
+
secondaryBorderColor: "#93c5fd",
|
|
47
|
+
tertiaryColor: "#fafafa",
|
|
48
|
+
tertiaryTextColor: "#18181b",
|
|
49
|
+
tertiaryBorderColor: "#d4d4d8",
|
|
50
|
+
mainBkg: "#f4f4f5",
|
|
51
|
+
nodeBorder: "#a1a1aa",
|
|
52
|
+
clusterBkg: "#fafafa",
|
|
53
|
+
clusterBorder: "#d4d4d8",
|
|
54
|
+
edgeLabelBackground: "#ffffff",
|
|
55
|
+
noteBkgColor: "#fefce8",
|
|
56
|
+
noteBorderColor: "#eab308",
|
|
57
|
+
noteTextColor: "#27272a",
|
|
58
|
+
actorBkg: "#f4f4f5",
|
|
59
|
+
actorBorder: "#a1a1aa",
|
|
60
|
+
actorTextColor: "#18181b",
|
|
61
|
+
signalColor: "#52525b",
|
|
62
|
+
signalTextColor: "#27272a",
|
|
63
|
+
labelBoxBkgColor: "#f4f4f5",
|
|
64
|
+
labelBoxBorderColor: "#a1a1aa",
|
|
65
|
+
activationBkgColor: "#e4e4e7",
|
|
66
|
+
activationBorderColor: "#a1a1aa",
|
|
67
|
+
attributeBackgroundColorOdd: "#ffffff",
|
|
68
|
+
attributeBackgroundColorEven: "#fafafa",
|
|
69
|
+
sectionBkgColor: "#f4f4f5",
|
|
70
|
+
altSectionBkgColor: "#ffffff",
|
|
71
|
+
sectionBkgColor2: "#fafafa",
|
|
72
|
+
gridColor: "#e4e4e7",
|
|
73
|
+
taskBkgColor: "#dbeafe",
|
|
74
|
+
taskBorderColor: "#3b82f6",
|
|
75
|
+
taskTextColor: "#18181b",
|
|
76
|
+
taskTextLightColor: "#18181b",
|
|
77
|
+
taskTextDarkColor: "#18181b",
|
|
78
|
+
taskTextOutsideColor: "#27272a",
|
|
79
|
+
activeTaskBkgColor: "#bfdbfe",
|
|
80
|
+
activeTaskBorderColor: "#2563eb",
|
|
81
|
+
doneTaskBkgColor: "#e4e4e7",
|
|
82
|
+
doneTaskBorderColor: "#a1a1aa",
|
|
83
|
+
critBkgColor: "#ffe4e6",
|
|
84
|
+
critBorderColor: "#e11d48",
|
|
85
|
+
todayLineColor: "#e11d48",
|
|
86
|
+
pieStrokeColor: "#ffffff",
|
|
87
|
+
pieOuterStrokeColor: "#d4d4d8",
|
|
88
|
+
pieSectionTextColor: "#18181b",
|
|
89
|
+
pieTitleTextColor: "#18181b",
|
|
90
|
+
pieLegendTextColor: "#27272a",
|
|
91
|
+
series: lightSeries,
|
|
92
|
+
seriesText: "#18181b",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const dark: typeof light = {
|
|
96
|
+
darkMode: true,
|
|
97
|
+
background: "#11151b",
|
|
98
|
+
textColor: "#e4e4e7",
|
|
99
|
+
titleColor: "#f4f4f5",
|
|
100
|
+
lineColor: "#a1a1aa",
|
|
101
|
+
primaryColor: "#20242a",
|
|
102
|
+
primaryTextColor: "#f4f4f5",
|
|
103
|
+
primaryBorderColor: "#5b6573",
|
|
104
|
+
secondaryColor: "#172554",
|
|
105
|
+
secondaryTextColor: "#f4f4f5",
|
|
106
|
+
secondaryBorderColor: "#3b82f6",
|
|
107
|
+
tertiaryColor: "#181d24",
|
|
108
|
+
tertiaryTextColor: "#f4f4f5",
|
|
109
|
+
tertiaryBorderColor: "#3a4552",
|
|
110
|
+
mainBkg: "#20242a",
|
|
111
|
+
nodeBorder: "#5b6573",
|
|
112
|
+
clusterBkg: "#181d24",
|
|
113
|
+
clusterBorder: "#3a4552",
|
|
114
|
+
edgeLabelBackground: "#11151b",
|
|
115
|
+
noteBkgColor: "#422006",
|
|
116
|
+
noteBorderColor: "#a16207",
|
|
117
|
+
noteTextColor: "#fef9c3",
|
|
118
|
+
actorBkg: "#20242a",
|
|
119
|
+
actorBorder: "#5b6573",
|
|
120
|
+
actorTextColor: "#f4f4f5",
|
|
121
|
+
signalColor: "#a1a1aa",
|
|
122
|
+
signalTextColor: "#e4e4e7",
|
|
123
|
+
labelBoxBkgColor: "#20242a",
|
|
124
|
+
labelBoxBorderColor: "#5b6573",
|
|
125
|
+
activationBkgColor: "#29313a",
|
|
126
|
+
activationBorderColor: "#5b6573",
|
|
127
|
+
attributeBackgroundColorOdd: "#181d24",
|
|
128
|
+
attributeBackgroundColorEven: "#20242a",
|
|
129
|
+
sectionBkgColor: "#181d24",
|
|
130
|
+
altSectionBkgColor: "#11151b",
|
|
131
|
+
sectionBkgColor2: "#1c2129",
|
|
132
|
+
gridColor: "#29313a",
|
|
133
|
+
taskBkgColor: "#1e3a8a",
|
|
134
|
+
taskBorderColor: "#60a5fa",
|
|
135
|
+
taskTextColor: "#f4f4f5",
|
|
136
|
+
taskTextLightColor: "#f4f4f5",
|
|
137
|
+
taskTextDarkColor: "#f4f4f5",
|
|
138
|
+
taskTextOutsideColor: "#e4e4e7",
|
|
139
|
+
activeTaskBkgColor: "#1e40af",
|
|
140
|
+
activeTaskBorderColor: "#93c5fd",
|
|
141
|
+
doneTaskBkgColor: "#3f3f46",
|
|
142
|
+
doneTaskBorderColor: "#71717a",
|
|
143
|
+
critBkgColor: "#881337",
|
|
144
|
+
critBorderColor: "#fb7185",
|
|
145
|
+
todayLineColor: "#fb7185",
|
|
146
|
+
pieStrokeColor: "#11151b",
|
|
147
|
+
pieOuterStrokeColor: "#3a4552",
|
|
148
|
+
pieSectionTextColor: "#f4f4f5",
|
|
149
|
+
pieTitleTextColor: "#f4f4f5",
|
|
150
|
+
pieLegendTextColor: "#e4e4e7",
|
|
151
|
+
series: darkSeries,
|
|
152
|
+
seriesText: "#f4f4f5",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Mermaid configuration that follows Cloud's light or dark tokens.
|
|
157
|
+
*
|
|
158
|
+
* Diagrams keep Mermaid 11's flat look and layouts; an author may still
|
|
159
|
+
* choose `layout: elk` in front matter, which loads ELK on demand. Author
|
|
160
|
+
* directives cannot change security, theme, or CSS.
|
|
161
|
+
*/
|
|
162
|
+
export function mermaidConfig(options: { dark: boolean }): MermaidConfig {
|
|
163
|
+
const { series, seriesText, ...palette } = options.dark ? dark : light;
|
|
164
|
+
const themeVariables: Record<string, string | boolean> = {
|
|
165
|
+
...palette,
|
|
166
|
+
fontFamily: '"IBM Plex Sans", ui-sans-serif, system-ui, sans-serif',
|
|
167
|
+
};
|
|
168
|
+
series.forEach((color, index) => {
|
|
169
|
+
themeVariables[`pie${index + 1}`] = color;
|
|
170
|
+
themeVariables[`cScale${index}`] = color;
|
|
171
|
+
themeVariables[`cScaleLabel${index}`] = seriesText;
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
startOnLoad: false,
|
|
175
|
+
securityLevel: "strict",
|
|
176
|
+
suppressErrorRendering: true,
|
|
177
|
+
theme: "base",
|
|
178
|
+
look: "classic",
|
|
179
|
+
// Mermaid 12 lays these types out with ELK by default. Keep dagre so ELK
|
|
180
|
+
// loads only for diagrams whose front matter asks for `layout: elk`.
|
|
181
|
+
flowchart: { layout: "dagre" },
|
|
182
|
+
class: { layout: "dagre" },
|
|
183
|
+
state: { layout: "dagre" },
|
|
184
|
+
er: { layout: "dagre" },
|
|
185
|
+
requirement: { layout: "dagre" },
|
|
186
|
+
themeVariables,
|
|
187
|
+
secure: [
|
|
188
|
+
"secure",
|
|
189
|
+
"securityLevel",
|
|
190
|
+
"startOnLoad",
|
|
191
|
+
"maxTextSize",
|
|
192
|
+
"maxEdges",
|
|
193
|
+
"suppressErrorRendering",
|
|
194
|
+
"htmlLabels",
|
|
195
|
+
"themeCSS",
|
|
196
|
+
"themeVariables",
|
|
197
|
+
"theme",
|
|
198
|
+
"fontFamily",
|
|
199
|
+
"altFontFamily",
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}
|