@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,61 @@
|
|
|
1
|
+
/** Weather icon codes from Brightsky API */
|
|
2
|
+
export type WeatherIcon =
|
|
3
|
+
| "clear-day"
|
|
4
|
+
| "clear-night"
|
|
5
|
+
| "partly-cloudy-day"
|
|
6
|
+
| "partly-cloudy-night"
|
|
7
|
+
| "cloudy"
|
|
8
|
+
| "fog"
|
|
9
|
+
| "wind"
|
|
10
|
+
| "rain"
|
|
11
|
+
| "sleet"
|
|
12
|
+
| "snow"
|
|
13
|
+
| "hail"
|
|
14
|
+
| "thunderstorm";
|
|
15
|
+
|
|
16
|
+
/** Current weather data */
|
|
17
|
+
export type CurrentWeather = {
|
|
18
|
+
temperature: number; // deg C
|
|
19
|
+
icon: WeatherIcon;
|
|
20
|
+
cloudCover: number; // 0-100%
|
|
21
|
+
windSpeed: number; // km/h
|
|
22
|
+
windGust: number | null; // km/h
|
|
23
|
+
windDirection: number | null; // degrees
|
|
24
|
+
humidity: number | null; // 0-100%
|
|
25
|
+
precipitation: number; // mm in last hour
|
|
26
|
+
pressure: number | null; // hPa
|
|
27
|
+
visibility: number | null; // meters
|
|
28
|
+
dewPoint: number | null; // deg C
|
|
29
|
+
sunshine: number | null; // minutes in last hour
|
|
30
|
+
stationName: string;
|
|
31
|
+
timestamp: string; // ISO timestamp
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Hourly forecast entry */
|
|
35
|
+
export type HourlyForecast = {
|
|
36
|
+
timestamp: string;
|
|
37
|
+
temperature: number;
|
|
38
|
+
icon: WeatherIcon;
|
|
39
|
+
precipitation: number;
|
|
40
|
+
precipitationProbability: number | null; // 0-100%
|
|
41
|
+
windSpeed: number;
|
|
42
|
+
cloudCover: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Daily forecast summary */
|
|
46
|
+
export type DailyForecast = {
|
|
47
|
+
date: string; // YYYY-MM-DD
|
|
48
|
+
tempMin: number;
|
|
49
|
+
tempMax: number;
|
|
50
|
+
icon: WeatherIcon;
|
|
51
|
+
precipitation: number; // total mm
|
|
52
|
+
precipitationProbability: number | null; // max probability for the day
|
|
53
|
+
sunshine: number; // total minutes
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Full weather data with forecasts */
|
|
57
|
+
export type WeatherData = {
|
|
58
|
+
current: CurrentWeather;
|
|
59
|
+
hourly: HourlyForecast[];
|
|
60
|
+
daily: DailyForecast[];
|
|
61
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { WeatherIcon } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Map Brightsky icon to Tabler icon name. */
|
|
4
|
+
const getTablerIcon = (icon: WeatherIcon): string => {
|
|
5
|
+
const iconMap: Record<WeatherIcon, string> = {
|
|
6
|
+
"clear-day": "sun",
|
|
7
|
+
"clear-night": "moon",
|
|
8
|
+
"partly-cloudy-day": "sun-moon",
|
|
9
|
+
"partly-cloudy-night": "sun-moon",
|
|
10
|
+
cloudy: "cloud",
|
|
11
|
+
fog: "mist",
|
|
12
|
+
wind: "wind",
|
|
13
|
+
rain: "cloud-rain",
|
|
14
|
+
sleet: "cloud-snow",
|
|
15
|
+
snow: "snowflake",
|
|
16
|
+
hail: "cloud-snow",
|
|
17
|
+
thunderstorm: "cloud-storm",
|
|
18
|
+
};
|
|
19
|
+
return iconMap[icon] ?? "cloud";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Get Tailwind color class for temperature. */
|
|
23
|
+
const getTempColorClass = (temp: number): string => {
|
|
24
|
+
if (temp <= 0) return "text-blue-400";
|
|
25
|
+
if (temp <= 10) return "text-cyan-500";
|
|
26
|
+
if (temp <= 20) return "text-emerald-500";
|
|
27
|
+
if (temp <= 25) return "text-amber-500";
|
|
28
|
+
return "text-red-500";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Get temperature color class for an average of min/max. */
|
|
32
|
+
const getAvgTempColorClass = (tempMin: number, tempMax: number): string => {
|
|
33
|
+
return getTempColorClass((tempMin + tempMax) / 2);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Format temperature with degree symbol. */
|
|
37
|
+
const formatTemp = (temp: number): string => `${temp}°`;
|
|
38
|
+
|
|
39
|
+
/** Format temperature range (e.g., "12° / 5°"). */
|
|
40
|
+
const formatTempRange = (tempMax: number, tempMin: number): string => `${tempMax}° / ${tempMin}°`;
|
|
41
|
+
|
|
42
|
+
export const weatherUiService = {
|
|
43
|
+
getTablerIcon,
|
|
44
|
+
getTempColorClass,
|
|
45
|
+
getAvgTempColorClass,
|
|
46
|
+
formatTemp,
|
|
47
|
+
formatTempRange,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type WeatherUiService = typeof weatherUiService;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { UserProfile, UserProvider } from "../contracts/shared";
|
|
2
|
+
|
|
3
|
+
type AccountLike = {
|
|
4
|
+
provider: UserProvider;
|
|
5
|
+
profile: UserProfile;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type SupplementalRole = "admin" | "group-manager";
|
|
9
|
+
|
|
10
|
+
export const getAccountTypeLabel = (user: Pick<AccountLike, "profile">): string =>
|
|
11
|
+
user.profile === "user" ? "Full account" : "Guest account";
|
|
12
|
+
|
|
13
|
+
export const getManagementLabel = (user: Pick<AccountLike, "provider">): string =>
|
|
14
|
+
user.provider === "ipa" ? "FreeIPA" : "Local";
|
|
15
|
+
|
|
16
|
+
export const getSupplementalRoleLabel = (role: SupplementalRole): string =>
|
|
17
|
+
role === "group-manager" ? "Group Manager" : "Admin";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { User } from "../contracts/shared";
|
|
2
|
+
|
|
3
|
+
export const isAdminUser = (user: Pick<User, "roles">): boolean => user.roles.includes("admin");
|
|
4
|
+
|
|
5
|
+
export const isGroupManagerUser = (user: Pick<User, "roles">): boolean => user.roles.includes("group-manager");
|
|
6
|
+
|
|
7
|
+
export const canManageAnyGroups = (user: Pick<User, "roles">): boolean => isAdminUser(user) || isGroupManagerUser(user);
|
|
8
|
+
|
|
9
|
+
export const canManageGroup = (user: Pick<User, "roles" | "managesGroupIds">, groupId: string): boolean =>
|
|
10
|
+
isAdminUser(user) || user.managesGroupIds.includes(groupId);
|
|
11
|
+
|
|
12
|
+
export const getDefaultGroupScope = (user: Pick<User, "roles">): "all" | "managed" | "member" => {
|
|
13
|
+
if (isAdminUser(user)) return "all";
|
|
14
|
+
return canManageAnyGroups(user) ? "managed" : "member";
|
|
15
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated list of tabler icon options for use in select inputs.
|
|
3
|
+
* Each entry has an `id` (icon class without `ti-` prefix is the tabler name,
|
|
4
|
+
* but stored as `ti-<name>` to match usage like `ti ti-<name>`),
|
|
5
|
+
* a human-readable `label`, and an `icon` class string for rendering.
|
|
6
|
+
*/
|
|
7
|
+
export const ICON_OPTIONS = [
|
|
8
|
+
// Documents & Writing
|
|
9
|
+
{ id: "ti-notebook", label: "Notebook", icon: "ti ti-notebook" },
|
|
10
|
+
{ id: "ti-book", label: "Book", icon: "ti ti-book" },
|
|
11
|
+
{ id: "ti-note", label: "Note", icon: "ti ti-note" },
|
|
12
|
+
{ id: "ti-notes", label: "Notes", icon: "ti ti-notes" },
|
|
13
|
+
{ id: "ti-file-text", label: "Document", icon: "ti ti-file-text" },
|
|
14
|
+
{ id: "ti-file-code", label: "Code", icon: "ti ti-file-code" },
|
|
15
|
+
{ id: "ti-clipboard", label: "Clipboard", icon: "ti ti-clipboard" },
|
|
16
|
+
{ id: "ti-list-check", label: "Checklist", icon: "ti ti-list-check" },
|
|
17
|
+
{ id: "ti-bookmark", label: "Bookmark", icon: "ti ti-bookmark" },
|
|
18
|
+
{ id: "ti-pencil", label: "Pencil", icon: "ti ti-pencil" },
|
|
19
|
+
{ id: "ti-quote", label: "Quote", icon: "ti ti-quote" },
|
|
20
|
+
{ id: "ti-tag", label: "Tag", icon: "ti ti-tag" },
|
|
21
|
+
// Objects
|
|
22
|
+
{ id: "ti-star", label: "Star", icon: "ti ti-star" },
|
|
23
|
+
{ id: "ti-heart", label: "Heart", icon: "ti ti-heart" },
|
|
24
|
+
{ id: "ti-diamond", label: "Diamond", icon: "ti ti-diamond" },
|
|
25
|
+
{ id: "ti-crown", label: "Crown", icon: "ti ti-crown" },
|
|
26
|
+
{ id: "ti-trophy", label: "Trophy", icon: "ti ti-trophy" },
|
|
27
|
+
{ id: "ti-flag", label: "Flag", icon: "ti ti-flag" },
|
|
28
|
+
{ id: "ti-key", label: "Key", icon: "ti ti-key" },
|
|
29
|
+
{ id: "ti-lock", label: "Lock", icon: "ti ti-lock" },
|
|
30
|
+
{ id: "ti-shield", label: "Shield", icon: "ti ti-shield" },
|
|
31
|
+
{ id: "ti-gift", label: "Gift", icon: "ti ti-gift" },
|
|
32
|
+
{ id: "ti-bell", label: "Bell", icon: "ti ti-bell" },
|
|
33
|
+
{ id: "ti-lamp", label: "Lamp", icon: "ti ti-lamp" },
|
|
34
|
+
{ id: "ti-bolt", label: "Bolt", icon: "ti ti-bolt" },
|
|
35
|
+
{ id: "ti-bulb", label: "Idea", icon: "ti ti-bulb" },
|
|
36
|
+
{ id: "ti-puzzle", label: "Puzzle", icon: "ti ti-puzzle" },
|
|
37
|
+
{ id: "ti-eye", label: "Eye", icon: "ti ti-eye" },
|
|
38
|
+
{ id: "ti-brain", label: "Brain", icon: "ti ti-brain" },
|
|
39
|
+
{ id: "ti-compass", label: "Compass", icon: "ti ti-compass" },
|
|
40
|
+
{ id: "ti-wand", label: "Wand", icon: "ti ti-wand" },
|
|
41
|
+
{ id: "ti-sword", label: "Sword", icon: "ti ti-sword" },
|
|
42
|
+
{ id: "ti-anchor", label: "Anchor", icon: "ti ti-anchor" },
|
|
43
|
+
{ id: "ti-camera", label: "Camera", icon: "ti ti-camera" },
|
|
44
|
+
{ id: "ti-music", label: "Music", icon: "ti ti-music" },
|
|
45
|
+
{ id: "ti-palette", label: "Palette", icon: "ti ti-palette" },
|
|
46
|
+
{ id: "ti-paint", label: "Paint", icon: "ti ti-paint" },
|
|
47
|
+
{ id: "ti-coffee", label: "Coffee", icon: "ti ti-coffee" },
|
|
48
|
+
// Animals
|
|
49
|
+
{ id: "ti-cat", label: "Cat", icon: "ti ti-cat" },
|
|
50
|
+
{ id: "ti-dog", label: "Dog", icon: "ti ti-dog" },
|
|
51
|
+
{ id: "ti-fish", label: "Fish", icon: "ti ti-fish" },
|
|
52
|
+
{ id: "ti-bug", label: "Bug", icon: "ti ti-bug" },
|
|
53
|
+
{ id: "ti-butterfly", label: "Butterfly", icon: "ti ti-butterfly" },
|
|
54
|
+
{ id: "ti-feather", label: "Feather", icon: "ti ti-feather" },
|
|
55
|
+
{ id: "ti-paw", label: "Paw", icon: "ti ti-paw" },
|
|
56
|
+
{ id: "ti-deer", label: "Deer", icon: "ti ti-deer" },
|
|
57
|
+
{ id: "ti-horse", label: "Horse", icon: "ti ti-horse" },
|
|
58
|
+
{ id: "ti-pig", label: "Pig", icon: "ti ti-pig" },
|
|
59
|
+
{ id: "ti-spider", label: "Spider", icon: "ti ti-spider" },
|
|
60
|
+
{ id: "ti-bat", label: "Bat", icon: "ti ti-bat" },
|
|
61
|
+
// Nature & Weather
|
|
62
|
+
{ id: "ti-flower", label: "Flower", icon: "ti ti-flower" },
|
|
63
|
+
{ id: "ti-leaf", label: "Leaf", icon: "ti ti-leaf" },
|
|
64
|
+
{ id: "ti-tree", label: "Tree", icon: "ti ti-tree" },
|
|
65
|
+
{ id: "ti-plant", label: "Plant", icon: "ti ti-plant" },
|
|
66
|
+
{ id: "ti-seeding", label: "Seeding", icon: "ti ti-seeding" },
|
|
67
|
+
{ id: "ti-mushroom", label: "Mushroom", icon: "ti ti-mushroom" },
|
|
68
|
+
{ id: "ti-cactus", label: "Cactus", icon: "ti ti-cactus" },
|
|
69
|
+
{ id: "ti-sun", label: "Sun", icon: "ti ti-sun" },
|
|
70
|
+
{ id: "ti-moon", label: "Moon", icon: "ti ti-moon" },
|
|
71
|
+
{ id: "ti-cloud", label: "Cloud", icon: "ti ti-cloud" },
|
|
72
|
+
{ id: "ti-snowflake", label: "Snowflake", icon: "ti ti-snowflake" },
|
|
73
|
+
{ id: "ti-flame", label: "Flame", icon: "ti ti-flame" },
|
|
74
|
+
{ id: "ti-rainbow", label: "Rainbow", icon: "ti ti-rainbow" },
|
|
75
|
+
{ id: "ti-tornado", label: "Tornado", icon: "ti ti-tornado" },
|
|
76
|
+
{ id: "ti-mountain", label: "Mountain", icon: "ti ti-mountain" },
|
|
77
|
+
// Travel & Space
|
|
78
|
+
{ id: "ti-home", label: "Home", icon: "ti ti-home" },
|
|
79
|
+
{ id: "ti-world", label: "World", icon: "ti ti-world" },
|
|
80
|
+
{ id: "ti-globe", label: "Globe", icon: "ti ti-globe" },
|
|
81
|
+
{ id: "ti-map", label: "Map", icon: "ti ti-map" },
|
|
82
|
+
{ id: "ti-rocket", label: "Rocket", icon: "ti ti-rocket" },
|
|
83
|
+
{ id: "ti-planet", label: "Planet", icon: "ti ti-planet" },
|
|
84
|
+
{ id: "ti-meteor", label: "Meteor", icon: "ti ti-meteor" },
|
|
85
|
+
{ id: "ti-comet", label: "Comet", icon: "ti ti-comet" },
|
|
86
|
+
{ id: "ti-tent", label: "Tent", icon: "ti ti-tent" },
|
|
87
|
+
{ id: "ti-sailboat", label: "Sailboat", icon: "ti ti-sailboat" },
|
|
88
|
+
{ id: "ti-plane", label: "Plane", icon: "ti ti-plane" },
|
|
89
|
+
// Food & Drink
|
|
90
|
+
{ id: "ti-apple", label: "Apple", icon: "ti ti-apple" },
|
|
91
|
+
{ id: "ti-cherry", label: "Cherry", icon: "ti ti-cherry" },
|
|
92
|
+
{ id: "ti-lemon", label: "Lemon", icon: "ti ti-lemon" },
|
|
93
|
+
{ id: "ti-pizza", label: "Pizza", icon: "ti ti-pizza" },
|
|
94
|
+
{ id: "ti-cake", label: "Cake", icon: "ti ti-cake" },
|
|
95
|
+
{ id: "ti-cookie", label: "Cookie", icon: "ti ti-cookie" },
|
|
96
|
+
{ id: "ti-candy", label: "Candy", icon: "ti ti-candy" },
|
|
97
|
+
{ id: "ti-ice-cream", label: "Ice Cream", icon: "ti ti-ice-cream" },
|
|
98
|
+
// Fun
|
|
99
|
+
{ id: "ti-ghost", label: "Ghost", icon: "ti ti-ghost" },
|
|
100
|
+
{ id: "ti-alien", label: "Alien", icon: "ti ti-alien" },
|
|
101
|
+
{ id: "ti-atom", label: "Atom", icon: "ti ti-atom" },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
export type IconOption = (typeof ICON_OPTIONS)[number];
|
|
105
|
+
|
|
106
|
+
export const icons = {
|
|
107
|
+
ICON_OPTIONS,
|
|
108
|
+
options: ICON_OPTIONS,
|
|
109
|
+
} as const;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Cloud-specific shared utils (NOT in stdlib)
|
|
2
|
+
export * from "./account-display";
|
|
3
|
+
export * from "./account-session";
|
|
4
|
+
export type * from "./icons";
|
|
5
|
+
export { icons } from "./icons";
|
|
6
|
+
export { markdown } from "./markdown";
|
|
7
|
+
|
|
8
|
+
// Re-export from stdlib for backward compatibility
|
|
9
|
+
// Prefer importing directly from @valentinkolb/stdlib
|
|
10
|
+
export { dates, dates as calendar, encoding, fileIcons, gradients } from "@valentinkolb/stdlib";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize Mermaid diagrams within a container element.
|
|
3
|
+
* Call this in onMount() of your client-side component.
|
|
4
|
+
*
|
|
5
|
+
* @param container - The container element containing rendered markdown
|
|
6
|
+
*/
|
|
7
|
+
export async function initMermaid(container: HTMLElement): Promise<void> {
|
|
8
|
+
const mermaid = (await import("mermaid")).default;
|
|
9
|
+
// Initialize mermaid with theme detection
|
|
10
|
+
const isDark = document.documentElement.classList.contains("dark");
|
|
11
|
+
mermaid.initialize({
|
|
12
|
+
startOnLoad: false,
|
|
13
|
+
theme: "base",
|
|
14
|
+
themeVariables: isDark
|
|
15
|
+
? {
|
|
16
|
+
darkMode: true,
|
|
17
|
+
background: "#09090b",
|
|
18
|
+
textColor: "#e5e7eb",
|
|
19
|
+
lineColor: "#6b7280",
|
|
20
|
+
primaryTextColor: "#e5e7eb",
|
|
21
|
+
secondaryTextColor: "#e5e7eb",
|
|
22
|
+
tertiaryTextColor: "#e5e7eb",
|
|
23
|
+
noteTextColor: "#e5e7eb",
|
|
24
|
+
mainBkg: "#111827",
|
|
25
|
+
secondBkg: "#1f2937",
|
|
26
|
+
tertiaryColor: "#374151",
|
|
27
|
+
}
|
|
28
|
+
: {
|
|
29
|
+
darkMode: false,
|
|
30
|
+
background: "#ffffff",
|
|
31
|
+
textColor: "#111827",
|
|
32
|
+
lineColor: "#6b7280",
|
|
33
|
+
primaryTextColor: "#111827",
|
|
34
|
+
secondaryTextColor: "#111827",
|
|
35
|
+
tertiaryTextColor: "#111827",
|
|
36
|
+
noteTextColor: "#111827",
|
|
37
|
+
mainBkg: "#ffffff",
|
|
38
|
+
secondBkg: "#f9fafb",
|
|
39
|
+
tertiaryColor: "#f3f4f6",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Find mermaid blocks (rendered with fixed-height container from server)
|
|
44
|
+
const mermaidBlocks = container.querySelectorAll(".md-mermaid-block");
|
|
45
|
+
if (mermaidBlocks.length === 0) return;
|
|
46
|
+
|
|
47
|
+
const renderPromises = Array.from(mermaidBlocks).map(async (block, index) => {
|
|
48
|
+
const codeElement = block.querySelector("code.language-mermaid");
|
|
49
|
+
const innerContainer = block.querySelector(".h-full.w-full.flex") as HTMLElement;
|
|
50
|
+
if (!codeElement || !innerContainer) return;
|
|
51
|
+
|
|
52
|
+
const code = codeElement.textContent || "";
|
|
53
|
+
const mermaidId = `mermaid-${index}-${Date.now()}`;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Render mermaid to SVG
|
|
57
|
+
const { svg } = await mermaid.render(mermaidId, code);
|
|
58
|
+
|
|
59
|
+
// Remove loading indicator and hidden pre
|
|
60
|
+
const loading = innerContainer.querySelector(".md-mermaid-loading");
|
|
61
|
+
const pre = innerContainer.querySelector("pre");
|
|
62
|
+
loading?.remove();
|
|
63
|
+
pre?.remove();
|
|
64
|
+
|
|
65
|
+
// Create container for SVG with scaling
|
|
66
|
+
const svgContainer = document.createElement("div");
|
|
67
|
+
svgContainer.className = "flex items-center justify-center w-full h-full";
|
|
68
|
+
svgContainer.innerHTML = svg;
|
|
69
|
+
|
|
70
|
+
// Scale SVG to fit container
|
|
71
|
+
const svgElement = svgContainer.querySelector("svg");
|
|
72
|
+
if (svgElement) {
|
|
73
|
+
svgElement.style.maxWidth = "100%";
|
|
74
|
+
svgElement.style.maxHeight = "100%";
|
|
75
|
+
svgElement.style.width = "auto";
|
|
76
|
+
svgElement.style.height = "auto";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
innerContainer.appendChild(svgContainer);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Show error
|
|
82
|
+
const loading = innerContainer.querySelector(".md-mermaid-loading");
|
|
83
|
+
if (loading) {
|
|
84
|
+
loading.innerHTML = `
|
|
85
|
+
<div class="flex flex-col items-center gap-2 text-red-500">
|
|
86
|
+
<i class="ti ti-alert-circle text-xl"></i>
|
|
87
|
+
<span class="text-sm">Invalid mermaid syntax</span>
|
|
88
|
+
</div>
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await Promise.all(renderPromises);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Set external links to open in new tab.
|
|
99
|
+
* Call this in onMount() of your client-side component.
|
|
100
|
+
*
|
|
101
|
+
* @param container - The container element containing rendered markdown
|
|
102
|
+
*/
|
|
103
|
+
export function initExternalLinks(container: HTMLElement): void {
|
|
104
|
+
const links = container.querySelectorAll("a");
|
|
105
|
+
links.forEach((link) => {
|
|
106
|
+
if (link.href && !link.href.startsWith(window.location.origin)) {
|
|
107
|
+
link.target = "_blank";
|
|
108
|
+
link.rel = "noopener noreferrer";
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Initialize all markdown enhancements (Mermaid + external links).
|
|
115
|
+
* KaTeX is rendered server-side and doesn't need client initialization.
|
|
116
|
+
* Call this in onMount() of your client-side component.
|
|
117
|
+
*
|
|
118
|
+
* @param container - The container element containing rendered markdown
|
|
119
|
+
*/
|
|
120
|
+
export async function initMarkdownEnhancements(container: HTMLElement): Promise<void> {
|
|
121
|
+
await initMermaid(container);
|
|
122
|
+
initExternalLinks(container);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const markdownClient = {
|
|
126
|
+
initMermaid,
|
|
127
|
+
initExternalLinks,
|
|
128
|
+
initEnhancements: initMarkdownEnhancements,
|
|
129
|
+
initMarkdownEnhancements,
|
|
130
|
+
} as const;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code extension for marked
|
|
3
|
+
*
|
|
4
|
+
* Renders code blocks and inline code with consistent styling.
|
|
5
|
+
* For now, this provides basic code formatting without syntax highlighting.
|
|
6
|
+
* Full syntax highlighting can be added later with highlight.js if needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MarkedExtension, Tokens } from "marked";
|
|
10
|
+
import { escapeHtml } from "../shared";
|
|
11
|
+
|
|
12
|
+
export function codeExtension(): MarkedExtension {
|
|
13
|
+
return {
|
|
14
|
+
renderer: {
|
|
15
|
+
code(token: Tokens.Code): string {
|
|
16
|
+
const { text, lang } = token;
|
|
17
|
+
const escapedCode = escapeHtml(text);
|
|
18
|
+
const isMermaid = lang?.toLowerCase() === "mermaid";
|
|
19
|
+
|
|
20
|
+
// Language class for syntax highlighting / mermaid detection
|
|
21
|
+
const langClass = lang ? ` language-${escapeHtml(lang)}` : "";
|
|
22
|
+
|
|
23
|
+
// Special rendering for mermaid blocks with fixed height container
|
|
24
|
+
if (isMermaid) {
|
|
25
|
+
return (
|
|
26
|
+
`<div class="md-mermaid-block my-3 rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800" style="height: 400px;">` +
|
|
27
|
+
`<div class="h-full w-full flex items-center justify-center p-4">` +
|
|
28
|
+
`<pre class="hidden"><code class="language-mermaid">${escapedCode}</code></pre>` +
|
|
29
|
+
`<div class="md-mermaid-loading text-dimmed text-sm flex items-center gap-2">` +
|
|
30
|
+
`<i class="ti ti-loader-2 animate-spin"></i> Loading diagram...` +
|
|
31
|
+
`</div>` +
|
|
32
|
+
`</div>` +
|
|
33
|
+
`</div>`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Language badge if specified
|
|
38
|
+
const langBadge = lang
|
|
39
|
+
? `<span class="absolute top-2 right-2 text-xs text-gray-400 dark:text-gray-500 font-mono select-none">${escapeHtml(lang)}</span>`
|
|
40
|
+
: "";
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
`<div class="md-code-block relative my-3">` +
|
|
44
|
+
langBadge +
|
|
45
|
+
`<pre class="bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-md p-4 overflow-x-auto">` +
|
|
46
|
+
`<code class="text-sm font-mono text-gray-800 dark:text-gray-200 whitespace-pre${langClass}">${escapedCode}</code>` +
|
|
47
|
+
`</pre>` +
|
|
48
|
+
`</div>`
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
codespan(token: Tokens.Codespan): string {
|
|
53
|
+
const escapedCode = escapeHtml(token.text);
|
|
54
|
+
return `<code class="bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 px-1.5 py-0.5 rounded text-sm font-mono">${escapedCode}</code>`;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images extension for marked
|
|
3
|
+
*
|
|
4
|
+
* Renders images with the same visual style as the CodeMirror editor:
|
|
5
|
+
* - Centered figure with max-height constraint
|
|
6
|
+
* - Rounded border
|
|
7
|
+
* - Optional caption from alt text
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MarkedExtension, Tokens } from "marked";
|
|
11
|
+
import { escapeHtml, IMAGE_STYLES } from "../shared";
|
|
12
|
+
|
|
13
|
+
export function imagesExtension(): MarkedExtension {
|
|
14
|
+
return {
|
|
15
|
+
renderer: {
|
|
16
|
+
image(token: Tokens.Image): string {
|
|
17
|
+
const { href, title, text: alt } = token;
|
|
18
|
+
|
|
19
|
+
// Build the image element
|
|
20
|
+
const imgAttrs = [`src="${escapeHtml(href)}"`, `alt="${escapeHtml(alt || "")}"`, `loading="lazy"`, `class="${IMAGE_STYLES.img}"`];
|
|
21
|
+
|
|
22
|
+
if (title) {
|
|
23
|
+
imgAttrs.push(`title="${escapeHtml(title)}"`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const imgHtml = `<img ${imgAttrs.join(" ")} />`;
|
|
27
|
+
|
|
28
|
+
// Build caption if alt text provided
|
|
29
|
+
const captionHtml = alt ? `<figcaption class="${IMAGE_STYLES.caption}">${escapeHtml(alt)}</figcaption>` : "";
|
|
30
|
+
|
|
31
|
+
// Wrap in figure for centering
|
|
32
|
+
return (
|
|
33
|
+
`<div class="${IMAGE_STYLES.wrapper}">` +
|
|
34
|
+
`<figure class="${IMAGE_STYLES.figure}">` +
|
|
35
|
+
imgHtml +
|
|
36
|
+
captionHtml +
|
|
37
|
+
`</figure>` +
|
|
38
|
+
`</div>`
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Info Blocks Extension for Marked
|
|
3
|
+
*
|
|
4
|
+
* Renders custom info blocks with syntax:
|
|
5
|
+
* ::: note
|
|
6
|
+
* Content here
|
|
7
|
+
* :::
|
|
8
|
+
*
|
|
9
|
+
* Supported types: note, info, success, warning, danger
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { MarkedExtension, Tokens } from "marked";
|
|
13
|
+
import { escapeHtml } from "../shared";
|
|
14
|
+
|
|
15
|
+
type BlockType = "note" | "info" | "success" | "warning" | "danger";
|
|
16
|
+
|
|
17
|
+
const blockConfig: Record<BlockType, { icon: string; label: string; classes: string }> = {
|
|
18
|
+
note: {
|
|
19
|
+
icon: "ti-chevron-right",
|
|
20
|
+
label: "Note",
|
|
21
|
+
classes: "border-l-4 border-zinc-400 bg-zinc-50 dark:bg-zinc-800/50 text-zinc-800 dark:text-zinc-200",
|
|
22
|
+
},
|
|
23
|
+
info: {
|
|
24
|
+
icon: "ti-info-circle",
|
|
25
|
+
label: "Info",
|
|
26
|
+
classes: "border-l-4 border-blue-400 bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200",
|
|
27
|
+
},
|
|
28
|
+
success: {
|
|
29
|
+
icon: "ti-check",
|
|
30
|
+
label: "Success",
|
|
31
|
+
classes: "border-l-4 border-green-400 bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200",
|
|
32
|
+
},
|
|
33
|
+
warning: {
|
|
34
|
+
icon: "ti-alert-circle",
|
|
35
|
+
label: "Warning",
|
|
36
|
+
classes: "border-l-4 border-yellow-400 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200",
|
|
37
|
+
},
|
|
38
|
+
danger: {
|
|
39
|
+
icon: "ti-alert-hexagon",
|
|
40
|
+
label: "Danger",
|
|
41
|
+
classes: "border-l-4 border-red-400 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const renderInlineContent = (content: string): string => {
|
|
46
|
+
return content
|
|
47
|
+
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
|
48
|
+
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>")
|
|
49
|
+
.replace(/`([^`]+)`/g, '<code class="bg-black/10 dark:bg-white/10 px-1 py-0.5 rounded text-sm">$1</code>')
|
|
50
|
+
.replace(/\n/g, "<br>");
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function infoBlocksExtension(): MarkedExtension {
|
|
54
|
+
return {
|
|
55
|
+
extensions: [
|
|
56
|
+
{
|
|
57
|
+
name: "infoBlock",
|
|
58
|
+
level: "block",
|
|
59
|
+
start(src: string) {
|
|
60
|
+
return src.match(/^:::/)?.index;
|
|
61
|
+
},
|
|
62
|
+
tokenizer(src: string) {
|
|
63
|
+
const match = src.match(/^:::(\w+)\s*\n([\s\S]*?)\n:::/);
|
|
64
|
+
if (!match) return undefined;
|
|
65
|
+
|
|
66
|
+
const typeStr = match[1]?.toLowerCase() as BlockType;
|
|
67
|
+
if (!blockConfig[typeStr]) return undefined;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
type: "infoBlock",
|
|
71
|
+
raw: match[0],
|
|
72
|
+
blockType: typeStr,
|
|
73
|
+
content: match[2]?.trim() ?? "",
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
renderer(token: Tokens.Generic) {
|
|
77
|
+
const blockType = token.blockType as BlockType;
|
|
78
|
+
const config = blockConfig[blockType];
|
|
79
|
+
const content = escapeHtml(token.content as string);
|
|
80
|
+
const renderedContent = renderInlineContent(content);
|
|
81
|
+
|
|
82
|
+
return `<div class="info-block ${config.classes} p-4 rounded my-2">
|
|
83
|
+
<div class="flex items-center gap-1.5 font-semibold mb-1">
|
|
84
|
+
<i class="ti ${config.icon} shrink-0"></i>
|
|
85
|
+
<span>${config.label}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div>${renderedContent}</div>
|
|
88
|
+
</div>`;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|