@k2b/cloud 0.23.0 → 0.25.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 +3 -2
- package/src/_internal/capabilities.ts +230 -75
- package/src/_internal/define-app.ts +12 -3
- package/src/_internal/fixtures/filesv2-manifest-cloud-v0.29.0.json +887 -0
- package/src/_internal/page-responses.ts +1 -1
- package/src/_internal/registry.ts +78 -3
- package/src/access/PermissionEditor.tsx +32 -11
- package/src/access/managers.ts +32 -0
- package/src/access/messages.ts +6 -0
- package/src/ai/chat/blocks.tsx +2 -1
- package/src/ai/chat/messages.ts +6 -0
- package/src/ai/client/controller.ts +65 -20
- package/src/ai/client/transport.ts +5 -5
- package/src/ai/code-mode-skill.ts +2 -2
- package/src/ai/index.ts +0 -7
- package/src/ai/live-events.ts +1 -262
- package/src/ai/live.ts +31 -2
- package/src/ai/memory-learning.ts +7 -12
- package/src/ai/memory-workflow-evidence.ts +10 -2
- package/src/ai/migrate.ts +80 -18
- package/src/ai/model-access.ts +4 -2
- package/src/ai/prefs.ts +19 -5
- package/src/ai/projects.ts +5 -2
- package/src/ai/runtime.ts +1 -6
- package/src/ai/skills.ts +5 -2
- package/src/ai/solid.ts +0 -5
- package/src/ai/store.ts +14 -0
- package/src/api/help.ts +2 -2
- package/src/api/pwa-phone.ts +10 -3
- package/src/api/search/schemas.ts +34 -0
- package/src/api/search.ts +118 -50
- package/src/browser/CloudResourceSearch.browser-harness.tsx +11 -0
- package/src/browser/CloudResourceSearch.tsx +208 -80
- package/src/browser/FileChooser.tsx +600 -0
- package/src/browser/choose-files.browser-harness.tsx +32 -0
- package/src/browser/choose-files.tsx +70 -0
- package/src/browser/file-chooser-messages.ts +86 -0
- package/src/browser/file-providers.ts +217 -0
- package/src/browser/files.tsx +23 -0
- package/src/browser/resource-search-messages.ts +15 -2
- package/src/browser/search-stream.ts +151 -0
- package/src/cli/admin/account-administration.ts +2 -2
- package/src/contracts/capabilities.ts +89 -24
- package/src/contracts/capability-compatibility.ts +28 -14
- package/src/contracts/file-provider.ts +158 -0
- package/src/contracts/index.ts +1 -0
- package/src/events/live-engine.ts +11 -7
- package/src/events/live.ts +32 -32
- package/src/server/help.ts +3 -3
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +44 -11
- package/src/server/services/index.ts +1 -0
- package/src/services/index.ts +1 -1
- package/src/services/outbox.ts +29 -115
- package/src/services/pdf/markdown.ts +22 -2
- package/src/services/pwa-devices.ts +74 -8
- package/src/shared/markdown/formula.ts +122 -26
- package/src/shared/markdown/index.ts +27 -14
- package/src/ssr/PwaLayout.tsx +2 -1
- package/src/ssr/platform-messages.ts +2 -2
- package/src/styles/file-chooser.css +169 -0
- package/src/styles/global.css +1 -0
- package/src/styles/resource-search.css +19 -0
- package/src/ai/client/live-connection.ts +0 -141
- package/src/ai/live-messages.ts +0 -45
- package/src/ai/live-outbox.ts +0 -102
- package/src/ai/live-routes.ts +0 -412
- package/src/shared/markdown/extensions/info-blocks.ts +0 -79
|
@@ -475,6 +475,76 @@ const isTruthy = (v: EvalValue): boolean => {
|
|
|
475
475
|
return v.length > 0;
|
|
476
476
|
};
|
|
477
477
|
|
|
478
|
+
/** Cell text that is one decimal number and nothing else. Each digit has
|
|
479
|
+
* one place in the pattern, so a long run of digits never backtracks. */
|
|
480
|
+
const NUMBER_CELL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
481
|
+
|
|
482
|
+
/** A number, boolean, or text whose whole content is one number. */
|
|
483
|
+
const toExactNumber = (v: EvalValue): number | null =>
|
|
484
|
+
typeof v === "string" ? (NUMBER_CELL.test(v.trim()) ? Number(v.trim()) : null) : toNumber(v);
|
|
485
|
+
|
|
486
|
+
/** An ISO date or timestamp: `2026-10-12`, `2026-10-12 09:30`,
|
|
487
|
+
* `2026-10-12T09:30:00.250Z`, or with an offset such as `+02:00`. */
|
|
488
|
+
const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3})\d*)?)?(Z|[+-]\d{2}:?\d{2})?)?$/i;
|
|
489
|
+
|
|
490
|
+
/** An ISO date without a time, such as `2026-10-12`. */
|
|
491
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* The instant an ISO date or timestamp names, or null for anything else,
|
|
495
|
+
* including impossible dates such as `2026-02-30` and offsets such as
|
|
496
|
+
* `+00:60`. Text without an offset is local time, like the values TODAY()
|
|
497
|
+
* and NOW() return.
|
|
498
|
+
*/
|
|
499
|
+
const toDateTime = (v: EvalValue): number | null => {
|
|
500
|
+
if (typeof v !== "string") return null;
|
|
501
|
+
const match = ISO_DATE_TIME.exec(v.trim());
|
|
502
|
+
if (!match) return null;
|
|
503
|
+
const year = Number(match[1]);
|
|
504
|
+
const month = Number(match[2]);
|
|
505
|
+
const day = Number(match[3]);
|
|
506
|
+
const hour = Number(match[4] ?? 0);
|
|
507
|
+
const minute = Number(match[5] ?? 0);
|
|
508
|
+
const second = Number(match[6] ?? 0);
|
|
509
|
+
const ms = Number((match[7] ?? "").padEnd(3, "0"));
|
|
510
|
+
const zone = match[8];
|
|
511
|
+
if (hour > 23 || minute > 59 || second > 59) return null;
|
|
512
|
+
const utc = new Date(0);
|
|
513
|
+
utc.setUTCFullYear(year, month - 1, day);
|
|
514
|
+
if (utc.getUTCMonth() !== month - 1 || utc.getUTCDate() !== day) return null;
|
|
515
|
+
if (zone) {
|
|
516
|
+
utc.setUTCHours(hour, minute, second, ms);
|
|
517
|
+
if (zone.toUpperCase() === "Z") return utc.getTime();
|
|
518
|
+
const sign = zone.startsWith("-") ? -1 : 1;
|
|
519
|
+
const digits = zone.slice(1).replace(":", "");
|
|
520
|
+
const offsetHours = Number(digits.slice(0, 2));
|
|
521
|
+
const offsetMinutes = Number(digits.slice(2));
|
|
522
|
+
if (offsetHours > 23 || offsetMinutes > 59) return null;
|
|
523
|
+
return utc.getTime() - sign * (offsetHours * 60 + offsetMinutes) * 60_000;
|
|
524
|
+
}
|
|
525
|
+
const local = new Date(0);
|
|
526
|
+
local.setFullYear(year, month - 1, day);
|
|
527
|
+
local.setHours(hour, minute, second, ms);
|
|
528
|
+
return local.getTime();
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Equality for `==`, `!=`, COUNTIF, and SUMIF: dates by instant; numbers,
|
|
533
|
+
* also `5 €` against 5, by value; everything else, such as `1.2.3` or
|
|
534
|
+
* `3 apples`, by its exact text.
|
|
535
|
+
*/
|
|
536
|
+
const isEqual = (l: EvalValue, r: EvalValue): boolean => {
|
|
537
|
+
const ld = toDateTime(l);
|
|
538
|
+
const rd = toDateTime(r);
|
|
539
|
+
if (ld !== null && rd !== null) return ld === rd;
|
|
540
|
+
if (ld === null && rd === null && (toExactNumber(l) !== null || toExactNumber(r) !== null)) {
|
|
541
|
+
const ln = toNumber(l);
|
|
542
|
+
const rn = toNumber(r);
|
|
543
|
+
if (ln !== null && rn !== null) return ln === rn;
|
|
544
|
+
}
|
|
545
|
+
return toString(l) === toString(r);
|
|
546
|
+
};
|
|
547
|
+
|
|
478
548
|
const lookupColumn = (name: string, ctx: EvalContext): { index: number } | { suggestion?: string } => {
|
|
479
549
|
const lower = name.toLowerCase();
|
|
480
550
|
const idx = ctx.headers.findIndex((h) => h.toLowerCase() === lower);
|
|
@@ -492,8 +562,9 @@ const isCurrentFormulaCell = (row: number, col: number, ctx: EvalContext): boole
|
|
|
492
562
|
/**
|
|
493
563
|
* Resolve a single cell to its evaluated value. If the cell is itself
|
|
494
564
|
* a formula, recursively evaluate it (with cycle detection via
|
|
495
|
-
* `ctx._visited`).
|
|
496
|
-
*
|
|
565
|
+
* `ctx._visited`). A non-formula cell becomes a number only when its
|
|
566
|
+
* whole text is one; anything else, such as the ISO date `2026-10-12`
|
|
567
|
+
* that DATEDIFF needs intact, stays text.
|
|
497
568
|
*
|
|
498
569
|
* Used by both the column-reference resolver in `evaluateAst` and by
|
|
499
570
|
* the column / row aggregate functions, so a `=SUM(total)` aggregating
|
|
@@ -503,8 +574,8 @@ const isCurrentFormulaCell = (row: number, col: number, ctx: EvalContext): boole
|
|
|
503
574
|
const evaluateCell = (row: number, col: number, ctx: EvalContext): EvalResult => {
|
|
504
575
|
const cell = cellAt(row, col, ctx);
|
|
505
576
|
if (!cell.startsWith("=")) {
|
|
506
|
-
const
|
|
507
|
-
return
|
|
577
|
+
const text = cell.trim();
|
|
578
|
+
return NUMBER_CELL.test(text) ? ok(Number(text)) : ok(cell);
|
|
508
579
|
}
|
|
509
580
|
const key = `${row},${col}`;
|
|
510
581
|
if (ctx._visited?.has(key)) {
|
|
@@ -686,9 +757,8 @@ const FUNCTIONS: Record<string, FuncImpl> = {
|
|
|
686
757
|
if ("kind" in col) return col;
|
|
687
758
|
const valueArg = state.evaluate(args[1]!);
|
|
688
759
|
if (valueArg.kind === "error") return valueArg;
|
|
689
|
-
const target = toString(valueArg.value);
|
|
690
760
|
let count = 0;
|
|
691
|
-
for (const v of col.values) if (v
|
|
761
|
+
for (const v of col.values) if (isEqual(v, valueArg.value)) count++;
|
|
692
762
|
return ok(count);
|
|
693
763
|
},
|
|
694
764
|
SUMIF: (args, state) => {
|
|
@@ -717,13 +787,12 @@ const FUNCTIONS: Record<string, FuncImpl> = {
|
|
|
717
787
|
}
|
|
718
788
|
const condValueArg = state.evaluate(args[2]!);
|
|
719
789
|
if (condValueArg.kind === "error") return condValueArg;
|
|
720
|
-
const target = toString(condValueArg.value);
|
|
721
790
|
let total = 0;
|
|
722
791
|
for (let r = 0; r < state.ctx.rows.length; r++) {
|
|
723
792
|
if (isCurrentFormulaCell(r, condLookup.index, state.ctx) || isCurrentFormulaCell(r, sumLookup.index, state.ctx)) continue;
|
|
724
793
|
const condResult = evaluateCell(r, condLookup.index, state.ctx);
|
|
725
794
|
if (condResult.kind !== "ok") continue;
|
|
726
|
-
if (
|
|
795
|
+
if (!isEqual(condResult.value, condValueArg.value)) continue;
|
|
727
796
|
const sumResult = evaluateCell(r, sumLookup.index, state.ctx);
|
|
728
797
|
if (sumResult.kind !== "ok") continue;
|
|
729
798
|
const n = toNumber(sumResult.value);
|
|
@@ -989,17 +1058,25 @@ const FUNCTIONS: Record<string, FuncImpl> = {
|
|
|
989
1058
|
if (d1V.kind === "error") return d1V;
|
|
990
1059
|
const d2V = state.evaluate(args[1]!);
|
|
991
1060
|
if (d2V.kind === "error") return d2V;
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1061
|
+
// ISO values follow the comparison operators, so a date without a
|
|
1062
|
+
// time is local midnight and an impossible one such as 2026-02-30 is
|
|
1063
|
+
// no date; other text keeps the Date parser's reading.
|
|
1064
|
+
const instant = (v: EvalValue): number =>
|
|
1065
|
+
ISO_DATE_TIME.test(toString(v).trim()) ? (toDateTime(v) ?? Number.NaN) : new Date(toString(v)).getTime();
|
|
1066
|
+
const t1 = instant(d1V.value);
|
|
1067
|
+
const t2 = instant(d2V.value);
|
|
1068
|
+
if (Number.isNaN(t1)) return err("PARSE_ERROR", `DATEDIFF: first argument is not a valid date`);
|
|
1069
|
+
if (Number.isNaN(t2)) return err("PARSE_ERROR", `DATEDIFF: second argument is not a valid date`);
|
|
996
1070
|
let unit = "days";
|
|
997
1071
|
if (args.length === 3) {
|
|
998
1072
|
const u = state.evaluate(args[2]!);
|
|
999
1073
|
if (u.kind === "error") return u;
|
|
1000
1074
|
unit = toString(u.value).toLowerCase();
|
|
1001
1075
|
}
|
|
1002
|
-
|
|
1076
|
+
// Two dates are whole days apart, also across a daylight saving change.
|
|
1077
|
+
const dayMs = 1000 * 60 * 60 * 24;
|
|
1078
|
+
const wholeDays = ISO_DATE.test(toString(d1V.value).trim()) && ISO_DATE.test(toString(d2V.value).trim());
|
|
1079
|
+
const diffMs = wholeDays ? Math.round((t2 - t1) / dayMs) * dayMs : t2 - t1;
|
|
1003
1080
|
switch (unit) {
|
|
1004
1081
|
case "ms":
|
|
1005
1082
|
case "milliseconds":
|
|
@@ -1030,18 +1107,45 @@ const FUNCTION_NAMES = Object.keys(FUNCTIONS);
|
|
|
1030
1107
|
|
|
1031
1108
|
// -- Binop application --------------------------------------------------------
|
|
1032
1109
|
|
|
1110
|
+
/**
|
|
1111
|
+
* The two numbers the ordering operators compare, or null when the
|
|
1112
|
+
* values have no common order. Two dates compare as instants and never
|
|
1113
|
+
* with a number or other text, so `2026-10-12` is not `2026`. Text with
|
|
1114
|
+
* a unit, such as `5 €`, compares by its leading number, as does text
|
|
1115
|
+
* against text: `5 €` and `5.00 €` are neither smaller nor greater than
|
|
1116
|
+
* each other, even though `==` tells their texts apart.
|
|
1117
|
+
*/
|
|
1118
|
+
const orderedPair = (l: EvalValue, r: EvalValue): [number, number] | null => {
|
|
1119
|
+
const ld = toDateTime(l);
|
|
1120
|
+
const rd = toDateTime(r);
|
|
1121
|
+
if (ld !== null || rd !== null) return ld !== null && rd !== null ? [ld, rd] : null;
|
|
1122
|
+
const ln = toNumber(l);
|
|
1123
|
+
const rn = toNumber(r);
|
|
1124
|
+
return ln !== null && rn !== null ? [ln, rn] : null;
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1033
1127
|
const applyBinop = (op: BinOp, l: EvalValue, r: EvalValue): EvalResult => {
|
|
1034
1128
|
if (op === "==" || op === "!=") {
|
|
1035
|
-
|
|
1036
|
-
const ln = toNumber(l);
|
|
1037
|
-
const rn = toNumber(r);
|
|
1038
|
-
const equal = ln !== null && rn !== null ? ln === rn : toString(l) === toString(r);
|
|
1129
|
+
const equal = isEqual(l, r);
|
|
1039
1130
|
return ok((op === "==" ? equal : !equal) ? 1 : 0);
|
|
1040
1131
|
}
|
|
1132
|
+
if (op === "<" || op === "<=" || op === ">" || op === ">=") {
|
|
1133
|
+
const pair = orderedPair(l, r);
|
|
1134
|
+
if (pair === null) {
|
|
1135
|
+
return toDateTime(l) !== null || toDateTime(r) !== null
|
|
1136
|
+
? err("TYPE_ERROR", `Cannot compare a date with a value that is not a date using "${op}"`)
|
|
1137
|
+
: err("NON_NUMERIC", `Cannot compare non-numeric values with "${op}"`);
|
|
1138
|
+
}
|
|
1139
|
+
const [a, b] = pair;
|
|
1140
|
+
if (op === "<") return ok(a < b ? 1 : 0);
|
|
1141
|
+
if (op === "<=") return ok(a <= b ? 1 : 0);
|
|
1142
|
+
if (op === ">") return ok(a > b ? 1 : 0);
|
|
1143
|
+
return ok(a >= b ? 1 : 0);
|
|
1144
|
+
}
|
|
1041
1145
|
const ln = toNumber(l);
|
|
1042
1146
|
const rn = toNumber(r);
|
|
1043
1147
|
if (ln === null || rn === null) {
|
|
1044
|
-
return err("NON_NUMERIC", `Cannot
|
|
1148
|
+
return err("NON_NUMERIC", `Cannot compute non-numeric values with "${op}"`);
|
|
1045
1149
|
}
|
|
1046
1150
|
switch (op) {
|
|
1047
1151
|
case "+":
|
|
@@ -1053,14 +1157,6 @@ const applyBinop = (op: BinOp, l: EvalValue, r: EvalValue): EvalResult => {
|
|
|
1053
1157
|
case "/":
|
|
1054
1158
|
if (rn === 0) return err("DIV_BY_ZERO", `Division by zero`);
|
|
1055
1159
|
return ok(ln / rn);
|
|
1056
|
-
case "<":
|
|
1057
|
-
return ok(ln < rn ? 1 : 0);
|
|
1058
|
-
case "<=":
|
|
1059
|
-
return ok(ln <= rn ? 1 : 0);
|
|
1060
|
-
case ">":
|
|
1061
|
-
return ok(ln > rn ? 1 : 0);
|
|
1062
|
-
case ">=":
|
|
1063
|
-
return ok(ln >= rn ? 1 : 0);
|
|
1064
1160
|
}
|
|
1065
1161
|
};
|
|
1066
1162
|
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* the visual appearance of the CodeMirror editor extensions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { markdownInfoBlocks } from "@k2b/ui";
|
|
9
|
+
import { Marked, type MarkedExtension } from "marked";
|
|
9
10
|
import sanitizeHtml from "sanitize-html";
|
|
10
11
|
import { markdownClient } from "./client";
|
|
11
12
|
import { codeExtension } from "./extensions/code";
|
|
12
13
|
import { guidedHelpExtension } from "./extensions/guided-help";
|
|
13
14
|
import { imagesExtension } from "./extensions/images";
|
|
14
|
-
import { infoBlocksExtension } from "./extensions/info-blocks";
|
|
15
15
|
import { katexExtension } from "./extensions/katex";
|
|
16
16
|
import { linksExtension } from "./extensions/links";
|
|
17
17
|
import { markExtension } from "./extensions/mark";
|
|
@@ -23,7 +23,7 @@ import { taskListExtension } from "./extensions/task-list";
|
|
|
23
23
|
type MarkdownProfile = "content" | "help";
|
|
24
24
|
type LinkStyle = "widget" | "plain";
|
|
25
25
|
|
|
26
|
-
const createMarked = (profile: MarkdownProfile
|
|
26
|
+
const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: MarkedExtension) => {
|
|
27
27
|
const marked = new Marked();
|
|
28
28
|
|
|
29
29
|
marked.use({
|
|
@@ -33,7 +33,7 @@ const createMarked = (profile: MarkdownProfile = "content", links: LinkStyle = "
|
|
|
33
33
|
|
|
34
34
|
// Apply extensions in order
|
|
35
35
|
// Note: katexExtension must come before codeExtension to handle ```math blocks
|
|
36
|
-
marked.use(
|
|
36
|
+
marked.use(infoBlocks);
|
|
37
37
|
marked.use(taskListExtension());
|
|
38
38
|
marked.use(tablesExtension());
|
|
39
39
|
// Plain links keep marked's own renderer: an anchor around the link text.
|
|
@@ -49,17 +49,29 @@ const createMarked = (profile: MarkdownProfile = "content", links: LinkStyle = "
|
|
|
49
49
|
return marked;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// One instance per UI message catalog, not per requested locale: request
|
|
53
|
+
// headers choose the locale, and `markdownInfoBlocks()` returns one extension
|
|
54
|
+
// for every locale that shares a catalog, so the cache stays bounded.
|
|
55
|
+
const instances = new Map<MarkedExtension, Map<string, Marked>>();
|
|
56
|
+
const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale?: string): Marked => {
|
|
57
|
+
const infoBlocks = markdownInfoBlocks({ locale });
|
|
58
|
+
const byStyle = instances.get(infoBlocks) ?? new Map<string, Marked>();
|
|
59
|
+
instances.set(infoBlocks, byStyle);
|
|
60
|
+
const key = `${profile}:${links}`;
|
|
61
|
+
const instance = byStyle.get(key) ?? createMarked(profile, links, infoBlocks);
|
|
62
|
+
byStyle.set(key, instance);
|
|
63
|
+
return instance;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const marked = markedFor("content");
|
|
55
67
|
|
|
56
68
|
export type MarkdownRenderOptions = {
|
|
57
69
|
/** `"plain"` renders each link as an ordinary anchor around its text, for HTML read outside Cloud such as email. */
|
|
58
70
|
links?: LinkStyle;
|
|
71
|
+
/** Request locale for the screen-reader names of untitled info blocks. Defaults to English on the server and the page language in the browser. */
|
|
72
|
+
locale?: string;
|
|
59
73
|
};
|
|
60
74
|
|
|
61
|
-
const markedFor = ({ links = "widget" }: MarkdownRenderOptions): Marked => (links === "plain" ? plainLinksMarked : marked);
|
|
62
|
-
|
|
63
75
|
const sanitizeRenderedHtml = (html: string): string =>
|
|
64
76
|
sanitizeHtml(html, {
|
|
65
77
|
allowedTags: [
|
|
@@ -136,7 +148,8 @@ const sanitizeRenderedHtml = (html: string): string =>
|
|
|
136
148
|
*
|
|
137
149
|
* Supported features:
|
|
138
150
|
* - GFM (GitHub Flavored Markdown)
|
|
139
|
-
* -
|
|
151
|
+
* - Info blocks (:::note, :::info, :::success, :::warning, :::danger), shared
|
|
152
|
+
* with `MarkdownView` from `@k2b/ui`
|
|
140
153
|
* - Task lists with checkboxes
|
|
141
154
|
* - Tables with cell formatting
|
|
142
155
|
* - Styled links and images
|
|
@@ -160,7 +173,7 @@ const sanitizeRenderedHtml = (html: string): string =>
|
|
|
160
173
|
export function renderMarkdown(content: string, options: MarkdownRenderOptions = {}): string {
|
|
161
174
|
if (!content || typeof content !== "string") return "";
|
|
162
175
|
|
|
163
|
-
const html = markedFor(options).parse(content);
|
|
176
|
+
const html = markedFor("content", options.links, options.locale).parse(content);
|
|
164
177
|
if (typeof html !== "string") return "";
|
|
165
178
|
|
|
166
179
|
return sanitizeRenderedHtml(html);
|
|
@@ -172,7 +185,7 @@ export function renderMarkdown(content: string, options: MarkdownRenderOptions =
|
|
|
172
185
|
export function renderMarkdownSync(content: string, options: MarkdownRenderOptions = {}): string {
|
|
173
186
|
if (!content || typeof content !== "string") return "";
|
|
174
187
|
|
|
175
|
-
const html = markedFor(options).parse(content);
|
|
188
|
+
const html = markedFor("content", options.links, options.locale).parse(content);
|
|
176
189
|
if (typeof html !== "string") return "";
|
|
177
190
|
|
|
178
191
|
return sanitizeRenderedHtml(html);
|
|
@@ -182,9 +195,9 @@ export function renderMarkdownSync(content: string, options: MarkdownRenderOptio
|
|
|
182
195
|
* Render trusted documentation Markdown with guided sections and internal
|
|
183
196
|
* navigation. All fenced code remains visible source, as in ordinary Markdown.
|
|
184
197
|
*/
|
|
185
|
-
export function renderHelpMarkdown(content: string): string {
|
|
198
|
+
export function renderHelpMarkdown(content: string, locale?: string): string {
|
|
186
199
|
if (!content || typeof content !== "string") return "";
|
|
187
|
-
const html =
|
|
200
|
+
const html = markedFor("help", "widget", locale).parse(content);
|
|
188
201
|
return typeof html === "string" ? sanitizeRenderedHtml(html) : "";
|
|
189
202
|
}
|
|
190
203
|
|
package/src/ssr/PwaLayout.tsx
CHANGED
|
@@ -48,8 +48,9 @@ export default function PwaLayout(props: PwaLayoutProps) {
|
|
|
48
48
|
const path = new URL(c.req.raw.url).pathname;
|
|
49
49
|
const tabs = parts.slice(0, TAB_PARTS);
|
|
50
50
|
const currentTab = tabs.find((part) => path === part.pwa.href || path.startsWith(`${part.pwa.href}/`));
|
|
51
|
+
// Start is titled with the installation's name; a part's first page carries the part's name.
|
|
51
52
|
const items: TabBarItem[] = [
|
|
52
|
-
{ id: "start", label: t.start, icon: "ti ti-home", href: PWA_SCOPE, current: !currentTab },
|
|
53
|
+
{ id: "start", label: t.start, title: cloud, icon: "ti ti-home", href: PWA_SCOPE, current: !currentTab },
|
|
53
54
|
...tabs.map((part) => ({ id: part.id, label: part.name, icon: part.icon, href: part.pwa.href, current: part === currentTab })),
|
|
54
55
|
];
|
|
55
56
|
|
|
@@ -72,7 +72,7 @@ export const platformMessages = i18n.define({
|
|
|
72
72
|
clearSearch: "Clear search",
|
|
73
73
|
},
|
|
74
74
|
de: {
|
|
75
|
-
admin: "
|
|
75
|
+
admin: "Administration",
|
|
76
76
|
platformAdministration: "Plattform verwalten.",
|
|
77
77
|
start: "Start",
|
|
78
78
|
overview: "Übersicht",
|
|
@@ -82,7 +82,7 @@ export const platformMessages = i18n.define({
|
|
|
82
82
|
appCredentials: "App-Zugänge",
|
|
83
83
|
ai: "KI",
|
|
84
84
|
settings: "Einstellungen",
|
|
85
|
-
appAdmin: "App-
|
|
85
|
+
appAdmin: "App-Administration",
|
|
86
86
|
userManagement: "Accounts & Anmeldung",
|
|
87
87
|
accountSignIn: "Anmeldung",
|
|
88
88
|
accountRegistration: "Registrierung & Anfragen",
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* The shared file chooser (@k2b/cloud/browser/files). It opens in the workspace
|
|
2
|
+
frame, which takes the whole screen on phones. On wider screens it keeps one
|
|
3
|
+
fixed, smaller size, so moving between sources, folders, and states never
|
|
4
|
+
resizes or moves the frame. Unlayered, like the shared frame rules. */
|
|
5
|
+
@media (min-width: 48rem) {
|
|
6
|
+
.k2b-ui .k2b-dialog.k2b-panel-dialog-frame.cloud-file-chooser-dialog {
|
|
7
|
+
width: min(var(--k2b-dialog-available-width), 44rem);
|
|
8
|
+
height: min(40rem, 86dvh, var(--k2b-dialog-available-height));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.cloud-file-chooser {
|
|
13
|
+
display: contents;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Path and filter sit between header and body with the panel's own inset and
|
|
17
|
+
keep one height in every view. */
|
|
18
|
+
.cloud-file-chooser__toolbar {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-wrap: wrap;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 0.5rem 0.75rem;
|
|
23
|
+
padding: 0 var(--k2b-panel-dialog-inset) 0.75rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.cloud-file-chooser__crumbs {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex: 1 1 18rem;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 0.25rem;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* One line: a deep path scrolls sideways instead of wrapping and pushing the list down. */
|
|
35
|
+
.cloud-file-chooser__crumbs ol {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 0.125rem;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
margin: 0;
|
|
41
|
+
padding: 0;
|
|
42
|
+
overflow-x: auto;
|
|
43
|
+
list-style: none;
|
|
44
|
+
scrollbar-width: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.cloud-file-chooser__crumbs li {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex: none;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 0.25rem;
|
|
52
|
+
color: var(--k2b-text-muted);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.cloud-file-chooser__filter {
|
|
56
|
+
flex: 1 1 14rem;
|
|
57
|
+
max-width: 100%;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@media (min-width: 40rem) {
|
|
61
|
+
.cloud-file-chooser__filter {
|
|
62
|
+
flex: 0 0 14rem;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.cloud-file-chooser__icon {
|
|
67
|
+
font-size: 1.375rem;
|
|
68
|
+
color: var(--k2b-text-secondary);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.cloud-file-chooser__icon[data-selected="true"] {
|
|
72
|
+
color: var(--k2b-action);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.cloud-file-chooser__meta {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: 0.375rem;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.cloud-file-chooser__tag {
|
|
84
|
+
flex: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.cloud-file-chooser__state {
|
|
88
|
+
min-height: 100%;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.cloud-file-chooser__more {
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-direction: column;
|
|
94
|
+
align-items: flex-start;
|
|
95
|
+
gap: 0.5rem;
|
|
96
|
+
padding: 0.75rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.cloud-file-chooser__more:empty {
|
|
100
|
+
display: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.cloud-file-chooser__downloads {
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
gap: 1rem;
|
|
107
|
+
margin: 0;
|
|
108
|
+
padding: 0.25rem 0;
|
|
109
|
+
list-style: none;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.cloud-file-chooser__download-line {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: baseline;
|
|
115
|
+
justify-content: space-between;
|
|
116
|
+
gap: 0.75rem;
|
|
117
|
+
margin-bottom: 0.375rem;
|
|
118
|
+
font-size: 0.875rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.cloud-file-chooser__download-name {
|
|
122
|
+
min-width: 0;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
text-overflow: ellipsis;
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.cloud-file-chooser__download-state {
|
|
129
|
+
flex: none;
|
|
130
|
+
font-variant-numeric: tabular-nums;
|
|
131
|
+
color: var(--k2b-text-secondary);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.cloud-file-chooser__download-state[data-state="error"] {
|
|
135
|
+
color: var(--k2b-danger-text);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* One line that gives way to the buttons: a longer status never wraps the footer and moves the body. */
|
|
139
|
+
.cloud-file-chooser__status {
|
|
140
|
+
flex: 1 1 0;
|
|
141
|
+
min-width: 0;
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
font-size: 0.8125rem;
|
|
144
|
+
line-height: 1.25rem;
|
|
145
|
+
color: var(--k2b-text-secondary);
|
|
146
|
+
text-overflow: ellipsis;
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Phones have no room beside the buttons: the status takes its own line above them. The line stays when it is
|
|
151
|
+
empty, so the footer keeps one height in every view. */
|
|
152
|
+
@media (max-width: 39.999rem) {
|
|
153
|
+
.cloud-file-chooser__status {
|
|
154
|
+
flex-basis: 100%;
|
|
155
|
+
min-height: 1.25rem;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Rows keep their own padding; pulling the list out by it lines icons up with the title and path. */
|
|
160
|
+
.cloud-file-chooser .k2b-file-grid {
|
|
161
|
+
margin-inline: -0.75rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.cloud-file-chooser__actions {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex: none;
|
|
167
|
+
gap: 0.5rem;
|
|
168
|
+
margin-left: auto;
|
|
169
|
+
}
|
package/src/styles/global.css
CHANGED
|
@@ -214,6 +214,24 @@
|
|
|
214
214
|
color: var(--k2b-text-muted);
|
|
215
215
|
font-size: 0.8125rem;
|
|
216
216
|
}
|
|
217
|
+
/* Apps still searching and apps that did not answer: one calm line each, below the results. */
|
|
218
|
+
.cloud-resource-search__status {
|
|
219
|
+
display: grid;
|
|
220
|
+
gap: 0.125rem;
|
|
221
|
+
padding: 0 1.125rem 1.5rem;
|
|
222
|
+
color: var(--k2b-text-muted);
|
|
223
|
+
font-size: 0.8125rem;
|
|
224
|
+
}
|
|
225
|
+
.cloud-resource-search__status p {
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-wrap: wrap;
|
|
228
|
+
align-items: center;
|
|
229
|
+
gap: 0.375rem;
|
|
230
|
+
min-height: 1.75rem;
|
|
231
|
+
}
|
|
232
|
+
.cloud-resource-search__status i {
|
|
233
|
+
font-size: 0.875rem;
|
|
234
|
+
}
|
|
217
235
|
.cloud-resource-search__tag-heading {
|
|
218
236
|
display: flex;
|
|
219
237
|
align-items: center;
|
|
@@ -386,6 +404,7 @@
|
|
|
386
404
|
padding-inline: 0;
|
|
387
405
|
}
|
|
388
406
|
.cloud-resource-search__hint,
|
|
407
|
+
.cloud-resource-search__status,
|
|
389
408
|
.cloud-resource-search__idle,
|
|
390
409
|
.cloud-resource-search__footer,
|
|
391
410
|
.cloud-resource-search__count,
|