@valentinkolb/cloud 0.4.0 → 0.5.1

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 (194) hide show
  1. package/package.json +18 -6
  2. package/scripts/preload.ts +78 -23
  3. package/src/_internal/define-app.ts +53 -46
  4. package/src/api/accounts-entities.ts +4 -0
  5. package/src/api/admin-core-settings.ts +98 -0
  6. package/src/api/announcements.ts +131 -0
  7. package/src/api/auth/schemas.ts +24 -0
  8. package/src/api/auth.ts +116 -13
  9. package/src/api/index.ts +7 -2
  10. package/src/api/me.ts +203 -14
  11. package/src/api/search/schemas.ts +1 -0
  12. package/src/api/search.ts +62 -8
  13. package/src/config/ssr.ts +2 -9
  14. package/src/contracts/announcements.test.ts +37 -0
  15. package/src/contracts/announcements.ts +121 -0
  16. package/src/contracts/app.ts +2 -0
  17. package/src/contracts/index.ts +3 -2
  18. package/src/contracts/registry.ts +2 -0
  19. package/src/contracts/shared.ts +108 -1
  20. package/src/desktop/index.ts +704 -0
  21. package/src/desktop/solid.tsx +938 -0
  22. package/src/server/api/index.ts +1 -1
  23. package/src/server/api/respond.ts +50 -10
  24. package/src/server/index.ts +44 -38
  25. package/src/server/middleware/auth.ts +98 -9
  26. package/src/server/middleware/index.ts +2 -1
  27. package/src/server/middleware/settings.ts +26 -0
  28. package/src/server/services/access.test.ts +197 -0
  29. package/src/server/services/access.ts +254 -6
  30. package/src/server/services/index.ts +14 -11
  31. package/src/server/services/pagination.ts +22 -0
  32. package/src/server/time.ts +45 -0
  33. package/src/services/account-lifecycle/index.ts +142 -18
  34. package/src/services/accounts/app.ts +658 -170
  35. package/src/services/accounts/authz.test.ts +77 -0
  36. package/src/services/accounts/authz.ts +22 -0
  37. package/src/services/accounts/entities.ts +84 -5
  38. package/src/services/accounts/groups.ts +30 -24
  39. package/src/services/accounts/model.test.ts +30 -0
  40. package/src/services/accounts/switching.test.ts +14 -0
  41. package/src/services/accounts/switching.ts +15 -6
  42. package/src/services/accounts/users.ts +75 -52
  43. package/src/services/announcements/index.test.ts +32 -0
  44. package/src/services/announcements/index.ts +224 -0
  45. package/src/services/audit/index.test.ts +84 -0
  46. package/src/services/audit/index.ts +431 -0
  47. package/src/services/auth-flows/index.ts +9 -2
  48. package/src/services/auth-flows/ipa.ts +47 -7
  49. package/src/services/auth-flows/magic-link.ts +92 -20
  50. package/src/services/auth-flows/password-reset.ts +284 -0
  51. package/src/services/auth-flows/proxy-return.test.ts +24 -0
  52. package/src/services/auth-flows/proxy-return.ts +49 -0
  53. package/src/services/gateway.ts +162 -0
  54. package/src/services/index.ts +44 -2
  55. package/src/services/ipa/effective-groups.test.ts +33 -0
  56. package/src/services/ipa/effective-groups.ts +70 -0
  57. package/src/services/ipa/profile.ts +45 -3
  58. package/src/services/ipa/search.ts +3 -5
  59. package/src/services/ipa/service-account.ts +15 -0
  60. package/src/services/ipa/sync-planning.test.ts +32 -0
  61. package/src/services/ipa/sync-planning.ts +22 -0
  62. package/src/services/ipa/sync.ts +110 -38
  63. package/src/services/notifications/index.ts +82 -11
  64. package/src/services/oauth-tokens.ts +104 -0
  65. package/src/services/postgres.ts +21 -6
  66. package/src/services/providers/local/auth.test.ts +22 -0
  67. package/src/services/providers/local/auth.ts +46 -3
  68. package/src/services/secrets.ts +10 -0
  69. package/src/services/service-account-credentials.test.ts +210 -0
  70. package/src/services/service-account-credentials.ts +715 -0
  71. package/src/services/service-accounts.ts +188 -0
  72. package/src/services/session/index.ts +7 -8
  73. package/src/services/settings/app.ts +4 -20
  74. package/src/services/settings/defaults.ts +79 -22
  75. package/src/services/settings/store.ts +47 -0
  76. package/src/services/weather/forecast.ts +40 -7
  77. package/src/services/webauthn.test.ts +36 -0
  78. package/src/services/webauthn.ts +384 -0
  79. package/src/shared/icons.ts +391 -100
  80. package/src/shared/index.ts +7 -0
  81. package/src/shared/markdown/extensions/code.ts +38 -1
  82. package/src/shared/markdown/extensions/images.ts +39 -3
  83. package/src/shared/markdown/extensions/info-blocks.ts +5 -5
  84. package/src/shared/markdown/extensions/mark.ts +48 -0
  85. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  86. package/src/shared/markdown/extensions/tables.ts +79 -58
  87. package/src/shared/markdown/formula.test.ts +1089 -0
  88. package/src/shared/markdown/formula.ts +1187 -0
  89. package/src/shared/markdown/index.ts +76 -2
  90. package/src/shared/mock-cover.ts +130 -0
  91. package/src/shared/redirect.test.ts +58 -0
  92. package/src/shared/redirect.ts +56 -0
  93. package/src/shared/theme.test.ts +24 -0
  94. package/src/shared/theme.ts +68 -0
  95. package/src/shared/time.ts +13 -0
  96. package/src/ssr/AdminLayout.tsx +7 -3
  97. package/src/ssr/AdminSidebar.tsx +115 -49
  98. package/src/ssr/AppLaunchpad.island.tsx +176 -0
  99. package/src/ssr/Footer.island.tsx +3 -8
  100. package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
  101. package/src/ssr/GlobalSearchDialog.tsx +545 -117
  102. package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
  103. package/src/ssr/Layout.tsx +74 -66
  104. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  105. package/src/ssr/LayoutHelp.tsx +266 -0
  106. package/src/ssr/NavMenu.island.tsx +0 -39
  107. package/src/ssr/ThemeToggleRail.island.tsx +3 -3
  108. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  109. package/src/ssr/islands/index.ts +13 -0
  110. package/src/styles/base-popover.css +5 -2
  111. package/src/styles/effects.css +87 -6
  112. package/src/styles/global.css +146 -9
  113. package/src/styles/input.css +3 -1
  114. package/src/styles/utilities-buttons.css +133 -27
  115. package/src/styles/utilities-code-display.css +67 -0
  116. package/src/styles/utilities-completion.css +223 -0
  117. package/src/styles/utilities-detail.css +73 -0
  118. package/src/styles/utilities-feedback.css +16 -15
  119. package/src/styles/utilities-layout.css +42 -2
  120. package/src/styles/utilities-markdown-editor.css +472 -0
  121. package/src/styles/utilities-navigation.css +63 -8
  122. package/src/styles/utilities-script.css +84 -0
  123. package/src/styles/utilities-table-tile.css +229 -0
  124. package/src/types/ambient.d.ts +9 -0
  125. package/src/ui/completion/behaviors.test.ts +95 -0
  126. package/src/ui/completion/behaviors.ts +205 -0
  127. package/src/ui/completion/engine.ts +368 -0
  128. package/src/ui/completion/index.ts +40 -0
  129. package/src/ui/completion/overlay.ts +92 -0
  130. package/src/ui/dialog-core.ts +173 -45
  131. package/src/ui/filter/FilterChip.tsx +42 -40
  132. package/src/ui/index.ts +11 -12
  133. package/src/ui/input/AutocompleteEditor.tsx +656 -0
  134. package/src/ui/input/CheckboxCard.tsx +91 -0
  135. package/src/ui/input/Combobox.tsx +375 -0
  136. package/src/ui/input/DatePicker.tsx +846 -0
  137. package/src/ui/input/DateTimeInput.tsx +29 -4
  138. package/src/ui/input/FileDropzone.tsx +116 -0
  139. package/src/ui/input/IconInput.tsx +116 -0
  140. package/src/ui/input/ImageInput.tsx +19 -2
  141. package/src/ui/input/MultiSelectInput.tsx +448 -0
  142. package/src/ui/input/NumberInput.tsx +417 -61
  143. package/src/ui/input/SegmentedControl.tsx +2 -2
  144. package/src/ui/input/Select.tsx +172 -10
  145. package/src/ui/input/Slider.tsx +3 -4
  146. package/src/ui/input/Switch.tsx +3 -2
  147. package/src/ui/input/TemplateEditor.tsx +212 -0
  148. package/src/ui/input/TextInput.tsx +144 -13
  149. package/src/ui/input/index.ts +53 -8
  150. package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
  151. package/src/ui/input/markdown/Toolbar.tsx +90 -0
  152. package/src/ui/input/markdown/actions.ts +233 -0
  153. package/src/ui/input/markdown/active-formats.ts +94 -0
  154. package/src/ui/input/markdown/behaviors.ts +193 -0
  155. package/src/ui/input/markdown/code-zone.ts +23 -0
  156. package/src/ui/input/markdown/highlight.ts +316 -0
  157. package/src/ui/layout.ts +22 -0
  158. package/src/ui/misc/AppOverview.tsx +105 -0
  159. package/src/ui/misc/AppWorkspace.tsx +607 -0
  160. package/src/ui/misc/Calendar.tsx +1291 -0
  161. package/src/ui/misc/Chart.tsx +162 -0
  162. package/src/ui/misc/CodeDisplay.tsx +54 -0
  163. package/src/ui/misc/ContextMenu.tsx +2 -2
  164. package/src/ui/misc/DataTable.tsx +269 -0
  165. package/src/ui/misc/DockWorkspace.tsx +425 -0
  166. package/src/ui/misc/Docs.tsx +153 -0
  167. package/src/ui/misc/Dropdown.tsx +2 -2
  168. package/src/ui/misc/EntitySearch.tsx +260 -129
  169. package/src/ui/misc/LinkCard.tsx +14 -2
  170. package/src/ui/misc/LogEntriesTable.tsx +34 -31
  171. package/src/ui/misc/Pagination.tsx +31 -12
  172. package/src/ui/misc/PanelDialog.tsx +109 -0
  173. package/src/ui/misc/Panes.tsx +873 -0
  174. package/src/ui/misc/PermissionEditor.tsx +358 -262
  175. package/src/ui/misc/Placeholder.tsx +40 -0
  176. package/src/ui/misc/ProgressBar.tsx +1 -1
  177. package/src/ui/misc/ResourceApiKeys.tsx +260 -0
  178. package/src/ui/misc/SettingsModal.tsx +150 -0
  179. package/src/ui/misc/StatCell.tsx +182 -40
  180. package/src/ui/misc/StatGrid.tsx +149 -0
  181. package/src/ui/misc/StructuredDataPreview.tsx +107 -0
  182. package/src/ui/misc/code-highlight.ts +213 -0
  183. package/src/ui/misc/index.ts +93 -12
  184. package/src/ui/prompts.tsx +362 -312
  185. package/src/ui/toast.ts +384 -0
  186. package/src/ui/widgets/Widget.tsx +12 -4
  187. package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
  188. package/src/ui/ipa/GroupView.tsx +0 -36
  189. package/src/ui/ipa/LoginBtn.tsx +0 -16
  190. package/src/ui/ipa/UserView.tsx +0 -58
  191. package/src/ui/ipa/index.ts +0 -4
  192. package/src/ui/navigation.ts +0 -32
  193. package/src/ui/sidebar.tsx +0 -468
  194. /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
