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}