diff --git a/web/src/app.css b/web/src/app.css
index 48ae7fb8d..8c2135fb4 100644
--- a/web/src/app.css
+++ b/web/src/app.css
@@ -138,6 +138,11 @@ html {
--color-shadow-ambient-strong: var(--color-alpha-dark-10);
--color-shadow-ambient-subtle: var(--color-alpha-dark-5);
+ --color-inverse-background: #1f2937;
+ --color-inverse-foreground: #f3f4f6;
+ --color-inverse-border: #374151;
+ --color-inverse-shadow-inner: #111827;
+
--shadow-small: 0 2px 4px var(--color-shadow-ambient-strong);
--shadow-regular:
0 4px 12px var(--color-shadow-ambient-subtle), 0 2px 4px var(--color-shadow-ambient-subtle);
@@ -761,6 +766,11 @@ html {
--color-shadow-ambient-strong: transparent;
--color-shadow-ambient-subtle: transparent;
+ --color-inverse-background: #ffffff;
+ --color-inverse-foreground: #111827;
+ --color-inverse-border: #e5e7eb;
+ --color-inverse-shadow-inner: #e5e7eb;
+
/* --- terminal (Dark) --- catppuccin macchiato */
--color-ansi-black: #a5adcb;
--color-ansi-red: #ed8796;
diff --git a/web/src/lib/components/ui/Button.svelte b/web/src/lib/components/ui/Button.svelte
index 05c57ccc7..b93569f70 100644
--- a/web/src/lib/components/ui/Button.svelte
+++ b/web/src/lib/components/ui/Button.svelte
@@ -52,7 +52,7 @@
]
},
size: {
- mini: "min-h-6 gap-2xs px-2xs py-3xs typography-paragraph-mini",
+ mini: "min-h-6 gap-2xs px-2xs py-3xs typography-paragraph-mini min-w-xl",
small: "min-h-7 gap-2xs px-2 py-3xs typography-paragraph-small",
regular: "min-h-8 gap-1.5 px-sm py-3xs typography-paragraph-regular",
large: "min-h-9 gap-xs px-sm py-3xs typography-paragraph-large"
diff --git a/web/src/lib/components/ui/Dropdown.svelte b/web/src/lib/components/ui/Dropdown.svelte
index 3e30c3d50..81411976c 100644
--- a/web/src/lib/components/ui/Dropdown.svelte
+++ b/web/src/lib/components/ui/Dropdown.svelte
@@ -42,6 +42,7 @@
xOffset?: number;
title?: string;
origin?: PopoverOrigin;
+ onOpenChange?: (open: boolean) => void;
}
// opening one dropdown in a group closes its siblings.
@@ -91,7 +92,8 @@
children,
xOffset,
title,
- origin
+ origin,
+ onOpenChange
}: DropdownProps = $props();
let open = $state(false);
@@ -114,6 +116,7 @@
const onToggle = (event: Event) => {
const { newState } = event as ToggleEvent;
open = newState === "open";
+ onOpenChange?.(open);
if (!open) return;
closeGroupExcept(group, close);
diff --git a/web/src/lib/components/ui/Kbd.stories.svelte b/web/src/lib/components/ui/Kbd.stories.svelte
new file mode 100644
index 000000000..ea0f05b7c
--- /dev/null
+++ b/web/src/lib/components/ui/Kbd.stories.svelte
@@ -0,0 +1,53 @@
+
+
+
+ {#snippet template()}
+ K
+ {/snippet}
+
+
+
+ {#snippet template()}
+
+ ⌘
+ ⇧
+ X
+
+ {/snippet}
+
+
+
+ {#snippet template()}
+
+ Ctrl
+ Shift
+ Enter
+
+ {/snippet}
+
+
+
+ {#snippet template()}
+
+ {#snippet content()}
+
+ Link
+ ⌘K
+
+ {/snippet}
+ Hover target
+
+ {/snippet}
+
diff --git a/web/src/lib/components/ui/Kbd.svelte b/web/src/lib/components/ui/Kbd.svelte
new file mode 100644
index 000000000..01f52b3b7
--- /dev/null
+++ b/web/src/lib/components/ui/Kbd.svelte
@@ -0,0 +1,32 @@
+
+
+
+
+{@render children()}
diff --git a/web/src/lib/components/ui/MarkdownEditor.stories.svelte b/web/src/lib/components/ui/MarkdownEditor.stories.svelte
index 052a00ad5..96142e6cf 100644
--- a/web/src/lib/components/ui/MarkdownEditor.stories.svelte
+++ b/web/src/lib/components/ui/MarkdownEditor.stories.svelte
@@ -98,3 +98,19 @@
await expect(Math.abs(editorLine(textarea) - 160)).toBeLessThan(1);
}}
/>
+ {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "List" });
+ const tip = () =>
+ Array.from(document.querySelectorAll('[role="tooltip"]')).find(
+ (bubble) => bubble.textContent?.trim() === "List"
+ );
+ await userEvent.hover(trigger);
+ await waitFor(() => expect(tip()?.matches(":popover-open")).toBe(true));
+ await userEvent.click(trigger);
+ await waitFor(() => expect(canvas.getByRole("menu")).toBeVisible());
+ await expect(tip()?.matches(":popover-open")).toBe(false);
+ }}
+/>
diff --git a/web/src/lib/components/ui/MarkdownEditor.svelte b/web/src/lib/components/ui/MarkdownEditor.svelte
index aa68d988a..b869232b7 100644
--- a/web/src/lib/components/ui/MarkdownEditor.svelte
+++ b/web/src/lib/components/ui/MarkdownEditor.svelte
@@ -20,6 +20,7 @@
import Dropdown from "$lib/components/ui/Dropdown.svelte";
import DropdownItem from "$lib/components/ui/DropdownItem.svelte";
import EmptyState from "$lib/components/ui/EmptyState.svelte";
+ import Kbd from "$lib/components/ui/Kbd.svelte";
import MarkdownTypeahead from "$lib/components/ui/MarkdownTypeahead.svelte";
import Pending from "$lib/components/ui/Pending.svelte";
import Textarea from "$lib/components/ui/Textarea.svelte";
@@ -121,10 +122,11 @@
let entryLine = 0;
let alignedScroll = 0;
let previewScrollLine: number | null = null;
- let modifier = $state("Ctrl+");
+ let mac = $state(false);
+ let listOpen = $state(false);
onMount(() => {
- if (/Mac|iPhone|iPad/.test(navigator.platform)) modifier = "⌘";
+ mac = /Mac|iPhone|iPad/.test(navigator.platform);
});
$effect(() => {
@@ -137,8 +139,25 @@
: null
);
- const shortcut = (tool: Tool) =>
- tool.key ? `${modifier}${tool.shift ? "Shift+" : ""}${tool.key.toUpperCase()}` : undefined;
+ const shortcutKeys = (tool: Tool): string[] =>
+ tool.key
+ ? [
+ mac ? "⌘" : "Ctrl",
+ ...(tool.shift ? [mac ? "⇧" : "Shift"] : []),
+ tool.key.toUpperCase()
+ ]
+ : [];
+
+ const ariaShortcut = (tool: Tool) =>
+ tool.key
+ ? ["Control", "Meta"]
+ .map((modifier) =>
+ [modifier, ...(tool.shift ? ["Shift"] : []), tool.key!.toUpperCase()].join(
+ "+"
+ )
+ )
+ .join(" ")
+ : undefined;
const apply = (format: (selection: Selection) => Edit) => {
if (!textareaEl) return;
@@ -199,14 +218,26 @@
{#snippet toolButton(tool: Tool)}
- {@const hint = shortcut(tool)}
-
+ {@const keys = shortcutKeys(tool)}
+ 0 ? "pt-1" : undefined}>
+ {#snippet content()}
+
+ {tool.label}
+ {#if keys.length > 0}
+
+ {#each keys as key (key)}
+ {key}
+ {/each}
+
+ {/if}
+
+ {/snippet}