@valentinkolb/cloud 0.1.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 +69 -0
- package/public/logo.svg +1 -0
- package/scripts/build.ts +113 -0
- package/scripts/preload.ts +73 -0
- package/src/_internal/define-app.ts +399 -0
- package/src/_internal/heartbeat.ts +33 -0
- package/src/_internal/registry.ts +100 -0
- package/src/_internal/runtime-context.ts +38 -0
- package/src/api/accounts-entities.ts +134 -0
- package/src/api/admin-lifecycle.ts +210 -0
- package/src/api/auth/schemas.ts +28 -0
- package/src/api/auth.ts +230 -0
- package/src/api/index.ts +66 -0
- package/src/api/me.ts +206 -0
- package/src/api/search/schemas.ts +43 -0
- package/src/api/search.ts +130 -0
- package/src/clients/core.ts +19 -0
- package/src/config/env.ts +23 -0
- package/src/config/index.ts +6 -0
- package/src/config/ssr.ts +58 -0
- package/src/contracts/app.ts +140 -0
- package/src/contracts/index.ts +5 -0
- package/src/contracts/profile.ts +67 -0
- package/src/contracts/registry.ts +50 -0
- package/src/contracts/settings-types.ts +84 -0
- package/src/contracts/shared.ts +258 -0
- package/src/contracts/widgets.ts +121 -0
- package/src/index.ts +6 -0
- package/src/server/api/index.ts +1 -0
- package/src/server/api/respond.ts +55 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/index.ts +62 -0
- package/src/server/middleware/auth.ts +168 -0
- package/src/server/middleware/index.ts +7 -0
- package/src/server/middleware/middleware.ts +47 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +126 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/validator.ts +35 -0
- package/src/server/services/access.ts +294 -0
- package/src/server/services/freeipa/client.ts +100 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +78 -0
- package/src/server/services/freeipa/tls.ts +48 -0
- package/src/server/services/freeipa/util.ts +60 -0
- package/src/server/services/geo.ts +154 -0
- package/src/server/services/index.ts +28 -0
- package/src/server/services/services.ts +13 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +907 -0
- package/src/services/account-lifecycle/scheduler.ts +347 -0
- package/src/services/account-model.ts +21 -0
- package/src/services/accounts/app.ts +966 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +45 -0
- package/src/services/accounts/entities.ts +529 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +246 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +64 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +491 -0
- package/src/services/accounts/model.ts +135 -0
- package/src/services/accounts/switching.ts +117 -0
- package/src/services/accounts/users.ts +714 -0
- package/src/services/auth-flows/index.ts +6 -0
- package/src/services/auth-flows/ipa.ts +128 -0
- package/src/services/auth-flows/magic-link.ts +119 -0
- package/src/services/freeipa-config.ts +89 -0
- package/src/services/index.ts +46 -0
- package/src/services/ipa/auth.ts +122 -0
- package/src/services/ipa/groups.ts +684 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/profile.ts +90 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/sync.ts +740 -0
- package/src/services/ipa/users.ts +794 -0
- package/src/services/logging/index.ts +294 -0
- package/src/services/notifications/email.ts +123 -0
- package/src/services/notifications/index.ts +413 -0
- package/src/services/postgres.ts +51 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +13 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +255 -0
- package/src/services/session/index.ts +137 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +101 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +824 -0
- package/src/services/settings/index.ts +203 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +179 -0
- package/src/services/settings/templates.ts +10 -0
- package/src/services/weather/forecast.ts +287 -0
- package/src/services/weather/geo.ts +110 -0
- package/src/services/weather/index.ts +99 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +125 -0
- package/src/services/weather/migrate.ts +22 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/shared/account-display.ts +17 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/icons.ts +109 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +58 -0
- package/src/shared/markdown/extensions/images.ts +43 -0
- package/src/shared/markdown/extensions/info-blocks.ts +93 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +34 -0
- package/src/shared/markdown/extensions/tables.ts +88 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/index.ts +97 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/ssr/AdminLayout.tsx +42 -0
- package/src/ssr/AdminSidebar.tsx +95 -0
- package/src/ssr/Footer.island.tsx +62 -0
- package/src/ssr/GlobalSearchDialog.tsx +389 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +106 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +42 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +99 -0
- package/src/ssr/Layout.tsx +326 -0
- package/src/ssr/MoreAppsDropdown.island.tsx +61 -0
- package/src/ssr/NavMenu.island.tsx +108 -0
- package/src/ssr/ThemeToggleRail.island.tsx +27 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/SearchBar.island.tsx +77 -0
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/runtime.ts +22 -0
- package/src/styles/base-popover.css +28 -0
- package/src/styles/effects.css +65 -0
- package/src/styles/global.css +133 -0
- package/src/styles/input.css +54 -0
- package/src/styles/tokens.css +35 -0
- package/src/styles/utilities-buttons.css +125 -0
- package/src/styles/utilities-feedback.css +65 -0
- package/src/styles/utilities-layout.css +122 -0
- package/src/styles/utilities-navigation.css +196 -0
- package/src/types/ambient.d.ts +8 -0
- package/src/ui/admin-settings.tsx +148 -0
- package/src/ui/dialog-core.ts +146 -0
- package/src/ui/filter/FilterChip.tsx +196 -0
- package/src/ui/filter/index.ts +2 -0
- package/src/ui/index.ts +19 -0
- package/src/ui/input/Checkbox.tsx +55 -0
- package/src/ui/input/ColorInput.tsx +122 -0
- package/src/ui/input/DateTimeInput.tsx +86 -0
- package/src/ui/input/ImageInput.tsx +170 -0
- package/src/ui/input/NumberInput.tsx +113 -0
- package/src/ui/input/PinInput.tsx +169 -0
- package/src/ui/input/SegmentedControl.tsx +99 -0
- package/src/ui/input/Select.tsx +288 -0
- package/src/ui/input/SelectChip.tsx +61 -0
- package/src/ui/input/Slider.tsx +118 -0
- package/src/ui/input/Switch.tsx +62 -0
- package/src/ui/input/TagsInput.tsx +115 -0
- package/src/ui/input/TextInput.tsx +160 -0
- package/src/ui/input/index.ts +13 -0
- package/src/ui/input/types.ts +42 -0
- package/src/ui/input/util.tsx +105 -0
- package/src/ui/ipa/Avatar.tsx +28 -0
- package/src/ui/ipa/GroupView.tsx +36 -0
- package/src/ui/ipa/LoginBtn.tsx +16 -0
- package/src/ui/ipa/UserView.tsx +58 -0
- package/src/ui/ipa/index.ts +4 -0
- package/src/ui/misc/ContextMenu.tsx +211 -0
- package/src/ui/misc/CopyButton.tsx +28 -0
- package/src/ui/misc/Dropdown.tsx +194 -0
- package/src/ui/misc/EntitySearch.tsx +213 -0
- package/src/ui/misc/Lightbox.tsx +194 -0
- package/src/ui/misc/LinkCard.tsx +34 -0
- package/src/ui/misc/LogEntriesTable.tsx +61 -0
- package/src/ui/misc/MarkdownView.tsx +65 -0
- package/src/ui/misc/Pagination.tsx +51 -0
- package/src/ui/misc/PermissionEditor.tsx +379 -0
- package/src/ui/misc/ProgressBar.tsx +47 -0
- package/src/ui/misc/RemoveBtn.tsx +27 -0
- package/src/ui/misc/StatCell.tsx +90 -0
- package/src/ui/misc/index.ts +18 -0
- package/src/ui/navigation.ts +32 -0
- package/src/ui/prompts.tsx +854 -0
- package/src/ui/sidebar.tsx +468 -0
- package/src/ui/widgets/Widget.tsx +62 -0
- package/src/ui/widgets/WidgetCard.tsx +19 -0
- package/src/ui/widgets/WidgetHero.tsx +39 -0
- package/src/ui/widgets/WidgetList.tsx +84 -0
- package/src/ui/widgets/WidgetPills.tsx +68 -0
- package/src/ui/widgets/WidgetStat.tsx +67 -0
- package/src/ui/widgets/WidgetStatus.tsx +62 -0
- package/src/ui/widgets/index.ts +9 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KaTeX extension for marked
|
|
3
|
+
*
|
|
4
|
+
* Renders LaTeX math expressions server-side:
|
|
5
|
+
* - Inline: $..$ or \(..\)
|
|
6
|
+
* - Block: $$...$$ or \[..\] or ```math
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MarkedExtension, Tokens } from "marked";
|
|
10
|
+
import katex from "katex";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Token shape produced by the math tokenizers below. The marked extension API
|
|
14
|
+
* types renderer params as `Tokens.Generic` (open `[index: string]: any`), so
|
|
15
|
+
* we narrow at the call boundary by reading `latex` off the typed sub-shape.
|
|
16
|
+
*/
|
|
17
|
+
type MathToken = Tokens.Generic & { latex: string };
|
|
18
|
+
|
|
19
|
+
function renderBlockMath(latex: string): string {
|
|
20
|
+
try {
|
|
21
|
+
const html = katex.renderToString(latex, {
|
|
22
|
+
throwOnError: false,
|
|
23
|
+
displayMode: true,
|
|
24
|
+
});
|
|
25
|
+
return `<div class="md-katex-block my-3 flex items-center justify-center">${html}</div>`;
|
|
26
|
+
} catch {
|
|
27
|
+
return `<div class="md-katex-block my-3 flex items-center justify-center text-red-500 text-sm"><i class="ti ti-alert-circle mr-1"></i> Invalid LaTeX</div>`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderInlineMath(latex: string): string {
|
|
32
|
+
try {
|
|
33
|
+
const html = katex.renderToString(latex, {
|
|
34
|
+
throwOnError: false,
|
|
35
|
+
displayMode: false,
|
|
36
|
+
});
|
|
37
|
+
return `<span class="md-katex-inline">${html}</span>`;
|
|
38
|
+
} catch {
|
|
39
|
+
return `<span class="md-katex-inline text-red-500 font-mono text-sm">$${latex}$</span>`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function katexExtension(): MarkedExtension {
|
|
44
|
+
return {
|
|
45
|
+
extensions: [
|
|
46
|
+
// ```math code blocks
|
|
47
|
+
{
|
|
48
|
+
name: "mathCodeBlock",
|
|
49
|
+
level: "block",
|
|
50
|
+
start(src: string) {
|
|
51
|
+
const match = src.match(/^```math/m);
|
|
52
|
+
return match ? match.index : undefined;
|
|
53
|
+
},
|
|
54
|
+
tokenizer(src: string) {
|
|
55
|
+
const match = /^```math\n([\s\S]*?)\n```/.exec(src);
|
|
56
|
+
if (match) {
|
|
57
|
+
return {
|
|
58
|
+
type: "mathCodeBlock",
|
|
59
|
+
raw: match[0],
|
|
60
|
+
latex: match[1]!.trim(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
},
|
|
65
|
+
renderer(token: Tokens.Generic) {
|
|
66
|
+
const latex = (token as MathToken).latex;
|
|
67
|
+
return renderBlockMath(latex);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
// Block math with $$ ... $$
|
|
71
|
+
{
|
|
72
|
+
name: "blockMath",
|
|
73
|
+
level: "block",
|
|
74
|
+
start(src: string) {
|
|
75
|
+
const match = src.match(/\$\$/);
|
|
76
|
+
return match ? match.index : undefined;
|
|
77
|
+
},
|
|
78
|
+
tokenizer(src: string) {
|
|
79
|
+
const match = /^\$\$([\s\S]+?)\$\$/.exec(src);
|
|
80
|
+
if (match) {
|
|
81
|
+
return {
|
|
82
|
+
type: "blockMath",
|
|
83
|
+
raw: match[0],
|
|
84
|
+
latex: match[1]!.trim(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
},
|
|
89
|
+
renderer(token: Tokens.Generic) {
|
|
90
|
+
const latex = (token as MathToken).latex;
|
|
91
|
+
return renderBlockMath(latex);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
// Inline math with $ ... $
|
|
95
|
+
{
|
|
96
|
+
name: "inlineMath",
|
|
97
|
+
level: "inline",
|
|
98
|
+
start(src: string) {
|
|
99
|
+
const match = src.match(/(?<!\$)\$(?!\$)/);
|
|
100
|
+
return match ? match.index : undefined;
|
|
101
|
+
},
|
|
102
|
+
tokenizer(src: string) {
|
|
103
|
+
const match = /^(?<!\$)\$(?!\$)([^$\n]+)\$(?!\$)/.exec(src);
|
|
104
|
+
if (match) {
|
|
105
|
+
return {
|
|
106
|
+
type: "inlineMath",
|
|
107
|
+
raw: match[0],
|
|
108
|
+
latex: match[1]!.trim(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
},
|
|
113
|
+
renderer(token: Tokens.Generic) {
|
|
114
|
+
const latex = (token as MathToken).latex;
|
|
115
|
+
return renderInlineMath(latex);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links extension for marked
|
|
3
|
+
*
|
|
4
|
+
* Renders links with the same visual style as the CodeMirror editor:
|
|
5
|
+
* - Shows [label] in bold followed by an arrow icon
|
|
6
|
+
* - Opens in new tab with noopener,noreferrer
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MarkedExtension, Tokens } from "marked";
|
|
10
|
+
import { escapeHtml, LINK_STYLES } from "../shared";
|
|
11
|
+
|
|
12
|
+
export function linksExtension(): MarkedExtension {
|
|
13
|
+
return {
|
|
14
|
+
renderer: {
|
|
15
|
+
link(token: Tokens.Link): string {
|
|
16
|
+
const { href, title, text } = token;
|
|
17
|
+
|
|
18
|
+
// Build title attribute if provided
|
|
19
|
+
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
|
20
|
+
|
|
21
|
+
// Match CodeMirror style: [label] with arrow icon
|
|
22
|
+
return (
|
|
23
|
+
`<span class="${LINK_STYLES.wrapper}">` +
|
|
24
|
+
`<span class="${LINK_STYLES.label}">[${escapeHtml(text)}]</span>` +
|
|
25
|
+
`<a href="${escapeHtml(href)}"${titleAttr} target="_blank" rel="noopener noreferrer" ` +
|
|
26
|
+
`class="${LINK_STYLES.icon}">` +
|
|
27
|
+
`<i class="ti ti-arrow-up-right text-xs"></i>` +
|
|
28
|
+
`</a>` +
|
|
29
|
+
`</span>`
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tables Extension for Marked
|
|
3
|
+
*
|
|
4
|
+
* Renders markdown tables with styling matching the CodeMirror extension.
|
|
5
|
+
* Supports cell formatting for NULL, booleans, dates, and numbers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MarkedExtension, Tokens } from "marked";
|
|
9
|
+
import { escapeHtml } from "../shared";
|
|
10
|
+
|
|
11
|
+
const formatCell = (cell: string): string => {
|
|
12
|
+
const trimmed = cell.trim();
|
|
13
|
+
|
|
14
|
+
// NULL
|
|
15
|
+
if (trimmed.toLowerCase() === "null") {
|
|
16
|
+
return '<span class="text-gray-400 italic">NULL</span>';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Boolean
|
|
20
|
+
if (trimmed.toLowerCase() === "true") {
|
|
21
|
+
return '<span class="text-green-600">true</span>';
|
|
22
|
+
}
|
|
23
|
+
if (trimmed.toLowerCase() === "false") {
|
|
24
|
+
return '<span class="text-red-600">false</span>';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ISO datetime
|
|
28
|
+
if (trimmed.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
|
|
29
|
+
try {
|
|
30
|
+
const date = new Date(trimmed);
|
|
31
|
+
return date.toLocaleString();
|
|
32
|
+
} catch {
|
|
33
|
+
return escapeHtml(trimmed);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Number with decimals
|
|
38
|
+
const num = parseFloat(trimmed);
|
|
39
|
+
if (!isNaN(num) && trimmed.includes(".") && !Number.isInteger(num)) {
|
|
40
|
+
return num.toFixed(4).replace(/\.?0+$/, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return escapeHtml(trimmed);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function tablesExtension(): MarkedExtension {
|
|
47
|
+
return {
|
|
48
|
+
renderer: {
|
|
49
|
+
table(token: Tokens.Table): string {
|
|
50
|
+
const headerCells = token.header
|
|
51
|
+
.map(
|
|
52
|
+
(cell) =>
|
|
53
|
+
`<th class="px-3 py-2 text-left font-medium bg-gray-100 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 whitespace-nowrap min-w-30">${this.parser.parseInline(
|
|
54
|
+
cell.tokens,
|
|
55
|
+
)}</th>`,
|
|
56
|
+
)
|
|
57
|
+
.join("");
|
|
58
|
+
|
|
59
|
+
const rows = token.rows
|
|
60
|
+
.map((row) => {
|
|
61
|
+
const cells = row
|
|
62
|
+
.map((cell, i) => {
|
|
63
|
+
const content = this.parser.parseInline(cell.tokens);
|
|
64
|
+
const bgClass = i % 2 === 0 ? "bg-gray-50 dark:bg-gray-800/20" : "";
|
|
65
|
+
return `<td class="px-3 py-2 whitespace-nowrap min-w-30 ${bgClass}">${formatCell(content)}</td>`;
|
|
66
|
+
})
|
|
67
|
+
.join("");
|
|
68
|
+
return `<tr class="hover:font-semibold">${cells}</tr>`;
|
|
69
|
+
})
|
|
70
|
+
.join("\n");
|
|
71
|
+
|
|
72
|
+
const rowCount = token.rows.length;
|
|
73
|
+
|
|
74
|
+
return `<div class="cm-table-widget my-2">
|
|
75
|
+
<div class="flex flex-col">
|
|
76
|
+
<div class="overflow-x-auto rounded">
|
|
77
|
+
<table class="min-w-full text-sm tabular-nums">
|
|
78
|
+
<thead><tr>${headerCells}</tr></thead>
|
|
79
|
+
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">${rows}</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="text-center text-xs mt-2">${rowCount} row${rowCount === 1 ? "" : "s"}</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>`;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task List Extension for Marked
|
|
3
|
+
*
|
|
4
|
+
* Renders task lists with checkboxes:
|
|
5
|
+
* - [ ] Unchecked item
|
|
6
|
+
* - [x] Checked item
|
|
7
|
+
*
|
|
8
|
+
* Also styles regular bullet lists consistently.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MarkedExtension, Tokens, RendererObject, RendererThis } from "marked";
|
|
12
|
+
|
|
13
|
+
export function taskListExtension(): MarkedExtension {
|
|
14
|
+
const renderer: RendererObject = {
|
|
15
|
+
listitem(this: RendererThis, token: Tokens.ListItem): string {
|
|
16
|
+
// Use parse() for block-level content in list items
|
|
17
|
+
let text = this.parser.parse(token.tokens);
|
|
18
|
+
const isTask = token.task;
|
|
19
|
+
const isChecked = token.checked;
|
|
20
|
+
|
|
21
|
+
if (isTask) {
|
|
22
|
+
// Remove any default checkbox that marked might have inserted
|
|
23
|
+
text = text.replace(/<input[^>]*type="checkbox"[^>]*>/gi, "");
|
|
24
|
+
|
|
25
|
+
const checkboxHtml = `<input type="checkbox" class="custom-list-task-marker mr-2" ${isChecked ? "checked" : ""} disabled />`;
|
|
26
|
+
const checkedClass = isChecked ? "custom-list-task-checked line-through opacity-70" : "custom-list-task-unchecked";
|
|
27
|
+
|
|
28
|
+
return `<li class="custom-list custom-list-task ${checkedClass} flex items-start gap-1">${checkboxHtml}<span>${text}</span></li>\n`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `<li class="custom-list custom-list-bullet">${text}</li>\n`;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
list(this: RendererThis & { listitem: (item: Tokens.ListItem) => string }, token: Tokens.List): string {
|
|
35
|
+
const ordered = token.ordered;
|
|
36
|
+
const start = token.start;
|
|
37
|
+
const body = token.items.map((item: Tokens.ListItem) => this.listitem(item)).join("");
|
|
38
|
+
|
|
39
|
+
const tag = ordered ? "ol" : "ul";
|
|
40
|
+
const startAttr = ordered && start !== 1 ? ` start="${start}"` : "";
|
|
41
|
+
const hasTaskItems = token.items.some((item: Tokens.ListItem) => item.task);
|
|
42
|
+
const listClass = ordered
|
|
43
|
+
? "list-decimal pl-6 my-2 space-y-1"
|
|
44
|
+
: hasTaskItems
|
|
45
|
+
? "pl-0 my-2 space-y-1 list-none"
|
|
46
|
+
: "pl-4 my-2 space-y-1";
|
|
47
|
+
|
|
48
|
+
return `<${tag}${startAttr} class="${listClass}">\n${body}</${tag}>\n`;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return { renderer };
|
|
53
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side Markdown renderer using marked.js
|
|
3
|
+
*
|
|
4
|
+
* This module provides markdown rendering that produces HTML matching
|
|
5
|
+
* the visual appearance of the CodeMirror editor extensions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Marked } from "marked";
|
|
9
|
+
import { infoBlocksExtension } from "./extensions/info-blocks";
|
|
10
|
+
import { taskListExtension } from "./extensions/task-list";
|
|
11
|
+
import { tablesExtension } from "./extensions/tables";
|
|
12
|
+
import { linksExtension } from "./extensions/links";
|
|
13
|
+
import { imagesExtension } from "./extensions/images";
|
|
14
|
+
import { codeExtension } from "./extensions/code";
|
|
15
|
+
import { katexExtension } from "./extensions/katex";
|
|
16
|
+
import { markdownClient } from "./client";
|
|
17
|
+
|
|
18
|
+
// Create a configured marked instance
|
|
19
|
+
const createMarked = () => {
|
|
20
|
+
const marked = new Marked();
|
|
21
|
+
|
|
22
|
+
marked.use({
|
|
23
|
+
breaks: true,
|
|
24
|
+
gfm: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Apply extensions in order
|
|
28
|
+
// Note: katexExtension must come before codeExtension to handle ```math blocks
|
|
29
|
+
marked.use(infoBlocksExtension());
|
|
30
|
+
marked.use(taskListExtension());
|
|
31
|
+
marked.use(tablesExtension());
|
|
32
|
+
marked.use(linksExtension());
|
|
33
|
+
marked.use(imagesExtension());
|
|
34
|
+
marked.use(katexExtension());
|
|
35
|
+
marked.use(codeExtension());
|
|
36
|
+
|
|
37
|
+
return marked;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const marked = createMarked();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render markdown to HTML for server-side display.
|
|
44
|
+
* The output matches the visual styling of the CodeMirror editor.
|
|
45
|
+
*
|
|
46
|
+
* Supported features:
|
|
47
|
+
* - GFM (GitHub Flavored Markdown)
|
|
48
|
+
* - Info blocks (:::note, :::info, :::success, :::warning, :::danger)
|
|
49
|
+
* - Task lists with checkboxes
|
|
50
|
+
* - Tables with cell formatting
|
|
51
|
+
* - Styled links and images
|
|
52
|
+
* - Code blocks with language badges
|
|
53
|
+
* - Mermaid diagram containers (requires client-side init)
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* // In page.tsx (server-side):
|
|
58
|
+
* import { renderMarkdown } from "@/shared/markdown";
|
|
59
|
+
* const html = renderMarkdown(markdownContent);
|
|
60
|
+
*
|
|
61
|
+
* // Pass to MarkdownView component:
|
|
62
|
+
* import MarkdownView from "@/ui/misc/MarkdownView";
|
|
63
|
+
* <MarkdownView html={html} />
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @see MarkdownView component for displaying the rendered HTML
|
|
67
|
+
* @see initMarkdownEnhancements for client-side Mermaid support
|
|
68
|
+
*/
|
|
69
|
+
export function renderMarkdown(content: string): string {
|
|
70
|
+
if (!content || typeof content !== "string") return "";
|
|
71
|
+
|
|
72
|
+
const html = marked.parse(content);
|
|
73
|
+
if (typeof html !== "string") return "";
|
|
74
|
+
|
|
75
|
+
return html;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render markdown to HTML synchronously.
|
|
80
|
+
*/
|
|
81
|
+
export function renderMarkdownSync(content: string): string {
|
|
82
|
+
if (!content || typeof content !== "string") return "";
|
|
83
|
+
|
|
84
|
+
const html = marked.parse(content);
|
|
85
|
+
if (typeof html !== "string") return "";
|
|
86
|
+
|
|
87
|
+
return html;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { marked };
|
|
91
|
+
|
|
92
|
+
export const markdown = {
|
|
93
|
+
render: renderMarkdown,
|
|
94
|
+
renderSync: renderMarkdownSync,
|
|
95
|
+
marked,
|
|
96
|
+
client: markdownClient,
|
|
97
|
+
} as const;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for markdown rendering
|
|
3
|
+
*
|
|
4
|
+
* Used by both CodeMirror editor extensions and marked renderer extensions
|
|
5
|
+
* to ensure consistent behavior and avoid code duplication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Escape HTML special characters to prevent XSS
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* escapeHtml('<script>alert("xss")</script>')
|
|
13
|
+
* // Returns: '<script>alert("xss")</script>'
|
|
14
|
+
*/
|
|
15
|
+
export const escapeHtml = (text: string): string => {
|
|
16
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Shared CSS Classes (match CodeMirror and marked renderer)
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/** Image widget styles */
|
|
24
|
+
export const IMAGE_STYLES = {
|
|
25
|
+
wrapper: "md-image-widget my-2",
|
|
26
|
+
figure: "flex flex-col items-center justify-center max-w-full",
|
|
27
|
+
img: "block max-h-[400px] rounded border border-gray-200 dark:border-gray-700",
|
|
28
|
+
caption: "text-sm text-gray-500 dark:text-gray-400 mt-2 italic",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/** Link widget styles */
|
|
32
|
+
export const LINK_STYLES = {
|
|
33
|
+
wrapper: "md-link-widget inline-flex items-center align-baseline",
|
|
34
|
+
label: "md-link-label font-bold text-gray-800 dark:text-gray-200",
|
|
35
|
+
icon: "md-link-icon inline-flex items-center cursor-pointer text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-500 hover:underline opacity-70 hover:opacity-100 transition-opacity",
|
|
36
|
+
} as const;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { JSX } from "solid-js/jsx-runtime";
|
|
2
|
+
import Layout from "./Layout";
|
|
3
|
+
import AdminSidebar from "./AdminSidebar";
|
|
4
|
+
import { getRuntimeContext, type RuntimeContext } from "./runtime";
|
|
5
|
+
type Breadcrumb = { title: string; href?: string };
|
|
6
|
+
type AdminLayoutContext = {
|
|
7
|
+
get(key: "user"): any;
|
|
8
|
+
get(key: "page"): any;
|
|
9
|
+
get(key: "runtime"): RuntimeContext;
|
|
10
|
+
get(key: "settings"): Record<string, any>;
|
|
11
|
+
req: { raw: { headers: Headers; url: string } };
|
|
12
|
+
};
|
|
13
|
+
type Props = {
|
|
14
|
+
children: JSX.Element;
|
|
15
|
+
c: AdminLayoutContext;
|
|
16
|
+
title: string;
|
|
17
|
+
/** Bypass the scroll wrapper — child manages its own overflow. */
|
|
18
|
+
stretch?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export default function AdminLayout({ children, c, title, stretch }: Props) {
|
|
21
|
+
const pathname = new URL(c.req.raw.url).pathname;
|
|
22
|
+
const runtime = getRuntimeContext(c);
|
|
23
|
+
const breadcrumbs: Breadcrumb[] = [
|
|
24
|
+
{ title: "Start", href: "/" },
|
|
25
|
+
{ title: "Admin", href: "/admin" },
|
|
26
|
+
];
|
|
27
|
+
if (title !== "Overview") {
|
|
28
|
+
breadcrumbs.push({ title });
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
<Layout c={c} fullWidth title={breadcrumbs}>
|
|
32
|
+
<div class="app-cols flex-1 min-h-0">
|
|
33
|
+
<AdminSidebar pathname={pathname} apps={runtime.apps} />
|
|
34
|
+
<div class="flex-1 min-w-0 min-h-0 flex flex-col">
|
|
35
|
+
<div class={`flex-1 min-h-0 ${stretch ? "flex flex-col" : "overflow-y-auto"}`} style="scrollbar-gutter: stable">
|
|
36
|
+
{children}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</Layout>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { RuntimeContext } from "./runtime";
|
|
2
|
+
|
|
3
|
+
type AdminLink = { href: string; icon: string; label: string };
|
|
4
|
+
|
|
5
|
+
const buildAdminLinks = (
|
|
6
|
+
apps: readonly RuntimeContext["apps"][number][]
|
|
7
|
+
): AdminLink[] => [
|
|
8
|
+
{ href: "/admin", icon: "ti-dashboard", label: "Overview" },
|
|
9
|
+
{ href: "/admin/gateway", icon: "ti-route-scan", label: "Apps & Gateway" },
|
|
10
|
+
...apps
|
|
11
|
+
.filter((app) => !!app.adminHref && app.adminHref !== "/admin/gateway")
|
|
12
|
+
.map((app) => ({
|
|
13
|
+
href: app.adminHref!,
|
|
14
|
+
icon: app.icon.replace(/^ti\s+/, ""),
|
|
15
|
+
label: app.name,
|
|
16
|
+
})),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function isActive(pathname: string, href: string): boolean {
|
|
20
|
+
if (href === "/admin") return pathname === "/admin";
|
|
21
|
+
return pathname.startsWith(href);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function AdminSidebar({
|
|
25
|
+
pathname,
|
|
26
|
+
apps,
|
|
27
|
+
}: {
|
|
28
|
+
pathname: string;
|
|
29
|
+
apps: readonly RuntimeContext["apps"][number][];
|
|
30
|
+
}) {
|
|
31
|
+
const adminLinks = buildAdminLinks(apps);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<nav class="sidebar-container-mobile">
|
|
36
|
+
<details class="group">
|
|
37
|
+
<summary class="sidebar-mobile-toggle">
|
|
38
|
+
<div class="w-8 h-8 rounded-lg bg-zinc-600 text-white grid place-items-center shrink-0 dark:bg-zinc-700">
|
|
39
|
+
<i class="ti ti-settings text-sm" />
|
|
40
|
+
</div>
|
|
41
|
+
<span class="font-semibold truncate flex-1">Admin</span>
|
|
42
|
+
<span class="ml-auto inline-flex h-7 w-7 items-center justify-center rounded-md text-dimmed transition-transform group-open:rotate-180">
|
|
43
|
+
<i class="ti ti-chevron-down text-sm" />
|
|
44
|
+
</span>
|
|
45
|
+
</summary>
|
|
46
|
+
<div class="sidebar-mobile-actions">
|
|
47
|
+
{adminLinks.map((link) => (
|
|
48
|
+
<a
|
|
49
|
+
href={link.href}
|
|
50
|
+
class={`sidebar-item-mobile ${
|
|
51
|
+
isActive(pathname, link.href)
|
|
52
|
+
? "border-blue-500/35 bg-blue-50/70 text-blue-700 dark:border-blue-400/40 dark:bg-blue-950/40 dark:text-blue-200"
|
|
53
|
+
: ""
|
|
54
|
+
}`}
|
|
55
|
+
aria-current={
|
|
56
|
+
isActive(pathname, link.href) ? "page" : undefined
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<i class={`ti ${link.icon}`} />
|
|
60
|
+
{link.label}
|
|
61
|
+
</a>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</details>
|
|
65
|
+
</nav>
|
|
66
|
+
|
|
67
|
+
<aside class="sidebar-container">
|
|
68
|
+
<div class="paper flex h-full min-h-0 flex-col gap-4 p-4">
|
|
69
|
+
<div class="flex items-center gap-3">
|
|
70
|
+
<div class="sidebar-header-icon bg-zinc-600 dark:bg-zinc-700">
|
|
71
|
+
<i class="ti ti-settings text-xs" />
|
|
72
|
+
</div>
|
|
73
|
+
<p class="sidebar-header-title">Admin</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="sidebar-body">
|
|
77
|
+
<section class="sidebar-group">
|
|
78
|
+
{adminLinks.map((link) => (
|
|
79
|
+
<a
|
|
80
|
+
href={link.href}
|
|
81
|
+
class={`sidebar-item ${
|
|
82
|
+
isActive(pathname, link.href) ? "sidebar-item-active" : ""
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
<i class={`ti ${link.icon} text-sm`} />
|
|
86
|
+
<span>{link.label}</span>
|
|
87
|
+
</a>
|
|
88
|
+
))}
|
|
89
|
+
</section>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</aside>
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createSignal, For, Show } from "solid-js";
|
|
2
|
+
import { cookies } from "@valentinkolb/stdlib/browser";
|
|
3
|
+
|
|
4
|
+
type FooterProps = {
|
|
5
|
+
isLoggedIn: boolean;
|
|
6
|
+
appName?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Legal/info links contributed by every running app via `defineApp.legalLinks`.
|
|
9
|
+
* Computed server-side via `listLegalLinks()` and passed in by the host page
|
|
10
|
+
* (Layout.tsx) so this island doesn't need direct registry access.
|
|
11
|
+
*/
|
|
12
|
+
legalLinks?: Array<{ label: string; href: string; icon?: string }>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function Footer(props: FooterProps) {
|
|
16
|
+
const [theme, setTheme] = createSignal(
|
|
17
|
+
typeof document !== "undefined" ? (document.documentElement.classList.contains("dark") ? "dark" : "light") : "dark",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const toggleTheme = () => {
|
|
21
|
+
const newTheme = theme() === "dark" ? "light" : "dark";
|
|
22
|
+
document.documentElement.classList.remove("dark", "light");
|
|
23
|
+
document.documentElement.classList.add(newTheme);
|
|
24
|
+
cookies.writeCookie("theme", newTheme);
|
|
25
|
+
setTheme(newTheme);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<footer class="shrink-0 flex items-center justify-center gap-4 py-2 px-3 text-xs text-dimmed ">
|
|
30
|
+
<For each={props.legalLinks ?? []}>
|
|
31
|
+
{(link) => (
|
|
32
|
+
<a href={link.href} class="hover:text-primary transition-colors flex items-center gap-1">
|
|
33
|
+
<Show when={link.icon}>
|
|
34
|
+
<i class={`${link.icon} text-xs`} />
|
|
35
|
+
</Show>
|
|
36
|
+
{link.label}
|
|
37
|
+
</a>
|
|
38
|
+
)}
|
|
39
|
+
</For>
|
|
40
|
+
<button type="button" onClick={toggleTheme} class="hidden md:flex hover:text-primary transition-colors items-center gap-1">
|
|
41
|
+
<i class={`ti ${theme() === "dark" ? "ti-sunset-2" : "ti-moon-stars"} text-xs`} />
|
|
42
|
+
{theme() === "dark" ? "Light" : "Dark"}
|
|
43
|
+
</button>
|
|
44
|
+
{props.isLoggedIn ? (
|
|
45
|
+
<a href="/me" class="hover:text-primary transition-colors flex items-center gap-1">
|
|
46
|
+
<i class="ti ti-user text-xs" />
|
|
47
|
+
Account
|
|
48
|
+
</a>
|
|
49
|
+
) : (
|
|
50
|
+
<a href="/auth/login" class="hover:text-primary transition-colors flex items-center gap-1">
|
|
51
|
+
<i class="ti ti-login text-xs" />
|
|
52
|
+
Login
|
|
53
|
+
</a>
|
|
54
|
+
)}
|
|
55
|
+
{props.appName && (
|
|
56
|
+
<span class="hidden md:inline text-zinc-400 dark:text-zinc-600">
|
|
57
|
+
Copyright © {new Date().getFullYear()} {props.appName}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</footer>
|
|
61
|
+
);
|
|
62
|
+
}
|