From 6e0571964cd79c1d0671507ab4697f833b170b6e Mon Sep 17 00:00:00 2001 From: Kian Bazarjani Date: Sat, 28 Feb 2026 00:20:04 -0500 Subject: [PATCH 1/2] chore: update menu examples --- apps/web/content/docs/dropdown-menu/index.mdx | 2 +- apps/web/registry/__index__.tsx | 13 +++++++++++ .../deep-search-linear/index.tsx | 7 +++++- .../deep-search-subpages-linear/index.tsx | 6 ++--- .../dropdown-menu/subpage-linear/index.tsx | 14 ++--------- apps/web/registry/ui/dropdown-menu/index.tsx | 23 +------------------ 6 files changed, 26 insertions(+), 39 deletions(-) diff --git a/apps/web/content/docs/dropdown-menu/index.mdx b/apps/web/content/docs/dropdown-menu/index.mdx index 4201afd2..da845aef 100644 --- a/apps/web/content/docs/dropdown-menu/index.mdx +++ b/apps/web/content/docs/dropdown-menu/index.mdx @@ -209,7 +209,7 @@ Then, compose with the same parts for the root menu to render the popup. Use subpages to model deeper menu flows without relying on deep-search/data-first APIs. - + ### Virtualized diff --git a/apps/web/registry/__index__.tsx b/apps/web/registry/__index__.tsx index db54a5ba..d7366a3d 100644 --- a/apps/web/registry/__index__.tsx +++ b/apps/web/registry/__index__.tsx @@ -114,6 +114,19 @@ export const examples: RegistryIndex = { 'registry/examples/dropdown-menu/subpage-linear/icons.tsx', ], }, + 'dropdown-menu-linear-subpage-label-creation': { + name: 'dropdown-menu-linear-subpage-label-creation', + type: 'registry:example', + component: React.lazy( + () => + import( + '@/registry/examples/dropdown-menu/linear-subpage-label-creation' + ), + ), + files: [ + 'registry/examples/dropdown-menu/linear-subpage-label-creation/index.tsx', + ], + }, 'dropdown-menu-search': { name: 'dropdown-menu-search', type: 'registry:example', diff --git a/apps/web/registry/examples/dropdown-menu/deep-search-linear/index.tsx b/apps/web/registry/examples/dropdown-menu/deep-search-linear/index.tsx index 08579e37..b9802fd7 100644 --- a/apps/web/registry/examples/dropdown-menu/deep-search-linear/index.tsx +++ b/apps/web/registry/examples/dropdown-menu/deep-search-linear/index.tsx @@ -275,11 +275,16 @@ function buildMenuContent(): NodeDef[] { ) return [ + aiFilterSubpage, + { + kind: 'separator', + id: 'ai-filter-separator', + render: ({ props }) => , + }, statusMenu, assigneeMenu, priorityMenu, labelsMenu, - aiFilterSubpage, projectPropertiesMenu, ] } diff --git a/apps/web/registry/examples/dropdown-menu/deep-search-subpages-linear/index.tsx b/apps/web/registry/examples/dropdown-menu/deep-search-subpages-linear/index.tsx index c5578019..804745c8 100644 --- a/apps/web/registry/examples/dropdown-menu/deep-search-subpages-linear/index.tsx +++ b/apps/web/registry/examples/dropdown-menu/deep-search-subpages-linear/index.tsx @@ -277,7 +277,7 @@ function createLabelsSubmenu(params: { - + ( <> @@ -810,9 +810,9 @@ export default function DropdownMenuDeepSearchSubpagesLinear() { debug={{ showSafeTriangleArea: { enabled: true, - showMissState: true, + showMissState: false, missColor: '#ff4d4f', - missFreezeDuration: 180, + missFreezeDuration: 2000, }, }} > diff --git a/apps/web/registry/examples/dropdown-menu/subpage-linear/index.tsx b/apps/web/registry/examples/dropdown-menu/subpage-linear/index.tsx index 4b3dfdb4..2f40c223 100644 --- a/apps/web/registry/examples/dropdown-menu/subpage-linear/index.tsx +++ b/apps/web/registry/examples/dropdown-menu/subpage-linear/index.tsx @@ -221,7 +221,7 @@ function SubmenuSurface({ export default function DropdownMenuSubpageLinear() { return ( - }> + }> Filter @@ -242,13 +242,6 @@ export default function DropdownMenuSubpageLinear() { - - - AI Filter - - - - } label="Status" /> @@ -326,10 +319,7 @@ export default function DropdownMenuSubpageLinear() { label="Project properties" /> - + { - console.log('highlight changed to', id, 'at index', index) - }} - {...props} - /> - ) + return } const Trigger = Primitive.Trigger @@ -892,13 +884,6 @@ function Submenu({ > >[1], ) => { - console.log('[DropdownMenu.Submenu] onOpenChange:', { - open, - reason: eventDetails.reason, - hasEvent: !!eventDetails.event, - hasCancel: typeof eventDetails.cancel === 'function', - }) - // Prevent closing when clicking on feedback toolbar elements if ( !open && @@ -910,13 +895,7 @@ function Submenu({ const feedbackToolbar = target?.closest( '[data-feedback-toolbar="true"]', ) - console.log('[DropdownMenu.Submenu] outside-press check:', { - target: target?.tagName, - targetClasses: target?.className, - feedbackToolbar: !!feedbackToolbar, - }) if (feedbackToolbar) { - console.log('[DropdownMenu.Submenu] Cancelling close!') eventDetails.cancel() return } From b6127742a961e4ad6bd07a9f79a782982e6fce1c Mon Sep 17 00:00:00 2001 From: Kian Bazarjani Date: Sat, 28 Feb 2026 18:04:58 -0500 Subject: [PATCH 2/2] update subpage example --- apps/web/content/docs/dropdown-menu/index.mdx | 12 +- apps/web/registry/__index__.tsx | 8 + .../examples/dropdown-menu/subpage/index.tsx | 213 ++++++++++++++++++ 3 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 apps/web/registry/examples/dropdown-menu/subpage/index.tsx diff --git a/apps/web/content/docs/dropdown-menu/index.mdx b/apps/web/content/docs/dropdown-menu/index.mdx index da845aef..dfe737e5 100644 --- a/apps/web/content/docs/dropdown-menu/index.mdx +++ b/apps/web/content/docs/dropdown-menu/index.mdx @@ -205,11 +205,17 @@ Then, compose with the same parts for the root menu to render the popup. -### Subpage navigation +### Subpage -Use subpages to model deeper menu flows without relying on deep-search/data-first APIs. +Use subpages as a natural replacement to modals, great for workflows such as item creation. - +Use the `` part, give it a `pageId`, and wrap it around a new `` with the subpage content. + +Render a `` and wire it up using the `targetPageId` prop. + + + + ### Virtualized diff --git a/apps/web/registry/__index__.tsx b/apps/web/registry/__index__.tsx index d7366a3d..a1ec655b 100644 --- a/apps/web/registry/__index__.tsx +++ b/apps/web/registry/__index__.tsx @@ -114,6 +114,14 @@ export const examples: RegistryIndex = { 'registry/examples/dropdown-menu/subpage-linear/icons.tsx', ], }, + 'dropdown-menu-subpage': { + name: 'dropdown-menu-subpage', + type: 'registry:example', + component: React.lazy( + () => import('@/registry/examples/dropdown-menu/subpage'), + ), + files: ['registry/examples/dropdown-menu/subpage/index.tsx'], + }, 'dropdown-menu-linear-subpage-label-creation': { name: 'dropdown-menu-linear-subpage-label-creation', type: 'registry:example', diff --git a/apps/web/registry/examples/dropdown-menu/subpage/index.tsx b/apps/web/registry/examples/dropdown-menu/subpage/index.tsx new file mode 100644 index 00000000..9bc49e0d --- /dev/null +++ b/apps/web/registry/examples/dropdown-menu/subpage/index.tsx @@ -0,0 +1,213 @@ +import { PlusIcon } from 'lucide-react' +import React from 'react' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' +import { DropdownMenu } from '@/registry/ui/dropdown-menu' + +const CREATABLE_LABEL_COLORS = [ + { id: 'red', label: 'Red' }, + { id: 'orange', label: 'Orange' }, + { id: 'green', label: 'Green' }, + { id: 'blue', label: 'Blue' }, + { id: 'violet', label: 'Violet' }, + { id: 'pink', label: 'Pink' }, +] as const + +export const LABEL_STYLES_BG = { + red: 'bg-red-500', + orange: 'bg-orange-500', + amber: 'bg-amber-500', + yellow: 'bg-yellow-500', + lime: 'bg-lime-500', + green: 'bg-green-500', + emerald: 'bg-emerald-500', + teal: 'bg-teal-500', + cyan: 'bg-cyan-500', + sky: 'bg-sky-500', + blue: 'bg-blue-500', + indigo: 'bg-indigo-500', + violet: 'bg-violet-500', + purple: 'bg-purple-500', + fuchsia: 'bg-fuchsia-500', + pink: 'bg-pink-500', + rose: 'bg-rose-500', + neutral: 'bg-neutral-500', +} as const + +export type TW_COLOR = keyof typeof LABEL_STYLES_BG + +type LabelRecord = { + id: string + name: string + color: TW_COLOR + checked?: boolean +} + +const labelsData: LabelRecord[] = [ + { id: 'frontend', name: 'Frontend', color: 'orange' }, + { id: 'backend', name: 'Backend', color: 'teal' }, + { id: 'api', name: 'API', color: 'red' }, + { id: 'security', name: 'Security', color: 'sky' }, + { id: 'testing', name: 'Testing', color: 'yellow' }, + { id: 'documentation', name: 'Documentation', color: 'rose' }, +] + +export default function SubpageDemo() { + const [labels, setLabels] = React.useState(labelsData) + const [search, setSearch] = React.useState('') + + const selectedLabels = React.useMemo( + () => labels.filter((label) => label.checked), + [labels], + ) + + const hasSelectedLabels = React.useMemo( + () => selectedLabels.length > 0, + [selectedLabels.length], + ) + + const normalizedSearch = React.useMemo( + () => search.trim().toLowerCase(), + [search], + ) + + const foundExactMatch = React.useMemo( + () => labels.find((label) => normalizedSearch === label.name.toLowerCase()), + [labels, normalizedSearch], + ) + + function createLabel(value: LabelRecord) { + setLabels((prev) => [...prev, { ...value, checked: true }]) + toast(`Created label: ${value.name}`) + } + + function handleLabelCheckedChange(id: string, checked: boolean) { + setLabels((prev) => + prev.map((label) => (label.id === id ? { ...label, checked } : label)), + ) + } + + return ( +
+
+ Select labels to add them to the list. + + Then, search for a label that doesn't exist (e.g. "Bug") and create + it. + +
+ +
+ {selectedLabels.map((label) => ( +
+
+ {label.name} +
+ ))} + + } + > + + {selectedLabels.length === 0 && 'Add label'} + +
+ + + + + + + {labels.map((label) => ( + + handleLabelCheckedChange(label.id, checked) + } + > + + +
+ + {label.name} + + ))} + {normalizedSearch.length > 0 && !foundExactMatch && ( + + } /> + + Create new label:{' '} + + "{search}" + + + + )} + + + + + + + + {CREATABLE_LABEL_COLORS.map((color) => ( + + createLabel({ + id: search, + name: search, + color: color.id, + }) + } + > + +
+ + + {color.label} + + ))} + + + + + + + +
+ ) +}