kcg-code 0.0.0-stage → 0.2.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/README.md +230 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +84 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +147 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +114 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +101 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +327 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +362 -0
- package/src/lib/skills-catalog.ts +138 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +427 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pemilih Project di halaman Skills — pil ringkas "Install to <project>"
|
|
3
|
+
* yang membuka popover berisi daftar Project dengan kotak cari.
|
|
4
|
+
*
|
|
5
|
+
* Kotak cari hanya muncul bila Project cukup banyak. Navigasi keyboard:
|
|
6
|
+
* ↑/↓ memindah sorotan, Enter memilih, Esc menutup (bawaan Popover).
|
|
7
|
+
*/
|
|
8
|
+
import { CheckIcon, ChevronsUpDownIcon, FolderIcon, SearchIcon } from "lucide-react";
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
import type { Project } from "@/types";
|
|
13
|
+
|
|
14
|
+
/** Kotak cari tampil mulai jumlah Project ini. */
|
|
15
|
+
const SEARCH_THRESHOLD = 6;
|
|
16
|
+
|
|
17
|
+
export interface SkillsProjectPickerProps {
|
|
18
|
+
projects: Project[];
|
|
19
|
+
selected: Project | null;
|
|
20
|
+
onSelect: (projectId: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SkillsProjectPicker({ projects, selected, onSelect }: SkillsProjectPickerProps) {
|
|
24
|
+
const [open, setOpen] = useState(false);
|
|
25
|
+
const [query, setQuery] = useState("");
|
|
26
|
+
const [highlight, setHighlight] = useState(0);
|
|
27
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
29
|
+
const showSearch = projects.length >= SEARCH_THRESHOLD;
|
|
30
|
+
|
|
31
|
+
const filtered = useMemo(() => {
|
|
32
|
+
const q = query.trim().toLowerCase();
|
|
33
|
+
if (q === "") return projects;
|
|
34
|
+
return projects.filter(
|
|
35
|
+
(p) => p.name.toLowerCase().includes(q) || p.path.toLowerCase().includes(q),
|
|
36
|
+
);
|
|
37
|
+
}, [projects, query]);
|
|
38
|
+
|
|
39
|
+
// Buka: reset pencarian, sorot Project terpilih.
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!open) return;
|
|
42
|
+
setQuery("");
|
|
43
|
+
const idx = projects.findIndex((p) => p.id === selected?.id);
|
|
44
|
+
setHighlight(idx >= 0 ? idx : 0);
|
|
45
|
+
}, [open, projects, selected?.id]);
|
|
46
|
+
|
|
47
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reset saat query berubah
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setHighlight(0);
|
|
50
|
+
}, [query]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
listRef.current
|
|
54
|
+
?.querySelector<HTMLElement>(`[data-index="${highlight}"]`)
|
|
55
|
+
?.scrollIntoView({ block: "nearest" });
|
|
56
|
+
}, [highlight]);
|
|
57
|
+
|
|
58
|
+
const pick = (p: Project | undefined) => {
|
|
59
|
+
if (!p) return;
|
|
60
|
+
onSelect(p.id);
|
|
61
|
+
setOpen(false);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onKeyDown = (e: React.KeyboardEvent) => {
|
|
65
|
+
if (e.key === "ArrowDown") {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
setHighlight((h) => Math.min(h + 1, filtered.length - 1));
|
|
68
|
+
} else if (e.key === "ArrowUp") {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
setHighlight((h) => Math.max(h - 1, 0));
|
|
71
|
+
} else if (e.key === "Enter") {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
pick(filtered[highlight]);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const activeId = filtered[highlight] ? `skills-project-${highlight}` : undefined;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
81
|
+
<PopoverTrigger asChild>
|
|
82
|
+
{/* Pemilih konteks yang ringkas (pil), terpisah dari kolom cari:
|
|
83
|
+
menjawab "skill dipasang ke mana", bukan bagian dari pencarian. */}
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
aria-haspopup="listbox"
|
|
87
|
+
aria-label={
|
|
88
|
+
selected ? `Install to project ${selected.name}. Change project` : "Select a project"
|
|
89
|
+
}
|
|
90
|
+
className="group inline-flex h-9 max-w-full min-w-0 items-center gap-2 rounded-full border bg-card pr-2.5 pl-3 text-sm shadow-xs transition-colors outline-none hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=open]:border-ring"
|
|
91
|
+
>
|
|
92
|
+
<FolderIcon
|
|
93
|
+
className={cn("size-4 shrink-0", selected ? "text-primary" : "text-muted-foreground")}
|
|
94
|
+
aria-hidden
|
|
95
|
+
/>
|
|
96
|
+
<span className="shrink-0 text-muted-foreground">Install to</span>
|
|
97
|
+
<span className={cn("truncate font-medium", !selected && "text-muted-foreground")}>
|
|
98
|
+
{selected?.name ?? "Select a project"}
|
|
99
|
+
</span>
|
|
100
|
+
<ChevronsUpDownIcon className="size-3.5 shrink-0 text-muted-foreground" aria-hidden />
|
|
101
|
+
</button>
|
|
102
|
+
</PopoverTrigger>
|
|
103
|
+
<PopoverContent
|
|
104
|
+
align="end"
|
|
105
|
+
className="w-80 max-w-[calc(100vw-2rem)] p-0"
|
|
106
|
+
onOpenAutoFocus={(e) => {
|
|
107
|
+
// Fokus ke kotak cari (bila ada), selain itu ke daftar agar ↑/↓ jalan.
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
(searchRef.current ?? listRef.current)?.focus();
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{showSearch && (
|
|
113
|
+
<div className="flex items-center gap-2 border-b px-3">
|
|
114
|
+
<SearchIcon className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
115
|
+
<input
|
|
116
|
+
ref={searchRef}
|
|
117
|
+
type="text"
|
|
118
|
+
role="combobox"
|
|
119
|
+
aria-expanded
|
|
120
|
+
aria-controls="skills-project-list"
|
|
121
|
+
aria-activedescendant={activeId}
|
|
122
|
+
aria-label="Search projects"
|
|
123
|
+
value={query}
|
|
124
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
125
|
+
onKeyDown={onKeyDown}
|
|
126
|
+
placeholder="Search projects…"
|
|
127
|
+
className="h-10 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
<div
|
|
132
|
+
ref={listRef}
|
|
133
|
+
id="skills-project-list"
|
|
134
|
+
role="listbox"
|
|
135
|
+
aria-label="Projects"
|
|
136
|
+
aria-activedescendant={showSearch ? undefined : activeId}
|
|
137
|
+
tabIndex={showSearch ? -1 : 0}
|
|
138
|
+
onKeyDown={showSearch ? undefined : onKeyDown}
|
|
139
|
+
className="max-h-72 overflow-y-auto p-1 outline-none"
|
|
140
|
+
>
|
|
141
|
+
{filtered.length === 0 ? (
|
|
142
|
+
<p className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
143
|
+
No projects match “{query.trim()}”.
|
|
144
|
+
</p>
|
|
145
|
+
) : (
|
|
146
|
+
filtered.map((p, i) => {
|
|
147
|
+
const isSelected = p.id === selected?.id;
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
key={p.id}
|
|
151
|
+
id={`skills-project-${i}`}
|
|
152
|
+
data-index={i}
|
|
153
|
+
role="option"
|
|
154
|
+
aria-selected={isSelected}
|
|
155
|
+
tabIndex={-1}
|
|
156
|
+
onMouseMove={() => setHighlight(i)}
|
|
157
|
+
onClick={() => pick(p)}
|
|
158
|
+
onKeyDown={() => {}}
|
|
159
|
+
className={cn(
|
|
160
|
+
"flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-sm",
|
|
161
|
+
i === highlight && "bg-accent text-accent-foreground",
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
<FolderIcon className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
165
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
166
|
+
<span className="truncate font-medium">{p.name}</span>
|
|
167
|
+
<span className="truncate text-xs text-muted-foreground">{p.path}</span>
|
|
168
|
+
</span>
|
|
169
|
+
{isSelected && <CheckIcon className="size-4 shrink-0" aria-hidden />}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
})
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
</PopoverContent>
|
|
176
|
+
</Popover>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kerangka dialog ala AdwDialog (libadwaita) di atas `DialogContent`:
|
|
3
|
+
* - Header bar: slot kiri (`start`), judul + subjudul di tengah
|
|
4
|
+
* (AdwWindowTitle), slot kanan (`end`). Tanpa `end` -> tombol tutup bundar.
|
|
5
|
+
* - Tinggi dipatok (isi panjang men-scroll di dalam), sudut 2xl.
|
|
6
|
+
* - Layar sempit (< sm): tampil sebagai bottom sheet dengan pegangan, seperti
|
|
7
|
+
* AdwDialog di jendela sempit; padding bawah menghormati safe-area iOS.
|
|
8
|
+
* - Fokus awal: desktop -> kolom cari pertama (bila ada); HP -> dialog itu
|
|
9
|
+
* sendiri agar keyboard virtual tidak langsung muncul.
|
|
10
|
+
* - Esc saat kolom cari berisi: kosongkan cari dulu (lihat `SearchEntry`),
|
|
11
|
+
* tidak menutup dialog.
|
|
12
|
+
*/
|
|
13
|
+
import { XIcon } from "lucide-react";
|
|
14
|
+
import { type ReactNode, useRef } from "react";
|
|
15
|
+
import {
|
|
16
|
+
DialogClose,
|
|
17
|
+
DialogContent,
|
|
18
|
+
DialogDescription,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
} from "@/components/ui/dialog";
|
|
22
|
+
import { cn } from "@/lib/utils";
|
|
23
|
+
|
|
24
|
+
export interface AdwDialogContentProps {
|
|
25
|
+
title: ReactNode;
|
|
26
|
+
subtitle?: ReactNode;
|
|
27
|
+
/** Aksi di kiri header bar (mis. "Cancel"). */
|
|
28
|
+
start?: ReactNode;
|
|
29
|
+
/** Aksi di kanan header bar (mis. tombol utama). Kosong -> tombol tutup. */
|
|
30
|
+
end?: ReactNode;
|
|
31
|
+
/** Kunci tombol tutup bawaan (mis. saat menyimpan). */
|
|
32
|
+
closeDisabled?: boolean;
|
|
33
|
+
/** Kelas area isi di bawah header bar. */
|
|
34
|
+
bodyClassName?: string;
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function AdwDialogContent({
|
|
39
|
+
title,
|
|
40
|
+
subtitle,
|
|
41
|
+
start,
|
|
42
|
+
end,
|
|
43
|
+
closeDisabled = false,
|
|
44
|
+
bodyClassName,
|
|
45
|
+
children,
|
|
46
|
+
}: AdwDialogContentProps) {
|
|
47
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
const hasSides = start !== undefined || end !== undefined;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<DialogContent
|
|
52
|
+
ref={contentRef}
|
|
53
|
+
showCloseButton={false}
|
|
54
|
+
onOpenAutoFocus={(e) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
const wide = window.matchMedia("(min-width: 640px)").matches;
|
|
57
|
+
const search = contentRef.current?.querySelector<HTMLInputElement>('input[type="search"]');
|
|
58
|
+
(wide && search ? search : contentRef.current)?.focus();
|
|
59
|
+
}}
|
|
60
|
+
onEscapeKeyDown={(e) => {
|
|
61
|
+
const el = document.activeElement;
|
|
62
|
+
if (el instanceof HTMLInputElement && el.type === "search" && el.value !== "") {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
className={cn(
|
|
67
|
+
"flex h-[min(640px,85dvh)] flex-col gap-0 overflow-hidden rounded-2xl p-0 sm:max-w-md",
|
|
68
|
+
"max-sm:top-auto max-sm:bottom-0 max-sm:left-0 max-sm:h-[85dvh] max-sm:max-w-none max-sm:translate-x-0 max-sm:translate-y-0 max-sm:rounded-b-none max-sm:border-x-0 max-sm:border-b-0",
|
|
69
|
+
"max-sm:data-[state=closed]:slide-out-to-bottom-8 max-sm:data-[state=open]:slide-in-from-bottom-8",
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{/* Pegangan sheet (dekoratif) — hanya di HP. */}
|
|
73
|
+
<span
|
|
74
|
+
aria-hidden
|
|
75
|
+
className="mx-auto mt-2 h-1 w-9 shrink-0 rounded-full bg-muted-foreground/30 sm:hidden"
|
|
76
|
+
/>
|
|
77
|
+
<DialogHeader
|
|
78
|
+
className={cn(
|
|
79
|
+
"grid shrink-0 items-center gap-2 px-3 pt-2.5 pb-2 sm:pt-3",
|
|
80
|
+
// Kolom kiri/kanan selebar slot terlebar agar judul tetap di tengah.
|
|
81
|
+
hasSides ? "grid-cols-[1fr_auto_1fr]" : "grid-cols-[2rem_1fr_2rem]",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<div className="flex min-w-0 justify-start">{start}</div>
|
|
85
|
+
<div className="flex min-w-0 flex-col items-center gap-0.5 text-center">
|
|
86
|
+
<DialogTitle className="truncate text-[15px] leading-5 font-bold">{title}</DialogTitle>
|
|
87
|
+
{subtitle && (
|
|
88
|
+
<DialogDescription className="truncate text-xs leading-4">{subtitle}</DialogDescription>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex min-w-0 justify-end">
|
|
92
|
+
{end ?? (
|
|
93
|
+
<DialogClose
|
|
94
|
+
disabled={closeDisabled}
|
|
95
|
+
aria-label="Close"
|
|
96
|
+
className="flex size-7 items-center justify-center rounded-full bg-black/[0.06] text-foreground/80 transition-colors outline-none hover:bg-black/10 focus-visible:ring-2 focus-visible:ring-ring/60 disabled:opacity-50 dark:bg-white/[0.08] dark:hover:bg-white/15"
|
|
97
|
+
>
|
|
98
|
+
<XIcon className="size-3.5" strokeWidth={2.5} />
|
|
99
|
+
</DialogClose>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</DialogHeader>
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
className={cn(
|
|
106
|
+
"flex min-h-0 flex-1 flex-col gap-3 px-4 pt-1 pb-[max(1rem,env(safe-area-inset-bottom))]",
|
|
107
|
+
bodyClassName,
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
</div>
|
|
112
|
+
</DialogContent>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toggle group ala AdwToggleGroup (libadwaita 1.7): palung terisi berisi
|
|
3
|
+
* beberapa pilihan sejajar; pilihan aktif "terangkat" (latar terang +
|
|
4
|
+
* bayangan di mode terang, lebih terang dari palung di mode gelap).
|
|
5
|
+
*
|
|
6
|
+
* Satu pilihan selalu aktif (bukan multi-select). Tombol memakai
|
|
7
|
+
* `aria-pressed` di dalam `role="group"` berlabel.
|
|
8
|
+
*/
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
export interface AdwToggleOption<T extends string> {
|
|
13
|
+
value: T;
|
|
14
|
+
label: ReactNode;
|
|
15
|
+
/** Ikon / logo di kiri label. */
|
|
16
|
+
icon?: ReactNode;
|
|
17
|
+
/** Label aksesibel bila `label` bukan teks biasa. */
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AdwToggleGroupProps<T extends string> {
|
|
23
|
+
/** Label grup untuk pembaca layar. */
|
|
24
|
+
label: string;
|
|
25
|
+
value: T;
|
|
26
|
+
options: readonly AdwToggleOption<T>[];
|
|
27
|
+
onChange: (value: T) => void;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function AdwToggleGroup<T extends string>({
|
|
33
|
+
label,
|
|
34
|
+
value,
|
|
35
|
+
options,
|
|
36
|
+
onChange,
|
|
37
|
+
disabled = false,
|
|
38
|
+
className,
|
|
39
|
+
}: AdwToggleGroupProps<T>) {
|
|
40
|
+
return (
|
|
41
|
+
// biome-ignore lint/a11y/useSemanticElements: grup tombol segmented, bukan fieldset form.
|
|
42
|
+
<div
|
|
43
|
+
role="group"
|
|
44
|
+
aria-label={label}
|
|
45
|
+
className={cn(
|
|
46
|
+
"grid shrink-0 auto-cols-fr grid-flow-col gap-0.5 rounded-[10px] bg-black/[0.06] p-[3px] dark:bg-white/[0.07]",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{options.map((o) => {
|
|
51
|
+
const active = o.value === value;
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
key={o.value}
|
|
55
|
+
type="button"
|
|
56
|
+
aria-pressed={active}
|
|
57
|
+
aria-label={o.ariaLabel}
|
|
58
|
+
title={o.title}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
onClick={() => onChange(o.value)}
|
|
61
|
+
className={cn(
|
|
62
|
+
"flex h-8 min-w-0 items-center justify-center gap-2 rounded-[7px] px-2 text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:opacity-60",
|
|
63
|
+
active
|
|
64
|
+
? "bg-white text-foreground shadow-sm dark:bg-white/[0.15] dark:shadow-none"
|
|
65
|
+
: "text-foreground/70 hover:bg-black/[0.04] hover:text-foreground dark:hover:bg-white/[0.05]",
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{o.icon}
|
|
69
|
+
<span className="truncate">{o.label}</span>
|
|
70
|
+
</button>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
|
10
|
+
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function AlertDialogTrigger({
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
|
16
|
+
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
|
20
|
+
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function AlertDialogOverlay({
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
|
27
|
+
return (
|
|
28
|
+
<AlertDialogPrimitive.Overlay
|
|
29
|
+
data-slot="alert-dialog-overlay"
|
|
30
|
+
className={cn(
|
|
31
|
+
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function AlertDialogContent({
|
|
40
|
+
className,
|
|
41
|
+
size = "default",
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
|
44
|
+
size?: "default" | "sm";
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<AlertDialogPortal>
|
|
48
|
+
<AlertDialogOverlay />
|
|
49
|
+
<AlertDialogPrimitive.Content
|
|
50
|
+
data-slot="alert-dialog-content"
|
|
51
|
+
data-size={size}
|
|
52
|
+
className={cn(
|
|
53
|
+
"group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[size=default]:sm:max-w-lg",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
</AlertDialogPortal>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
data-slot="alert-dialog-header"
|
|
66
|
+
className={cn(
|
|
67
|
+
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
data-slot="alert-dialog-footer"
|
|
79
|
+
className={cn(
|
|
80
|
+
"flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function AlertDialogTitle({
|
|
89
|
+
className,
|
|
90
|
+
...props
|
|
91
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
92
|
+
return (
|
|
93
|
+
<AlertDialogPrimitive.Title
|
|
94
|
+
data-slot="alert-dialog-title"
|
|
95
|
+
className={cn(
|
|
96
|
+
"min-w-0 wrap-anywhere text-lg font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function AlertDialogDescription({
|
|
105
|
+
className,
|
|
106
|
+
...props
|
|
107
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
108
|
+
return (
|
|
109
|
+
<AlertDialogPrimitive.Description
|
|
110
|
+
data-slot="alert-dialog-description"
|
|
111
|
+
className={cn("min-w-0 wrap-anywhere text-sm text-muted-foreground", className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
data-slot="alert-dialog-media"
|
|
121
|
+
className={cn(
|
|
122
|
+
"mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
|
|
123
|
+
className,
|
|
124
|
+
)}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function AlertDialogAction({
|
|
131
|
+
className,
|
|
132
|
+
variant = "default",
|
|
133
|
+
size = "default",
|
|
134
|
+
...props
|
|
135
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
|
136
|
+
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
|
137
|
+
return (
|
|
138
|
+
<Button variant={variant} size={size} asChild>
|
|
139
|
+
<AlertDialogPrimitive.Action
|
|
140
|
+
data-slot="alert-dialog-action"
|
|
141
|
+
className={cn(className)}
|
|
142
|
+
{...props}
|
|
143
|
+
/>
|
|
144
|
+
</Button>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function AlertDialogCancel({
|
|
149
|
+
className,
|
|
150
|
+
variant = "outline",
|
|
151
|
+
size = "default",
|
|
152
|
+
...props
|
|
153
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
|
154
|
+
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
|
155
|
+
return (
|
|
156
|
+
<Button variant={variant} size={size} asChild>
|
|
157
|
+
<AlertDialogPrimitive.Cancel
|
|
158
|
+
data-slot="alert-dialog-cancel"
|
|
159
|
+
className={cn(className)}
|
|
160
|
+
{...props}
|
|
161
|
+
/>
|
|
162
|
+
</Button>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
AlertDialog,
|
|
168
|
+
AlertDialogAction,
|
|
169
|
+
AlertDialogCancel,
|
|
170
|
+
AlertDialogContent,
|
|
171
|
+
AlertDialogDescription,
|
|
172
|
+
AlertDialogFooter,
|
|
173
|
+
AlertDialogHeader,
|
|
174
|
+
AlertDialogMedia,
|
|
175
|
+
AlertDialogOverlay,
|
|
176
|
+
AlertDialogPortal,
|
|
177
|
+
AlertDialogTitle,
|
|
178
|
+
AlertDialogTrigger,
|
|
179
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-card text-card-foreground",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
function Alert({
|
|
23
|
+
className,
|
|
24
|
+
variant,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
data-slot="alert"
|
|
30
|
+
role="alert"
|
|
31
|
+
className={cn(alertVariants({ variant }), className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
data-slot="alert-title"
|
|
41
|
+
className={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
data-slot="alert-description"
|
|
51
|
+
className={cn(
|
|
52
|
+
"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Alert, AlertDescription, AlertTitle };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import { Slot } from "radix-ui";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
13
|
+
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
|
|
16
|
+
outline:
|
|
17
|
+
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
18
|
+
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
19
|
+
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant = "default",
|
|
31
|
+
asChild = false,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
34
|
+
const Comp = asChild ? Slot.Root : "span";
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Comp
|
|
38
|
+
data-slot="badge"
|
|
39
|
+
data-variant={variant}
|
|
40
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Badge, badgeVariants };
|