-
+
+ {flowQuickCreate ? (
+
{
+ if (event.key !== "Escape") return;
+ event.preventDefault();
+ dismissFlowQuickCreate();
+ containerRef.current?.focus({ preventScroll: true });
+ }}
+ >
+
{t("diagram.quickCreateTitle")}
+
+
+
+
+
+
+ ) : null}
{nodeEditor ? (
{
+ const normalized = value / 255;
+ return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
+};
+
+const luminance = (hex) => {
+ const channels = hex.match(/[a-f\d]{2}/gi).map((value) => channel(Number.parseInt(value, 16)));
+ return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
+};
+
+const contrast = (foreground, background) => {
+ const values = [luminance(foreground), luminance(background)].sort((left, right) => right - left);
+ return (values[0] + 0.05) / (values[1] + 0.05);
+};
+
+describe("diagram appearance palettes", () => {
+ test("provides distinct night-mode surfaces for every diagram theme", () => {
+ for (const theme of Object.keys(DIAGRAM_THEME_PALETTES)) {
+ const light = resolveDiagramPalette(theme, "light");
+ const dark = resolveDiagramPalette(theme, "dark");
+ expect(dark.canvas).not.toBe(light.canvas);
+ expect(luminance(dark.canvas)).toBeLessThan(0.02);
+ expect(contrast(dark.nodeText, dark.nodeFill)).toBeGreaterThanOrEqual(4.5);
+ expect(contrast(dark.topicText, dark.topicFill)).toBeGreaterThanOrEqual(3);
+ }
+ });
+});
diff --git a/apps/web/src/lib/diagram-theme.ts b/apps/web/src/lib/diagram-theme.ts
new file mode 100644
index 00000000..3e9b3e02
--- /dev/null
+++ b/apps/web/src/lib/diagram-theme.ts
@@ -0,0 +1,109 @@
+import type { DiagramTheme } from "@edgeever/shared";
+
+export type DiagramAppearance = "light" | "dark";
+
+export type DiagramPalette = {
+ topicFill: string;
+ topicText: string;
+ nodeFill: string;
+ nodeText: string;
+ nodeStroke: string;
+ topicStroke: string;
+ mindMapEdge: string;
+ flowEdge: string;
+ canvas: string;
+ grid: string;
+ gridStrong: string;
+};
+
+const BRAND_GREEN = "#16A06E";
+
+export const DIAGRAM_THEME_PALETTES: Record
> = {
+ brand: {
+ light: {
+ topicFill: BRAND_GREEN,
+ topicText: "#FFFFFF",
+ nodeFill: "#F0F8F4",
+ nodeText: "#173B2E",
+ nodeStroke: "#B8DFD0",
+ topicStroke: "#12845B",
+ mindMapEdge: "#55B891",
+ flowEdge: "#408A6D",
+ canvas: "#F8FAF9",
+ grid: "#E6EEE9",
+ gridStrong: "#CFDDD5",
+ },
+ dark: {
+ topicFill: BRAND_GREEN,
+ topicText: "#F4FFF9",
+ nodeFill: "#18211D",
+ nodeText: "#E8F2ED",
+ nodeStroke: "#3B5248",
+ topicStroke: "#58CDA4",
+ mindMapEdge: "#4DB58B",
+ flowEdge: "#72B99B",
+ canvas: "#101311",
+ grid: "#1D2722",
+ gridStrong: "#2B3A33",
+ },
+ },
+ ocean: {
+ light: {
+ topicFill: "#DEF1E9",
+ topicText: "#0F4432",
+ nodeFill: "#FFFEFA",
+ nodeText: "#26352E",
+ nodeStroke: "#D5E3DB",
+ topicStroke: BRAND_GREEN,
+ mindMapEdge: "#8ACCB2",
+ flowEdge: "#6B9281",
+ canvas: "#FBFCFA",
+ grid: "#EBF0EC",
+ gridStrong: "#D9E3DC",
+ },
+ dark: {
+ topicFill: "#244D3D",
+ topicText: "#DFF7ED",
+ nodeFill: "#17201C",
+ nodeText: "#E6F0EB",
+ nodeStroke: "#3B554A",
+ topicStroke: "#65C9A4",
+ mindMapEdge: "#55B992",
+ flowEdge: "#77AD97",
+ canvas: "#111713",
+ grid: "#202A25",
+ gridStrong: "#304039",
+ },
+ },
+ ink: {
+ light: {
+ topicFill: BRAND_GREEN,
+ topicText: "#F5FBF8",
+ nodeFill: "#19261F",
+ nodeText: "#E5EEE9",
+ nodeStroke: "#3C594A",
+ topicStroke: "#68D6B0",
+ mindMapEdge: "#58BA94",
+ flowEdge: "#68A78D",
+ canvas: "#101512",
+ grid: "#202B25",
+ gridStrong: "#304038",
+ },
+ dark: {
+ topicFill: BRAND_GREEN,
+ topicText: "#F5FBF8",
+ nodeFill: "#151D19",
+ nodeText: "#E7F0EB",
+ nodeStroke: "#3C594A",
+ topicStroke: "#68D6B0",
+ mindMapEdge: "#58BA94",
+ flowEdge: "#72B296",
+ canvas: "#0B0F0D",
+ grid: "#19211D",
+ gridStrong: "#29362F",
+ },
+ },
+};
+
+export const resolveDiagramPalette = (theme: DiagramTheme, appearance: DiagramAppearance) =>
+ DIAGRAM_THEME_PALETTES[theme][appearance];
diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css
index 22036c33..e459c26b 100644
--- a/apps/web/src/styles/globals.css
+++ b/apps/web/src/styles/globals.css
@@ -499,16 +499,21 @@ html[data-edgeever-extension-theme] .edgeever-memo-divider {
box-shadow: 0 2px 10px rgb(104 214 176 / 0.22) !important;
}
-@media (hover: hover) and (pointer: fine) {
- .edgeever-diagram-canvas .x6-port-body {
- opacity: 0;
- transition: opacity 120ms ease;
- }
+.edgeever-diagram-canvas .x6-port-body {
+ opacity: 0;
+ transition: opacity 120ms ease, filter 120ms ease;
+}
- .edgeever-diagram-canvas .x6-node:hover .x6-port-body,
- .edgeever-diagram-canvas .x6-port-body:hover {
- opacity: 1;
- }
+.edgeever-diagram-canvas[data-diagram-kind="flowchart"] .x6-port-body {
+ cursor: crosshair;
+ opacity: 0.72;
+}
+
+.edgeever-diagram-canvas[data-diagram-kind="flowchart"] .x6-node:hover .x6-port-body,
+.edgeever-diagram-canvas[data-diagram-kind="flowchart"] .x6-port-body:hover,
+.edgeever-diagram-canvas[data-diagram-kind="flowchart"] .x6-available-magnet {
+ filter: drop-shadow(0 1px 3px rgb(var(--brand-green-rgb) / 0.28));
+ opacity: 1;
}
/* Curated reading themes should feel like part of the note canvas in dark
diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts
index 3862e1e8..a0ab9c59 100644
--- a/packages/shared/src/i18n/en-US.ts
+++ b/packages/shared/src/i18n/en-US.ts
@@ -252,8 +252,12 @@ export const enUS = {
addStep: "Add step",
addDecision: "Add decision",
addTerminator: "Add start or end",
+ connectHint: "Drag a handle to connect, or drop on canvas to create the next node",
newTopic: "New topic",
newStep: "New step",
+ newDecision: "Decision",
+ newTerminator: "Start / end",
+ quickCreateTitle: "Choose the next node type",
nodeText: "Node text",
editNode: "Edit node text",
deleteSelection: "Delete selection",
diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts
index b23eff6a..568d8336 100644
--- a/packages/shared/src/i18n/zh-CN.ts
+++ b/packages/shared/src/i18n/zh-CN.ts
@@ -252,8 +252,12 @@ export const zhCN = {
addStep: "添加步骤",
addDecision: "添加判断节点",
addTerminator: "添加起止节点",
+ connectHint: "拖动端点连线;松开在空白处可创建后续节点",
newTopic: "新主题",
newStep: "新步骤",
+ newDecision: "判断条件",
+ newTerminator: "开始/结束",
+ quickCreateTitle: "选择后续节点类型",
nodeText: "节点文字",
editNode: "编辑节点文字",
deleteSelection: "删除所选内容",