@autono/open-pages 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +108 -0
  3. package/bin.js +2 -0
  4. package/dist/build-TP72kiw7.js +35 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +100 -0
  7. package/dist/config-C9or4m4J.d.ts +23 -0
  8. package/dist/config-L0IbktJ0.js +3135 -0
  9. package/dist/design-C13iz9_4.js +35 -0
  10. package/dist/dev-I2IyEh5W.js +97 -0
  11. package/dist/export-B-kBRWB1.js +4 -0
  12. package/dist/export-ic28osQP.js +131 -0
  13. package/dist/index.d.ts +56 -0
  14. package/dist/index.js +3 -0
  15. package/dist/locale/index.d.ts +24 -0
  16. package/dist/locale/index.js +881 -0
  17. package/dist/open-pages-plugin-D_DN_zjh.js +632 -0
  18. package/dist/preview-BrDuvgAN.js +21 -0
  19. package/dist/sync-Be_hxSOF.js +139 -0
  20. package/dist/sync-lKEXyPOh.js +3 -0
  21. package/dist/types-C5NCdDT-.d.ts +219 -0
  22. package/dist/update-CV62NhBT.js +37 -0
  23. package/dist/update-package-Cxt57pz7.js +152 -0
  24. package/dist/version-B6AiiGvT.js +26 -0
  25. package/dist/version-CeffgdfZ.js +3 -0
  26. package/dist/vite/index.d.ts +14 -0
  27. package/dist/vite/index.js +7 -0
  28. package/env.d.ts +59 -0
  29. package/package.json +106 -0
  30. package/skills/apply-comments/SKILL.md +86 -0
  31. package/skills/create-page/SKILL.md +92 -0
  32. package/skills/create-theme/SKILL.md +223 -0
  33. package/skills/current-page/SKILL.md +106 -0
  34. package/skills/page-authoring/SKILL.md +148 -0
  35. package/skills/page-authoring/references/assets-and-fonts.md +85 -0
  36. package/skills/page-authoring/references/html-pages.md +65 -0
  37. package/skills/page-authoring/references/interactivity.md +77 -0
  38. package/skills/page-authoring/references/layout-and-responsive.md +71 -0
  39. package/skills/page-authoring/references/typography-and-color.md +60 -0
  40. package/src/app/app.tsx +45 -0
  41. package/src/app/components/asset-view.tsx +1665 -0
  42. package/src/app/components/command/command-menu.tsx +227 -0
  43. package/src/app/components/command/command.tsx +142 -0
  44. package/src/app/components/command/home-command-menu.tsx +96 -0
  45. package/src/app/components/icon-tooltip.tsx +34 -0
  46. package/src/app/components/language-toggle.tsx +46 -0
  47. package/src/app/components/sidebar/folder-item.tsx +269 -0
  48. package/src/app/components/sidebar/icon-picker.tsx +61 -0
  49. package/src/app/components/sidebar/sidebar-footer.tsx +139 -0
  50. package/src/app/components/sidebar/sidebar.tsx +302 -0
  51. package/src/app/components/theme-toggle.tsx +66 -0
  52. package/src/app/components/themes/theme-detail.tsx +226 -0
  53. package/src/app/components/themes/themes-gallery.tsx +104 -0
  54. package/src/app/components/ui/badge.tsx +48 -0
  55. package/src/app/components/ui/button.tsx +93 -0
  56. package/src/app/components/ui/card.tsx +92 -0
  57. package/src/app/components/ui/context-menu.tsx +247 -0
  58. package/src/app/components/ui/dialog.tsx +155 -0
  59. package/src/app/components/ui/dropdown-menu.tsx +267 -0
  60. package/src/app/components/ui/input.tsx +25 -0
  61. package/src/app/components/ui/label.tsx +21 -0
  62. package/src/app/components/ui/popover.tsx +82 -0
  63. package/src/app/components/ui/progress.tsx +33 -0
  64. package/src/app/components/ui/scroll-area.tsx +53 -0
  65. package/src/app/components/ui/select.tsx +195 -0
  66. package/src/app/components/ui/separator.tsx +26 -0
  67. package/src/app/components/ui/slider.tsx +68 -0
  68. package/src/app/components/ui/sonner.tsx +48 -0
  69. package/src/app/components/ui/tabs.tsx +79 -0
  70. package/src/app/components/ui/textarea.tsx +22 -0
  71. package/src/app/components/ui/toggle-group.tsx +84 -0
  72. package/src/app/components/ui/toggle.tsx +45 -0
  73. package/src/app/components/ui/tooltip.tsx +75 -0
  74. package/src/app/favicon.ico +0 -0
  75. package/src/app/frame/inspect.ts +140 -0
  76. package/src/app/frame/main.tsx +91 -0
  77. package/src/app/frame.html +12 -0
  78. package/src/app/index.html +13 -0
  79. package/src/app/lib/asset-filter.test.ts +176 -0
  80. package/src/app/lib/asset-filter.ts +80 -0
  81. package/src/app/lib/assets.ts +257 -0
  82. package/src/app/lib/design.ts +58 -0
  83. package/src/app/lib/folders.ts +239 -0
  84. package/src/app/lib/frame.ts +38 -0
  85. package/src/app/lib/locale-store.ts +67 -0
  86. package/src/app/lib/page-thumb.tsx +46 -0
  87. package/src/app/lib/pages.ts +28 -0
  88. package/src/app/lib/sdk.ts +43 -0
  89. package/src/app/lib/themes.ts +22 -0
  90. package/src/app/lib/use-locale.ts +8 -0
  91. package/src/app/lib/use-page-module.ts +48 -0
  92. package/src/app/lib/use-page-titles.ts +29 -0
  93. package/src/app/lib/use-restart-server.ts +78 -0
  94. package/src/app/lib/utils.test.ts +25 -0
  95. package/src/app/lib/utils.ts +6 -0
  96. package/src/app/main.tsx +14 -0
  97. package/src/app/routes/assets.tsx +9 -0
  98. package/src/app/routes/home-shell.tsx +252 -0
  99. package/src/app/routes/home.tsx +846 -0
  100. package/src/app/routes/page.tsx +329 -0
  101. package/src/app/routes/themes.tsx +34 -0
  102. package/src/app/styles.css +405 -0
  103. package/src/app/virtual.d.ts +53 -0
  104. package/src/locale/en.ts +218 -0
  105. package/src/locale/format.ts +12 -0
  106. package/src/locale/index.ts +6 -0
  107. package/src/locale/ja.ts +221 -0
  108. package/src/locale/types.ts +215 -0
  109. package/src/locale/zh-cn.ts +218 -0
  110. package/src/locale/zh-tw.ts +218 -0
