fix(web): keep note titles visible on compact desktop windows

Notebook and tag controls no longer refuse to shrink, so a scaled
desktop window cannot collapse the title field to a couple of characters.
This commit is contained in:
tianma-if committed 2026-09-30 18:55:00 +08:00
1 parent 36cec919ae
commit b9535e071d
10 files changed
+44 -15

No files matched your search

@@ -444,7 +444,7 @@ describe("diagram editor canvas surface", () => {
expect(status).toBeGreaterThan(metadata);
expect(headerEnd).toBeGreaterThan(status);
expect(toolbar).toBeGreaterThan(headerEnd);
expect(source).toContain('rowClassName="shrink-0 flex-nowrap"');
expect(source).toContain("rowClassName={MEMO_EDITOR_METADATA_ROW_CLASS_NAME}");
expect(source).toContain("nextTitleStatusClearance");
expect(source).toContain('cn(MEMO_EDITOR_TOP_ROW_CLASS_NAME, "border-b-0")');
expect(source).not.toContain("MEMO_EDITOR_TITLE_REGION_CLASS_NAME");
@@ -154,6 +154,7 @@ import { MemoEditorMetadataRow } from "@/components/MemoEditorMetadataRow";
import { MemoEditorFocusModeButton, MemoEditorTopRowLeading, MemoEditorUpdatedLabel } from "@/components/MemoEditorTopRowLeading";
import { MemoEditorToolbarDivider } from "@/components/MemoEditorToolbarChrome";
import {
MEMO_EDITOR_METADATA_ROW_CLASS_NAME,
MEMO_EDITOR_TOP_ROW_CLASS_NAME,
nextTitleStatusClearance,
} from "@/components/MemoEditorChromeDensity";
@@ -3089,7 +3090,6 @@ export const DiagramEditorPane = ({
className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-4 sm:flex-nowrap"
>
<MemoEditorTopRowLeading
className="min-w-0 flex-1"
mobileBackButton={(
<Tooltip>
<TooltipTrigger asChild>
@@ -3123,7 +3123,7 @@ export const DiagramEditorPane = ({
)}
/>
<MemoEditorMetadataRow
rowClassName="shrink-0 flex-nowrap"
rowClassName={MEMO_EDITOR_METADATA_ROW_CLASS_NAME}
contentMarkdown={memo.contentMarkdown}
disabled={readOnly}
mobileNotebookPickerOpen={mobileNotebookSheetOpen}
+2 -2
View File
@@ -37,6 +37,7 @@ import { MemoEditorMetadataRow } from "@/components/MemoEditorMetadataRow";
import { MemoEditorFocusModeButton, MemoEditorTopRowLeading, MemoEditorUpdatedLabel } from "@/components/MemoEditorTopRowLeading";
import { MemoEditorToolbarDivider } from "@/components/MemoEditorToolbarChrome";
import {
MEMO_EDITOR_METADATA_ROW_CLASS_NAME,
MEMO_EDITOR_READING_GUTTER_CLASS_NAME,
MEMO_EDITOR_READING_GUTTER_PROPERTIES_CLASS_NAME,
MEMO_EDITOR_TOP_ROW_CLASS_NAME,
@@ -3650,7 +3651,6 @@ const RichEditorPane = ({
className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-4 sm:flex-nowrap"
>
<MemoEditorTopRowLeading
className="min-w-0 flex-1"
mobileBackButton={(
<Button
className="lg:hidden"
@@ -3679,7 +3679,7 @@ const RichEditorPane = ({
)}
/>
<MemoEditorMetadataRow
rowClassName="shrink-0 flex-nowrap"
rowClassName={MEMO_EDITOR_METADATA_ROW_CLASS_NAME}
contentMarkdown={currentMarkdownForAi}
disabled={effectiveReadOnly}
mobileNotebookPickerOpen={mobileNotebookSheetOpen}
+2 -2
View File
@@ -147,11 +147,11 @@ export const EditorTagPicker = ({ contentMarkdown, disabled, loadTags, title, va
return (
<>
<div className="flex min-w-0 max-w-full items-center gap-1">
<div className="flex min-w-0 max-w-full flex-1 items-center gap-1 overflow-hidden">
<button
type="button"
disabled={disabled}
className="flex h-7 min-w-0 max-w-[32rem] items-center gap-1 rounded-md border border-transparent px-1.5 text-left text-xs text-slate-500 outline-none transition hover:border-slate-200 hover:bg-slate-50 focus-visible:border-slate-300 focus-visible:ring-2 focus-visible:ring-slate-400/25 disabled:opacity-50 sm:gap-1.5 sm:px-2"
className="flex h-7 min-w-0 flex-1 overflow-hidden max-w-[32rem] items-center gap-1 rounded-md border border-transparent px-1.5 text-left text-xs text-slate-500 outline-none transition hover:border-slate-200 hover:bg-slate-50 focus-visible:border-slate-300 focus-visible:ring-2 focus-visible:ring-slate-400/25 disabled:opacity-50 sm:gap-1.5 sm:px-2"
aria-label={t("editor.tagPicker.open")}
onClick={() => setOpen(true)}
>
@@ -13,7 +13,7 @@ import { IconTooltip } from "@/components/editor/EditorPaneChrome";
import { MemoEditorHeaderActions } from "@/components/MemoEditorHeaderActions";
import { MemoEditorMetadataRow } from "@/components/MemoEditorMetadataRow";
import { MemoEditorFocusModeButton, MemoEditorTopRowLeading, MemoEditorUpdatedLabel } from "@/components/MemoEditorTopRowLeading";
import { MEMO_EDITOR_TOP_ROW_CLASS_NAME, nextTitleStatusClearance } from "@/components/MemoEditorChromeDensity";
import { MEMO_EDITOR_METADATA_ROW_CLASS_NAME, MEMO_EDITOR_TOP_ROW_CLASS_NAME, nextTitleStatusClearance } from "@/components/MemoEditorChromeDensity";
import { MemoEditorToolbarDivider } from "@/components/MemoEditorToolbarChrome";
import { MemoTitleInput } from "@/components/MemoTitleInput";
import { api } from "@/lib/api";
@@ -550,7 +550,6 @@ export default function InfographicEditorPane({
<div className="min-w-0 w-full" style={titleStatusClearancePx > 0 ? { paddingRight: titleStatusClearancePx } : undefined}>
<div ref={setHeaderTitleSlot} className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-4 sm:flex-nowrap">
<MemoEditorTopRowLeading
className="min-w-0 flex-1"
mobileBackButton={(
<IconTooltip label={t("common.back")}>
<Button variant="ghost" size="icon" className="lg:hidden" onClick={onBackToList} aria-label={t("common.back")}><ChevronLeft className="h-4 w-4" /></Button>
@@ -561,7 +560,7 @@ export default function InfographicEditorPane({
)}
/>
<MemoEditorMetadataRow
rowClassName="shrink-0 flex-nowrap"
rowClassName={MEMO_EDITOR_METADATA_ROW_CLASS_NAME}
contentMarkdown={syntax}
disabled={readOnly}
mobileNotebookPickerOpen={mobileNotebookSheetOpen}
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { nextTitleStatusClearance } from "./MemoEditorChromeDensity.ts";
import { readFileSync } from "node:fs";
import { MEMO_EDITOR_METADATA_ROW_CLASS_NAME, nextTitleStatusClearance } from "./MemoEditorChromeDensity.ts";
describe("title status clearance", () => {
test("pads the title until it clears the status cluster", () => {
@@ -16,3 +17,25 @@ describe("title status clearance", () => {
expect(nextTitleStatusClearance(480, 2000, 1000)).toBe(480);
});
});
describe("title row width", () => {
test("keeps the title field ahead of notebook and tags on a compact desktop row", () => {
expect(MEMO_EDITOR_METADATA_ROW_CLASS_NAME).toContain("min-w-0");
expect(MEMO_EDITOR_METADATA_ROW_CLASS_NAME).toContain("shrink");
expect(MEMO_EDITOR_METADATA_ROW_CLASS_NAME).not.toContain("shrink-0");
expect(MEMO_EDITOR_METADATA_ROW_CLASS_NAME).not.toContain("max-w-[40%]");
const leading = readFileSync(new URL("./MemoEditorTopRowLeading.tsx", import.meta.url), "utf8");
expect(leading).toContain("basis-full");
expect(leading).toContain("sm:min-w-[min(12rem,45%)]");
expect(leading).toContain("sm:flex-1");
const metadata = readFileSync(new URL("./MemoEditorMetadataRow.tsx", import.meta.url), "utf8");
expect(metadata).toContain("min-w-0 max-w-[18rem] shrink");
expect(metadata).toContain("[&>span]:truncate");
expect(metadata).not.toContain("min-w-[9rem]");
for (const file of ["EditorPane.tsx", "DiagramEditorPane.tsx", "InfographicEditorPane.tsx"]) {
const source = readFileSync(new URL(`./${file}`, import.meta.url), "utf8");
expect(source).toContain("rowClassName={MEMO_EDITOR_METADATA_ROW_CLASS_NAME}");
expect(source).not.toContain('className="min-w-0 flex-1"');
}
});
});
@@ -7,6 +7,13 @@ export const MEMO_EDITOR_READING_GUTTER_CLASS_NAME =
export const MEMO_EDITOR_TOP_ROW_CLASS_NAME =
"relative flex min-h-12 items-center justify-between gap-2 border-b border-slate-100 py-2 sm:min-h-9 sm:py-0.5";
/**
* Notebook and tags share the title row and shrink before the title field.
* The title's own minimum width is what keeps typed text visible.
*/
export const MEMO_EDITOR_METADATA_ROW_CLASS_NAME =
"min-w-0 max-w-full shrink overflow-hidden flex-nowrap";
export const MEMO_EDITOR_TOOLBAR_PADDING_CLASS_NAME =
"px-3 py-2 sm:px-4 sm:py-0.5";
@@ -60,13 +60,13 @@ export const MemoEditorMetadataRow = ({
<span className="min-w-0 truncate">{currentNotebookLabel}</span>
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-slate-400" />
</button>
<div className="hidden min-w-[9rem] max-w-[18rem] sm:block">
<div className="hidden min-w-0 max-w-[18rem] shrink overflow-hidden sm:block">
<Select
value={selectedNotebookId}
disabled={disabled || notebookUpdatePending}
onValueChange={onNotebookChange}
>
<SelectTrigger className="h-7 min-w-0 whitespace-nowrap border-transparent bg-transparent px-2 text-xs font-medium text-slate-600 hover:border-slate-200 hover:bg-slate-50 hover:text-slate-900">
<SelectTrigger className="h-7 w-auto max-w-full min-w-0 overflow-hidden border-transparent bg-transparent px-2 text-xs font-medium text-slate-600 hover:border-slate-200 hover:bg-slate-50 hover:text-slate-900 [&_svg]:shrink-0 [&>span]:min-w-0 [&>span]:truncate">
<SelectValue placeholder={t("editor.notebookPlaceholder")}>{currentNotebookLabel}</SelectValue>
</SelectTrigger>
<SelectContent className="max-h-60 rounded-md border border-slate-200 bg-card py-1 shadow-md">
@@ -46,7 +46,7 @@ export const MemoEditorTopRowLeading = ({
mobileBackButton?: ReactNode;
titleInput?: ReactNode;
}) => (
<div className={cn("flex min-w-0 flex-1 items-center gap-2 text-sm", className)}>
<div className={cn("flex min-w-0 basis-full items-center gap-2 text-sm sm:min-w-[min(12rem,45%)] sm:flex-1", className)}>
{mobileBackButton}
{titleInput && <div className="min-w-0 flex-1">{titleInput}</div>}
</div>
+1 -1
View File
@@ -23,7 +23,7 @@ export const MemoTitleInput = ({
{...props}
aria-label={ariaLabel ?? placeholder}
className={cn(
"block w-full rounded-md border-0 bg-transparent px-2 py-1 text-sm font-semibold leading-tight text-slate-900 outline-none transition placeholder:text-slate-300 hover:bg-slate-100/60 focus-visible:bg-muted focus-visible:shadow-[inset_3px_0_0_rgb(var(--slate-950-rgb))] read-only:text-slate-600 read-only:hover:bg-transparent read-only:focus-visible:bg-transparent read-only:focus-visible:shadow-none sm:text-base",
"block w-full min-w-0 rounded-md border-0 bg-transparent px-2 py-1 text-sm font-semibold leading-tight text-slate-900 outline-none transition placeholder:text-slate-300 hover:bg-slate-100/60 focus-visible:bg-muted focus-visible:shadow-[inset_3px_0_0_rgb(var(--slate-950-rgb))] read-only:text-slate-600 read-only:hover:bg-transparent read-only:focus-visible:bg-transparent read-only:focus-visible:shadow-none sm:text-base",
className
)}
maxLength={160}