@@ -19,6 +19,9 @@
19
19
  @apply rounded-lg transition-colors duration-150;
20
20
  @apply bg-white border border-zinc-100;
21
21
  @apply dark:bg-zinc-900 dark:border-zinc-800;
22
+ /* Gentle inset bevel (clip-safe). Flows through the token so the Layout
23
+ header's inline `box-shadow: var(--theme-shadow-elevated)` stays consistent. */
24
+ box-shadow: var(--theme-shadow-elevated);
22
25
  }
23
26
 
24
27
  @utility dialog-panel {
@@ -27,11 +30,19 @@
27
30
  @apply backdrop:bg-black/40;
28
31
  @apply rounded-lg bg-white dark:bg-zinc-900;
29
32
  @apply border border-zinc-200 dark:border-zinc-800;
30
- @apply shadow-lg;
33
+ /* Floating dialog (portaled) — outer shadow is fine here. */
34
+ box-shadow: var(--theme-shadow-float);
31
35
  }
32
36
 
33
37
  @utility paper-highlighted {
34
- @apply paper border-zinc-300 dark:border-zinc-700;
38
+ @apply paper;
39
+ background: linear-gradient(90deg, rgb(239 246 255 / 0.34), rgb(255 255 255 / 0.82)), var(--color-white);
40
+ }
41
+
42
+ .dark .paper-highlighted,
43
+ .dark .hover\:paper-highlighted:hover {
44
+ background: rgb(23 37 84 / 0.2);
45
+ box-shadow: var(--theme-shadow-elevated);
35
46
  }
