| author | |
| committer | |
| log | c14f6015bfe479d313355cb234af1a5c16dbb15f |
| tree | de94f5fba1002627d60fd94d847fd9d502eb5a75 |
| parent | 0ff87f251dafb32703d5f531e7b248ad0a581ee2 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 563 insertions(+), 229 deletions(-)
apps/web/src/components/GitActionsControl.tsx+175-106| ... | @@ -79,7 +79,16 @@ import { | ... | @@ -79,7 +79,16 @@ import { |
| 79 | } from "~/components/ui/dialog"; | 79 | } from "~/components/ui/dialog"; |
| 80 | import { Group, GroupSeparator } from "~/components/ui/group"; | 80 | import { Group, GroupSeparator } from "~/components/ui/group"; |
| 81 | import { Input } from "~/components/ui/input"; | 81 | import { Input } from "~/components/ui/input"; |
| 82 | import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu"; | 82 | import { |
| 83 | Menu, | ||
| 84 | MenuItem, | ||
| 85 | MenuItemLabel, | ||
| 86 | MenuPopup, | ||
| 87 | MenuSub, | ||
| 88 | MenuSubTrigger, | ||
| 89 | MenuSubPopup, | ||
| 90 | MenuTrigger, | ||
| 91 | } from "~/components/ui/menu"; | ||
| 83 | import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; | 92 | import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; |
| 84 | import { ScrollArea } from "~/components/ui/scroll-area"; | 93 | import { ScrollArea } from "~/components/ui/scroll-area"; |
| 85 | import { Textarea } from "~/components/ui/textarea"; | 94 | import { Textarea } from "~/components/ui/textarea"; |
| ... | @@ -108,6 +117,7 @@ import { useOpenLink } from "~/browser/useOpenLink"; | ... | @@ -108,6 +117,7 @@ import { useOpenLink } from "~/browser/useOpenLink"; |
| 108 | import { useOpenPrLink } from "~/lib/openPullRequestLink"; | 117 | import { useOpenPrLink } from "~/lib/openPullRequestLink"; |
| 109 | 118 | ||
| 110 | interface GitActionsControlProps { | 119 | interface GitActionsControlProps { |
| 120 | presentation?: "toolbar" | "menu"; | ||
| 111 | gitCwd: string | null; | 121 | gitCwd: string | null; |
| 112 | activeThreadRef: ScopedThreadRef | null; | 122 | activeThreadRef: ScopedThreadRef | null; |
| 113 | draftId?: DraftId; | 123 | draftId?: DraftId; |
| ... | @@ -374,24 +384,25 @@ function GitActionItemIcon({ | ... | @@ -374,24 +384,25 @@ function GitActionItemIcon({ |
| 374 | function GitQuickActionIcon({ | 384 | function GitQuickActionIcon({ |
| 375 | quickAction, | 385 | quickAction, |
| 376 | SourceControlIcon, | 386 | SourceControlIcon, |
| 387 | className = "size-3.5", | ||
| 377 | }: { | 388 | }: { |
| 378 | quickAction: GitQuickAction; | 389 | quickAction: GitQuickAction; |
| 390 | className?: string; | ||
| 379 | SourceControlIcon: ReturnType<typeof getSourceControlPresentation>["Icon"]; | 391 | SourceControlIcon: ReturnType<typeof getSourceControlPresentation>["Icon"]; |
| 380 | }) { | 392 | }) { |
| 381 | const iconClassName = "size-3.5"; | 393 | if (quickAction.kind === "open_pr") return <SourceControlIcon className={className} />; |
| 382 | if (quickAction.kind === "open_pr") return <SourceControlIcon className={iconClassName} />; | 394 | if (quickAction.kind === "open_publish") return <CloudUploadIcon className={className} />; |
| 383 | if (quickAction.kind === "open_publish") return <CloudUploadIcon className={iconClassName} />; | 395 | if (quickAction.kind === "run_pull") return <CloudDownloadIcon className={className} />; |
| 384 | if (quickAction.kind === "run_pull") return <CloudDownloadIcon className={iconClassName} />; | ||
| 385 | if (quickAction.kind === "run_action") { | 396 | if (quickAction.kind === "run_action") { |
| 386 | if (quickAction.action === "commit") return <GitCommitIcon className={iconClassName} />; | 397 | if (quickAction.action === "commit") return <GitCommitIcon className={className} />; |
| 387 | if (quickAction.action === "push" || quickAction.action === "commit_push") { | 398 | if (quickAction.action === "push" || quickAction.action === "commit_push") { |
| 388 | return <CloudUploadIcon className={iconClassName} />; | 399 | return <CloudUploadIcon className={className} />; |
| 389 | } | 400 | } |
| 390 | return <SourceControlIcon className={iconClassName} />; | 401 | return <SourceControlIcon className={className} />; |
| 391 | } | 402 | } |
| 392 | if (quickAction.label === "Commit") return <GitCommitIcon className={iconClassName} />; | 403 | if (quickAction.label === "Commit") return <GitCommitIcon className={className} />; |
| 393 | if (quickAction.label === "Push") return <CloudUploadIcon className={iconClassName} />; | 404 | if (quickAction.label === "Push") return <CloudUploadIcon className={className} />; |
| 394 | return <InfoIcon className={iconClassName} />; | 405 | return <InfoIcon className={className} />; |
| 395 | } | 406 | } |
| 396 | 407 | ||
| 397 | interface PublishRepositoryDialogProps { | 408 | interface PublishRepositoryDialogProps { |
| ... | @@ -944,6 +955,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { | ... | @@ -944,6 +955,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 944 | } | 955 | } |
| 945 | 956 | ||
| 946 | export default function GitActionsControl({ | 957 | export default function GitActionsControl({ |
| 958 | presentation = "toolbar", | ||
| 947 | gitCwd, | 959 | gitCwd, |
| 948 | activeThreadRef, | 960 | activeThreadRef, |
| 949 | draftId, | 961 | draftId, |
| ... | @@ -1632,33 +1644,161 @@ export default function GitActionsControl({ | ... | @@ -1632,33 +1644,161 @@ export default function GitActionsControl({ |
| 1632 | 1644 | ||
| 1633 | const canPublishRepository = isRepo && gitStatusForActions !== null && !hasPrimaryRemote; | 1645 | const canPublishRepository = isRepo && gitStatusForActions !== null && !hasPrimaryRemote; |
| 1634 | 1646 | ||
| 1635 | if (!gitCwd) return null; | 1647 | const initializeGit = () => { |
| 1636 | 1648 | void (async () => { | |
| 1637 | return ( | 1649 | const result = await initAction.run(); |
| 1650 | if (result._tag === "Success" || isAtomCommandInterrupted(result)) { | ||
| 1651 | return; | ||
| 1652 | } | ||
| 1653 | const error = squashAtomCommandFailure(result); | ||
| 1654 | toastManager.add( | ||
| 1655 | stackedThreadToast({ | ||
| 1656 | type: "error", | ||
| 1657 | title: "Git initialization failed", | ||
| 1658 | description: error instanceof Error ? error.message : "An error occurred.", | ||
| 1659 | ...(threadToastData !== undefined ? { data: threadToastData } : {}), | ||
| 1660 | }), | ||
| 1661 | ); | ||
| 1662 | })(); | ||
| 1663 | }; | ||
| 1664 | const gitItems = ( | ||
| 1638 | <> | 1665 | <> |
| 1639 | {!isRepo ? ( | 1666 | {gitActionMenuItems.map((item) => { |
| 1640 | <Button | 1667 | const disabledReason = getMenuActionDisabledReason({ |
| 1641 | variant="outline" | 1668 | item, |
| 1642 | size="xs" | 1669 | gitStatus: gitStatusForActions, |
| 1643 | disabled={initAction.isPending} | 1670 | isBusy: isGitActionRunning, |
| 1671 | hasPrimaryRemote, | ||
| 1672 | }); | ||
| 1673 | if (item.disabled && disabledReason && presentation === "menu") { | ||
| 1674 | return ( | ||
| 1675 | <div key={`${item.id}-${item.label}`}> | ||
| 1676 | <MenuItem density={presentation === "menu" ? "touch" : "default"} disabled> | ||
| 1677 | <GitActionItemIcon icon={item.icon} SourceControlIcon={SourceControlIcon} /> | ||
| 1678 | <MenuItemLabel>{item.label}</MenuItemLabel> | ||
| 1679 | </MenuItem> | ||
| 1680 | <p className="max-w-64 px-2 pb-2 text-xs text-muted-foreground">{disabledReason}</p> | ||
| 1681 | </div> | ||
| 1682 | ); | ||
| 1683 | } | ||
| 1684 | if (item.disabled && disabledReason) { | ||
| 1685 | return ( | ||
| 1686 | <Popover key={`${item.id}-${item.label}`}> | ||
| 1687 | <PopoverTrigger | ||
| 1688 | openOnHover | ||
| 1689 | nativeButton={false} | ||
| 1690 | render={<span className="block w-max cursor-not-allowed" />} | ||
| 1691 | > | ||
| 1692 | <MenuItem | ||
| 1693 | density={presentation === "menu" ? "touch" : "default"} | ||
| 1694 | className="w-full" | ||
| 1695 | disabled | ||
| 1696 | > | ||
| 1697 | <GitActionItemIcon icon={item.icon} SourceControlIcon={SourceControlIcon} /> | ||
| 1698 | <MenuItemLabel>{item.label}</MenuItemLabel> | ||
| 1699 | </MenuItem> | ||
| 1700 | </PopoverTrigger> | ||
| 1701 | <PopoverPopup tooltipStyle side="left" align="center"> | ||
| 1702 | {disabledReason} | ||
| 1703 | </PopoverPopup> | ||
| 1704 | </Popover> | ||
| 1705 | ); | ||
| 1706 | } | ||
| 1707 | |||
| 1708 | return ( | ||
| 1709 | <MenuItem | ||
| 1710 | density={presentation === "menu" ? "touch" : "default"} | ||
| 1711 | key={`${item.id}-${item.label}`} | ||
| 1712 | disabled={item.disabled} | ||
| 1713 | onClick={() => { | ||
| 1714 | openDialogForMenuItem(item); | ||
| 1715 | }} | ||
| 1716 | > | ||
| 1717 | <GitActionItemIcon icon={item.icon} SourceControlIcon={SourceControlIcon} /> | ||
| 1718 | <MenuItemLabel>{item.label}</MenuItemLabel> | ||
| 1719 | </MenuItem> | ||
| 1720 | ); | ||
| 1721 | })} | ||
| 1722 | {canPublishRepository ? ( | ||
| 1723 | <MenuItem | ||
| 1724 | density={presentation === "menu" ? "touch" : "default"} | ||
| 1725 | disabled={isGitActionRunning} | ||
| 1644 | onClick={() => { | 1726 | onClick={() => { |
| 1645 | void (async () => { | 1727 | setIsPublishDialogOpen(true); |
| 1646 | const result = await initAction.run(); | ||
| 1647 | if (result._tag === "Success" || isAtomCommandInterrupted(result)) { | ||
| 1648 | return; | ||
| 1649 | } | ||
| 1650 | const error = squashAtomCommandFailure(result); | ||
| 1651 | toastManager.add( | ||
| 1652 | stackedThreadToast({ | ||
| 1653 | type: "error", | ||
| 1654 | title: "Git initialization failed", | ||
| 1655 | description: error instanceof Error ? error.message : "An error occurred.", | ||
| 1656 | ...(threadToastData !== undefined ? { data: threadToastData } : {}), | ||
| 1657 | }), | ||
| 1658 | ); | ||
| 1659 | })(); | ||
| 1660 | }} | 1728 | }} |
| 1661 | > | 1729 | > |
| 1730 | <CloudUploadIcon /> | ||
| 1731 | <MenuItemLabel>Publish repository...</MenuItemLabel> | ||
| 1732 | </MenuItem> | ||
| 1733 | ) : null} | ||
| 1734 | {gitStatusForActions?.refName === null && ( | ||
| 1735 | <p className="px-2 py-1.5 text-xs text-warning"> | ||
| 1736 | Detached HEAD: create and check out a branch to enable push and pull request actions. | ||
| 1737 | </p> | ||
| 1738 | )} | ||
| 1739 | {gitStatusForActions && | ||
| 1740 | gitStatusForActions.refName !== null && | ||
| 1741 | !gitStatusForActions.hasWorkingTreeChanges && | ||
| 1742 | gitStatusForActions.behindCount > 0 && | ||
| 1743 | gitStatusForActions.aheadCount === 0 && ( | ||
| 1744 | <p className="px-2 py-1.5 text-xs text-warning">Behind upstream. Pull/rebase first.</p> | ||
| 1745 | )} | ||
| 1746 | {gitStatusError && <p className="px-2 py-1.5 text-xs text-destructive">{gitStatusError}</p>} | ||
| 1747 | </> | ||
| 1748 | ); | ||
| 1749 | |||
| 1750 | if (!gitCwd) return null; | ||
| 1751 | |||
| 1752 | return ( | ||
| 1753 | <> | ||
| 1754 | {presentation === "menu" ? ( | ||
| 1755 | !isRepo ? ( | ||
| 1756 | <MenuItem | ||
| 1757 | density={presentation === "menu" ? "touch" : "default"} | ||
| 1758 | |||
| 1759 | disabled={initAction.isPending} | ||
| 1760 | onClick={initializeGit} | ||
| 1761 | > | ||
| 1762 | <GitBranchPlusIcon className="size-4" /> | ||
| 1763 | <MenuItemLabel> | ||
| 1764 | {initAction.isPending ? "Initializing..." : "Initialize Git"} | ||
| 1765 | </MenuItemLabel> | ||
| 1766 | </MenuItem> | ||
| 1767 | ) : ( | ||
| 1768 | <> | ||
| 1769 | <MenuItem | ||
| 1770 | density={presentation === "menu" ? "touch" : "default"} | ||
| 1771 | |||
| 1772 | disabled={isGitActionRunning || quickAction.disabled || !!quickActionDisabledReason} | ||
| 1773 | onClick={runQuickAction} | ||
| 1774 | > | ||
| 1775 | <GitQuickActionIcon | ||
| 1776 | className="size-4" | ||
| 1777 | quickAction={quickAction} | ||
| 1778 | SourceControlIcon={SourceControlIcon} | ||
| 1779 | /> | ||
| 1780 | <MenuItemLabel>{quickAction.label}</MenuItemLabel> | ||
| 1781 | </MenuItem> | ||
| 1782 | {quickActionDisabledReason && ( | ||
| 1783 | <p className="max-w-64 px-2 py-1.5 text-xs text-warning"> | ||
| 1784 | {quickActionDisabledReason} | ||
| 1785 | </p> | ||
| 1786 | )} | ||
| 1787 | <MenuSub | ||
| 1788 | onOpenChange={(open) => { | ||
| 1789 | if (open) requestVcsStatusRefresh(refreshVcsStatus, activeEnvironmentId, gitCwd); | ||
| 1790 | }} | ||
| 1791 | > | ||
| 1792 | <MenuSubTrigger density="touch" disabled={isGitActionRunning}> | ||
| 1793 | <SourceControlIcon className="size-4" /> | ||
| 1794 | <MenuItemLabel>Git actions</MenuItemLabel> | ||
| 1795 | </MenuSubTrigger> | ||
| 1796 | <MenuSubPopup className="min-w-32 max-w-[calc(100vw-2rem)]">{gitItems}</MenuSubPopup> | ||
| 1797 | </MenuSub> | ||
| 1798 | </> | ||
| 1799 | ) | ||
| 1800 | ) : !isRepo ? ( | ||
| 1801 | <Button variant="outline" size="xs" disabled={initAction.isPending} onClick={initializeGit}> | ||
| 1662 | <GitBranchPlusIcon className="size-3.5" aria-hidden /> | 1802 | <GitBranchPlusIcon className="size-3.5" aria-hidden /> |
| 1663 | <span className="ml-0.5"> | 1803 | <span className="ml-0.5"> |
| 1664 | {initAction.isPending ? "Initializing..." : "Initialize Git"} | 1804 | {initAction.isPending ? "Initializing..." : "Initialize Git"} |
| ... | @@ -1720,78 +1860,7 @@ export default function GitActionsControl({ | ... | @@ -1720,78 +1860,7 @@ export default function GitActionsControl({ |
| 1720 | <ChevronDownIcon aria-hidden="true" className="size-4" /> | 1860 | <ChevronDownIcon aria-hidden="true" className="size-4" /> |
| 1721 | </MenuTrigger> | 1861 | </MenuTrigger> |
| 1722 | <MenuPopup align="end" className="w-full"> | 1862 | <MenuPopup align="end" className="w-full"> |
| 1723 | {gitActionMenuItems.map((item) => { | 1863 | {gitItems} |
| 1724 | const disabledReason = getMenuActionDisabledReason({ | ||
| 1725 | item, | ||
| 1726 | gitStatus: gitStatusForActions, | ||
| 1727 | isBusy: isGitActionRunning, | ||
| 1728 | hasPrimaryRemote, | ||
| 1729 | }); | ||
| 1730 | if (item.disabled && disabledReason) { | ||
| 1731 | return ( | ||
| 1732 | <Popover key={`${item.id}-${item.label}`}> | ||
| 1733 | <PopoverTrigger | ||
| 1734 | openOnHover | ||
| 1735 | nativeButton={false} | ||
| 1736 | render={<span className="block w-max cursor-not-allowed" />} | ||
| 1737 | > | ||
| 1738 | <MenuItem className="w-full" disabled> | ||
| 1739 | <GitActionItemIcon | ||
| 1740 | icon={item.icon} | ||
| 1741 | SourceControlIcon={SourceControlIcon} | ||
| 1742 | /> | ||
| 1743 | {item.label} | ||
| 1744 | </MenuItem> | ||
| 1745 | </PopoverTrigger> | ||
| 1746 | <PopoverPopup tooltipStyle side="left" align="center"> | ||
| 1747 | {disabledReason} | ||
| 1748 | </PopoverPopup> | ||
| 1749 | </Popover> | ||
| 1750 | ); | ||
| 1751 | } | ||
| 1752 | |||
| 1753 | return ( | ||
| 1754 | <MenuItem | ||
| 1755 | key={`${item.id}-${item.label}`} | ||
| 1756 | disabled={item.disabled} | ||
| 1757 | onClick={() => { | ||
| 1758 | openDialogForMenuItem(item); | ||
| 1759 | }} | ||
| 1760 | > | ||
| 1761 | <GitActionItemIcon icon={item.icon} SourceControlIcon={SourceControlIcon} /> | ||
| 1762 | {item.label} | ||
| 1763 | </MenuItem> | ||
| 1764 | ); | ||
| 1765 | })} | ||
| 1766 | {canPublishRepository ? ( | ||
| 1767 | <MenuItem | ||
| 1768 | disabled={isGitActionRunning} | ||
| 1769 | onClick={() => { | ||
| 1770 | setIsPublishDialogOpen(true); | ||
| 1771 | }} | ||
| 1772 | > | ||
| 1773 | <CloudUploadIcon /> | ||
| 1774 | Publish repository... | ||
| 1775 | </MenuItem> | ||
| 1776 | ) : null} | ||
| 1777 | {gitStatusForActions?.refName === null && ( | ||
| 1778 | <p className="px-2 py-1.5 text-xs text-warning"> | ||
| 1779 | Detached HEAD: create and check out a branch to enable push and pull request | ||
| 1780 | actions. | ||
| 1781 | </p> | ||
| 1782 | )} | ||
| 1783 | {gitStatusForActions && | ||
| 1784 | gitStatusForActions.refName !== null && | ||
| 1785 | !gitStatusForActions.hasWorkingTreeChanges && | ||
| 1786 | gitStatusForActions.behindCount > 0 && | ||
| 1787 | gitStatusForActions.aheadCount === 0 && ( | ||
| 1788 | <p className="px-2 py-1.5 text-xs text-warning"> | ||
| 1789 | Behind upstream. Pull/rebase first. | ||
| 1790 | </p> | ||
| 1791 | )} | ||
| 1792 | {gitStatusError && ( | ||
| 1793 | <p className="px-2 py-1.5 text-xs text-destructive">{gitStatusError}</p> | ||
| 1794 | )} | ||
| 1795 | </MenuPopup> | 1864 | </MenuPopup> |
| 1796 | </Menu> | 1865 | </Menu> |
| 1797 | </Group> | 1866 | </Group> |
apps/web/src/components/ProjectScriptsControl.tsx+127-55| ... | @@ -28,10 +28,14 @@ import { | ... | @@ -28,10 +28,14 @@ import { |
| 28 | MenuGroup, | 28 | MenuGroup, |
| 29 | MenuGroupLabel, | 29 | MenuGroupLabel, |
| 30 | MenuItem, | 30 | MenuItem, |
| 31 | MenuItemLabel, | ||
| 31 | MenuPopup, | 32 | MenuPopup, |
| 32 | MenuSeparator, | 33 | MenuSeparator, |
| 33 | MenuShortcut, | 34 | MenuShortcut, |
| 34 | MenuTrigger, | 35 | MenuTrigger, |
| 36 | MenuSub, | ||
| 37 | MenuSubTrigger, | ||
| 38 | MenuSubPopup, | ||
| 35 | } from "./ui/menu"; | 39 | } from "./ui/menu"; |
| 36 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | 40 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 37 | 41 | ||
| ... | @@ -40,6 +44,8 @@ export type { NewProjectScriptInput, ProjectScriptActionResult }; | ... | @@ -40,6 +44,8 @@ export type { NewProjectScriptInput, ProjectScriptActionResult }; |
| 40 | const NO_FILE_SCRIPTS: ReadonlyArray<T3ProjectFileScript> = []; | 44 | const NO_FILE_SCRIPTS: ReadonlyArray<T3ProjectFileScript> = []; |
| 41 | 45 | ||
| 42 | interface ProjectScriptsControlProps { | 46 | interface ProjectScriptsControlProps { |
| 47 | presentation?: "toolbar" | "menu"; | ||
| 48 | onRequestMenuClose?: () => void; | ||
| 43 | scripts: ReadonlyArray<ProjectScript>; | 49 | scripts: ReadonlyArray<ProjectScript>; |
| 44 | /** Scripts declared in the project's checked-in t3.json, offered for import. */ | 50 | /** Scripts declared in the project's checked-in t3.json, offered for import. */ |
| 45 | fileScripts?: ReadonlyArray<T3ProjectFileScript>; | 51 | fileScripts?: ReadonlyArray<T3ProjectFileScript>; |
| ... | @@ -55,6 +61,8 @@ interface ProjectScriptsControlProps { | ... | @@ -55,6 +61,8 @@ interface ProjectScriptsControlProps { |
| 55 | } | 61 | } |
| 56 | 62 | ||
| 57 | export default function ProjectScriptsControl({ | 63 | export default function ProjectScriptsControl({ |
| 64 | presentation = "toolbar", | ||
| 65 | onRequestMenuClose, | ||
| 58 | scripts, | 66 | scripts, |
| 59 | fileScripts = NO_FILE_SCRIPTS, | 67 | fileScripts = NO_FILE_SCRIPTS, |
| 60 | keybindings, | 68 | keybindings, |
| ... | @@ -65,9 +73,13 @@ export default function ProjectScriptsControl({ | ... | @@ -65,9 +73,13 @@ export default function ProjectScriptsControl({ |
| 65 | onDeleteScript, | 73 | onDeleteScript, |
| 66 | }: ProjectScriptsControlProps) { | 74 | }: ProjectScriptsControlProps) { |
| 67 | const [actionsMenuOpen, setActionsMenuOpen] = useState({ | 75 | const [actionsMenuOpen, setActionsMenuOpen] = useState({ |
| 76 | presentation, | ||
| 68 | scripts: false, | 77 | scripts: false, |
| 69 | imports: false, | 78 | imports: false, |
| 70 | }); | 79 | }); |
| 80 | if (actionsMenuOpen.presentation !== presentation) { | ||
| 81 | setActionsMenuOpen({ presentation, scripts: false, imports: false }); | ||
| 82 | } | ||
| 71 | const [editorRequest, setEditorRequest] = useState<ProjectScriptEditorRequest | null>(null); | 83 | const [editorRequest, setEditorRequest] = useState<ProjectScriptEditorRequest | null>(null); |
| 72 | 84 | ||
| 73 | const primaryScript = useMemo(() => { | 85 | const primaryScript = useMemo(() => { |
| ... | @@ -97,7 +109,8 @@ export default function ProjectScriptsControl({ | ... | @@ -97,7 +109,8 @@ export default function ProjectScriptsControl({ |
| 97 | }; | 109 | }; |
| 98 | 110 | ||
| 99 | const openEditDialog = (script: ProjectScript) => { | 111 | const openEditDialog = (script: ProjectScript) => { |
| 100 | setActionsMenuOpen({ scripts: false, imports: false }); | 112 | onRequestMenuClose?.(); |
| 113 | setActionsMenuOpen({ presentation, scripts: false, imports: false }); | ||
| 101 | setEditorRequest(editorRequestForScript(script, keybindings)); | 114 | setEditorRequest(editorRequestForScript(script, keybindings)); |
| 102 | }; | 115 | }; |
| 103 | 116 | ||
| ... | @@ -138,12 +151,13 @@ export default function ProjectScriptsControl({ | ... | @@ -138,12 +151,13 @@ export default function ProjectScriptsControl({ |
| 138 | <MenuGroupLabel>From t3.json</MenuGroupLabel> | 151 | <MenuGroupLabel>From t3.json</MenuGroupLabel> |
| 139 | {importableScripts.map((fileScript) => ( | 152 | {importableScripts.map((fileScript) => ( |
| 140 | <MenuItem | 153 | <MenuItem |
| 154 | density={presentation === "menu" ? "touch" : "default"} | ||
| 141 | key={`${fileScript.name} ${fileScript.command}`} | 155 | key={`${fileScript.name} ${fileScript.command}`} |
| 142 | className={dropdownItemClassName} | 156 | className={dropdownItemClassName} |
| 143 | onClick={() => void importFileScript(fileScript)} | 157 | onClick={() => void importFileScript(fileScript)} |
| 144 | > | 158 | > |
| 145 | <ScriptIcon icon={fileScript.icon ?? "play"} className="size-4" /> | 159 | <ScriptIcon icon={fileScript.icon ?? "play"} className="size-4" /> |
| 146 | <span className="truncate">{fileScript.name}</span> | 160 | <MenuItemLabel className="truncate">{fileScript.name}</MenuItemLabel> |
| 147 | <MenuShortcut className="ms-auto"> | 161 | <MenuShortcut className="ms-auto"> |
| 148 | <DownloadIcon className="size-3.5" aria-label="Import" /> | 162 | <DownloadIcon className="size-3.5" aria-label="Import" /> |
| 149 | </MenuShortcut> | 163 | </MenuShortcut> |
| ... | @@ -153,9 +167,112 @@ export default function ProjectScriptsControl({ | ... | @@ -153,9 +167,112 @@ export default function ProjectScriptsControl({ |
| 153 | </> | 167 | </> |
| 154 | ); | 168 | ); |
| 155 | 169 | ||
| 170 | const scriptItems = ( | ||
| 171 | <> | ||
| 172 | {scripts.map((script) => { | ||
| 173 | const shortcutLabel = shortcutLabelForCommand( | ||
| 174 | keybindings, | ||
| 175 | commandForProjectScript(script.id), | ||
| 176 | ); | ||
| 177 | return ( | ||
| 178 | <MenuItem | ||
| 179 | density={presentation === "menu" ? "touch" : "default"} | ||
| 180 | key={script.id} | ||
| 181 | className={`group ${dropdownItemClassName}`} | ||
| 182 | onClick={() => onRunScript(script)} | ||
| 183 | > | ||
| 184 | <ScriptIcon icon={script.icon} className="size-4" /> | ||
| 185 | <MenuItemLabel className="truncate"> | ||
| 186 | {script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name} | ||
| 187 | </MenuItemLabel> | ||
| 188 | <span className="relative ms-auto flex h-6 min-w-6 items-center justify-end"> | ||
| 189 | {shortcutLabel && ( | ||
| 190 | <MenuShortcut | ||
| 191 | className={ | ||
| 192 | presentation === "menu" | ||
| 193 | ? "ms-0 mr-7" | ||
| 194 | : "ms-0 transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0" | ||
| 195 | } | ||
| 196 | > | ||
| 197 | {shortcutLabel} | ||
| 198 | </MenuShortcut> | ||
| 199 | )} | ||
| 200 | <Button | ||
| 201 | type="button" | ||
| 202 | variant="ghost" | ||
| 203 | size="icon-xs" | ||
| 204 | className={`absolute right-0 top-1/2 size-6 -translate-y-1/2 ${presentation === "menu" ? "" : "opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto group-focus-visible:opacity-100 group-focus-visible:pointer-events-auto"}`} | ||
| 205 | aria-label={`Edit ${script.name}`} | ||
| 206 | onPointerDown={(event) => { | ||
| 207 | event.preventDefault(); | ||
| 208 | event.stopPropagation(); | ||
| 209 | }} | ||
| 210 | onClick={(event) => { | ||
| 211 | event.preventDefault(); | ||
| 212 | event.stopPropagation(); | ||
| 213 | openEditDialog(script); | ||
| 214 | }} | ||
| 215 | > | ||
| 216 | <SettingsIcon className="size-3.5" /> | ||
| 217 | </Button> | ||
| 218 | </span> | ||
| 219 | </MenuItem> | ||
| 220 | ); | ||
| 221 | })} | ||
| 222 | {importMenuItems} | ||
| 223 | <MenuItem | ||
| 224 | density={presentation === "menu" ? "touch" : "default"} | ||
| 225 | className={dropdownItemClassName} | ||
| 226 | onClick={openAddDialog} | ||
| 227 | > | ||
| 228 | <PlusIcon className="size-4" /> | ||
| 229 | <MenuItemLabel>Add action</MenuItemLabel> | ||
| 230 | </MenuItem> | ||
| 231 | </> | ||
| 232 | ); | ||
| 233 | |||
| 156 | return ( | 234 | return ( |
| 157 | <> | 235 | <> |
| 158 | {primaryScript ? ( | 236 | {presentation === "menu" ? ( |
| 237 | <> | ||
| 238 | {primaryScript && ( | ||
| 239 | <MenuItem | ||
| 240 | density={presentation === "menu" ? "touch" : "default"} | ||
| 241 | onClick={() => onRunScript(primaryScript)} | ||
| 242 | > | ||
| 243 | <ScriptIcon icon={primaryScript.icon} className="size-4" /> | ||
| 244 | <MenuItemLabel className="truncate">Run {primaryScript.name}</MenuItemLabel> | ||
| 245 | <MenuShortcut> | ||
| 246 | {shortcutLabelForCommand(keybindings, commandForProjectScript(primaryScript.id))} | ||
| 247 | </MenuShortcut> | ||
| 248 | </MenuItem> | ||
| 249 | )} | ||
| 250 | {primaryScript || importableScripts.length > 0 ? ( | ||
| 251 | <MenuSub | ||
| 252 | open={actionsMenuOpen.scripts} | ||
| 253 | onOpenChange={(open) => | ||
| 254 | setActionsMenuOpen({ presentation, scripts: open, imports: false }) | ||
| 255 | } | ||
| 256 | > | ||
| 257 | <MenuSubTrigger density="touch"> | ||
| 258 | <ScriptIcon icon="play" className="size-4" /> | ||
| 259 | <MenuItemLabel>Project actions</MenuItemLabel> | ||
| 260 | </MenuSubTrigger> | ||
| 261 | <MenuSubPopup className="min-w-32 max-w-[calc(100vw-2rem)]"> | ||
| 262 | {scriptItems} | ||
| 263 | </MenuSubPopup> | ||
| 264 | </MenuSub> | ||
| 265 | ) : ( | ||
| 266 | <MenuItem | ||
| 267 | density={presentation === "menu" ? "touch" : "default"} | ||
| 268 | onClick={openAddDialog} | ||
| 269 | > | ||
| 270 | <PlusIcon className="size-4" /> | ||
| 271 | <MenuItemLabel>Add project action…</MenuItemLabel> | ||
| 272 | </MenuItem> | ||
| 273 | )} | ||
| 274 | </> | ||
| 275 | ) : primaryScript ? ( | ||
| 159 | <Group aria-label="Project scripts"> | 276 | <Group aria-label="Project scripts"> |
| 160 | <Tooltip> | 277 | <Tooltip> |
| 161 | <TooltipTrigger | 278 | <TooltipTrigger |
| ... | @@ -183,70 +300,25 @@ export default function ProjectScriptsControl({ | ... | @@ -183,70 +300,25 @@ export default function ProjectScriptsControl({ |
| 183 | <Menu | 300 | <Menu |
| 184 | highlightItemOnHover={false} | 301 | highlightItemOnHover={false} |
| 185 | open={actionsMenuOpen.scripts} | 302 | open={actionsMenuOpen.scripts} |
| 186 | onOpenChange={(open) => setActionsMenuOpen({ scripts: open, imports: false })} | 303 | onOpenChange={(open) => |
| 304 | setActionsMenuOpen({ presentation, scripts: open, imports: false }) | ||
| 305 | } | ||
| 187 | > | 306 | > |
| 188 | <MenuTrigger | 307 | <MenuTrigger |
| 189 | render={<Button size="icon-xs" variant="outline" aria-label="Script actions" />} | 308 | render={<Button size="icon-xs" variant="outline" aria-label="Script actions" />} |
| 190 | > | 309 | > |
| 191 | <ChevronDownIcon className="size-4" /> | 310 | <ChevronDownIcon className="size-4" /> |
| 192 | </MenuTrigger> | 311 | </MenuTrigger> |
| 193 | <MenuPopup align="end"> | 312 | <MenuPopup align="end">{scriptItems}</MenuPopup> |
| 194 | {scripts.map((script) => { | ||
| 195 | const shortcutLabel = shortcutLabelForCommand( | ||
| 196 | keybindings, | ||
| 197 | commandForProjectScript(script.id), | ||
| 198 | ); | ||
| 199 | return ( | ||
| 200 | <MenuItem | ||
| 201 | key={script.id} | ||
| 202 | className={`group ${dropdownItemClassName}`} | ||
| 203 | onClick={() => onRunScript(script)} | ||
| 204 | > | ||
| 205 | <ScriptIcon icon={script.icon} className="size-4" /> | ||
| 206 | <span className="truncate"> | ||
| 207 | {script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name} | ||
| 208 | </span> | ||
| 209 | <span className="relative ms-auto flex h-6 min-w-6 items-center justify-end"> | ||
| 210 | {shortcutLabel && ( | ||
| 211 | <MenuShortcut className="ms-0 transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"> | ||
| 212 | {shortcutLabel} | ||
| 213 | </MenuShortcut> | ||
| 214 | )} | ||
| 215 | <Button | ||
| 216 | type="button" | ||
| 217 | variant="ghost" | ||
| 218 | size="icon-xs" | ||
| 219 | className="absolute right-0 top-1/2 size-6 -translate-y-1/2 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto group-focus-visible:opacity-100 group-focus-visible:pointer-events-auto" | ||
| 220 | aria-label={`Edit ${script.name}`} | ||
| 221 | onPointerDown={(event) => { | ||
| 222 | event.preventDefault(); | ||
| 223 | event.stopPropagation(); | ||
| 224 | }} | ||
| 225 | onClick={(event) => { | ||
| 226 | event.preventDefault(); | ||
| 227 | event.stopPropagation(); | ||
| 228 | openEditDialog(script); | ||
| 229 | }} | ||
| 230 | > | ||
| 231 | <SettingsIcon className="size-3.5" /> | ||
| 232 | </Button> | ||
| 233 | </span> | ||
| 234 | </MenuItem> | ||
| 235 | ); | ||
| 236 | })} | ||
| 237 | {importMenuItems} | ||
| 238 | <MenuItem className={dropdownItemClassName} onClick={openAddDialog}> | ||
| 239 | <PlusIcon className="size-4" /> | ||
| 240 | Add action | ||
| 241 | </MenuItem> | ||
| 242 | </MenuPopup> | ||
| 243 | </Menu> | 313 | </Menu> |
| 244 | </Group> | 314 | </Group> |
| 245 | ) : importableScripts.length > 0 ? ( | 315 | ) : importableScripts.length > 0 ? ( |
| 246 | <Menu | 316 | <Menu |
| 247 | highlightItemOnHover={false} | 317 | highlightItemOnHover={false} |
| 248 | open={actionsMenuOpen.imports} | 318 | open={actionsMenuOpen.imports} |
| 249 | onOpenChange={(open) => setActionsMenuOpen({ scripts: false, imports: open })} | 319 | onOpenChange={(open) => |
| 320 | setActionsMenuOpen({ presentation, scripts: false, imports: open }) | ||
| 321 | } | ||
| 250 | > | 322 | > |
| 251 | <MenuTrigger render={<Button size="xs" variant="outline" aria-label="Project actions" />}> | 323 | <MenuTrigger render={<Button size="xs" variant="outline" aria-label="Project actions" />}> |
| 252 | <PlusIcon className="size-3.5" /> | 324 | <PlusIcon className="size-3.5" /> |
apps/web/src/components/WorkspaceBreadcrumb.tsx+26-3| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import type { ReactNode } from "react"; | 1 | import type { ComponentProps, ReactNode } from "react"; |
| 2 | 2 | ||
| 3 | import { cn } from "../lib/utils"; | 3 | import { cn } from "../lib/utils"; |
| 4 | 4 | ||
| ... | @@ -26,6 +26,23 @@ interface WorkspaceBreadcrumbItemProps { | ... | @@ -26,6 +26,23 @@ interface WorkspaceBreadcrumbItemProps { |
| 26 | readonly current?: boolean; | 26 | readonly current?: boolean; |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | export function WorkspaceBreadcrumbText({ children, className, ...props }: ComponentProps<"span">) { | ||
| 30 | return ( | ||
| 31 | <span | ||
| 32 | data-slot="workspace-breadcrumb-text" | ||
| 33 | // Center the capital letters with adjacent icons, not the font's leading. | ||
| 34 | // Padding preserves accents and descenders when a label is truncated. | ||
| 35 | className={cn( | ||
| 36 | "block min-w-0 truncate [text-box:trim-both_cap_alphabetic] supports-[text-box:trim-both_cap_alphabetic]:py-[0.5em]", | ||
| 37 | className, | ||
| 38 | )} | ||
| 39 | {...props} | ||
| 40 | > | ||
| 41 | {children} | ||
| 42 | </span> | ||
| 43 | ); | ||
| 44 | } | ||
| 45 | |||
| 29 | export function WorkspaceBreadcrumbItem({ | 46 | export function WorkspaceBreadcrumbItem({ |
| 30 | children, | 47 | children, |
| 31 | className, | 48 | className, |
| ... | @@ -45,10 +62,16 @@ export function WorkspaceBreadcrumbItem({ | ... | @@ -45,10 +62,16 @@ export function WorkspaceBreadcrumbItem({ |
| 45 | ); | 62 | ); |
| 46 | } | 63 | } |
| 47 | 64 | ||
| 48 | export function WorkspaceBreadcrumbSeparator({ className }: { readonly className?: string }) { | 65 | export function WorkspaceBreadcrumbSeparator({ |
| 66 | className, | ||
| 67 | children = "/", | ||
| 68 | }: { | ||
| 69 | readonly className?: string; | ||
| 70 | readonly children?: ReactNode; | ||
| 71 | }) { | ||
| 49 | return ( | 72 | return ( |
| 50 | <li aria-hidden="true" className={cn("flex shrink-0 items-center text-icon-muted", className)}> | 73 | <li aria-hidden="true" className={cn("flex shrink-0 items-center text-icon-muted", className)}> |
| 51 | / | 74 | {children} |
| 52 | </li> | 75 | </li> |
| 53 | ); | 76 | ); |
| 54 | } | 77 | } |
apps/web/src/components/chat/ChatHeader.tsx+125-39| ... | @@ -11,7 +11,7 @@ import { | ... | @@ -11,7 +11,7 @@ import { |
| 11 | isAtomCommandInterrupted, | 11 | isAtomCommandInterrupted, |
| 12 | squashAtomCommandFailure, | 12 | squashAtomCommandFailure, |
| 13 | } from "@t3tools/client-runtime/state/runtime"; | 13 | } from "@t3tools/client-runtime/state/runtime"; |
| 14 | import { ChevronDownIcon } from "lucide-react"; | 14 | import { ChevronDownIcon, EllipsisIcon } from "lucide-react"; |
| 15 | import { | 15 | import { |
| 16 | memo, | 16 | memo, |
| 17 | useCallback, | 17 | useCallback, |
| ... | @@ -22,6 +22,7 @@ import { | ... | @@ -22,6 +22,7 @@ import { |
| 22 | type KeyboardEvent as ReactKeyboardEvent, | 22 | type KeyboardEvent as ReactKeyboardEvent, |
| 23 | type MouseEvent as ReactMouseEvent, | 23 | type MouseEvent as ReactMouseEvent, |
| 24 | } from "react"; | 24 | } from "react"; |
| 25 | import { createPortal } from "react-dom"; | ||
| 25 | import GitActionsControl from "../GitActionsControl"; | 26 | import GitActionsControl from "../GitActionsControl"; |
| 26 | import { isTrailingDoubleClick } from "../Sidebar.logic"; | 27 | import { isTrailingDoubleClick } from "../Sidebar.logic"; |
| 27 | import { type DraftId } from "~/composerDraftStore"; | 28 | import { type DraftId } from "~/composerDraftStore"; |
| ... | @@ -45,8 +46,12 @@ import { | ... | @@ -45,8 +46,12 @@ import { |
| 45 | WorkspaceBreadcrumb, | 46 | WorkspaceBreadcrumb, |
| 46 | WorkspaceBreadcrumbItem, | 47 | WorkspaceBreadcrumbItem, |
| 47 | WorkspaceBreadcrumbSeparator, | 48 | WorkspaceBreadcrumbSeparator, |
| 49 | WorkspaceBreadcrumbText, | ||
| 48 | } from "../WorkspaceBreadcrumb"; | 50 | } from "../WorkspaceBreadcrumb"; |
| 49 | import { cn } from "~/lib/utils"; | 51 | import { cn } from "~/lib/utils"; |
| 52 | import { useIsMobile } from "~/hooks/useMediaQuery"; | ||
| 53 | import { Button } from "../ui/button"; | ||
| 54 | import { Menu, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu"; | ||
| 50 | 55 | ||
| 51 | interface ChatHeaderProps { | 56 | interface ChatHeaderProps { |
| 52 | activeThreadEnvironmentId: EnvironmentId; | 57 | activeThreadEnvironmentId: EnvironmentId; |
| ... | @@ -155,6 +160,40 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -155,6 +160,40 @@ export const ChatHeader = memo(function ChatHeader({ |
| 155 | breakpoint: { value: HEADER_ACTIONS_EXPANDED_BREAKPOINT_REM, unit: "rem" }, | 160 | breakpoint: { value: HEADER_ACTIONS_EXPANDED_BREAKPOINT_REM, unit: "rem" }, |
| 156 | }); | 161 | }); |
| 157 | }, [panelAnimationDurationMs, panelAnimationsActive]); | 162 | }, [panelAnimationDurationMs, panelAnimationsActive]); |
| 163 | const isMobile = useIsMobile(); | ||
| 164 | // Side panels can leave a desktop header narrower than a phone. | ||
| 165 | const [isNarrowHeader, setIsNarrowHeader] = useState(false); | ||
| 166 | useEffect(() => { | ||
| 167 | const container = headerActionsRef.current?.parentElement; | ||
| 168 | if (!container) return; | ||
| 169 | const update = () => setIsNarrowHeader(container.clientWidth < 512); | ||
| 170 | update(); | ||
| 171 | const observer = new ResizeObserver(update); | ||
| 172 | observer.observe(container); | ||
| 173 | return () => observer.disconnect(); | ||
| 174 | }, []); | ||
| 175 | const actionsCollapsed = isMobile || isNarrowHeader; | ||
| 176 | const [actionsOpen, setActionsOpen] = useState(false); | ||
| 177 | const [actionsContainer] = useState(() => { | ||
| 178 | const container = document.createElement("div"); | ||
| 179 | container.className = "contents"; | ||
| 180 | return container; | ||
| 181 | }); | ||
| 182 | // Reparent the DOM host, not the React controls: rotating a phone or resizing | ||
| 183 | // a window must not discard an unsaved script or Git dialog. | ||
| 184 | const mountInlineActions = useCallback( | ||
| 185 | (node: HTMLDivElement | null) => { | ||
| 186 | if (node && !actionsCollapsed) node.appendChild(actionsContainer); | ||
| 187 | }, | ||
| 188 | [actionsContainer, actionsCollapsed], | ||
| 189 | ); | ||
| 190 | const mountMenuActions = useCallback( | ||
| 191 | (node: HTMLDivElement | null) => { | ||
| 192 | if (node && actionsCollapsed) node.appendChild(actionsContainer); | ||
| 193 | }, | ||
| 194 | [actionsContainer, actionsCollapsed], | ||
| 195 | ); | ||
| 196 | if (!actionsCollapsed && actionsOpen) setActionsOpen(false); | ||
| 158 | const primaryEnvironmentId = usePrimaryEnvironmentId(); | 197 | const primaryEnvironmentId = usePrimaryEnvironmentId(); |
| 159 | const activeProjectName = activeProject?.title; | 198 | const activeProjectName = activeProject?.title; |
| 160 | const activeProjectCwd = activeProject?.workspaceRoot ?? null; | 199 | const activeProjectCwd = activeProject?.workspaceRoot ?? null; |
| ... | @@ -312,6 +351,50 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -312,6 +351,50 @@ export const ChatHeader = memo(function ChatHeader({ |
| 312 | }, | 351 | }, |
| 313 | [commitRename], | 352 | [commitRename], |
| 314 | ); | 353 | ); |
| 354 | const headerActions = ( | ||
| 355 | <> | ||
| 356 | {activeProjectScripts && ( | ||
| 357 | <> | ||
| 358 | <ProjectScriptsControl | ||
| 359 | onRequestMenuClose={() => setActionsOpen(false)} | ||
| 360 | presentation={actionsCollapsed ? "menu" : "toolbar"} | ||
| 361 | scripts={activeProjectScripts} | ||
| 362 | fileScripts={fileScripts} | ||
| 363 | keybindings={keybindings} | ||
| 364 | preferredScriptId={preferredScriptId} | ||
| 365 | onRunScript={onRunProjectScript} | ||
| 366 | onAddScript={onAddProjectScript} | ||
| 367 | onUpdateScript={onUpdateProjectScript} | ||
| 368 | onDeleteScript={onDeleteProjectScript} | ||
| 369 | /> | ||
| 370 | </> | ||
| 371 | )} | ||
| 372 | {showOpenInPicker && ( | ||
| 373 | <> | ||
| 374 | {actionsCollapsed && activeProjectScripts && <MenuSeparator />} | ||
| 375 | <OpenInPicker | ||
| 376 | presentation={actionsCollapsed ? "menu" : "toolbar"} | ||
| 377 | environmentId={activeThreadEnvironmentId} | ||
| 378 | keybindings={keybindings} | ||
| 379 | availableEditors={availableEditors} | ||
| 380 | openInCwd={openInCwd} | ||
| 381 | /> | ||
| 382 | </> | ||
| 383 | )} | ||
| 384 | {activeProjectName && gitCwd && ( | ||
| 385 | <> | ||
| 386 | {actionsCollapsed && (activeProjectScripts || showOpenInPicker) && <MenuSeparator />} | ||
| 387 | <GitActionsControl | ||
| 388 | presentation={actionsCollapsed ? "menu" : "toolbar"} | ||
| 389 | gitCwd={gitCwd} | ||
| 390 | activeThreadRef={scopeThreadRef(activeThreadEnvironmentId, activeThreadId)} | ||
| 391 | onOpenPullRequest={onOpenPullRequest} | ||
| 392 | {...(draftId ? { draftId } : {})} | ||
| 393 | /> | ||
| 394 | </> | ||
| 395 | )} | ||
| 396 | </> | ||
| 397 | ); | ||
| 315 | return ( | 398 | return ( |
| 316 | <div | 399 | <div |
| 317 | className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3" | 400 | className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3" |
| ... | @@ -339,12 +422,16 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -339,12 +422,16 @@ export const ChatHeader = memo(function ChatHeader({ |
| 339 | } | 422 | } |
| 340 | > | 423 | > |
| 341 | <ProjectFavicon project={activeProject} className="size-3.5" /> | 424 | <ProjectFavicon project={activeProject} className="size-3.5" /> |
| 342 | <span className="max-w-40 truncate">{activeProjectName}</span> | 425 | <WorkspaceBreadcrumbText className="max-w-40"> |
| 426 | {activeProjectName} | ||
| 427 | </WorkspaceBreadcrumbText> | ||
| 343 | </TooltipTrigger> | 428 | </TooltipTrigger> |
| 344 | <TooltipPopup side="top">New thread in {activeProjectName}</TooltipPopup> | 429 | <TooltipPopup side="top">New thread in {activeProjectName}</TooltipPopup> |
| 345 | </Tooltip> | 430 | </Tooltip> |
| 346 | </WorkspaceBreadcrumbItem> | 431 | </WorkspaceBreadcrumbItem> |
| 347 | <WorkspaceBreadcrumbSeparator /> | 432 | <WorkspaceBreadcrumbSeparator> |
| 433 | <WorkspaceBreadcrumbText>/</WorkspaceBreadcrumbText> | ||
| 434 | </WorkspaceBreadcrumbSeparator> | ||
| 348 | </> | 435 | </> |
| 349 | ) : null} | 436 | ) : null} |
| 350 | <WorkspaceBreadcrumbItem current className="min-w-10 flex-1"> | 437 | <WorkspaceBreadcrumbItem current className="min-w-10 flex-1"> |
| ... | @@ -377,7 +464,9 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -377,7 +464,9 @@ export const ChatHeader = memo(function ChatHeader({ |
| 377 | /> | 464 | /> |
| 378 | } | 465 | } |
| 379 | > | 466 | > |
| 380 | <h2 className="min-w-0 truncate">{activeThreadTitle}</h2> | 467 | <h2 className="min-w-0"> |
| 468 | <WorkspaceBreadcrumbText>{activeThreadTitle}</WorkspaceBreadcrumbText> | ||
| 469 | </h2> | ||
| 381 | <ChevronDownIcon | 470 | <ChevronDownIcon |
| 382 | aria-hidden | 471 | aria-hidden |
| 383 | data-thread-title-chevron | 472 | data-thread-title-chevron |
| ... | @@ -389,12 +478,10 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -389,12 +478,10 @@ export const ChatHeader = memo(function ChatHeader({ |
| 389 | ) : ( | 478 | ) : ( |
| 390 | <Tooltip> | 479 | <Tooltip> |
| 391 | <TooltipTrigger | 480 | <TooltipTrigger |
| 392 | render={ | 481 | render={<h2 aria-label={activeThreadTitle} className="min-w-0 flex-1" />} |
| 393 | <h2 aria-label={activeThreadTitle} className="min-w-0 flex-1 truncate"> | 482 | > |
| 394 | {activeThreadTitle} | 483 | <WorkspaceBreadcrumbText>{activeThreadTitle}</WorkspaceBreadcrumbText> |
| 395 | </h2> | 484 | </TooltipTrigger> |
| 396 | } | ||
| 397 | /> | ||
| 398 | <TooltipPopup side="top">{activeThreadTitle}</TooltipPopup> | 485 | <TooltipPopup side="top">{activeThreadTitle}</TooltipPopup> |
| 399 | </Tooltip> | 486 | </Tooltip> |
| 400 | )} | 487 | )} |
| ... | @@ -405,38 +492,37 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -405,38 +492,37 @@ export const ChatHeader = memo(function ChatHeader({ |
| 405 | data-chat-header-actions | 492 | data-chat-header-actions |
| 406 | className={cn( | 493 | className={cn( |
| 407 | "flex shrink-0 items-center justify-end gap-2 @3xl/header-actions:gap-3", | 494 | "flex shrink-0 items-center justify-end gap-2 @3xl/header-actions:gap-3", |
| 408 | rightPanelOpen ? "pr-0" : "pr-16", | 495 | // Reserve two panel toggles plus their 4px gaps and 1px edge inset. |
| 496 | // The page header adds 8px more right padding at sm. | ||
| 497 | rightPanelOpen ? "pr-0" : "pr-[calc(--spacing(18)+1px)] sm:pr-[calc(--spacing(14)+1px)]", | ||
| 409 | "[[data-panel-animations=true]_&]:motion-safe:transition-[padding-right] [[data-panel-animations=true]_&]:motion-safe:[transition-duration:var(--panel-animation-duration)] [[data-panel-animations=true]_&]:motion-safe:ease-out", | 498 | "[[data-panel-animations=true]_&]:motion-safe:transition-[padding-right] [[data-panel-animations=true]_&]:motion-safe:[transition-duration:var(--panel-animation-duration)] [[data-panel-animations=true]_&]:motion-safe:ease-out", |
| 410 | )} | 499 | )} |
| 411 | > | 500 | > |
| 412 | {activeProjectScripts && ( | 501 | <Menu open={actionsCollapsed && actionsOpen} onOpenChange={setActionsOpen}> |
| 413 | <ProjectScriptsControl | 502 | <MenuTrigger |
| 414 | scripts={activeProjectScripts} | 503 | className={ |
| 415 | fileScripts={fileScripts} | 504 | actionsCollapsed && |
| 416 | keybindings={keybindings} | 505 | (activeProjectScripts || showOpenInPicker || (activeProjectName && gitCwd)) |
| 417 | preferredScriptId={preferredScriptId} | 506 | ? undefined |
| 418 | onRunScript={onRunProjectScript} | 507 | : "hidden" |
| 419 | onAddScript={onAddProjectScript} | 508 | } |
| 420 | onUpdateScript={onUpdateProjectScript} | 509 | render={<Button size="icon-sm" variant="ghost" aria-label="More header actions" />} |
| 421 | onDeleteScript={onDeleteProjectScript} | 510 | > |
| 422 | /> | 511 | <EllipsisIcon className="size-4" /> |
| 423 | )} | 512 | </MenuTrigger> |
| 424 | {showOpenInPicker && ( | 513 | <div ref={mountInlineActions} className="contents" /> |
| 425 | <OpenInPicker | 514 | <MenuPopup |
| 426 | environmentId={activeThreadEnvironmentId} | 515 | data-chat-header-actions |
| 427 | keybindings={keybindings} | 516 | keepMounted |
| 428 | availableEditors={availableEditors} | 517 | aria-label="Header actions" |
| 429 | openInCwd={openInCwd} | 518 | align="end" |
| 430 | /> | 519 | className="min-w-56 max-w-[calc(100vw-2rem)]" |
| 431 | )} | 520 | finalFocus={actionsCollapsed ? undefined : false} |
| 432 | {activeProjectName && ( | 521 | > |
| 433 | <GitActionsControl | 522 | <div ref={mountMenuActions} className="contents" /> |
| 434 | gitCwd={gitCwd} | 523 | {createPortal(headerActions, actionsContainer)} |
| 435 | activeThreadRef={scopeThreadRef(activeThreadEnvironmentId, activeThreadId)} | 524 | </MenuPopup> |
| 436 | onOpenPullRequest={onOpenPullRequest} | 525 | </Menu> |
| 437 | {...(draftId ? { draftId } : {})} | ||
| 438 | /> | ||
| 439 | )} | ||
| 440 | </div> | 526 | </div> |
| 441 | </div> | 527 | </div> |
| 442 | ); | 528 | ); |
apps/web/src/components/chat/OpenInPicker.tsx+78-23| ... | @@ -15,10 +15,20 @@ import { | ... | @@ -15,10 +15,20 @@ import { |
| 15 | useRemoteOpenState, | 15 | useRemoteOpenState, |
| 16 | } from "../../remoteOpen"; | 16 | } from "../../remoteOpen"; |
| 17 | import { useEnvironment } from "../../state/environments"; | 17 | import { useEnvironment } from "../../state/environments"; |
| 18 | import { ChevronDownIcon, FolderClosedIcon } from "lucide-react"; | 18 | import { ChevronDownIcon, FolderClosedIcon, SquareArrowOutUpRightIcon } from "lucide-react"; |
| 19 | import { Button } from "../ui/button"; | 19 | import { Button } from "../ui/button"; |
| 20 | import { Group, GroupSeparator } from "../ui/group"; | 20 | import { Group, GroupSeparator } from "../ui/group"; |
| 21 | import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu"; | 21 | import { |
| 22 | Menu, | ||
| 23 | MenuItem, | ||
| 24 | MenuItemLabel, | ||
| 25 | MenuPopup, | ||
| 26 | MenuShortcut, | ||
| 27 | MenuSub, | ||
| 28 | MenuSubTrigger, | ||
| 29 | MenuSubPopup, | ||
| 30 | MenuTrigger, | ||
| 31 | } from "../ui/menu"; | ||
| 22 | import { | 32 | import { |
| 23 | AntigravityIcon, | 33 | AntigravityIcon, |
| 24 | CursorIcon, | 34 | CursorIcon, |
| ... | @@ -187,6 +197,7 @@ export const OpenInPicker = memo(function OpenInPicker({ | ... | @@ -187,6 +197,7 @@ export const OpenInPicker = memo(function OpenInPicker({ |
| 187 | keybindings, | 197 | keybindings, |
| 188 | availableEditors, | 198 | availableEditors, |
| 189 | openInCwd, | 199 | openInCwd, |
| 200 | presentation = "toolbar", | ||
| 190 | compact = false, | 201 | compact = false, |
| 191 | enableShortcut = true, | 202 | enableShortcut = true, |
| 192 | }: { | 203 | }: { |
| ... | @@ -194,6 +205,7 @@ export const OpenInPicker = memo(function OpenInPicker({ | ... | @@ -194,6 +205,7 @@ export const OpenInPicker = memo(function OpenInPicker({ |
| 194 | keybindings: ResolvedKeybindingsConfig; | 205 | keybindings: ResolvedKeybindingsConfig; |
| 195 | availableEditors: ReadonlyArray<EditorId>; | 206 | availableEditors: ReadonlyArray<EditorId>; |
| 196 | openInCwd: string | null; | 207 | openInCwd: string | null; |
| 208 | presentation?: "toolbar" | "menu"; | ||
| 197 | compact?: boolean; | 209 | compact?: boolean; |
| 198 | enableShortcut?: boolean; | 210 | enableShortcut?: boolean; |
| 199 | }) { | 211 | }) { |
| ... | @@ -274,6 +286,69 @@ export const OpenInPicker = memo(function OpenInPicker({ | ... | @@ -274,6 +286,69 @@ export const OpenInPicker = memo(function OpenInPicker({ |
| 274 | return () => window.removeEventListener("keydown", handler); | 286 | return () => window.removeEventListener("keydown", handler); |
| 275 | }, [enableShortcut, keybindings, openInCwd, openInEditor, preferredEditor]); | 287 | }, [enableShortcut, keybindings, openInCwd, openInEditor, preferredEditor]); |
| 276 | 288 | ||
| 289 | const editorItems = ( | ||
| 290 | <> | ||
| 291 | {remote.mode === "remote-unavailable" ? ( | ||
| 292 | <MenuItem density={presentation === "menu" ? "touch" : "default"} disabled> | ||
| 293 | No SSH route to {environmentLabel} | ||
| 294 | </MenuItem> | ||
| 295 | ) : ( | ||
| 296 | <> | ||
| 297 | {options.length === 0 && ( | ||
| 298 | <MenuItem density={presentation === "menu" ? "touch" : "default"} disabled> | ||
| 299 | No installed editors found | ||
| 300 | </MenuItem> | ||
| 301 | )} | ||
| 302 | {options.map(({ label, Icon, value, kind }) => ( | ||
| 303 | <MenuItem | ||
| 304 | density={presentation === "menu" ? "touch" : "default"} | ||
| 305 | key={value} | ||
| 306 | onClick={() => openInEditor(value)} | ||
| 307 | > | ||
| 308 | <Icon aria-hidden="true" className={getOpenInIconClass(kind)} /> | ||
| 309 | <MenuItemLabel>{label}</MenuItemLabel> | ||
| 310 | {value === preferredEditor && openFavoriteEditorShortcutLabel && ( | ||
| 311 | <MenuShortcut>{openFavoriteEditorShortcutLabel}</MenuShortcut> | ||
| 312 | )} | ||
| 313 | </MenuItem> | ||
| 314 | ))} | ||
| 315 | {remote.mode === "remote-links" && !remoteHintSeen && ( | ||
| 316 | <MenuItem density={presentation === "menu" ? "touch" : "default"} disabled> | ||
| 317 | Opens over SSH. Needs your key on {environmentLabel} | ||
| 318 | </MenuItem> | ||
| 319 | )} | ||
| 320 | </> | ||
| 321 | )} | ||
| 322 | </> | ||
| 323 | ); | ||
| 324 | if (presentation === "menu") { | ||
| 325 | return ( | ||
| 326 | <> | ||
| 327 | {primaryOption && ( | ||
| 328 | <MenuItem | ||
| 329 | density={presentation === "menu" ? "touch" : "default"} | ||
| 330 | |||
| 331 | disabled={!openInCwd || remote.mode === "remote-unavailable"} | ||
| 332 | onClick={() => openInEditor(preferredEditor)} | ||
| 333 | > | ||
| 334 | <primaryOption.Icon className={cn("size-4", getOpenInIconClass(primaryOption.kind))} /> | ||
| 335 | <MenuItemLabel className="truncate">Open in {primaryOption.label}</MenuItemLabel> | ||
| 336 | {openFavoriteEditorShortcutLabel && ( | ||
| 337 | <MenuShortcut>{openFavoriteEditorShortcutLabel}</MenuShortcut> | ||
| 338 | )} | ||
| 339 | </MenuItem> | ||
| 340 | )} | ||
| 341 | <MenuSub> | ||
| 342 | <MenuSubTrigger density="touch"> | ||
| 343 | <SquareArrowOutUpRightIcon className="size-4" /> | ||
| 344 | <MenuItemLabel>Open in…</MenuItemLabel> | ||
| 345 | </MenuSubTrigger> | ||
| 346 | <MenuSubPopup className="min-w-32 max-w-[calc(100vw-2rem)]">{editorItems}</MenuSubPopup> | ||
| 347 | </MenuSub> | ||
| 348 | </> | ||
| 349 | ); | ||
| 350 | } | ||
| 351 | |||
| 277 | return ( | 352 | return ( |
| 278 | <Group aria-label="Open in editor"> | 353 | <Group aria-label="Open in editor"> |
| 279 | <Button | 354 | <Button |
| ... | @@ -307,27 +382,7 @@ export const OpenInPicker = memo(function OpenInPicker({ | ... | @@ -307,27 +382,7 @@ export const OpenInPicker = memo(function OpenInPicker({ |
| 307 | > | 382 | > |
| 308 | <ChevronDownIcon aria-hidden="true" className="size-4" /> | 383 | <ChevronDownIcon aria-hidden="true" className="size-4" /> |
| 309 | </MenuTrigger> | 384 | </MenuTrigger> |
| 310 | <MenuPopup align="end"> | 385 | <MenuPopup align="end">{editorItems}</MenuPopup> |
| 311 | {remote.mode === "remote-unavailable" ? ( | ||
| 312 | <MenuItem disabled>No SSH route to {environmentLabel}</MenuItem> | ||
| 313 | ) : ( | ||
| 314 | <> | ||
| 315 | {options.length === 0 && <MenuItem disabled>No installed editors found</MenuItem>} | ||
| 316 | {options.map(({ label, Icon, value, kind }) => ( | ||
| 317 | <MenuItem key={value} onClick={() => openInEditor(value)}> | ||
| 318 | <Icon aria-hidden="true" className={getOpenInIconClass(kind)} /> | ||
| 319 | {label} | ||
| 320 | {value === preferredEditor && openFavoriteEditorShortcutLabel && ( | ||
| 321 | <MenuShortcut>{openFavoriteEditorShortcutLabel}</MenuShortcut> | ||
| 322 | )} | ||
| 323 | </MenuItem> | ||
| 324 | ))} | ||
| 325 | {remote.mode === "remote-links" && !remoteHintSeen && ( | ||
| 326 | <MenuItem disabled>Opens over SSH. Needs your key on {environmentLabel}</MenuItem> | ||
| 327 | )} | ||
| 328 | </> | ||
| 329 | )} | ||
| 330 | </MenuPopup> | ||
| 331 | </Menu> | 386 | </Menu> |
| 332 | </Group> | 387 | </Group> |
| 333 | ); | 388 | ); |
apps/web/src/components/ui/menu.tsx+27-1| ... | @@ -25,6 +25,7 @@ function MenuPopup({ | ... | @@ -25,6 +25,7 @@ function MenuPopup({ |
| 25 | alignOffset, | 25 | alignOffset, |
| 26 | side = "bottom", | 26 | side = "bottom", |
| 27 | anchor, | 27 | anchor, |
| 28 | keepMounted = false, | ||
| 28 | ...props | 29 | ...props |
| 29 | }: MenuPrimitive.Popup.Props & { | 30 | }: MenuPrimitive.Popup.Props & { |
| 30 | align?: MenuPrimitive.Positioner.Props["align"]; | 31 | align?: MenuPrimitive.Positioner.Props["align"]; |
| ... | @@ -32,6 +33,7 @@ function MenuPopup({ | ... | @@ -32,6 +33,7 @@ function MenuPopup({ |
| 32 | alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]; | 33 | alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]; |
| 33 | side?: MenuPrimitive.Positioner.Props["side"]; | 34 | side?: MenuPrimitive.Positioner.Props["side"]; |
| 34 | anchor?: MenuPrimitive.Positioner.Props["anchor"]; | 35 | anchor?: MenuPrimitive.Positioner.Props["anchor"]; |
| 36 | keepMounted?: boolean; | ||
| 35 | }) { | 37 | }) { |
| 36 | const hasExplicitWidthClass = | 38 | const hasExplicitWidthClass = |
| 37 | typeof className === "string" && | 39 | typeof className === "string" && |
| ... | @@ -41,7 +43,7 @@ function MenuPopup({ | ... | @@ -41,7 +43,7 @@ function MenuPopup({ |
| 41 | }); | 43 | }); |
| 42 | 44 | ||
| 43 | return ( | 45 | return ( |
| 44 | <MenuPrimitive.Portal> | 46 | <MenuPrimitive.Portal keepMounted={keepMounted}> |
| 45 | <MenuPrimitive.Positioner | 47 | <MenuPrimitive.Positioner |
| 46 | align={align} | 48 | align={align} |
| 47 | alignOffset={alignOffset} | 49 | alignOffset={alignOffset} |
| ... | @@ -74,10 +76,12 @@ function MenuGroup(props: MenuPrimitive.Group.Props) { | ... | @@ -74,10 +76,12 @@ function MenuGroup(props: MenuPrimitive.Group.Props) { |
| 74 | function MenuItem({ | 76 | function MenuItem({ |
| 75 | className, | 77 | className, |
| 76 | inset, | 78 | inset, |
| 79 | density = "default", | ||
| 77 | variant = "default", | 80 | variant = "default", |
| 78 | ...props | 81 | ...props |
| 79 | }: MenuPrimitive.Item.Props & { | 82 | }: MenuPrimitive.Item.Props & { |
| 80 | inset?: boolean; | 83 | inset?: boolean; |
| 84 | density?: "default" | "touch"; | ||
| 81 | variant?: "default" | "destructive" | "ghost"; | 85 | variant?: "default" | "destructive" | "ghost"; |
| 82 | }) { | 86 | }) { |
| 83 | return ( | 87 | return ( |
| ... | @@ -90,8 +94,10 @@ function MenuItem({ | ... | @@ -90,8 +94,10 @@ function MenuItem({ |
| 90 | size: "compact", | 94 | size: "compact", |
| 91 | className: "h-auto min-h-7 w-full sm:text-xs", | 95 | className: "h-auto min-h-7 w-full sm:text-xs", |
| 92 | }), | 96 | }), |
| 97 | density === "touch" && "min-h-10 sm:min-h-10", | ||
| 93 | className, | 98 | className, |
| 94 | )} | 99 | )} |
| 100 | data-density={density} | ||
| 95 | data-inset={inset} | 101 | data-inset={inset} |
| 96 | data-slot="menu-item" | 102 | data-slot="menu-item" |
| 97 | data-variant={variant} | 103 | data-variant={variant} |
| ... | @@ -100,6 +106,21 @@ function MenuItem({ | ... | @@ -100,6 +106,21 @@ function MenuItem({ |
| 100 | ); | 106 | ); |
| 101 | } | 107 | } |
| 102 | 108 | ||
| 109 | // Trim font leading so visible letters center with the icons in touch rows. | ||
| 110 | // Symmetric padding keeps accents and descenders inside truncated labels. | ||
| 111 | function MenuItemLabel({ className, ...props }: React.ComponentProps<"span">) { | ||
| 112 | return ( | ||
| 113 | <span | ||
| 114 | data-slot="menu-item-label" | ||
| 115 | className={cn( | ||
| 116 | "min-w-0 in-data-[density=touch]:[text-box:trim-both_cap_alphabetic] supports-[text-box:trim-both_cap_alphabetic]:in-data-[density=touch]:py-[0.5em]", | ||
| 117 | className, | ||
| 118 | )} | ||
| 119 | {...props} | ||
| 120 | /> | ||
| 121 | ); | ||
| 122 | } | ||
| 123 | |||
| 103 | function MenuCheckboxItem({ | 124 | function MenuCheckboxItem({ |
| 104 | className, | 125 | className, |
| 105 | children, | 126 | children, |
| ... | @@ -247,10 +268,12 @@ function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) { | ... | @@ -247,10 +268,12 @@ function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) { |
| 247 | function MenuSubTrigger({ | 268 | function MenuSubTrigger({ |
| 248 | className, | 269 | className, |
| 249 | inset, | 270 | inset, |
| 271 | density = "default", | ||
| 250 | children, | 272 | children, |
| 251 | ...props | 273 | ...props |
| 252 | }: MenuPrimitive.SubmenuTrigger.Props & { | 274 | }: MenuPrimitive.SubmenuTrigger.Props & { |
| 253 | inset?: boolean; | 275 | inset?: boolean; |
| 276 | density?: "default" | "touch"; | ||
| 254 | }) { | 277 | }) { |
| 255 | return ( | 278 | return ( |
| 256 | <MenuPrimitive.SubmenuTrigger | 279 | <MenuPrimitive.SubmenuTrigger |
| ... | @@ -261,8 +284,10 @@ function MenuSubTrigger({ | ... | @@ -261,8 +284,10 @@ function MenuSubTrigger({ |
| 261 | // also a direct svg — on a sub-trigger with no leading icon it is the | 284 | // also a direct svg — on a sub-trigger with no leading icon it is the |
| 262 | // only one, and these rules would take away its `ms-auto` alignment. | 285 | // only one, and these rules would take away its `ms-auto` alignment. |
| 263 | "[&>svg:not(:last-child)]:-mx-0.5 flex min-h-8 cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:cursor-not-allowed data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&>svg:not(:last-child):not([class*='opacity-'])]:opacity-80 [&_svg]:pointer-events-none [&>svg]:shrink-0", | 286 | "[&>svg:not(:last-child)]:-mx-0.5 flex min-h-8 cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:cursor-not-allowed data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&>svg:not(:last-child):not([class*='opacity-'])]:opacity-80 [&_svg]:pointer-events-none [&>svg]:shrink-0", |
| 287 | density === "touch" && "min-h-10 sm:min-h-10", | ||
| 264 | className, | 288 | className, |
| 265 | )} | 289 | )} |
| 290 | data-density={density} | ||
| 266 | data-inset={inset} | 291 | data-inset={inset} |
| 267 | data-slot="menu-sub-trigger" | 292 | data-slot="menu-sub-trigger" |
| 268 | {...props} | 293 | {...props} |
| ... | @@ -309,6 +334,7 @@ export { | ... | @@ -309,6 +334,7 @@ export { |
| 309 | MenuGroup, | 334 | MenuGroup, |
| 310 | MenuGroup as DropdownMenuGroup, | 335 | MenuGroup as DropdownMenuGroup, |
| 311 | MenuItem, | 336 | MenuItem, |
| 337 | MenuItemLabel, | ||
| 312 | MenuItem as DropdownMenuItem, | 338 | MenuItem as DropdownMenuItem, |
| 313 | MenuCheckboxItem, | 339 | MenuCheckboxItem, |
| 314 | MenuCheckboxItem as DropdownMenuCheckboxItem, | 340 | MenuCheckboxItem as DropdownMenuCheckboxItem, |
apps/web/src/components/ui/sidebar.tsx+5-2| ... | @@ -155,7 +155,10 @@ function SidebarProvider({ | ... | @@ -155,7 +155,10 @@ function SidebarProvider({ |
| 155 | <SidebarContext value={contextValue}> | 155 | <SidebarContext value={contextValue}> |
| 156 | <div | 156 | <div |
| 157 | // Inset layouts opt into bg-sidebar through className. | 157 | // Inset layouts opt into bg-sidebar through className. |
| 158 | className={cn("group/sidebar-wrapper flex min-h-svh w-full", className)} | 158 | className={cn( |
| 159 | "group/sidebar-wrapper flex min-h-svh w-full max-sm:[--workspace-titlebar-control-size:--spacing(8)]", | ||
| 160 | className, | ||
| 161 | )} | ||
| 159 | data-sidebar-state={state} | 162 | data-sidebar-state={state} |
| 160 | data-slot="sidebar-wrapper" | 163 | data-slot="sidebar-wrapper" |
| 161 | style={ | 164 | style={ |
| ... | @@ -337,7 +340,7 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t | ... | @@ -337,7 +340,7 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t |
| 337 | variant="ghost" | 340 | variant="ghost" |
| 338 | {...props} | 341 | {...props} |
| 339 | > | 342 | > |
| 340 | {isOpen ? <PanelLeftCloseIcon /> : <PanelLeftIcon />} | 343 | {isOpen ? <PanelLeftCloseIcon className="size-4" /> : <PanelLeftIcon className="size-4" />} |
| 341 | <span className="sr-only">Toggle Sidebar</span> | 344 | <span className="sr-only">Toggle Sidebar</span> |
| 342 | </Button> | 345 | </Button> |
| 343 | ); | 346 | ); |