Compare commits

...
5 Commits
Author SHA1 Message Date
github-actions[bot] fc294f7090 chore: bump version to 0.9.1 2026-03-09 10:00:54 +00:00
Frederic CabassutandClaude Opus 4.6 29a3988e2c fix: use correct MCP server type in settings page config
Change "type": "streamable-http" to "type": "http" to match Claude
Code's actual MCP schema. Also clarify that the config targets
.claude/settings.json specifically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 10:47:25 +01:00
Frederic CabassutandClaude Opus 4.6 6957bdc64b perf: lazy-load zxcvbn password library to reduce initial bundle size
- Dynamic import zxcvbn (820KB) in PasswordStrengthMeter so it only
  loads when user starts typing a password
- Raise chunkSizeWarningLimit to 850KB for the lazy-loaded chunk

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 10:42:27 +01:00
Frederic CabassutandClaude Opus 4.6 e09c2a9c02 fix: resolve all svelte-check warnings (a11y + state_referenced_locally)
- Add svelte-ignore for intentional state_referenced_locally patterns
- Wrap clickable images in button elements for keyboard accessibility
- Add proper ARIA roles (dialog, textbox) with tabindex and focus management
- Fix lightbox Escape key handling to not close parent editor overlay

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 02:37:07 +01:00
github-actions[bot] 1f0bbd570e chore: pin docker-compose to v0.9.0 [skip ci] 2026-03-09 01:36:11 +00:00
15 changed files with 66 additions and 26 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
services:
crumbs:
image: ghcr.io/bretzel-app/crumbs:0.8.1
image: ghcr.io/bretzel-app/crumbs:0.9.0
container_name: crumbs
ports:
- "3000:3000"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "crumbs",
"private": true,
"version": "0.9.0",
"version": "0.9.1",
"type": "module",
"license": "MIT",
"author": "Bretzel (https://bretzel.app)",
+1
View File
@@ -19,6 +19,7 @@
let { content, onChange }: Props = $props();
// svelte-ignore state_referenced_locally
let items = $state<ChecklistItem[]>(parseChecklist(content));
let doneExpanded = $state(true);
const flipDurationMs = 150;
+12 -3
View File
@@ -9,6 +9,8 @@
const { src, alt, onClose }: Props = $props();
let dialogEl: HTMLDivElement | undefined = $state();
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.stopPropagation();
@@ -21,14 +23,21 @@
onClose();
}
}
$effect(() => {
dialogEl?.focus();
});
</script>
<svelte:window onkeydown={handleKeydown} />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={dialogEl}
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 animate-[fade-in_150ms_ease-out]"
onclick={handleBackgroundClick}
onkeydown={handleKeydown}
role="dialog"
aria-modal="true"
aria-label="Image lightbox"
tabindex="-1"
data-testid="image-lightbox"
>
<button
+10 -6
View File
@@ -119,14 +119,18 @@
{@const isPending = 'pending' in attachment && attachment.pending}
{@const imgSrc = isPending ? attachment.path : `/api/notes/${noteId}/attachments?attachmentId=${attachment.id}`}
<div class="group relative">
<!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_click_events_have_key_events -->
<img
src={imgSrc}
alt={attachment.filename}
class="h-20 w-20 cursor-pointer rounded-sm object-cover {isPending ? 'opacity-70' : ''}"
<button
type="button"
class="h-20 w-20 cursor-pointer rounded-sm p-0 border-0 bg-transparent overflow-hidden"
onclick={() => { lightboxSrc = imgSrc; lightboxAlt = attachment.filename; }}
data-testid="attachment-thumbnail"
/>
>
<img
src={imgSrc}
alt={attachment.filename}
class="h-20 w-20 object-cover {isPending ? 'opacity-70' : ''}"
/>
</button>
{#if isPending}
<div class="absolute bottom-0.5 left-0.5 rounded-sm bg-black/50 p-0.5" title="Pending sync">
<RefreshCw class="h-3 w-3 text-white" />
+12 -7
View File
@@ -73,6 +73,7 @@
}
</script>
<!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
<article
class="group relative cursor-pointer rounded-sm border border-[var(--border-subtle)] p-4 transition-all hover:border-[var(--primary)] shadow-[var(--card-shadow)] hover:shadow-[var(--card-shadow-hover)] {fullHeight ? 'h-full' : ''}"
style={cardStyle}
@@ -88,15 +89,19 @@
<div class="-mx-4 -mt-4 mb-3 flex overflow-hidden rounded-t-sm" data-testid="card-thumbnails">
{#each featuredAttachments.slice(0, 3) as attachment}
<div class="relative min-w-0 flex-1">
<!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_click_events_have_key_events -->
<img
src="/api/notes/{note.id}/attachments?attachmentId={attachment.id}&thumb=1"
alt={attachment.filename}
class="h-24 w-full cursor-pointer object-cover"
loading="lazy"
<button
type="button"
class="h-24 w-full cursor-pointer p-0 border-0 bg-transparent"
onclick={(e) => { e.stopPropagation(); lightboxSrc = `/api/notes/${note.id}/attachments?attachmentId=${attachment.id}`; lightboxAlt = attachment.filename; }}
data-testid="card-thumbnail"
/>
>
<img
src="/api/notes/{note.id}/attachments?attachmentId={attachment.id}&thumb=1"
alt={attachment.filename}
class="h-24 w-full object-cover"
loading="lazy"
/>
</button>
</div>
{/each}
{#if featuredAttachments.length > 3}
+1
View File
@@ -33,6 +33,7 @@
let checklistMode = $state(note?.checklistMode ?? false);
let showColorPicker = $state(false);
let showImageUpload = $state(false);
// svelte-ignore state_referenced_locally
let rawMarkdownMode = $state(note?.checklistMode ?? false);
let textareaEl: HTMLTextAreaElement | undefined = $state();
let tiptapEditor: Editor | undefined = $state();
@@ -1,9 +1,19 @@
<script lang="ts">
import zxcvbn from 'zxcvbn';
import type { ZXCVBNResult } from 'zxcvbn';
let { password = '' }: { password: string } = $props();
const result = $derived(password.length > 0 ? zxcvbn(password) : null);
let result = $state<ZXCVBNResult | null>(null);
$effect(() => {
if (password.length > 0) {
import('zxcvbn').then((mod) => {
result = mod.default(password);
});
} else {
result = null;
}
});
const labels = ['Very weak', 'Weak', 'Fair', 'Strong', 'Very strong'];
const colors = [
+4 -1
View File
@@ -57,11 +57,14 @@
});
</script>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
bind:this={element}
class="tiptap-wrapper prose prose-sm flex min-h-[300px] max-w-none flex-col px-4 py-2 text-[var(--text)]"
data-testid="tiptap-editor"
role="textbox"
aria-multiline="true"
tabindex="0"
onclick={() => editor?.commands.focus()}
></div>
+2
View File
@@ -14,7 +14,9 @@
let { data } = $props();
// Profile state
// svelte-ignore state_referenced_locally
let displayName = $state(data.user?.displayName || '');
// svelte-ignore state_referenced_locally
let email = $state(data.user?.email || '');
let profileMsg = $state('');
let profileError = $state(false);
+4 -4
View File
@@ -26,7 +26,7 @@
let mcpJsonConfig = $derived(`{
"mcpServers": {
"crumbs": {
"type": "streamable-http",
"type": "http",
"url": "${baseUrl}/api/mcp",
"headers": {
"Authorization": "Bearer ${activeApiKey}"
@@ -37,12 +37,12 @@
let llmInstallPrompt = $derived(`Install the Crumbs MCP server with the following configuration:
- Type: Streamable HTTP
- Type: HTTP (Streamable HTTP)
- URL: ${baseUrl}/api/mcp
- Authentication: Bearer token in the Authorization header
- API Key: ${activeApiKey}
Add this to your MCP client settings (e.g. Claude Code's .claude/settings.json or Claude Desktop's config):
Add this to your MCP client settings (e.g. Claude Code's .claude/settings.json):
${mcpJsonConfig}`);
@@ -233,7 +233,7 @@ ${mcpJsonConfig}`);
<section class="rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-6 shadow-[var(--card-shadow)]">
<h2 class="mb-4 text-lg font-semibold text-[var(--text)]">Client Configuration</h2>
<p class="mb-3 text-xs text-[var(--text-muted)]">
Add this to your MCP client config (e.g. Claude Code settings):
Add this to your <code class="rounded-sm bg-[var(--bg-base)] px-1 py-0.5">.claude/settings.json</code> (Claude Code):
</p>
<div class="relative">
<pre class="overflow-x-auto rounded-sm bg-[var(--bg-base)] p-3 pr-10 text-xs text-[var(--text)]">{mcpJsonConfig}</pre>
@@ -3,6 +3,7 @@
import PasswordStrengthMeter from '$lib/components/PasswordStrengthMeter.svelte';
let { data } = $props();
// svelte-ignore state_referenced_locally
let users = $state<User[]>(data.users);
// Create user form
+1
View File
@@ -42,6 +42,7 @@
}
}
// svelte-ignore state_referenced_locally
const providers = data.providers ?? [];
</script>
+3
View File
@@ -4,6 +4,9 @@ import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
chunkSizeWarningLimit: 850
},
plugins: [
tailwindcss(),
sveltekit(),
+1 -1
View File
@@ -249,7 +249,7 @@
<li><a href="https://github.com/bretzel-app/crumbs/blob/main/docs/DEPLOYMENT.md" target="_blank" rel="noopener">Docs</a></li>
<li><a href="https://github.com/bretzel-app/crumbs/blob/main/LICENSE" target="_blank" rel="noopener">License</a></li>
</ul>
<span class="footer-version">v0.9.0</span>
<span class="footer-version">v0.9.1</span>
</div>
</footer>