36
47
 
37
48
  @utility no-scrollbar {
@@ -43,6 +54,35 @@
43
54
  scrollbar-width: none;
44
55
  }
45
56
 
57
+ .panes-tab-strip {
58
+ scrollbar-width: thin;
59
+ scrollbar-color: transparent transparent;
60
+ }
61
+
62
+ .panes-tab-strip::-webkit-scrollbar-thumb {
63
+ background: transparent;
64
+ }
65
+
66
+ .panes-tab-strip:hover,
67
+ .panes-tab-strip:focus-within {
68
+ scrollbar-color: rgb(161 161 170 / 0.45) transparent;
69
+ }
70
+
71
+ .panes-tab-strip:hover::-webkit-scrollbar-thumb,
72
+ .panes-tab-strip:focus-within::-webkit-scrollbar-thumb {
73
+ background: rgb(161 161 170 / 0.45);
74
+ }
75
+
76
+ .dark .panes-tab-strip:hover,
77
+ .dark .panes-tab-strip:focus-within {
78
+ scrollbar-color: rgb(82 82 91 / 0.6) transparent;
79
+ }
80
+
81
+ .dark .panes-tab-strip:hover::-webkit-scrollbar-thumb,
82
+ .dark .panes-tab-strip:focus-within::-webkit-scrollbar-thumb {
83
+ background: rgb(82 82 91 / 0.6);
84
+ }
85
+
46
86
  @utility ellipsis {
47
87
  @apply overflow-hidden text-ellipsis whitespace-nowrap;
48
88
  }
