forked from mirror/crumbs-original
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ecc6b74d | ||
|
|
521acc718c | ||
|
|
082b8632c6 | ||
|
|
6bdcc3c214 | ||
|
|
e8c30c518c | ||
|
|
84145de3b1 | ||
|
|
ea66e23178 | ||
|
|
feda983393 | ||
|
|
bf5bfdd98e | ||
|
|
12ff6d9c4d | ||
|
|
44963bf684 | ||
|
|
dfaf1c474d |
@@ -171,7 +171,8 @@ A `Makefile` wraps all common tasks for tool-agnostic usage. Run `make help` to
|
||||
- **Conventional commits**: Use `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:` prefixes
|
||||
- **Commit regularly**: Bundle related changes into logical commits after each step or feature slice — don't accumulate a large diff
|
||||
- **Main branch**: `main` — use as base for all PRs and feature branches
|
||||
- **Feature branches**: Use `feat/<name>` branches with worktrees for isolation
|
||||
- **Always work in a worktree**: Never edit files in the main checkout. Before touching any file, create a dedicated worktree (`EnterWorktree` or `git worktree add .worktrees/<name> -b <branch>`) on a fresh branch from `origin/main`, do all work, commits, and PR creation from there. This keeps the main checkout clean and lets several tasks run side by side without clobbering each other's uncommitted changes.
|
||||
- **Feature branches**: Use `feat/<name>` (or `fix/<name>`) branches, one per worktree
|
||||
- **Rebase before PR**: Always `git rebase origin/main` (not merge) before pushing or creating a PR to keep a clean linear history and avoid merge conflicts in the PR
|
||||
|
||||
## CI/CD
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
crumbs:
|
||||
image: ghcr.io/bretzel-app/crumbs:0.34.0
|
||||
image: ghcr.io/bretzel-app/crumbs:0.34.1
|
||||
container_name: crumbs
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
+26
-27
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "crumbs",
|
||||
"private": true,
|
||||
"version": "0.34.1",
|
||||
"version": "0.34.2",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.15.9",
|
||||
"license": "MIT",
|
||||
@@ -36,57 +36,55 @@
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"@sveltejs/adapter-node": "^5.5.7",
|
||||
"@tiptap/core": "^3.29.2",
|
||||
"@tiptap/extension-link": "^3.29.2",
|
||||
"@tiptap/extension-placeholder": "^3.29.2",
|
||||
"@tiptap/extension-table": "^3.29.2",
|
||||
"@tiptap/extension-table-cell": "^3.29.2",
|
||||
"@tiptap/extension-table-header": "^3.29.2",
|
||||
"@tiptap/extension-table-row": "^3.29.2",
|
||||
"@tiptap/extension-task-item": "^3.29.2",
|
||||
"@tiptap/extension-task-list": "^3.29.2",
|
||||
"@tiptap/extension-text-align": "^3.29.2",
|
||||
"@tiptap/extension-underline": "^3.29.2",
|
||||
"@tiptap/pm": "^3.29.2",
|
||||
"@tiptap/starter-kit": "^3.29.2",
|
||||
"@tiptap/core": "^3.31.0",
|
||||
"@tiptap/extension-link": "^3.31.0",
|
||||
"@tiptap/extension-placeholder": "^3.31.0",
|
||||
"@tiptap/extension-table": "^3.31.0",
|
||||
"@tiptap/extension-table-cell": "^3.31.0",
|
||||
"@tiptap/extension-table-header": "^3.31.0",
|
||||
"@tiptap/extension-table-row": "^3.31.0",
|
||||
"@tiptap/extension-task-item": "^3.31.0",
|
||||
"@tiptap/extension-task-list": "^3.31.0",
|
||||
"@tiptap/extension-text-align": "^3.31.0",
|
||||
"@tiptap/extension-underline": "^3.31.0",
|
||||
"@tiptap/pm": "^3.31.0",
|
||||
"@tiptap/starter-kit": "^3.31.0",
|
||||
"arctic": "^3.7.0",
|
||||
"argon2": "^0.43.1",
|
||||
"argon2": "^0.45.1",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"idb": "^8.0.3",
|
||||
"lucide-svelte": "^0.577.0",
|
||||
"markdown-it": "^14.3.0",
|
||||
"markdown-it": "^15.0.1",
|
||||
"nanoid": "^5.1.16",
|
||||
"node-diff3": "^3.2.1",
|
||||
"nodemailer": "^9.0.3",
|
||||
"nodemailer": "^9.1.0",
|
||||
"sanitize-html": "^2.17.7",
|
||||
"svelte-dnd-action": "^0.9.77",
|
||||
"svelte-dnd-action": "^0.9.79",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"uuid": "^11.1.1",
|
||||
"zod": "^4.4.3",
|
||||
"uuid": "^14.0.2",
|
||||
"zod": "^4.5.4",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@sveltejs/kit": "^2.70.2",
|
||||
"@sveltejs/kit": "^2.70.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/sanitize-html": "^2.16.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/zxcvbn": "^4.4.5",
|
||||
"@vite-pwa/sveltekit": "^1.1.0",
|
||||
"@vitest/coverage-v8": "^3.2.7",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"jsdom": "^29.1.1",
|
||||
"nyc": "^18.0.0",
|
||||
"svelte": "^5.56.8",
|
||||
"svelte-check": "^4.7.4",
|
||||
"svelte": "^5.57.0",
|
||||
"svelte-check": "^4.7.6",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tsx": "^4.23.5",
|
||||
"tsx": "^4.23.13",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.6",
|
||||
"vite-plugin-istanbul": "^7.2.1",
|
||||
@@ -108,7 +106,8 @@
|
||||
"hono": ">=4.12.7",
|
||||
"devalue": ">=5.8.1",
|
||||
"path-to-regexp@8": ">=8.4.0",
|
||||
"fast-uri": ">=3.1.2"
|
||||
"fast-uri": ">=3.1.2",
|
||||
"postcss>nanoid": ">=3.3.18 <4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+835
-797
File diff suppressed because it is too large
Load Diff
+17
-2
@@ -88,14 +88,29 @@ body::before {
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Prose overrides for parchment theme */
|
||||
/* Prose overrides for parchment theme.
|
||||
The typography plugin gives every element type its own --tw-prose-* variable,
|
||||
each defaulting to a fixed grey. Anything not remapped here stays that grey in
|
||||
dark mode (bold text was near-black on ink — see issue #86), so map them all. */
|
||||
.prose {
|
||||
--tw-prose-body: var(--text);
|
||||
--tw-prose-headings: var(--text);
|
||||
--tw-prose-lead: var(--text-muted);
|
||||
--tw-prose-links: var(--primary);
|
||||
--tw-prose-bold: var(--text);
|
||||
--tw-prose-counters: var(--text-muted);
|
||||
--tw-prose-bullets: var(--text-muted);
|
||||
--tw-prose-hr: var(--border-subtle);
|
||||
--tw-prose-quotes: var(--text);
|
||||
--tw-prose-quote-borders: var(--border-subtle);
|
||||
--tw-prose-captions: var(--text-muted);
|
||||
--tw-prose-kbd: var(--text);
|
||||
--tw-prose-kbd-shadows: color-mix(in oklab, var(--text) 10%, transparent);
|
||||
--tw-prose-code: var(--text);
|
||||
--tw-prose-pre-bg: var(--bg-surface);
|
||||
--tw-prose-pre-code: var(--text);
|
||||
--tw-prose-pre-bg: var(--bg-surface);
|
||||
--tw-prose-th-borders: var(--border-subtle);
|
||||
--tw-prose-td-borders: var(--border-subtle);
|
||||
}
|
||||
|
||||
.prose :where(code):not(:where([class~="not-prose"] *)) {
|
||||
|
||||
@@ -95,6 +95,25 @@ describe('renderMarkdown', () => {
|
||||
expect(html).toContain('disabled');
|
||||
expect(html).toContain('class="task-checkbox"');
|
||||
});
|
||||
|
||||
it('should render an empty task item as a checkbox, not literal brackets', () => {
|
||||
const html = renderMarkdown('- [ ] Train tickets\n- [ ]');
|
||||
expect(html).not.toContain('[ ]');
|
||||
expect(html.match(/type="checkbox"/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should render an empty checked task item as a checked checkbox', () => {
|
||||
const html = renderMarkdown('- [x]');
|
||||
expect(html).not.toContain('[x]');
|
||||
expect(html).toContain('<input type="checkbox" checked disabled class="task-checkbox">');
|
||||
expect(html).toContain('class="task-list"');
|
||||
});
|
||||
|
||||
it('should not treat brackets glued to text as a task marker', () => {
|
||||
const html = renderMarkdown('- [ ]x');
|
||||
expect(html).not.toContain('type="checkbox"');
|
||||
expect(html).toContain('[ ]x');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripMarkdown', () => {
|
||||
|
||||
@@ -33,12 +33,16 @@ md.core.ruler.after('inline', 'task-lists', (state) => {
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (tokens[i].type !== 'inline') continue;
|
||||
const content = tokens[i].content;
|
||||
if (content.startsWith('[ ] ') || content.startsWith('[x] ')) {
|
||||
const checked = content.startsWith('[x] ');
|
||||
// markdown-it trims trailing whitespace from inline content, so an item
|
||||
// with no text after the marker arrives as exactly "[ ]" — match the
|
||||
// marker followed by a space or end-of-content.
|
||||
const marker = /^\[( |x)\](?: |$)/.exec(content);
|
||||
if (marker) {
|
||||
const checked = marker[1] === 'x';
|
||||
const checkbox = checked
|
||||
? '<input type="checkbox" checked disabled class="task-checkbox" /> '
|
||||
: '<input type="checkbox" disabled class="task-checkbox" /> ';
|
||||
tokens[i].content = content.slice(4);
|
||||
tokens[i].content = content.slice(marker[0].length);
|
||||
tokens[i].children = md.parseInline(tokens[i].content, state.env)[0].children;
|
||||
// Prepend checkbox
|
||||
const token = new state.Token('html_inline', '', 0);
|
||||
@@ -48,7 +52,7 @@ md.core.ruler.after('inline', 'task-lists', (state) => {
|
||||
// Add task-list class to parent <ul> (once)
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (tokens[j].type === 'bullet_list_open') {
|
||||
const cls = tokens[j].attrGet('class') ?? '';
|
||||
const cls = String(tokens[j].attrGet('class') ?? '');
|
||||
if (!cls.includes('task-list')) {
|
||||
tokens[j].attrJoin('class', 'task-list');
|
||||
}
|
||||
@@ -66,7 +70,7 @@ md.core.ruler.after('task-lists', 'note-links', (state) => {
|
||||
const children = token.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (children[i].type !== 'link_open') continue;
|
||||
const href = children[i].attrGet('href') ?? '';
|
||||
const href = String(children[i].attrGet('href') ?? '');
|
||||
if (!href.startsWith('crumb-note://')) continue;
|
||||
|
||||
let closeIndex = i + 1;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect, createNote } from './helpers/fixtures.js';
|
||||
import { test, expect, createNote, noteCard } from './helpers/fixtures.js';
|
||||
|
||||
test.describe.serial('Dark mode', () => {
|
||||
test('Scenario: Theme toggle persists selection across page reload', async ({
|
||||
@@ -110,4 +110,38 @@ test.describe.serial('Dark mode', () => {
|
||||
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
});
|
||||
|
||||
test('Scenario: Bold text in notes uses the theme text colour in dark mode', async ({
|
||||
authenticatedPage: page
|
||||
}) => {
|
||||
// Given a note with bold content exists
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await createNote(page, 'Bold Contrast Note', 'Plain then **bold words** here');
|
||||
|
||||
// And dark theme is active
|
||||
await page.goto('/settings/preferences');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByTestId('pref-theme-dark').click();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||
|
||||
// When viewing the note on the notes page
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
const preview = noteCard(page, 'Bold Contrast Note').getByTestId('note-content-preview');
|
||||
const boldInCard = preview.locator('strong');
|
||||
await expect(boldInCard).toHaveText('bold words');
|
||||
|
||||
// Then the bold text is rendered in the dark theme's text colour (--text = #ece3d3),
|
||||
// not the typography plugin's hardcoded near-black
|
||||
await expect(boldInCard).toHaveCSS('color', 'rgb(236, 227, 211)');
|
||||
|
||||
// And the same holds inside the editor
|
||||
await noteCard(page, 'Bold Contrast Note').click();
|
||||
const boldInEditor = page.getByTestId('tiptap-editor').locator('.tiptap strong');
|
||||
await expect(boldInEditor).toHaveText('bold words');
|
||||
await expect(boldInEditor).toHaveCSS('color', 'rgb(236, 227, 211)');
|
||||
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -292,7 +292,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.34.1</span>
|
||||
<span class="footer-version">v0.34.2</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user