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
Binary file added .github/images/notes-studio-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/images/notes-studio-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 87 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,9 @@ Rules:
- **Never invent a background colour.** `bg-white`, `bg-black`, `bg-gray-*`,
`bg-slate-*`, `bg-[…]` and raw `oklch()`/`hsl()` in a component are all
violations. If the surface you need is not in the table, the table is wrong —
extend it here.
extend it here. The one narrow, deliberate exception is the four [tool
fills](#tool-fills): a coloured card background on the Notes creation tools and
nothing else.
- `surface-hover` and `surface-selected` are translucent so that they adapt to
whatever they are drawn on. Do not add alpha to them again (`bg-surface-hover/50`
is a bug).
Expand Down Expand Up @@ -272,6 +274,42 @@ blue-tinted neutral in the palette. They have been deleted; do not reintroduce
them — the `secondary` **variant** name is unrelated to a `--secondary` colour
and is spelled with `bg-muted`.

### Tool fills

Implementation: `components/notebook/noteTools.ts` (the token on each tool),
`assets/theme.css` (values and Tailwind mappings), and
`components/notebook/NotePanel.tsx` (the only consumer).

These four tokens exist for the Notes panel's creation tools — create note, mind
map, quiz, Anki — which are the one place a **card** carries colour. They are not
a surface tier, not an accent and not chart colours: a decorative pastel fill that
lets the four action cards be told apart at a glance. The grid is `2 x 2`, in the
order create note, mind map, quiz, Anki.

| Token | Tailwind | Light | Dark | Tool |
| ---------------- | ----------------- | ------------------------ | ---------------------- | ----------- |
| `--tool-note` | `bg-tool-note` | `oklch(0.935 0.045 250)` | `oklch(0.32 0.04 250)` | Create note |
| `--tool-mindmap` | `bg-tool-mindmap` | `oklch(0.935 0.045 300)` | `oklch(0.32 0.04 300)` | Mind map |
| `--tool-quiz` | `bg-tool-quiz` | `oklch(0.935 0.045 150)` | `oklch(0.32 0.04 150)` | Quiz |
| `--tool-anki` | `bg-tool-anki` | `oklch(0.935 0.045 85)` | `oklch(0.32 0.045 85)` | Anki |

Rules:

- **Scope is exactly these four cards.** A fifth use — a badge, a tinted list row,
a status chip — is a rule change and belongs in this document before it belongs in
the code. `--chart-*` and `--primary` may not be borrowed for this either; the
chart guard stays as it is.
- **Text and icons stay neutral.** The icon stays `text-muted-foreground` (T2,
neutral) and the label stays `text-foreground` (T1, `text-sm font-medium`). Colour
carries the tool's identity; the glyph and the word carry the meaning, so colour is
never the only carrier of anything.
- **Low saturation on purpose.** Chroma stays at 0.04–0.05 so a pastel tile never
competes with the `--primary` action or a chart. They sit deliberately outside the
surface ladder: light values are a step _below_ `--surface-base` (a recessed tile)
and dark values a step _above_ it (a muted raise), so the card reads as a distinct
tile rather than as a panel in either scheme. Both stay above `--border`, so the
card's hairline is visible in both themes.

## Window chrome

The title bar is the one piece of the interface the OS and the renderer draw
Expand Down Expand Up @@ -1039,6 +1077,54 @@ area, so a panel-level empty state opts out with `border-none`
(`DocumentList`). The primitive keeps its dashed hairline — that opt-out is the
exception, and it belongs to the panel that asked for it, not to `Empty`.

### Tool card

Implementation: `components/notebook/NotePanel.tsx`, over the four
[Tool fills](#tool-fills). The Notes panel's creation grid is a `2 x 2` of these,
rendered as the first block **inside** the panel's content scroll area so a short
window scrolls the cards and the item list together. The panel header keeps the
`Notes` title only — it carries no tool buttons.

```tsx
<div className="grid grid-cols-2 gap-2 p-2">
<Button
type="button"
variant="ghost"
className="group relative h-auto w-full overflow-hidden flex-col items-start justify-start gap-2 whitespace-normal rounded-lg border border-border bg-tool-note p-3 text-left hover:bg-tool-note"
>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-surface-hover opacity-0 transition-opacity group-hover:opacity-100"
/>
<span className="relative flex flex-col items-start gap-2">
<Icon className="text-muted-foreground" />
<span className="text-sm font-medium text-foreground">{label}</span>
</span>
</Button>
</div>
```

- **Density:** `p-3` card padding; `gap-2` between icon and label and between
cards. Height is content-driven (never `h-9`): a wrapped English label
("Generate Anki Cards") sets the row height in a 260px panel.
- **Radius:** `rounded-lg` (8px), the container radius — a card is a container even
though it is a button. Border `border border-border`; **no shadow**.
- **Label never truncates:** `whitespace-normal` overrides `Button`'s default
`whitespace-nowrap`, which would clip the label at the panel minimum. The icon
sits above a left-aligned label (`flex-col items-start`), not beside it.
- **Hover is composited, not swapped.** The tool fill is opaque, so
`hover:bg-surface-hover` would replace it; the sanctioned overlay from
[Raised card](#raised-card) adds the translucent fill on top instead, and
each card's matching `hover:bg-tool-*` keeps its opaque fill instead of the
`ghost` variant's default hover fill.
- **Focus and disabled** come from `Button`:
`focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background`
and `disabled:pointer-events-none disabled:opacity-50`.
- **No selected state.** These are actions, not a mode switch: pressing one runs and
returns (a blank note opens the editor; the other three generate an artifact), so
no card stays pressed. That state is unreachable and is recorded here rather than
invented.

### Form field

Implementation: `components/ui/field.tsx` — `Field`, `FieldSet`, `FieldLegend`,
Expand Down
28 changes: 28 additions & 0 deletions src/renderer/src/assets/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@
--chart-4: oklch(0.8249 0.097 82.2597);
--chart-5: oklch(0.6936 0.1635 318.2073);

/*
* Tool fills. Four low-saturation card backgrounds used **only** by the Notes
* panel's creation-tool cards (create note / mind map / quiz / Anki), in that
* order. This is a sanctioned, narrow exception to the surface ladder: the tool
* cards are a decorative grid of action buttons, and a pastel tint is what lets
* the four be told apart at a glance. Nothing else may use these tokens, they
* carry neutral text and icons (the icon stays T2 and the label T1), and they
* are not chart colours — see DESIGN.md, "Tool fills". Light values sit a step
* below `--surface-base` (a recessed tile); the dark values below are the
* matching muted raise above it.
*/
--tool-note: oklch(0.935 0.045 250);
--tool-mindmap: oklch(0.935 0.045 300);
--tool-quiz: oklch(0.935 0.045 150);
--tool-anki: oklch(0.935 0.045 85);

/* Elevation: floating layers, plus a small lift for knobs and handles. */
--shadow-elevation: 0 6px 16px -4px hsl(0 0% 0% / 0.12), 0 2px 4px -1px hsl(0 0% 0% / 0.06);
--shadow-control: 0 1px 2px 0 hsl(0 0% 0% / 0.16);
Expand Down Expand Up @@ -202,6 +218,14 @@
--chart-4: oklch(0.8249 0.097 82.2597);
--chart-5: oklch(0.6936 0.1635 318.2073);

/* Tool fills (Notes creation cards). A muted raise above `--surface-base`, and
above `--border` (0.3052), so the card's hairline stays visible in dark mode
the same way the light tile's does. */
--tool-note: oklch(0.32 0.04 250);
--tool-mindmap: oklch(0.32 0.04 300);
--tool-quiz: oklch(0.32 0.04 150);
--tool-anki: oklch(0.32 0.045 85);

--shadow-elevation: 0 8px 22px -6px hsl(0 0% 0% / 0.55), 0 2px 4px -1px hsl(0 0% 0% / 0.35);
--shadow-control: 0 1px 2px 0 hsl(0 0% 0% / 0.5);

Expand Down Expand Up @@ -259,6 +283,10 @@
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-tool-note: var(--tool-note);
--color-tool-mindmap: var(--tool-mindmap);
--color-tool-quiz: var(--tool-quiz);
--color-tool-anki: var(--tool-anki);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
Expand Down
104 changes: 57 additions & 47 deletions src/renderer/src/components/notebook/NotePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { ReactElement, useEffect, useRef, useState } from 'react'
import { Save, Trash2, ArrowLeft, Network, FileText, ClipboardCheck, Layers } from 'lucide-react'
import { Save, Trash2, ArrowLeft } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useParams } from 'react-router-dom'
import { cn } from '@/lib/utils'
import { useItemStore } from '../../store/itemStore'
import { useUIStore } from '../../store/uiStore'
import { setupMindMapListeners } from '../../store/mindmapStore'
import NoteEditor from './note/NoteEditor'
import ItemList from './item/ItemList'
import { NOTE_TOOLS, type NoteTool, type NoteToolId } from './noteTools'
import QuizStartDialog, { QuizStartParams } from './quiz/QuizStartDialog'
import AnkiConfigDialog from './anki/AnkiConfigDialog'
import { ScrollArea } from '../ui/scroll-area'
Expand Down Expand Up @@ -137,6 +139,40 @@ function NoteEditorPanel({
)
}

// 创建工具卡片:图标在上、左对齐标签在下。悬停用合成填充(半透明 surface-hover
// 叠在彩色卡片上),不替换彩色底色。这些是动作按钮,没有持续选中态。
function NoteToolCard({
tool,
label,
onClick
}: {
tool: NoteTool
label: string
onClick: () => void
}): ReactElement {
const { Icon } = tool
return (
<Button
type="button"
variant="ghost"
onClick={onClick}
className={cn(
'group relative h-auto w-full overflow-hidden flex-col items-start justify-start gap-2 whitespace-normal rounded-lg border border-border p-3 text-left',
tool.surfaceClassName
)}
>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-surface-hover opacity-0 transition-opacity group-hover:opacity-100"
/>
<span className="relative flex flex-col items-start gap-2">
<Icon className="text-muted-foreground" />
<span className="text-sm font-medium text-foreground">{label}</span>
</span>
</Button>
)
}

export default function NotePanel(): ReactElement {
const { t } = useTranslation('notebook')
const { id: notebookId } = useParams()
Expand Down Expand Up @@ -324,6 +360,14 @@ export default function NotePanel(): ReactElement {
setShowQuizStartDialog(true)
}

// 四张工具卡片的动作,按 noteTools 中的顺序引用
const toolActions: Record<NoteToolId, () => void> = {
note: handleCreateNote,
mindmap: handleGenerateMindMap,
quiz: handleGenerateQuiz,
anki: handleGenerateAnki
}

// 开始生成答题
const handleQuizStart = async (params: QuizStartParams) => {
if (!notebookId) return
Expand Down Expand Up @@ -378,62 +422,28 @@ export default function NotePanel(): ReactElement {
) : (
// 列表页面
<>
{/* 顶部工具栏 */}
{/* 顶部标题栏:只保留标题,工具入口移到下方内容区 */}
<PanelHeader
draggable
left={
<span className="text-sm font-medium text-foreground truncate w-full select-none">
{t('notes')}
</span>
}
right={
<>
<Button
onClick={handleGenerateMindMap}
variant="ghost"
size="icon"
className="w-8 h-8"
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
title={t('generateMindMap')}
>
<Network className="w-4 h-4" />
</Button>
<Button
onClick={handleGenerateQuiz}
variant="ghost"
size="icon"
className="w-8 h-8"
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
title={t('generateQuiz')}
>
<ClipboardCheck className="w-4 h-4" />
</Button>
<Button
onClick={handleGenerateAnki}
variant="ghost"
size="icon"
className="w-8 h-8"
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
title={t('generateAnki')}
>
<Layers className="w-4 h-4" />
</Button>
<Button
onClick={handleCreateNote}
variant="ghost"
size="icon"
className="w-8 h-8"
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
title={t('createNote')}
>
<FileText className="w-4 h-4" />
</Button>
</>
}
/>

{/* Items 列表(笔记 + 思维导图等) */}
{/* 内容区:工具卡片与条目列表共用同一个滚动区,矮窗口下一起滚动 */}
<ScrollArea className="flex-1">
<div className="grid grid-cols-2 gap-2 p-2">
{NOTE_TOOLS.map((tool) => (
<NoteToolCard
key={tool.id}
tool={tool}
label={t(tool.labelKey)}
onClick={toolActions[tool.id]}
/>
))}
</div>
<ItemList
items={items}
currentNote={currentNote}
Expand Down
49 changes: 49 additions & 0 deletions src/renderer/src/components/notebook/noteTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FileText, Network, ClipboardCheck, Layers } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'

/**
* The Notes panel's four creation tools, in the order they render (a 2 x 2 grid).
*
* They are action buttons, not a mode switch: pressing one runs and returns — a
* blank note opens the editor, the other three generate an artifact — so there is
* no persistent selected state to express, and none is invented. `surfaceClassName`
* is the card's background token from DESIGN.md, "Tool fills"; those four tokens
* exist for these cards only.
*/
export type NoteToolId = 'note' | 'mindmap' | 'quiz' | 'anki'

export interface NoteTool {
id: NoteToolId
/** i18n key in the `notebook` namespace. */
labelKey: string
Icon: LucideIcon
/** The sanctioned tool-card background utility (DESIGN.md, "Tool fills"). */
surfaceClassName: string
}

export const NOTE_TOOLS: readonly NoteTool[] = [
{
id: 'note',
labelKey: 'createNote',
Icon: FileText,
surfaceClassName: 'bg-tool-note hover:bg-tool-note'
},
{
id: 'mindmap',
labelKey: 'generateMindMap',
Icon: Network,
surfaceClassName: 'bg-tool-mindmap hover:bg-tool-mindmap'
},
{
id: 'quiz',
labelKey: 'generateQuiz',
Icon: ClipboardCheck,
surfaceClassName: 'bg-tool-quiz hover:bg-tool-quiz'
},
{
id: 'anki',
labelKey: 'generateAnki',
Icon: Layers,
surfaceClassName: 'bg-tool-anki hover:bg-tool-anki'
}
]
2 changes: 1 addition & 1 deletion src/renderer/src/locales/en-US/notebook.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"noteSaved": "Saved",
"excerptSourceMissing": "This excerpt's source is no longer available.",
"noNotesYet": "No notes yet",
"noNotesYetDesc": "Click the \"+\" button in the top right to create a note",
"noNotesYetDesc": "Use the Create Note card above to add a note",
"unsavedChangesWarning": "You have unsaved changes. Are you sure you want to leave?",
"unsavedChangesTitle": "Unsaved Changes",
"unsavedChangesMessage": "You have unsaved changes. Are you sure you want to switch to another Notebook?",
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/locales/zh-CN/notebook.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"delete": "删除",
"confirmDeleteNote": "确定要删除这个笔记吗?",
"noNotesYet": "暂无笔记",
"noNotesYetDesc": "点击右上角的\"+\"按钮创建笔记",
"noNotesYetDesc": "点击上方的\"创建笔记\"卡片添加笔记",
"deleteNote": "删除笔记",
"deleteNoteWarning": "确定要删除这个笔记吗?此操作无法撤销。",
"unsavedChangesWarning": "您有未保存的修改,确定要离开吗?",
Expand Down
Loading
Loading