@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.
- package/LICENSE +23 -0
- package/README.md +108 -0
- package/bin.js +2 -0
- package/dist/build-TP72kiw7.js +35 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +100 -0
- package/dist/config-C9or4m4J.d.ts +23 -0
- package/dist/config-L0IbktJ0.js +3135 -0
- package/dist/design-C13iz9_4.js +35 -0
- package/dist/dev-I2IyEh5W.js +97 -0
- package/dist/export-B-kBRWB1.js +4 -0
- package/dist/export-ic28osQP.js +131 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +3 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +881 -0
- package/dist/open-pages-plugin-D_DN_zjh.js +632 -0
- package/dist/preview-BrDuvgAN.js +21 -0
- package/dist/sync-Be_hxSOF.js +139 -0
- package/dist/sync-lKEXyPOh.js +3 -0
- package/dist/types-C5NCdDT-.d.ts +219 -0
- package/dist/update-CV62NhBT.js +37 -0
- package/dist/update-package-Cxt57pz7.js +152 -0
- package/dist/version-B6AiiGvT.js +26 -0
- package/dist/version-CeffgdfZ.js +3 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +7 -0
- package/env.d.ts +59 -0
- package/package.json +106 -0
- package/skills/apply-comments/SKILL.md +86 -0
- package/skills/create-page/SKILL.md +92 -0
- package/skills/create-theme/SKILL.md +223 -0
- package/skills/current-page/SKILL.md +106 -0
- package/skills/page-authoring/SKILL.md +148 -0
- package/skills/page-authoring/references/assets-and-fonts.md +85 -0
- package/skills/page-authoring/references/html-pages.md +65 -0
- package/skills/page-authoring/references/interactivity.md +77 -0
- package/skills/page-authoring/references/layout-and-responsive.md +71 -0
- package/skills/page-authoring/references/typography-and-color.md +60 -0
- package/src/app/app.tsx +45 -0
- package/src/app/components/asset-view.tsx +1665 -0
- package/src/app/components/command/command-menu.tsx +227 -0
- package/src/app/components/command/command.tsx +142 -0
- package/src/app/components/command/home-command-menu.tsx +96 -0
- package/src/app/components/icon-tooltip.tsx +34 -0
- package/src/app/components/language-toggle.tsx +46 -0
- package/src/app/components/sidebar/folder-item.tsx +269 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +139 -0
- package/src/app/components/sidebar/sidebar.tsx +302 -0
- package/src/app/components/theme-toggle.tsx +66 -0
- package/src/app/components/themes/theme-detail.tsx +226 -0
- package/src/app/components/themes/themes-gallery.tsx +104 -0
- package/src/app/components/ui/badge.tsx +48 -0
- package/src/app/components/ui/button.tsx +93 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +247 -0
- package/src/app/components/ui/dialog.tsx +155 -0
- package/src/app/components/ui/dropdown-menu.tsx +267 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +21 -0
- package/src/app/components/ui/popover.tsx +82 -0
- package/src/app/components/ui/progress.tsx +33 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +195 -0
- package/src/app/components/ui/separator.tsx +26 -0
- package/src/app/components/ui/slider.tsx +68 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +84 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +75 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/frame/inspect.ts +140 -0
- package/src/app/frame/main.tsx +91 -0
- package/src/app/frame.html +12 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/asset-filter.test.ts +176 -0
- package/src/app/lib/asset-filter.ts +80 -0
- package/src/app/lib/assets.ts +257 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/frame.ts +38 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-thumb.tsx +46 -0
- package/src/app/lib/pages.ts +28 -0
- package/src/app/lib/sdk.ts +43 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-page-module.ts +48 -0
- package/src/app/lib/use-page-titles.ts +29 -0
- package/src/app/lib/use-restart-server.ts +78 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +252 -0
- package/src/app/routes/home.tsx +846 -0
- package/src/app/routes/page.tsx +329 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +405 -0
- package/src/app/virtual.d.ts +53 -0
- package/src/locale/en.ts +218 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +221 -0
- package/src/locale/types.ts +215 -0
- package/src/locale/zh-cn.ts +218 -0
- 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
|
+
}
|