@@ -0,0 +1,227 @@
1
+ import { Check, Languages, Loader2, Monitor, Moon, RotateCw, Search, Sun } from 'lucide-react';
2
+ import { useTheme } from 'next-themes';
3
+ import { type ReactNode, useEffect, useMemo, useState } from 'react';
4
+ import { IconTooltip } from '@/components/icon-tooltip';
5
+ import { buttonVariants } from '@/components/ui/button';
6
+ import { TooltipTrigger } from '@/components/ui/tooltip';
7
+ import { LOCALE_OPTIONS, setLocale } from '@/lib/locale-store';
8
+ import { format, useLocale } from '@/lib/use-locale';
9
+ import { useRestartServer } from '@/lib/use-restart-server';
10
+ import {
11
+ Command,
12
+ CommandDialog,
13
+ CommandEmpty,
14
+ CommandGroup,
15
+ CommandInput,
16
+ CommandItem,
17
+ CommandList,
18
+ CommandShortcut,
19
+ } from './command';
20
+
21
+ export type CommandSpec = {
22
+ id: string;
23
+ label: string;
24
+ icon?: ReactNode;
25
+ /** Extra match terms — ASCII aliases keep commands findable under any locale. */
26
+ keywords?: string[];
27
+ shortcut?: string;
28
+ disabled?: boolean;
29
+ active?: boolean;
30
+ run: () => void | Promise<void>;
31
+ };
32
+
33
+ export type CommandGroupSpec = {
34
+ id: string;
35
+ heading: string;
36
+ items: CommandSpec[];
37
+ /** Long lists (deck pages) only surface once the user types, so the idle menu stays scannable. */
38
+ searchOnly?: boolean;
39
+ };
40
+
41
+ export function CommandMenu({
42
+ open,
43
+ onOpenChange,
44
+ groups,
45
+ placeholder,
46
+ }: {
47
+ open: boolean;
48
+ onOpenChange: (open: boolean) => void;
49
+ groups: CommandGroupSpec[];
50
+ placeholder: string;
51
+ }) {
52
+ const t = useLocale();
53
+ const [search, setSearch] = useState('');
54
+ const sharedGroups = useSharedCommandGroups();
55
+
56
+ useEffect(() => {
57
+ const onKey = (event: KeyboardEvent) => {
58
+ if (event.key !== 'k' && event.key !== 'K') return;
59
+ if (!event.metaKey && !event.ctrlKey) return;
60
+ // Auto-repeat while the combo is held would toggle the menu shut again.
61
+ if (event.repeat) return;
62
+ event.preventDefault();
63
+ onOpenChange(!open);
64
+ };
65
+ window.addEventListener('keydown', onKey);
66
+ return () => window.removeEventListener('keydown', onKey);
67
+ }, [open, onOpenChange]);
68
+
69
+ useEffect(() => {
70
+ if (!open) setSearch('');
71
+ }, [open]);
72
+
73
+ const searching = search.trim().length > 0;
74
+ const visibleGroups = [...groups, ...sharedGroups].filter(
75
+ (group) => group.items.length > 0 && (searching || !group.searchOnly),
76
+ );
77
+
78
+ const run = (item: CommandSpec) => {
79
+ if (item.disabled) return;
80
+ // Close first: exports, fullscreen and panels all expect an unobstructed page.
81
+ onOpenChange(false);
82
+ void item.run();
83
+ };
84
+
85
+ return (
86
+ <CommandDialog
87
+ open={open}
88
+ onOpenChange={onOpenChange}
89
+ title={t.commandMenu.trigger}
90
+ description={placeholder}
91
+ >
92
+ <Command label={t.commandMenu.trigger} loop>
93
+ <CommandInput value={search} onValueChange={setSearch} placeholder={placeholder} />
94
+ <CommandList>
95
+ <CommandEmpty>{t.commandMenu.empty}</CommandEmpty>
96
+ {visibleGroups.map((group) => (
97
+ <CommandGroup key={group.id} heading={group.heading}>
98
+ {group.items.map((item) => (
99
+ <CommandItem
100
+ key={item.id}
101
+ value={item.id}
102
+ keywords={[item.label, ...(item.keywords ?? [])]}
103
+ disabled={item.disabled}
104
+ onSelect={() => run(item)}
105
+ >
106
+ {item.icon}
107
+ <span className="min-w-0 flex-1 truncate">{item.label}</span>
108
+ {item.active && <Check className="ml-auto opacity-100 text-brand" aria-hidden />}
109
+ {item.shortcut && <CommandShortcut>{item.shortcut}</CommandShortcut>}
110
+ </CommandItem>
111
+ ))}
112
+ </CommandGroup>
113
+ ))}
114
+ </CommandList>
115
+ <CommandFooter />
116
+ </Command>
117
+ </CommandDialog>
118
+ );
119
+ }
120
+
121
+ const IS_APPLE =
122
+ typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
123
+
124
+ export const COMMAND_MENU_SHORTCUT = IS_APPLE ? '⌘K' : 'Ctrl K';
125
+
126
+ export function CommandMenuTrigger({ onClick }: { onClick: () => void }) {
127
+ const t = useLocale();
128
+ return (
129
+ <IconTooltip label={t.commandMenu.triggerTooltip} shortcut={COMMAND_MENU_SHORTCUT}>
130
+ <TooltipTrigger
131
+ type="button"
132
+ onClick={onClick}
133
+ aria-label={t.commandMenu.triggerAria}
134
+ aria-haspopup="dialog"
135
+ className={buttonVariants({ variant: 'ghost', size: 'icon-sm' })}
136
+ >
137
+ <Search className="size-3.5" />
138
+ </TooltipTrigger>
139
+ </IconTooltip>
140
+ );
141
+ }
142
+
143
+ function CommandFooter() {
144
+ const t = useLocale();
145
+ return (
146
+ <div className="flex items-center justify-between gap-3 border-t border-hairline bg-muted/30 px-3 py-2 text-[10.5px] text-muted-foreground">
147
+ <Hint keys="esc" label={t.commandMenu.hintClose} />
148
+ <span className="inline-flex items-center gap-3">
149
+ <Hint keys="↑↓" label={t.commandMenu.hintNavigate} />
150
+ <Hint keys="↵" label={t.commandMenu.hintSelect} />
151
+ </span>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function Hint({ keys, label }: { keys: string; label: string }) {
157
+ return (
158
+ <span className="inline-flex items-center gap-1">
159
+ <kbd className="rounded-[3px] border border-hairline bg-card px-1 py-0.5 font-mono text-[9.5px] leading-none">
160
+ {keys}
161
+ </kbd>
162
+ {label}
163
+ </span>
164
+ );
165
+ }
166
+
167
+ function useSharedCommandGroups(): CommandGroupSpec[] {
168
+ const t = useLocale();
169
+ const { theme, setTheme } = useTheme();
170
+ const { canRestart, restarting, restartServer } = useRestartServer();
171
+
172
+ return useMemo(() => {
173
+ const appearance: CommandSpec[] = [
174
+ {
175
+ id: 'theme-light',
176
+ label: format(t.commandMenu.themeItem, { name: t.themeToggle.light }),
177
+ icon: <Sun />,
178
+ keywords: ['theme', 'light', 'appearance'],
179
+ active: theme === 'light',
180
+ run: () => setTheme('light'),
181
+ },
182
+ {
183
+ id: 'theme-dark',
184
+ label: format(t.commandMenu.themeItem, { name: t.themeToggle.dark }),
185
+ icon: <Moon />,
186
+ keywords: ['theme', 'dark', 'appearance'],
187
+ active: theme === 'dark',
188
+ run: () => setTheme('dark'),
189
+ },
190
+ {
191
+ id: 'theme-system',
192
+ label: format(t.commandMenu.themeItem, { name: t.themeToggle.system }),
193
+ icon: <Monitor />,
194
+ keywords: ['theme', 'system', 'appearance'],
195
+ active: theme === 'system',
196
+ run: () => setTheme('system'),
197
+ },
198
+ ...LOCALE_OPTIONS.map((option) => ({
199
+ id: `locale-${option.id}`,
200
+ label: format(t.commandMenu.languageItem, { name: option.label }),
201
+ icon: <Languages />,
202
+ keywords: ['language', 'locale', option.id],
203
+ active: t.id === option.id,
204
+ run: () => setLocale(option.id),
205
+ })),
206
+ ];
207
+
208
+ const developer: CommandSpec[] =
209
+ import.meta.env.DEV && canRestart
210
+ ? [
211
+ {
212
+ id: 'restart-dev-server',
213
+ label: restarting ? t.home.restartingServer : t.home.restartServer,
214
+ icon: restarting ? <Loader2 className="animate-spin" /> : <RotateCw />,
215
+ keywords: ['restart', 'dev', 'server', 'reload'],
216
+ disabled: restarting,
217
+ run: restartServer,
218
+ },
219
+ ]
220
+ : [];
221
+
222
+ return [
223
+ { id: 'appearance', heading: t.commandMenu.groupAppearance, items: appearance },
224
+ { id: 'developer', heading: t.commandMenu.groupDeveloper, items: developer },
225
+ ];
226
+ }, [t, theme, setTheme, canRestart, restarting, restartServer]);
227
+ }
@@ -0,0 +1,142 @@
1
+ import { Command as CommandPrimitive } from 'cmdk';
2
+ import { Search } from 'lucide-react';
3
+ import type * as React from 'react';
4
+ import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
8
+ return (
9
+ <CommandPrimitive
10
+ data-slot="command"
11
+ className={cn('flex h-full w-full flex-col overflow-hidden', className)}
12
+ {...props}
13
+ />
14
+ );
15
+ }
16
+
17
+ function CommandDialog({
18
+ open,
19
+ onOpenChange,
20
+ title,
21
+ description,
22
+ children,
23
+ }: {
24
+ open: boolean;
25
+ onOpenChange: (open: boolean) => void;
26
+ title: string;
27
+ description: string;
28
+ children: React.ReactNode;
29
+ }) {
30
+ return (
31
+ <Dialog open={open} onOpenChange={onOpenChange}>
32
+ <DialogContent
33
+ showCloseButton={false}
34
+ className="top-[14vh] translate-y-0 gap-0 overflow-hidden p-0 sm:max-w-[560px]"
35
+ >
36
+ <DialogTitle className="sr-only">{title}</DialogTitle>
37
+ <DialogDescription className="sr-only">{description}</DialogDescription>
38
+ {children}
39
+ </DialogContent>
40
+ </Dialog>
41
+ );
42
+ }
43
+
44
+ function CommandInput({
45
+ className,
46
+ ...props
47
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
48
+ return (
49
+ <div className="flex items-center gap-2 border-b border-hairline px-3">
50
+ <Search className="size-3.5 shrink-0 text-muted-foreground" aria-hidden />
51
+ <CommandPrimitive.Input
52
+ data-slot="command-input"
53
+ className={cn(
54
+ 'h-11 w-full bg-transparent text-[13px] outline-none placeholder:text-muted-foreground/70 disabled:opacity-50',
55
+ className,
56
+ )}
57
+ {...props}
58
+ />
59
+ </div>
60
+ );
61
+ }
62
+
63
+ function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
64
+ return (
65
+ <CommandPrimitive.List
66
+ data-slot="command-list"
67
+ className={cn(
68
+ 'max-h-[min(56vh,380px)] scroll-py-1 overflow-y-auto overscroll-contain p-1',
69
+ className,
70
+ )}
71
+ {...props}
72
+ />
73
+ );
74
+ }
75
+
76
+ function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
77
+ return (
78
+ <CommandPrimitive.Empty
79
+ data-slot="command-empty"
80
+ className="px-3 py-10 text-center text-[12.5px] text-muted-foreground"
81
+ {...props}
82
+ />
83
+ );
84
+ }
85
+
86
+ function CommandGroup({
87
+ className,
88
+ ...props
89
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
90
+ return (
91
+ <CommandPrimitive.Group
92
+ data-slot="command-group"
93
+ className={cn(
94
+ 'overflow-hidden pt-1.5 pb-1 [&:not(:first-child)]:mt-1 [&:not(:first-child)]:border-t [&:not(:first-child)]:border-hairline',
95
+ '[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pt-1 [&_[cmdk-group-heading]]:pb-1.5',
96
+ '[&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:tracking-[0.08em] [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:text-muted-foreground/70',
97
+ className,
98
+ )}
99
+ {...props}
100
+ />
101
+ );
102
+ }
103
+
104
+ function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
105
+ return (
106
+ <CommandPrimitive.Item
107
+ data-slot="command-item"
108
+ className={cn(
109
+ 'relative flex cursor-default select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-[12.5px] outline-none',
110
+ 'data-[selected=true]:bg-muted data-[selected=true]:text-foreground',
111
+ 'data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-45',
112
+ "[&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:text-muted-foreground [&_svg:not([class*='opacity'])]:opacity-80",
113
+ className,
114
+ )}
115
+ {...props}
116
+ />
117
+ );
118
+ }
119
+
120
+ function CommandShortcut({ className, ...props }: React.ComponentProps<'kbd'>) {
121
+ return (
122
+ <kbd
123
+ data-slot="command-shortcut"
124
+ className={cn(
125
+ 'ml-auto shrink-0 rounded-[3px] bg-muted px-1.5 py-0.5 font-mono text-[9.5px] tracking-[0.04em] text-muted-foreground',
126
+ className,
127
+ )}
128
+ {...props}
129
+ />
130
+ );
131
+ }
132
+
133
+ export {
134
+ Command,
135
+ CommandDialog,
136
+ CommandEmpty,
137
+ CommandGroup,
138
+ CommandInput,
139
+ CommandItem,
140
+ CommandList,
141
+ CommandShortcut,
142
+ };
@@ -0,0 +1,96 @@
1
+ import { Image as ImageIcon, Palette, Presentation } from 'lucide-react';
2
+ import { useMemo } from 'react';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { useLocale } from '@/lib/use-locale';
5
+ import { usePageTitles } from '@/lib/use-page-titles';
6
+ import { pageIds } from '../../lib/pages';
7
+ import type { Folder } from '../../lib/sdk';
8
+ import { FolderIconChip } from '../sidebar/folder-item';
9
+ import { ALL_DOCS_ID, ASSETS_ID, DRAFT_ID, THEMES_ID } from '../sidebar/sidebar';
10
+ import { type CommandGroupSpec, CommandMenu, type CommandSpec } from './command-menu';
11
+
12
+ export function HomeCommandMenu({
13
+ open,
14
+ onOpenChange,
15
+ folders,
16
+ titleMap,
17
+ onSelectView,
18
+ }: {
19
+ open: boolean;
20
+ onOpenChange: (open: boolean) => void;
21
+ folders: Folder[];
22
+ titleMap: Record<string, string>;
23
+ onSelectView: (id: string) => void;
24
+ }) {
25
+ const t = useLocale();
26
+ const navigate = useNavigate();
27
+ const loadedTitles = usePageTitles(open);
28
+
29
+ const groups = useMemo<CommandGroupSpec[]>(() => {
30
+ const pages: CommandSpec[] = pageIds.map((id) => ({
31
+ id: `page-${id}`,
32
+ label: titleMap[id] ?? loadedTitles[id] ?? id,
33
+ icon: <Presentation />,
34
+ keywords: [id],
35
+ run: () => navigate(`/p/${id}`),
36
+ }));
37
+
38
+ const folderItems: CommandSpec[] = [
39
+ {
40
+ id: `view-${ALL_DOCS_ID}`,
41
+ label: t.home.pages,
42
+ icon: <FolderIconChip icon={{ type: 'emoji', value: '🎞️' }} />,
43
+ keywords: ['all', 'pages'],
44
+ run: () => onSelectView(ALL_DOCS_ID),
45
+ },
46
+ {
47
+ id: `view-${DRAFT_ID}`,
48
+ label: t.home.draft,
49
+ icon: <FolderIconChip icon={{ type: 'emoji', value: '📝' }} />,
50
+ keywords: ['draft', 'unsorted'],
51
+ run: () => onSelectView(DRAFT_ID),
52
+ },
53
+ ...folders.map((folder) => ({
54
+ id: `view-${folder.id}`,
55
+ label: folder.name,
56
+ icon: <FolderIconChip icon={folder.icon} />,
57
+ keywords: ['folder', folder.id],
58
+ run: () => onSelectView(folder.id),
59
+ })),
60
+ ];
61
+
62
+ const navigation: CommandSpec[] = [
63
+ {
64
+ id: `view-${THEMES_ID}`,
65
+ label: t.home.themes,
66
+ icon: <Palette />,
67
+ keywords: ['themes', 'design'],
68
+ run: () => onSelectView(THEMES_ID),
69
+ },
70
+ ];
71
+ if (import.meta.env.DEV) {
72
+ navigation.push({
73
+ id: `view-${ASSETS_ID}`,
74
+ label: t.home.assets,
75
+ icon: <ImageIcon />,
76
+ keywords: ['assets', 'images', 'files'],
77
+ run: () => onSelectView(ASSETS_ID),
78
+ });
79
+ }
80
+
81
+ return [
82
+ { id: 'pages', heading: t.commandMenu.groupDocs, items: pages },
83
+ { id: 'folders', heading: t.commandMenu.groupFolders, items: folderItems },
84
+ { id: 'navigation', heading: t.commandMenu.groupNavigation, items: navigation },
85
+ ];
86
+ }, [t, folders, titleMap, loadedTitles, navigate, onSelectView]);
87
+
88
+ return (
89
+ <CommandMenu
90
+ open={open}
91
+ onOpenChange={onOpenChange}
92
+ groups={groups}
93
+ placeholder={t.commandMenu.placeholder}
94
+ />
95
+ );
96
+ }
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Tooltip, TooltipContent, TooltipProvider } from '@/components/ui/tooltip';
3
+
4
+ /**
5
+ * Shared hover label for the toolbar icon buttons. `children` supplies the
6
+ * `TooltipTrigger`; a button that also opens a menu has to compose it inward
7
+ * (`<DropdownMenuTrigger render={<TooltipTrigger />}>`) — the other nesting
8
+ * order swallows the trigger ref and the tooltip never opens.
9
+ */
10
+ export function IconTooltip({
11
+ label,
12
+ shortcut,
13
+ children,
14
+ }: {
15
+ label: string;
16
+ shortcut?: string;
17
+ children: ReactNode;
18
+ }) {
19
+ return (
20
+ <TooltipProvider delay={200}>
21
+ <Tooltip>
22
+ {children}
23
+ <TooltipContent side="bottom" sideOffset={6} className="flex items-center gap-1.5">
24
+ {label}
25
+ {shortcut && (
26
+ <kbd className="rounded-[3px] bg-background/18 px-1 py-0.5 font-mono text-[9.5px] tracking-[0.04em] text-background/85">
27
+ {shortcut}
28
+ </kbd>
29
+ )}
30
+ </TooltipContent>
31
+ </Tooltip>
32
+ </TooltipProvider>
33
+ );
34
+ }
@@ -0,0 +1,46 @@
1
+ import { Languages } from 'lucide-react';
2
+ import { IconTooltip } from '@/components/icon-tooltip';
3
+ import { buttonVariants } from '@/components/ui/button';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '@/components/ui/dropdown-menu';
10
+ import { TooltipTrigger } from '@/components/ui/tooltip';
11
+ import { LOCALE_OPTIONS, setLocale } from '@/lib/locale-store';
12
+ import { useLocale } from '@/lib/use-locale';
13
+ import { cn } from '@/lib/utils';
14
+
15
+ export function LanguageToggle() {
16
+ const t = useLocale();
17
+
18
+ return (
19
+ <DropdownMenu>
20
+ <IconTooltip label={t.languageToggle.title}>
21
+ <DropdownMenuTrigger
22
+ render={
23
+ <TooltipTrigger
24
+ type="button"
25
+ aria-label={t.languageToggle.toggleAria}
26
+ className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }))}
27
+ />
28
+ }
29
+ >
30
+ <Languages className="size-3.5" />
31
+ </DropdownMenuTrigger>
32
+ </IconTooltip>
33
+ <DropdownMenuContent align="end" className="min-w-[140px]">
34
+ {LOCALE_OPTIONS.map((option) => (
35
+ <DropdownMenuItem
36
+ key={option.id}
37
+ onClick={() => setLocale(option.id)}
38
+ data-active={t.id === option.id}
39
+ >
40
+ {option.label}
41
+ </DropdownMenuItem>
42
+ ))}
43
+ </DropdownMenuContent>
44
+ </DropdownMenu>
45
+ );
46
+ }