Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
{
"name": "@freundebuch/frontend",
"version": "2.73.0",
"private": true,
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"dev": "vite dev --host",
"build": "vite build",
"preview": "vite preview",
"sync": "svelte-kit sync",
"test": "vitest run --passWithNoTests",
"test:unit": "vitest run --reporter=verbose",
"test:e2e": "playwright test",
"test:watch": "vitest",
"type-check": "svelte-check --tsconfig ./tsconfig.json",
"check": "aube run sync && svelte-check --tsconfig ./tsconfig.json"
},
"dependencies": {
"@better-auth/passkey": "1.5.5",
"@codemirror/commands": "6.10.3",
"@codemirror/lang-markdown": "6.5.0",
"@codemirror/language": "6.12.3",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.43.0",
"@lezer/common": "1.5.2",
"@lezer/highlight": "1.2.3",
"@sentry/sveltekit": "10.32.1",
"arktype": "2.1.29",
"better-auth": "1.5.5",
Expand All @@ -26,6 +18,7 @@
"i18next-browser-languagedetector": "8.2.0",
"isomorphic-dompurify": "2.35.0",
"leaflet": "1.9.4",
"markdown-it": "14.2.0",
"svelte-easy-crop": "5.0.0",
"svelte-heros-v2": "3.0.1",
"svelte-i18next": "2.2.2"
Expand All @@ -39,6 +32,7 @@
"@testing-library/svelte": "5.3.0",
"@types/d3": "7.4.3",
"@types/leaflet": "1.9.21",
"@types/markdown-it": "14.1.2",
"autoprefixer": "10.4.23",
"jsdom": "27.3.0",
"postcss": "8.5.6",
Expand All @@ -49,5 +43,20 @@
"typescript": "5.9.3",
"vite": "7.3.0",
"vitest": "4.0.16"
}
},
"scripts": {
"build": "vite build",
"check": "aube run sync && svelte-check --tsconfig ./tsconfig.json",
"dev": "vite dev --host",
"preview": "vite preview",
"sync": "svelte-kit sync",
"test": "vitest run --passWithNoTests",
"test:e2e": "playwright test",
"test:unit": "vitest run --reporter=verbose",
"test:watch": "vitest",
"type-check": "svelte-check --tsconfig ./tsconfig.json"
},
"license": "AGPL-3.0-only",
"private": true,
"type": "module"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DocumentText from 'svelte-heros-v2/DocumentText.svelte';
import MapPin from 'svelte-heros-v2/MapPin.svelte';
import Users from 'svelte-heros-v2/Users.svelte';
import { goto } from '$app/navigation';
import MarkdownView from '$lib/editor/markdown-view.svelte';
import { createI18n } from '$lib/i18n/index.js';
import { encounters } from '$lib/stores/encounters';
import type { Encounter } from '$shared';
Expand Down Expand Up @@ -134,8 +135,8 @@ async function handleDelete() {
<DocumentText class="w-5 h-5" strokeWidth="2" />
{$i18n.t('encounters.detail.notes')}
</h2>
<div class="p-3 bg-gray-50 rounded-lg font-body text-gray-700 whitespace-pre-wrap">
{encounter.description}
<div class="p-3 bg-gray-50 rounded-lg">
<MarkdownView source={encounter.description} />
</div>
</section>
{/if}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { autoFocus } from '$lib/actions/auto-focus';
import MarkdownEditor from '$lib/editor/markdown-editor.svelte';
import { createI18n } from '$lib/i18n/index.js';
import { encounters } from '$lib/stores/encounters';
import {
Expand Down Expand Up @@ -215,17 +216,18 @@ function handleCancel() {

<!-- Description -->
<div>
<label for="description" class="block text-sm font-body font-medium text-gray-700 mb-1">
<!-- Field label, associated to the editor via aria-labelledby. The editor
box is large and click/tab focusable, so no label click-to-focus
handler (which would need a role + a11y suppression on a static span). -->
<span id="notes-label" class="block text-sm font-body font-medium text-gray-700 mb-1">
{$i18n.t('encounters.form.notesLabel')} <span class="text-gray-400">{$i18n.t('encounters.form.optional')}</span>
</label>
<textarea
id="description"
</span>
<MarkdownEditor
bind:value={description}
rows="3"
labelledBy="notes-label"
placeholder={$i18n.t('encounters.form.notesPlaceholder')}
Comment on lines +222 to 228
disabled={isSubmitting}
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-forest focus:border-transparent font-body text-sm resize-none disabled:opacity-50"
></textarea>
/>
</div>

<!-- Form Actions -->
Expand Down
21 changes: 21 additions & 0 deletions apps/frontend/src/lib/editor/inline-preview/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Kenny Bergquist

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
207 changes: 207 additions & 0 deletions apps/frontend/src/lib/editor/inline-preview/atomic-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* Vendored from atomic-editor — https://github.com/kenforthewin/atomic-editor
* Pinned at commit 26f75e76491e3f9dd698ea91ab91efd5a5e84080 (MIT, see ./LICENSE).
* Local modifications are marked with `FB:` comments. See ./vendored.md.
*/
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { EditorView } from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import type { Extension } from '@codemirror/state';

// Package CSS custom properties. Every value below falls back to a
// dark-neutral default; consumers override by setting the prefixed vars
// (`--atomic-editor-*`) at any ancestor of the editor. The defaults are
// deliberately unscoped so the package is usable standalone without
// forcing the consumer to theme it first.

export const atomicEditorTheme: Extension = EditorView.theme(
{
'&': {
color: 'var(--atomic-editor-fg, #dcddde)',
backgroundColor: 'transparent',
fontFamily: 'var(--atomic-editor-font, system-ui, -apple-system, BlinkMacSystemFont, sans-serif)',
fontSize: 'var(--atomic-editor-body-size, 1rem)',
height: '100%',
},
'.cm-scroller': {
fontFamily: 'var(--atomic-editor-font, system-ui, -apple-system, BlinkMacSystemFont, sans-serif)',
lineHeight: 'var(--atomic-editor-body-leading, 1.7)',
overflow: 'auto',
},
'.cm-content': {
caretColor: 'var(--atomic-editor-accent-bright, #a78bfa)',
padding: '0',
paddingBottom: '40vh',
// CM6's base theme sets `min-width: max-content` on
// `.cm-content` so it always grows to fit its widest child.
// That defeats every width constraint on our block widgets
// (tables especially): a wide table tells `.cm-content`
// "I'm 800px", content grows to 800px, and the scroller
// shows horizontal scroll — the "editor overflows
// horizontally" behavior you see on mobile when a wide
// table enters the viewport. Forcing `min-width: 0` lets
// the content box stay at its parent width; wide children
// are expected to own their own horizontal scroll (see
// `.cm-atomic-table-scroll`) rather than pushing the
// content.
minWidth: '0',
},
'.cm-line': {
padding: '0',
// Force-wrap words that have no natural break opportunity —
// long URLs, base64 chunks, and code tokens that would
// otherwise overflow the line and push the scroll container
// wider than the viewport. Without this, long unbroken
// tokens blow past the reading column and we get the
// transient horizontal overflow on mobile.
overflowWrap: 'anywhere',
},
'&.cm-focused': {
outline: 'none',
},
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: 'var(--atomic-editor-accent-bright, #a78bfa)',
borderLeftWidth: '2px',
},
'&.cm-focused .cm-selectionBackground, ::selection, .cm-selectionBackground': {
backgroundColor:
'var(--atomic-editor-selection-bg, color-mix(in srgb, #7c3aed 28%, #1e1e1e 72%))',
},
'.cm-activeLine': {
backgroundColor: 'transparent',
},
'.cm-gutters': {
display: 'none',
},
'.cm-tooltip': {
backgroundColor: 'var(--atomic-editor-bg-surface, #2d2d2d)',
color: 'var(--atomic-editor-fg, #dcddde)',
border: '1px solid var(--atomic-editor-border, #3d3d3d)',
borderRadius: '6px',
},
'.cm-panels': {
backgroundColor: 'var(--atomic-editor-bg-panel, #252525)',
color: 'var(--atomic-editor-fg, #dcddde)',
borderColor: 'var(--atomic-editor-border, #3d3d3d)',
},
'.cm-panel.cm-search': {
padding: '8px 12px',
fontFamily: 'var(--atomic-editor-font, system-ui, sans-serif)',
},
'.cm-panel.cm-search input, .cm-panel.cm-search button, .cm-panel.cm-search label': {
fontFamily: 'var(--atomic-editor-font, system-ui, sans-serif)',
fontSize: '0.8125rem',
},
'.cm-panel.cm-search input[type=text]': {
backgroundColor: 'var(--atomic-editor-bg, #1e1e1e)',
color: 'var(--atomic-editor-fg, #dcddde)',
border: '1px solid var(--atomic-editor-border, #3d3d3d)',
borderRadius: '4px',
padding: '4px 8px',
},
'.cm-panel.cm-search button': {
backgroundColor: 'transparent',
color: 'var(--atomic-editor-fg-muted, #888)',
border: '1px solid var(--atomic-editor-border, #3d3d3d)',
borderRadius: '4px',
padding: '4px 10px',
cursor: 'pointer',
},
'.cm-searchMatch': {
backgroundColor:
'var(--atomic-editor-search-bg, color-mix(in srgb, #7c3aed 26%, transparent 74%))',
borderRadius: '2px',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor:
'var(--atomic-editor-search-bg-active, color-mix(in srgb, #7c3aed 60%, transparent 40%))',
outline: '1px solid var(--atomic-editor-accent-bright, #a78bfa)',
},
},
// FB: app is light-only; flipped from upstream `dark: true`. All colors
// come from `--atomic-editor-*` vars set by the markdown-editor host.
{ dark: false },
);

// Markdown syntax tinting plus highlight colors for tokens emitted by
// grammars nested inside fenced code blocks (see `code-languages.ts`).
// Punctuation tokens (#, *, `, [, ]) stay muted so the surrounding
// prose reads cleanly; headings and structural markdown tokens get
// real visual weight. Code-language tokens (keyword, string, number,
// etc.) adopt a Material Palenight palette tuned for dark backgrounds;
// override any color via the `--atomic-editor-hl-*` CSS variables.
export const atomicMarkdownHighlight = HighlightStyle.define([
{ tag: t.heading1, fontWeight: '700' },
{ tag: t.heading2, fontWeight: '700' },
{ tag: t.heading3, fontWeight: '700' },
{ tag: t.heading4, fontWeight: '700' },
{ tag: [t.heading5, t.heading6], fontWeight: '700' },

{ tag: t.strong, fontWeight: '700', color: 'var(--atomic-editor-fg, #dcddde)' },
{ tag: t.emphasis, fontStyle: 'italic', color: 'var(--atomic-editor-fg, #dcddde)' },
{ tag: t.strikethrough, textDecoration: 'line-through', color: 'var(--atomic-editor-fg-muted, #888)' },

{
tag: [t.monospace],
fontFamily: 'var(--atomic-editor-font-mono, ui-monospace, monospace)',
color: 'var(--atomic-editor-link, #60a5fa)',
},

{ tag: t.link, color: 'var(--atomic-editor-link, #60a5fa)' },
{ tag: t.url, color: 'var(--atomic-editor-link, #60a5fa)' },

{ tag: t.processingInstruction, color: 'var(--atomic-editor-fg-faint, #666)' },
{ tag: t.contentSeparator, color: 'var(--atomic-editor-fg-faint, #666)' },
{ tag: t.quote, color: 'var(--atomic-editor-fg-muted, #888)' },
{ tag: t.list, color: 'var(--atomic-editor-fg, #dcddde)' },
{ tag: t.meta, color: 'var(--atomic-editor-fg-faint, #666)' },

// Nested code-language tokens. `@codemirror/lang-markdown` wires the
// grammars from `code-languages.ts` into fenced blocks whose info
// string matches — each fence gets a real AST, so tags below apply.
{
tag: [t.keyword, t.modifier, t.operatorKeyword, t.controlKeyword, t.definitionKeyword, t.moduleKeyword, t.self],
color: 'var(--atomic-editor-hl-keyword, #c792ea)',
},
{
tag: [t.string, t.special(t.string), t.character, t.attributeValue],
color: 'var(--atomic-editor-hl-string, #c3e88d)',
},
{
tag: [t.number, t.integer, t.float, t.bool, t.null, t.atom],
color: 'var(--atomic-editor-hl-number, #f78c6c)',
},
{
tag: [t.comment, t.lineComment, t.blockComment, t.docComment],
color: 'var(--atomic-editor-hl-comment, #6a7a82)',
fontStyle: 'italic',
},
{
tag: [t.typeName, t.className, t.namespace, t.standard(t.variableName)],
color: 'var(--atomic-editor-hl-type, #ffcb6b)',
},
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.macroName],
color: 'var(--atomic-editor-hl-function, #82aaff)',
},
{
tag: [t.propertyName, t.attributeName, t.definition(t.propertyName)],
color: 'var(--atomic-editor-hl-property, #82aaff)',
},
{ tag: t.regexp, color: 'var(--atomic-editor-hl-regexp, #f07178)' },
{ tag: t.escape, color: 'var(--atomic-editor-hl-escape, #89ddff)' },
{
tag: [t.tagName, t.angleBracket],
color: 'var(--atomic-editor-hl-tag, #f07178)',
},
{
tag: [t.variableName, t.labelName, t.definition(t.variableName), t.local(t.variableName)],
color: 'var(--atomic-editor-hl-variable, #eeffff)',
},
{ tag: t.operator, color: 'var(--atomic-editor-hl-operator, #89ddff)' },
{ tag: t.invalid, color: 'var(--atomic-editor-hl-invalid, #ff5370)' },

{ tag: [t.punctuation, t.bracket, t.squareBracket, t.paren, t.brace], color: 'var(--atomic-editor-fg-muted, #888)' },
]);

export const atomicMarkdownSyntax = syntaxHighlighting(atomicMarkdownHighlight);
Loading
Loading