From fbfda2f15c5fafd755c6c60237d75ba05e3d981f Mon Sep 17 00:00:00 2001 From: lucaronin Date: Fri, 1 May 2026 21:02:42 +0200 Subject: [PATCH] fix(types): render css type colors --- docs/ABSTRACTIONS.md | 2 +- src/components/NoteItem.test.tsx | 54 ++++++++++++++++++++++++-------- src/components/Sidebar.test.tsx | 4 +-- src/utils/typeColors.test.ts | 10 ++++++ src/utils/typeColors.ts | 26 +++++++++++++-- 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 938a8b42..1bfe19d1 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -208,7 +208,7 @@ Each entity type can have a corresponding **type document**: any markdown note w | Property | Type | Description | |----------|------|-------------| | `icon` | string | Type icon as a Phosphor name (kebab-case, e.g., "cooking-pot") | -| `color` | string | Accent color: red, purple, blue, green, yellow, orange | +| `color` | string | Accent palette key (`red`, `purple`, `blue`, `green`, `yellow`, `orange`, `teal`, `pink`, `gray`) or a valid CSS color value such as `cyan`, `#22d3ee`, or `rgb(34, 211, 238)` | | `order` | number | Sidebar display order (lower = higher priority) | | `sidebar_label` | string | Custom label overriding auto-pluralization | | `template` | string | Markdown template for new notes of this type | diff --git a/src/components/NoteItem.test.tsx b/src/components/NoteItem.test.tsx index 2b3e7776..e244a7be 100644 --- a/src/components/NoteItem.test.tsx +++ b/src/components/NoteItem.test.tsx @@ -94,6 +94,33 @@ describe('NoteItem', () => { expect(onClickNote).toHaveBeenCalled() }) + it('uses CSS named colors from the Type document for note type indicators', () => { + const ideaType = makeEntry({ + path: '/vault/type/idea.md', + filename: 'idea.md', + title: 'Idea', + isA: 'Type', + color: 'cyan', + }) + const ideaEntry = makeEntry({ + path: '/vault/ideas/native-cyan-idea.md', + filename: 'native-cyan-idea.md', + title: 'Native Cyan Idea', + isA: 'Idea', + }) + + render( + , + ) + + expect(screen.getByTestId('type-icon')).toHaveStyle({ color: 'rgb(0, 255, 255)' }) + }) + it('shows the title with filename metadata when a change status is present', () => { const entry = { ...makeEntry({ filename: 'my-note.md', title: 'My Note Title' }), @@ -199,18 +226,18 @@ describe('NoteItem', () => { }) it('colors relationship chips by target type and opens the related note on Cmd+click only', () => { - const linkedProject = makeEntry({ - path: '/vault/project/build-app.md', + const linkedIdea = makeEntry({ + path: '/vault/ideas/build-app.md', filename: 'build-app.md', title: 'Build App', - isA: 'Project', + isA: 'Idea', }) - const projectType = makeEntry({ - path: '/vault/type/project.md', - filename: 'project.md', - title: 'Project', + const ideaType = makeEntry({ + path: '/vault/type/idea.md', + filename: 'idea.md', + title: 'Idea', isA: 'Type', - color: 'red', + color: 'cyan', icon: 'wrench', }) const sourceEntry = makeEntry({ @@ -218,7 +245,7 @@ describe('NoteItem', () => { filename: 'source.md', title: 'Source', isA: 'Note', - relationships: { 'Belongs to': ['[[project/build-app]]'] }, + relationships: { 'Belongs to': ['[[ideas/build-app]]'] }, }) const onClickNote = vi.fn() @@ -226,8 +253,8 @@ describe('NoteItem', () => { , @@ -236,13 +263,14 @@ describe('NoteItem', () => { const chip = screen.getByTestId('property-chip-belongs-to-0') expect(chip).toHaveTextContent('Build App') expect(chip.className).toContain('cursor-pointer') - expect(chip).toHaveStyle({ color: 'var(--accent-red)', backgroundColor: 'var(--accent-red-light)' }) + expect(chip).toHaveStyle({ color: 'rgb(0, 255, 255)' }) + expect(chip.getAttribute('style')).toContain('background-color: color-mix(in srgb, cyan 14%, transparent)') fireEvent.click(chip) expect(onClickNote).not.toHaveBeenCalled() fireEvent.click(chip, { metaKey: true }) - expect(onClickNote).toHaveBeenCalledWith(linkedProject, expect.objectContaining({ metaKey: true })) + expect(onClickNote).toHaveBeenCalledWith(linkedIdea, expect.objectContaining({ metaKey: true })) }) it('falls back to the built-in type icon for relationship chips when the Type has no custom icon', () => { diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index a16b2afa..9d6ba224 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -711,7 +711,7 @@ describe('Sidebar', () => { wordCount: 0, relationships: {}, icon: null, - color: 'orange', + color: 'cyan', order: null, sidebarLabel: null, template: null, sort: null, @@ -750,7 +750,7 @@ describe('Sidebar', () => { fireEvent.click(screen.getByTitle('Customize sections')) expect(screen.getByLabelText('Toggle Projects').querySelector('svg')).toHaveStyle({ color: 'var(--accent-green)' }) - expect(screen.getByLabelText('Toggle Recipes').querySelector('svg')).toHaveStyle({ color: 'var(--accent-orange)' }) + expect(screen.getByLabelText('Toggle Recipes').querySelector('svg')).toHaveStyle({ color: 'rgb(0, 255, 255)' }) }) it('calls onToggleTypeVisibility when toggling a section in the popover', () => { diff --git a/src/utils/typeColors.test.ts b/src/utils/typeColors.test.ts index ae05f680..d37ec9cc 100644 --- a/src/utils/typeColors.test.ts +++ b/src/utils/typeColors.test.ts @@ -32,6 +32,11 @@ describe('getTypeColor', () => { it('uses gray custom color key', () => { expect(getTypeColor('Config', 'gray')).toBe('var(--accent-gray)') }) + + it('uses valid CSS color values that are not palette keys', () => { + expect(getTypeColor('Idea', 'cyan')).toBe('cyan') + expect(getTypeColor('Idea', '#22d3ee')).toBe('#22d3ee') + }) }) describe('getTypeLightColor', () => { @@ -55,6 +60,11 @@ describe('getTypeLightColor', () => { it('uses gray custom color key for light variant', () => { expect(getTypeLightColor('Config', 'gray')).toBe('var(--accent-gray-light)') }) + + it('derives a light background for valid CSS color values that are not palette keys', () => { + expect(getTypeLightColor('Idea', 'cyan')).toBe('color-mix(in srgb, cyan 14%, transparent)') + expect(getTypeLightColor('Idea', '#22d3ee')).toBe('color-mix(in srgb, #22d3ee 14%, transparent)') + }) }) const baseEntry: VaultEntry = { diff --git a/src/utils/typeColors.ts b/src/utils/typeColors.ts index cc7ca6a8..6fb0ca2c 100644 --- a/src/utils/typeColors.ts +++ b/src/utils/typeColors.ts @@ -4,6 +4,7 @@ */ import type { VaultEntry } from '../types' +import { isValidCssColor } from './colorUtils' /** Builds a map from type name → Type document entry (for custom color/icon lookup). * Stores both original title and lowercase version so lookups work regardless @@ -65,14 +66,35 @@ const COLOR_KEY_TO_CSS_LIGHT: Record = Object.fromEntries( ACCENT_COLORS.map((c) => [c.key, c.cssLight]), ) +const CSS_COLOR_LIGHT_MIX = 14 + +function resolveCustomColor(customColorKey?: string | null): string | null { + const color = customColorKey?.trim() + if (!color) return null + + const paletteKey = color.toLowerCase() + return COLOR_KEY_TO_CSS[paletteKey] ?? (isValidCssColor(color) ? color : null) +} + +function resolveCustomLightColor(customColorKey?: string | null): string | null { + const color = customColorKey?.trim() + if (!color) return null + + const paletteKey = color.toLowerCase() + return COLOR_KEY_TO_CSS_LIGHT[paletteKey] + ?? (isValidCssColor(color) ? `color-mix(in srgb, ${color} ${CSS_COLOR_LIGHT_MIX}%, transparent)` : null) +} + /** Returns the CSS variable for the accent color of a given note type, with optional custom override */ export function getTypeColor(isA: string | null, customColorKey?: string | null): string { - if (customColorKey && COLOR_KEY_TO_CSS[customColorKey]) return COLOR_KEY_TO_CSS[customColorKey] + const customColor = resolveCustomColor(customColorKey) + if (customColor) return customColor return (isA && TYPE_COLOR_MAP[isA]) ?? DEFAULT_COLOR } /** Returns the CSS variable for the light/background variant of a given note type's color */ export function getTypeLightColor(isA: string | null, customColorKey?: string | null): string { - if (customColorKey && COLOR_KEY_TO_CSS_LIGHT[customColorKey]) return COLOR_KEY_TO_CSS_LIGHT[customColorKey] + const customLightColor = resolveCustomLightColor(customColorKey) + if (customLightColor) return customLightColor return (isA && TYPE_LIGHT_COLOR_MAP[isA]) ?? DEFAULT_LIGHT_COLOR }