@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.
Files changed (196) hide show
  1. package/package.json +69 -0
  2. package/public/logo.svg +1 -0
  3. package/scripts/build.ts +113 -0
  4. package/scripts/preload.ts +73 -0
  5. package/src/_internal/define-app.ts +399 -0
  6. package/src/_internal/heartbeat.ts +33 -0
  7. package/src/_internal/registry.ts +100 -0
  8. package/src/_internal/runtime-context.ts +38 -0
  9. package/src/api/accounts-entities.ts +134 -0
  10. package/src/api/admin-lifecycle.ts +210 -0
  11. package/src/api/auth/schemas.ts +28 -0
  12. package/src/api/auth.ts +230 -0
  13. package/src/api/index.ts +66 -0
  14. package/src/api/me.ts +206 -0
  15. package/src/api/search/schemas.ts +43 -0
  16. package/src/api/search.ts +130 -0
  17. package/src/clients/core.ts +19 -0
  18. package/src/config/env.ts +23 -0
  19. package/src/config/index.ts +6 -0
  20. package/src/config/ssr.ts +58 -0
  21. package/src/contracts/app.ts +140 -0
  22. package/src/contracts/index.ts +5 -0
  23. package/src/contracts/profile.ts +67 -0
  24. package/src/contracts/registry.ts +50 -0
  25. package/src/contracts/settings-types.ts +84 -0
  26. package/src/contracts/shared.ts +258 -0
  27. package/src/contracts/widgets.ts +121 -0
  28. package/src/index.ts +6 -0
  29. package/src/server/api/index.ts +1 -0
  30. package/src/server/api/respond.ts +55 -0
  31. package/src/server/api-client.ts +54 -0
  32. package/src/server/app-context.ts +39 -0
  33. package/src/server/index.ts +62 -0
  34. package/src/server/middleware/auth.ts +168 -0
  35. package/src/server/middleware/index.ts +7 -0
  36. package/src/server/middleware/middleware.ts +47 -0
  37. package/src/server/middleware/openapi.ts +126 -0
  38. package/src/server/middleware/rate-limit.ts +126 -0
  39. package/src/server/middleware/request-logger.ts +41 -0
  40. package/src/server/middleware/validator.ts +35 -0
  41. package/src/server/services/access.ts +294 -0
  42. package/src/server/services/freeipa/client.ts +100 -0
  43. package/src/server/services/freeipa/index.ts +9 -0
  44. package/src/server/services/freeipa/session.ts +78 -0
  45. package/src/server/services/freeipa/tls.ts +48 -0
  46. package/src/server/services/freeipa/util.ts +60 -0
  47. package/src/server/services/geo.ts +154 -0
  48. package/src/server/services/index.ts +28 -0
  49. package/src/server/services/services.ts +13 -0
  50. package/src/services/account-lifecycle/audit.ts +41 -0
  51. package/src/services/account-lifecycle/index.ts +907 -0
  52. package/src/services/account-lifecycle/scheduler.ts +347 -0
  53. package/src/services/account-model.ts +21 -0
  54. package/src/services/accounts/app.ts +966 -0
  55. package/src/services/accounts/authz.ts +22 -0
  56. package/src/services/accounts/base-group.ts +11 -0
  57. package/src/services/accounts/base-user.ts +45 -0
  58. package/src/services/accounts/entities.ts +529 -0
  59. package/src/services/accounts/group-sql.ts +106 -0
  60. package/src/services/accounts/groups.ts +246 -0
  61. package/src/services/accounts/index.ts +14 -0
  62. package/src/services/accounts/ipa-data.ts +64 -0
  63. package/src/services/accounts/lifecycle.ts +2 -0
  64. package/src/services/accounts/local-groups.ts +491 -0
  65. package/src/services/accounts/model.ts +135 -0
  66. package/src/services/accounts/switching.ts +117 -0
  67. package/src/services/accounts/users.ts +714 -0
  68. package/src/services/auth-flows/index.ts +6 -0
  69. package/src/services/auth-flows/ipa.ts +128 -0
  70. package/src/services/auth-flows/magic-link.ts +119 -0
  71. package/src/services/freeipa-config.ts +89 -0
  72. package/src/services/index.ts +46 -0
  73. package/src/services/ipa/auth.ts +122 -0
  74. package/src/services/ipa/groups.ts +684 -0
  75. package/src/services/ipa/guard.ts +17 -0
  76. package/src/services/ipa/index.ts +17 -0
  77. package/src/services/ipa/profile.ts +90 -0
  78. package/src/services/ipa/search.ts +154 -0
  79. package/src/services/ipa/sync.ts +740 -0
  80. package/src/services/ipa/users.ts +794 -0
  81. package/src/services/logging/index.ts +294 -0
  82. package/src/services/notifications/email.ts +123 -0
  83. package/src/services/notifications/index.ts +413 -0
  84. package/src/services/postgres.ts +51 -0
  85. package/src/services/providers/index.ts +27 -0
  86. package/src/services/providers/local/auth.ts +13 -0
  87. package/src/services/providers/local/index.ts +4 -0
  88. package/src/services/providers/local/users.ts +255 -0
  89. package/src/services/session/index.ts +137 -0
  90. package/src/services/settings/api.ts +61 -0
  91. package/src/services/settings/app.ts +101 -0
  92. package/src/services/settings/crypto.ts +69 -0
  93. package/src/services/settings/defaults.ts +824 -0
  94. package/src/services/settings/index.ts +203 -0
  95. package/src/services/settings/namespace.ts +9 -0
  96. package/src/services/settings/snapshot.ts +49 -0
  97. package/src/services/settings/store.ts +179 -0
  98. package/src/services/settings/templates.ts +10 -0
  99. package/src/services/weather/forecast.ts +287 -0
  100. package/src/services/weather/geo.ts +110 -0
  101. package/src/services/weather/index.ts +99 -0
  102. package/src/services/weather/location.ts +24 -0
  103. package/src/services/weather/locations.ts +125 -0
  104. package/src/services/weather/migrate.ts +22 -0
  105. package/src/services/weather/types.ts +61 -0
  106. package/src/services/weather/ui.ts +50 -0
  107. package/src/shared/account-display.ts +17 -0
  108. package/src/shared/account-session.ts +15 -0
  109. package/src/shared/icons.ts +109 -0
  110. package/src/shared/index.ts +10 -0
  111. package/src/shared/markdown/client.ts +130 -0
  112. package/src/shared/markdown/extensions/code.ts +58 -0
  113. package/src/shared/markdown/extensions/images.ts +43 -0
  114. package/src/shared/markdown/extensions/info-blocks.ts +93 -0
  115. package/src/shared/markdown/extensions/katex.ts +120 -0
  116. package/src/shared/markdown/extensions/links.ts +34 -0
  117. package/src/shared/markdown/extensions/tables.ts +88 -0
  118. package/src/shared/markdown/extensions/task-list.ts +53 -0
  119. package/src/shared/markdown/index.ts +97 -0
  120. package/src/shared/markdown/shared.ts +36 -0
  121. package/src/ssr/AdminLayout.tsx +42 -0
  122. package/src/ssr/AdminSidebar.tsx +95 -0
  123. package/src/ssr/Footer.island.tsx +62 -0
  124. package/src/ssr/GlobalSearchDialog.tsx +389 -0
  125. package/src/ssr/GlobalSearchHelpDialog.tsx +106 -0
  126. package/src/ssr/GlobalSearchTrigger.island.tsx +42 -0
  127. package/src/ssr/HotkeysHelpRail.island.tsx +99 -0
  128. package/src/ssr/Layout.tsx +326 -0
  129. package/src/ssr/MoreAppsDropdown.island.tsx +61 -0
  130. package/src/ssr/NavMenu.island.tsx +108 -0
  131. package/src/ssr/ThemeToggleRail.island.tsx +27 -0
  132. package/src/ssr/index.ts +5 -0
  133. package/src/ssr/islands/SearchBar.island.tsx +77 -0
  134. package/src/ssr/islands/index.ts +1 -0
  135. package/src/ssr/runtime.ts +22 -0
  136. package/src/styles/base-popover.css +28 -0
  137. package/src/styles/effects.css +65 -0
  138. package/src/styles/global.css +133 -0
  139. package/src/styles/input.css +54 -0
  140. package/src/styles/tokens.css +35 -0
  141. package/src/styles/utilities-buttons.css +125 -0
  142. package/src/styles/utilities-feedback.css +65 -0
  143. package/src/styles/utilities-layout.css +122 -0
  144. package/src/styles/utilities-navigation.css +196 -0
  145. package/src/types/ambient.d.ts +8 -0
  146. package/src/ui/admin-settings.tsx +148 -0
  147. package/src/ui/dialog-core.ts +146 -0
  148. package/src/ui/filter/FilterChip.tsx +196 -0
  149. package/src/ui/filter/index.ts +2 -0
  150. package/src/ui/index.ts +19 -0
  151. package/src/ui/input/Checkbox.tsx +55 -0
  152. package/src/ui/input/ColorInput.tsx +122 -0
  153. package/src/ui/input/DateTimeInput.tsx +86 -0
  154. package/src/ui/input/ImageInput.tsx +170 -0
  155. package/src/ui/input/NumberInput.tsx +113 -0
  156. package/src/ui/input/PinInput.tsx +169 -0
  157. package/src/ui/input/SegmentedControl.tsx +99 -0
  158. package/src/ui/input/Select.tsx +288 -0
  159. package/src/ui/input/SelectChip.tsx +61 -0
  160. package/src/ui/input/Slider.tsx +118 -0
  161. package/src/ui/input/Switch.tsx +62 -0
  162. package/src/ui/input/TagsInput.tsx +115 -0
  163. package/src/ui/input/TextInput.tsx +160 -0
  164. package/src/ui/input/index.ts +13 -0
  165. package/src/ui/input/types.ts +42 -0
  166. package/src/ui/input/util.tsx +105 -0
  167. package/src/ui/ipa/Avatar.tsx +28 -0
  168. package/src/ui/ipa/GroupView.tsx +36 -0
  169. package/src/ui/ipa/LoginBtn.tsx +16 -0
  170. package/src/ui/ipa/UserView.tsx +58 -0
  171. package/src/ui/ipa/index.ts +4 -0
  172. package/src/ui/misc/ContextMenu.tsx +211 -0
  173. package/src/ui/misc/CopyButton.tsx +28 -0
  174. package/src/ui/misc/Dropdown.tsx +194 -0
  175. package/src/ui/misc/EntitySearch.tsx +213 -0
  176. package/src/ui/misc/Lightbox.tsx +194 -0
  177. package/src/ui/misc/LinkCard.tsx +34 -0
  178. package/src/ui/misc/LogEntriesTable.tsx +61 -0
  179. package/src/ui/misc/MarkdownView.tsx +65 -0
  180. package/src/ui/misc/Pagination.tsx +51 -0
  181. package/src/ui/misc/PermissionEditor.tsx +379 -0
  182. package/src/ui/misc/ProgressBar.tsx +47 -0
  183. package/src/ui/misc/RemoveBtn.tsx +27 -0
  184. package/src/ui/misc/StatCell.tsx +90 -0
  185. package/src/ui/misc/index.ts +18 -0
  186. package/src/ui/navigation.ts +32 -0
  187. package/src/ui/prompts.tsx +854 -0
  188. package/src/ui/sidebar.tsx +468 -0
  189. package/src/ui/widgets/Widget.tsx +62 -0
  190. package/src/ui/widgets/WidgetCard.tsx +19 -0
  191. package/src/ui/widgets/WidgetHero.tsx +39 -0
  192. package/src/ui/widgets/WidgetList.tsx +84 -0
  193. package/src/ui/widgets/WidgetPills.tsx +68 -0
  194. package/src/ui/widgets/WidgetStat.tsx +67 -0
  195. package/src/ui/widgets/WidgetStatus.tsx +62 -0
  196. 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
+ }