@@ -0,0 +1,472 @@
1
+ /* Markdown editor — overtype-style layered textarea + preview.
2
+
3
+ The textarea has `color: transparent` so its glyphs are invisible; only
4
+ the cursor (caret-color) and selection show through. The preview div
5
+ beneath renders the same text with markdown syntax highlighted. Identical
6
+ font / padding / whitespace / wrapping on both layers is REQUIRED for
7
+ the cursor to line up exactly with the visible glyphs in the preview.
8
+
9
+ All glyph-affecting properties on `.md-editor-layer` use `!important` to
10
+ defeat any cascading style that would shift the layers relative to each
11
+ other. No font-size, font-weight, or font-style is ever varied between
12
+ the textarea and inner preview spans — bold uses a text-shadow trick,
13
+ italic uses colour. Any real weight/style change risks glyph-width
14
+ drift even on nominally monospace fonts. */
15
+
16
+ .md-editor {
17
+ position: relative;
18
+ @apply rounded-md border;
19
+ @apply bg-zinc-100 border-zinc-100;
20
+ @apply hover:bg-zinc-200/70 hover:border-zinc-200/70;
21
+ @apply dark:bg-zinc-800 dark:border-zinc-800;
22
+ @apply dark:hover:bg-zinc-700 dark:hover:border-zinc-700;
23
+ @apply transition-colors;
24
+ @apply overflow-hidden;
25
+ /* Recessed well — matches the `input` utility (default variant is field-shaped).
26
+ The `paper` variant below overrides this with a card bevel instead. */
27
+ box-shadow: var(--theme-recess);
28
+ }
29
+
30
+ .md-editor:focus-within {
31
+ @apply bg-white border-blue-400;
32
+ @apply dark:bg-zinc-900 dark:border-blue-400;
33
+ }
34
+
35
+ .md-editor[data-variant="paper"] {
36
+ @apply rounded-lg;
37
+ @apply bg-white border-zinc-100;
38
+ @apply hover:bg-white hover:border-zinc-100;
39
+ @apply dark:bg-zinc-900 dark:border-zinc-800;
40
+ @apply dark:hover:bg-zinc-900 dark:hover:border-zinc-800;
41
+ /* Paper variant = a document surface, so it gets the card bevel (raised),
42
+ not the recess of the field variant. */
43
+ box-shadow: var(--theme-shadow-elevated);
44
+ }
45
+
46
+ .md-editor[data-variant="paper"]:focus-within {
47
+ @apply bg-white border-blue-400;
48
+ @apply dark:bg-zinc-900 dark:border-blue-400;
49
+ }
50
+
51
+ .md-editor[data-variant="paper"] .md-editor-toolbar {
52
+ @apply bg-white border-zinc-100;
53
+ @apply dark:bg-zinc-900 dark:border-zinc-800;
54
+ }
55
+
56
+ .md-editor[data-variant="paper"] .md-editor-tool {
57
+ @apply hover:bg-zinc-100;
58
+ @apply dark:hover:bg-zinc-800;
59
+ }
60
+
61
+ .md-editor[data-disabled="true"] {
62
+ @apply opacity-50 cursor-not-allowed;
63
+ }
64
+
65
+ /* Error state — red border, both unfocused and focused, mirroring how
66
+ the standard `.input` utility would visually signal an error. The
67
+ error MESSAGE itself is rendered separately by `InputWrapper` below
68
+ the editor. */
69
+ .md-editor[data-error="true"],
70
+ .md-editor[data-error="true"]:focus-within {
71
+ @apply border-red-400 dark:border-red-500;
72
+ }
73
+
74
+ /* Toolbar bar above the editor surface */
75
+ .md-editor-toolbar {
76
+ @apply flex items-center gap-0.5 flex-wrap;
77
+ @apply px-1.5 py-1;
78
+ @apply border-b border-zinc-200 dark:border-zinc-700;
79
+ }
80
+
81
+ .md-editor:focus-within .md-editor-toolbar {
82
+ @apply border-zinc-200 dark:border-zinc-700;
83
+ }
84
+
85
+ .md-editor-tool {
86
+ @apply inline-flex items-center justify-center;
87
+ @apply w-7 h-7 rounded;
88
+ @apply text-zinc-500 dark:text-zinc-400;
89
+ @apply hover:bg-zinc-200 hover:text-zinc-800;
90
+ @apply dark:hover:bg-zinc-700 dark:hover:text-zinc-100;
91
+ @apply transition-colors;
92
+ font-size: 0.95rem;
93
+ }
94
+
95
+ /* Active formatting at the caret — blue tint + faint blue background
96
+ so the active state is unmistakable at a glance, without becoming
97
+ visually loud. Hover on an active button keeps the blue surface
98
+ tone (not the zinc one) so the colour doesn't flicker. */
99
+ .md-editor-tool[aria-pressed="true"] {
100
+ @apply text-blue-600 bg-blue-50;
101
+ @apply dark:text-blue-400 dark:bg-blue-950/40;
102
+ }
103
+
104
+ .md-editor-tool[aria-pressed="true"]:hover {
105
+ @apply bg-blue-100 text-blue-700;
106
+ @apply dark:bg-blue-900/40 dark:text-blue-300;
107
+ }
108
+
109
+ .md-editor-tool-sep {
110
+ @apply w-px h-4 bg-zinc-300 dark:bg-zinc-600 mx-1;
111
+ }
112
+
113
+ /* The two stacked layers. The wrapper has explicit height (set inline
114
+ via style="--md-h: ...em") so both layers can size to it via 100%. */
115
+ .md-editor-surface {
116
+ position: relative;
117
+ height: var(--md-h, 8rem);
118
+ }
119
+
120
+ .md-editor-layer {
121
+ /* These properties MUST be identical on textarea and preview.
122
+ `!important` because user-agent textarea defaults + any stray rule
123
+ could otherwise shift one layer relative to the other.
124
+
125
+ Bold + italic are NOT done via font-weight / font-style on this
126
+ layer — see the rules further down. We rely on text-shadow for
127
+ fake-bold and colour-only for italic so the glyph advance widths
128
+ stay identical between the textarea and the preview. */
129
+ position: absolute !important;
130
+ inset: 0 !important;
131
+ display: block !important;
132
+
133
+ /* Typography — identical glyph metrics on both layers.
134
+ Every property here is locked down so the textarea and preview
135
+ can never disagree on glyph advance widths. The four critical
136
+ subtleties:
137
+ 1. `font-synthesis: none` — only-loaded weights (400/500/600
138
+ for IBM Plex Mono) get used. Asking for 700 would synthesise
139
+ a faux-bold, and synthesised bold does NOT preserve mono
140
+ glyph widths.
141
+ 2. `font-variant-ligatures: none` + `font-feature-settings`
142
+ disable ligatures (`==` → `≡` etc.) and contextual
143
+ alternates. Both reshape glyph runs and break alignment.
144
+ 3. `letter-spacing: 0` (NOT `normal`) — `normal` lets the font
145
+ decide and some shells inject a fractional value.
146
+ 4. `text-rendering: geometricPrecision` — disables kerning so
147
+ even non-monospace fallback fonts don't shift glyph X
148
+ positions across layers. */
149
+ /* IBM Plex Mono is shipped via @fontsource (regular + italic + 500
150
+ + 600 weights — see global.css). Pinning the font here means the
151
+ editor looks identical across OSes and we control the italic
152
+ variant precisely. The bold/heading "weight" is faked via
153
+ text-shadow further down so we never actually request a weight
154
+ the loaded font lacks. The trailing `monospace` is a last-ditch
155
+ fallback if Plex Mono ever fails to load. */
156
+ font-family: "IBM Plex Mono", monospace !important;
157
+ font-size: 0.875rem !important; /* 14px */
158
+ line-height: 1.55 !important;
159
+ font-weight: 400 !important;
160
+ font-style: normal !important;
161
+ font-variant: normal !important;
162
+ font-variant-ligatures: none !important;
163
+ font-variant-numeric: tabular-nums !important;
164
+ font-variant-position: normal !important;
165
+ font-stretch: normal !important;
166
+ font-kerning: none !important;
167
+ font-synthesis: none !important;
168
+ -webkit-font-synthesis: none !important;
169
+ font-feature-settings:
170
+ "liga" 0,
171
+ "clig" 0,
172
+ "calt" 0 !important;
173
+ font-variation-settings: normal !important;
174
+ font-optical-sizing: none !important;
175
+ letter-spacing: 0 !important;
176
+ word-spacing: 0 !important;
177
+ text-align: left !important;
178
+ text-indent: 0 !important;
179
+ text-transform: none !important;
180
+ text-rendering: geometricPrecision !important;
181
+
182
+ /* Box model */
183
+ padding: 0.625rem 0.75rem !important;
184
+ margin: 0 !important;
185
+ border: 0 !important;
186
+ outline: 0 !important;
187
+ box-sizing: border-box !important;
188
+
189
+ /* Whitespace + wrapping — must agree on every char position */
190
+ white-space: pre-wrap !important;
191
+ word-wrap: break-word !important;
192
+ word-break: normal !important;
193
+ overflow-wrap: break-word !important;
194
+ tab-size: 2 !important;
195
+ -moz-tab-size: 2 !important;
196
+
197
+ /* Sizing */
198
+ width: 100% !important;
199
+ height: 100% !important;
200
+ min-height: 0 !important;
201
+ max-height: none !important;
202
+ min-width: 0 !important;
203
+ max-width: none !important;
204
+
205
+ /* Scrolling — both layers must reserve scrollbar space identically
206
+ or the content widths diverge and long lines wrap at different
207
+ columns. We force `overflow-y: scroll` on both so the gutter is
208
+ always present. The textarea shows its native scrollbar; the
209
+ preview's scrollbar is rendered but painted transparent (see
210
+ `.md-editor-preview` block) — the gutter stays, the visual goes. */
211
+ overflow-y: scroll !important;
212
+ overflow-x: hidden !important;
213
+ scrollbar-width: thin !important;
214
+
215
+ /* Background neutralised — wrapper owns the surface color */
216
+ background: transparent !important;
217
+
218
+ /* No animations that could shift glyph position */
219
+ animation: none !important;
220
+ transition: none !important;
221
+ }
222
+
223
+ .md-editor-input {
224
+ z-index: 2 !important;
225
+ resize: none !important;
226
+ appearance: none !important;
227
+ -webkit-appearance: none !important;
228
+ -moz-appearance: none !important;
229
+ /* Hide own glyphs, show only cursor.
230
+ `caret-color` must be an explicit colour, NOT `currentColor` — the
231
+ textarea's own `color` is `transparent`, so `currentColor` would
232
+ resolve to transparent and the cursor would vanish. We mirror the
233
+ preview's prose colour (zinc-900 light / zinc-100 dark). */
234
+ color: transparent !important;
235
+ -webkit-text-fill-color: transparent !important;
236
+ caret-color: #18181b !important; /* zinc-900 */
237
+ /* Prevent mobile double-tap zoom */
238
+ touch-action: manipulation !important;
239
+ }
240
+
241
+ .dark .md-editor-input {
242
+ caret-color: #f4f4f5 !important; /* zinc-100 */
243
+ }
244
+
245
+ /* Selection background visible even with transparent glyphs */
246
+ .md-editor-input::selection {
247
+ background: rgb(59 130 246 / 0.35);
248
+ color: transparent;
249
+ }
250
+
251
+ .md-editor-preview {
252
+ z-index: 1 !important;
253
+ /* Click + selection happen on the textarea, not here */
254
+ pointer-events: none !important;
255
+ user-select: none !important;
256
+ -webkit-user-select: none !important;
257
+ @apply text-zinc-900 dark:text-zinc-100;
258
+ /* Paint the preview's scrollbar transparent so only the textarea's
259
+ scrollbar (sitting on top, z-index: 2) is visible. We CAN'T set
260
+ `scrollbar-width: none` here — that would also remove the gutter
261
+ and the preview's content width would no longer match the
262
+ textarea's, breaking wrap alignment. The transparent-colour trick
263
+ preserves the gutter while hiding the visual. */
264
+ scrollbar-color: transparent transparent !important;
265
+ }
266
+
267
+ .md-editor-preview::-webkit-scrollbar-thumb,
268
+ .md-editor-preview::-webkit-scrollbar-track,
269
+ .md-editor-preview::-webkit-scrollbar-corner {
270
+ background: transparent !important;
271
+ border: 0 !important;
272
+ }
273
+
274
+ /* Critical alignment safeguard (overtype lesson, styles.js:256-264):
275
+ force EVERY descendant inside the preview to inherit font-family,
276
+ font-size, and line-height from the preview itself. Without this,
277
+ external CSS (Tailwind base, @tailwindcss/typography, etc.) can
278
+ set element-specific font-size on, say, `<span>` or implicit
279
+ `<h2>` elements. Any such per-element metric difference inflates
280
+ the line-box height and drifts the overlay relative to the
281
+ textarea — visible as a "cursor far to the right of the visible
282
+ text" symptom. Also reset margin/padding/border so no inner
283
+ element can add box-model space the textarea doesn't have. */
284
+ .md-editor-preview *,
285
+ .md-editor-preview *::before,
286
+ .md-editor-preview *::after {
287
+ font-family: inherit !important;
288
+ font-size: inherit !important;
289
+ line-height: inherit !important;
290
+ font-stretch: inherit !important;
291
+ letter-spacing: inherit !important;
292
+ word-spacing: inherit !important;
293
+ margin: 0 !important;
294
+ padding: 0 !important;
295
+ border: 0 !important;
296
+ text-indent: 0 !important;
297
+ vertical-align: baseline !important;
298
+ display: inline !important;
299
+ }
300
+
301
+ /* Placeholder shim — shown when the textarea is empty. MUST use the
302
+ exact same typography stack as `.md-editor-layer` so the cursor
303
+ position doesn't visibly jump when the first character is typed
304
+ and the placeholder fades out. */
305
+ .md-editor-placeholder {
306
+ position: absolute !important;
307
+ inset: 0 !important;
308
+ padding: 0.625rem 0.75rem !important;
309
+ font-family: "IBM Plex Mono", monospace !important;
310
+ font-size: 0.875rem !important;
311
+ line-height: 1.55 !important;
312
+ font-weight: 400 !important;
313
+ letter-spacing: 0 !important;
314
+ word-spacing: 0 !important;
315
+ pointer-events: none !important;
316
+ user-select: none !important;
317
+ z-index: 0 !important;
318
+ @apply text-zinc-400 dark:text-zinc-500;
319
+ white-space: pre-wrap !important;
320
+ word-wrap: break-word !important;
321
+ word-break: normal !important;
322
+ overflow-wrap: break-word !important;
323
+ }
324
+
325
+ /* Syntax highlighting classes — applied INSIDE the preview div by the
326
+ highlighter. Each rule must preserve glyph width (monospace font does
327
+ the heavy lifting; we only change weight, color, background). */
328
+
329
+ /* The visible markdown syntax characters (**, #, -, >, etc.) — kept
330
+ on-screen but dimmed so prose stands out. */
331
+ .md-syntax,
332
+ .md-marker {
333
+ @apply text-zinc-400 dark:text-zinc-500;
334
+ }
335
+
336
+ /* Bold + headers via `text-shadow` instead of `font-weight`.
337
+
338
+ Why: any font-weight change risks glyph-advance-width drift between
339
+ the textarea (locked at weight 400) and the preview. Even nominal
340
+ monospace fonts can have subtle weight-dependent width changes,
341
+ especially in browser rasterisers. `text-shadow: 0.5px 0 0
342
+ currentColor` paints a copy of each glyph 0.5px to the right —
343
+ visually equivalent to a heavier stroke, but the underlying glyph
344
+ advance is UNCHANGED. The cursor in the textarea (running at
345
+ weight 400) lines up perfectly with the visibly "bold" glyphs in
346
+ the preview because they ARE rendered at weight 400 underneath.
347
+
348
+ Italics: same logic — `font-style: italic` swaps in a different
349
+ font face whose glyph metrics may not perfectly match upright. We
350
+ simulate italic via colour + a transparent overlay we can't get
351
+ in CSS, so for italic we accept a colour-only differentiation. */
352
+ .md-h1,
353
+ .md-h2,
354
+ .md-h3,
355
+ .md-bold {
356
+ text-shadow: 0.5px 0 0 currentColor;
357
+ }
358
+
359
+ /* Slight tonal ladder so the heading levels are still distinguishable
360
+ at a glance, even though they share the same weight + size. */
361
+ .md-h1 {
362
+ @apply text-zinc-950 dark:text-white;
363
+ }
364
+
365
+ .md-h2 {
366
+ @apply text-zinc-900 dark:text-zinc-50;
367
+ }
368
+
369
+ .md-h3 {
370
+ @apply text-zinc-800 dark:text-zinc-200;
371
+ }
372
+
373
+ /* Italic: real `font-style: italic`. IBM Plex Mono Italic 400 is
374
+ loaded as a separate face (see global.css) and is designed as
375
+ monospace — its glyph advance widths match the regular face. The
376
+ `!important` defeats the layer-level `font-style: normal !important`
377
+ so the .md-italic span actually picks up italic. */
378
+ .md-italic {
379
+ font-style: italic !important;
380
+ }
381
+
382
+ /* Inline code — background only, no width change.
383
+ Negative padding would break alignment; we just paint behind the
384
+ glyphs and accept that the highlight doesn't have inset. */
385
+ .md-code {
386
+ @apply bg-zinc-200/60 dark:bg-zinc-700/50 rounded-sm;
387
+ }
388
+
389
+ /* Links — color + underline, both width-safe */
390
+ .md-link {
391
+ @apply text-blue-600 dark:text-blue-400 underline;
392
+ text-underline-offset: 2px;
393
+ }
394
+
395
+ /* Blockquote — color + inset box-shadow for the left bar (box-shadow
396
+ doesn't affect layout, unlike border-left or padding-left). The bar
397
+ sits visually outside the text column edge so prose stays aligned. */
398
+ .md-quote {
399
+ @apply text-zinc-600 dark:text-zinc-400;
400
+ box-shadow: inset 3px 0 0 var(--color-zinc-300);
401
+ }
402
+
403
+ .dark .md-quote {
404
+ box-shadow: inset 3px 0 0 var(--color-zinc-600);
405
+ }
406
+
407
+ /* Fenced code lines — background only */
408
+ .md-code-block {
409
+ @apply bg-zinc-200/40 dark:bg-zinc-800;
410
+ }
411
+
412
+ .md-code-block pre,
413
+ .md-script-source,
414
+ .md-mermaid-block,
415
+ .md-katex-block {
416
+ border: 1px solid rgb(228 228 231 / 0.72) !important;
417
+ border-radius: 0.42rem !important;
418
+ background: color-mix(in oklab, white 92%, rgb(244 244 245)) !important;
419
+ box-shadow:
420
+ inset 0 1px 0 rgb(255 255 255 / 0.46),
421
+ inset 0 -1px 0 rgb(0 0 0 / 0.025);
422
+ }
423
+
424
+ .md-katex-block {
425
+ padding: 0.75rem;
426
+ }
427
+
428
+ .dark .md-code-block pre,
429
+ .dark .md-script-source,
430
+ .dark .md-mermaid-block,
431
+ .dark .md-katex-block {
432
+ border-color: rgb(63 63 70 / 0.72) !important;
433
+ background: color-mix(in oklab, rgb(24 24 27) 90%, rgb(39 39 42)) !important;
434
+ box-shadow:
435
+ inset 0 1px 0 rgb(255 255 255 / 0.045),
436
+ inset 0 -1px 0 rgb(0 0 0 / 0.28);
437
+ }
438
+
439
+ /* Horizontal rule (---) — just dim the marker; can't draw an actual
440
+ rule line without inserting layout-shifting elements. */
441
+ .md-hr {
442
+ @apply text-zinc-400 dark:text-zinc-500;
443
+ }
444
+
445
+ /* Completion match — every occurrence of a registered completion
446
+ label (abbreviation or trigger result) is tinted blue so the
447
+ reader can see what's a recognised mention vs. plain prose.
448
+ No underline: a dotted underline reads too much like a spellcheck
449
+ squiggle and trips people into thinking something's broken. */
450
+ .md-completion-match {
451
+ @apply text-blue-600 dark:text-blue-400;
452
+ }
453
+
454
+ /* Stats footer — lines / words / chars, anchored at the bottom-left
455
+ of the editor. No border, no background tint — it inherits the
456
+ editor body colour and sits as a quiet meta-row separated from the
457
+ content only by its dim mono text and a hair of padding.
458
+ When the editor is empty we keep the element rendered (so the
459
+ editor's outer height stays constant — no layout shift across all
460
+ sibling fields when the user starts / clears typing) but flip
461
+ `visibility: hidden` to remove the stats from sight. */
462
+ .md-editor-stats {
463
+ @apply flex justify-start items-center gap-3;
464
+ @apply px-3 py-1;
465
+ @apply text-[10px] text-zinc-400 dark:text-zinc-500;
466
+ @apply font-mono select-none;
467
+ letter-spacing: 0.02em;
468
+ }
469
+
470
+ .md-editor-stats[data-empty="true"] {
471
+ visibility: hidden;
472
+ }
@@ -19,6 +19,8 @@
19
19
  @utility sidebar {
20
20
  @apply flex h-full min-h-0 flex-col gap-3 rounded-lg border border-zinc-200 bg-white;
21
21
  @apply dark:border-zinc-800 dark:bg-zinc-900;
22
+ /* Same gentle inset bevel as `paper` so the sidebar reads as a dimensional surface. */
23
+ box-shadow: var(--theme-shadow-elevated);
22
24
  }
23
25
 
24
26
  @utility sidebar-container {
@@ -77,6 +79,43 @@
77
79
  @apply mt-auto flex flex-col gap-2;
78
80
  }
79
81
 
82
+ @utility sidebar-icon-grid-wrap {
83
+ @apply flex flex-col gap-1;
84
+ }
85
+
86
+ @utility sidebar-icon-grid {
87
+ @apply grid gap-1.5;
88
+ }
89
+
90
+ @utility sidebar-icon-action {
91
+ @apply flex h-12 w-full min-w-0 items-center justify-center rounded-xl border-0 bg-zinc-100/70 text-zinc-600;
92
+ @apply transition-[background-color,color,transform,box-shadow] duration-150 ease-out hover:bg-zinc-200 hover:text-zinc-900;
93
+ @apply focus-ui;
94
+ @apply active:scale-[0.98];
95
+ @apply dark:bg-zinc-800/65 dark:text-zinc-300 dark:hover:bg-zinc-700 dark:hover:text-zinc-100;
96
+ /* Raised tile (matches the redesign); sinks on press */
97
+ box-shadow: var(--theme-bevel-top), var(--theme-bevel-bottom);
98
+ &:active {
99
+ box-shadow: var(--theme-press);
100
+ }
101
+ }
102
+
103
+ @utility sidebar-icon-action-active {
104
+ @apply bg-blue-50 text-blue-700 hover:bg-blue-100 hover:text-blue-800;
105
+ @apply dark:bg-blue-950/45 dark:text-blue-300 dark:hover:bg-blue-900/45 dark:hover:text-blue-200;
106
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-blue-500) 22%, transparent), var(--theme-bevel-top);
107
+ }
108
+
109
+ @utility sidebar-icon-action-success {
110
+ @apply bg-green-50 text-green-700 hover:bg-green-100 hover:text-green-800;
111
+ @apply dark:bg-green-950/35 dark:text-green-300 dark:hover:bg-green-900/45 dark:hover:text-green-200;
112
+ }
113
+
114
+ @utility sidebar-icon-action-danger {
115
+ @apply bg-red-50 text-red-700 hover:bg-red-100 hover:text-red-800;
116
+ @apply dark:bg-red-950/35 dark:text-red-300 dark:hover:bg-red-900/45 dark:hover:text-red-200;
117
+ }
118
+
80
119
  @utility sidebar-section {
81
120
  @apply flex flex-col gap-1;
82
121
  }
@@ -98,6 +137,7 @@
98
137
  display: flex !important;
99
138
  @apply min-h-8 items-center gap-2 rounded-lg px-2 py-1.5 text-xs leading-none text-dimmed;
100
139
  @apply transition-[background-color,color] duration-150 ease-out hover:text-secondary;
140
+ @apply focus-ui;
101
141
 
102
142
  &:hover {
103
143
  background: var(--theme-list-hover-bg);
@@ -109,10 +149,17 @@
109
149
  }
110
150
 
111
151
  @utility sidebar-item-active {
112
- @apply bg-blue-50 text-blue-700 font-medium;
113
- @apply hover:bg-blue-100 hover:text-blue-700;
114
- @apply dark:bg-blue-950 dark:text-blue-300;
115
- @apply dark:hover:bg-blue-900 dark:hover:text-blue-200;
152
+ position: relative;
153
+ background: var(--theme-list-active-bg);
154
+ @apply text-blue-700 font-medium;
155
+ @apply hover:text-blue-700;
156
+ @apply dark:text-blue-300 dark:hover:text-blue-200;
157
+ box-shadow: var(--theme-shadow-elevated);
158
+
159
+ &::before {
160
+ content: "";
161
+ @apply pointer-events-none absolute left-1 top-1/2 h-3.5 w-0.5 -translate-y-1/2 rounded-full bg-blue-500 dark:bg-blue-400;
162
+ }
116
163
  }
117
164
 
118
165
  @utility sidebar-item-meta {
@@ -155,10 +202,17 @@
155
202
 
156
203
  @utility list-item-active {
157
204
  border-left: var(--theme-list-active-border);
158
- @apply rounded-lg bg-blue-50 text-blue-700 font-medium;
159
- @apply hover:bg-blue-100 hover:text-blue-700;
160
- @apply dark:bg-blue-950 dark:text-blue-300;
161
- @apply dark:hover:bg-blue-900 dark:hover:text-blue-200;
205
+ position: relative;
206
+ background: var(--theme-list-active-bg);
207
+ @apply rounded-lg text-blue-700 font-medium;
208
+ @apply hover:text-blue-700;
209
+ @apply dark:text-blue-300 dark:hover:text-blue-200;
210
+ box-shadow: var(--theme-shadow-elevated);
211
+
212
+ &::before {
213
+ content: "";
214
+ @apply pointer-events-none absolute left-1 top-1/2 h-3.5 w-0.5 -translate-y-1/2 rounded-full bg-blue-500 dark:bg-blue-400;
215
+ }
162
216
  }
163
217
 
164
218
  @utility section-label {
@@ -181,6 +235,7 @@
181
235
  border-right: var(--theme-rail-item-active-border-r);
182
236
  @apply rounded-lg text-blue-600 dark:text-blue-300;
183
237
  @apply bg-blue-50 dark:bg-blue-950;
238
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-blue-500) 22%, transparent), var(--theme-bevel-top);
184
239
  }
185
240
 
186
241
  @utility bottom-bar-item {