Skip to content

Commit 272c7df

Browse files
committed
v0.12.87: fix ContextMenu, Terminal bugs; add dynamic items support
ContextMenu: - Remove inline-block wrapper div, use cloneElement instead (fixes layout breaking on flex/block triggers) - Divider uses <hr> instead of DaisyUI divider class (excessive padding) - items prop now accepts a function (e) => ContextMenuItem[] for dynamic menus - Add DynamicItems demo to docs Terminal: - Cursor defaults to foreground color instead of primary (purple) - User options.theme is now deep-merged with auto-generated theme instead of replacing it Icons: - Fix unused props destructuring in createIcon (TS error) Build: - Replace sed with cross-platform tsx script in sync-icons-prefixed - Sync prefixed packages
1 parent b0a83ab commit 272c7df

21 files changed

Lines changed: 158 additions & 87 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
"publish:icons-prefixed": "pnpm --filter @aster-ui/icons-prefixed publish --access public",
1515
"publish:prefixed": "pnpm --filter @aster-ui/prefixed publish --access public",
1616
"sync-prefixed": "rm -rf packages/prefixed/src && cp -r packages/asterui/src packages/prefixed/ && npx tsx scripts/daisyui-prefix.ts --prefix d- --dir packages/prefixed/src && jq --arg v $(jq -r .version packages/asterui/package.json) '.version = $v' packages/prefixed/package.json > tmp.json && mv tmp.json packages/prefixed/package.json && pnpm --filter @aster-ui/prefixed build",
17-
"sync-icons-prefixed": "rm -rf packages/icons-prefixed/src packages/icons-prefixed/scripts && cp -r packages/icons/src packages/icons/scripts packages/icons-prefixed/ && sed -i \"s/'asterui'/'@aster-ui\\/prefixed'/g\" packages/icons-prefixed/src/context.tsx packages/icons-prefixed/scripts/generate.ts && jq --arg v $(jq -r .version packages/icons/package.json) '.version = $v' packages/icons-prefixed/package.json > tmp.json && mv tmp.json packages/icons-prefixed/package.json"
17+
"sync-icons-prefixed": "rm -rf packages/icons-prefixed/src packages/icons-prefixed/scripts && cp -r packages/icons/src packages/icons/scripts packages/icons-prefixed/ && npx tsx scripts/replace-in-files.ts \"'asterui'\" \"'@aster-ui/prefixed'\" packages/icons-prefixed/src/context.tsx packages/icons-prefixed/scripts/generate.ts && jq --arg v $(jq -r .version packages/icons/package.json) '.version = $v' packages/icons-prefixed/package.json > tmp.json && mv tmp.json packages/icons-prefixed/package.json"
1818
}
1919
}

packages/asterui/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ description: All notable changes to AsterUI
77

88
All notable changes to AsterUI are documented here.
99

10+
## v0.12.87 (2026-03-21)
11+
12+
### Bug Fixes
13+
14+
- **ContextMenu**: Removed `inline-block` wrapper div that broke flex/block layout on trigger elements. Now uses `cloneElement` to attach the context menu handler directly to the trigger child.
15+
- **ContextMenu**: Divider now uses `<hr>` instead of DaisyUI `divider` class which had excessive padding.
16+
- **Terminal**: Cursor color now defaults to foreground (`base-content`) instead of primary color.
17+
- **Terminal**: User `options.theme` is now deep-merged with the auto-generated theme instead of replacing it, allowing individual color overrides without losing other theme colors.
18+
19+
### Features
20+
21+
- **ContextMenu**: `items` prop now accepts a function `(e: React.MouseEvent) => ContextMenuItem[]` for dynamic menu items based on the click event.
22+
1023
## v0.12.86 (2026-02-28)
1124

1225
### Bug Fixes

packages/asterui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "asterui",
3-
"version": "0.12.86",
3+
"version": "0.12.87",
44
"description": "React UI component library with DaisyUI",
55
"homepage": "https://asterui.com",
66
"repository": {

packages/asterui/src/components/ContextMenu.tsx

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@ import { useConfig } from '../providers/ConfigProvider'
44

55
// DaisyUI classes
66
const dMenu = 'menu'
7-
const dDivider = 'divider'
87

98
export interface ContextMenuItem {
109
key: string
11-
label: React.ReactNode
10+
label?: React.ReactNode
1211
icon?: React.ReactNode
1312
disabled?: boolean
1413
danger?: boolean
1514
divider?: boolean
1615
children?: ContextMenuItem[]
16+
'data-testid'?: string
1717
}
1818

1919
export interface ContextMenuProps {
20-
/** Element that triggers the context menu on right-click */
20+
/** Element that triggers the context menu on right-click (first child) */
2121
children: React.ReactNode
22-
/** Menu items (data-driven pattern) */
23-
items?: ContextMenuItem[]
22+
/** Menu items (data-driven pattern). Can be a static array or a function that receives the mouse event for dynamic items. */
23+
items?: ContextMenuItem[] | ((e: React.MouseEvent) => ContextMenuItem[])
2424
/** Callback when an item is selected */
2525
onSelect?: (key: string) => void
2626
/** Whether the context menu is disabled */
@@ -130,7 +130,7 @@ const ContextMenuItemComponent: React.FC<ContextMenuItemProps> = ({
130130
}
131131

132132
const ContextMenuDividerComponent: React.FC<ContextMenuDividerProps> = ({ className = '', 'data-testid': testId }) => {
133-
return <li className={`${dDivider} my-1 ${className}`} role="separator" data-testid={testId}></li>
133+
return <hr className={`border-base-300 my-1 ${className}`} role="separator" data-testid={testId} />
134134
}
135135

136136
const ContextMenuSubMenuComponent: React.FC<ContextMenuSubMenuProps> = ({
@@ -139,13 +139,13 @@ const ContextMenuSubMenuComponent: React.FC<ContextMenuSubMenuProps> = ({
139139
disabled = false,
140140
children,
141141
className = '',
142-
_key: _unusedKey,
142+
_key,
143143
'data-testid': testId,
144144
}) => {
145145
const { getTestId } = useContextMenuContext()
146146
const [showSubmenu, setShowSubmenu] = useState(false)
147147
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
148-
const submenuTestId = testId ?? (_unusedKey ? getTestId?.(`submenu-${_unusedKey}`) : undefined)
148+
const submenuTestId = testId ?? (_key ? getTestId?.(`submenu-${_key}`) : undefined)
149149

150150
const handleMouseEnter = () => {
151151
if (disabled) return
@@ -206,10 +206,10 @@ const MenuItem: React.FC<{
206206
}> = ({ item, onSelect, onClose, getTestId }) => {
207207
const [showSubmenu, setShowSubmenu] = useState(false)
208208
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
209-
const itemTestId = getTestId?.(`item-${item.key}`)
209+
const itemTestId = item['data-testid'] ?? getTestId?.(`item-${item.key}`)
210210

211211
if (item.divider) {
212-
return <li className={`${dDivider} my-1`} role="separator" data-testid={getTestId?.(`separator-${item.key}`)}></li>
212+
return <hr className="border-base-300 my-1" role="separator" data-testid={getTestId?.(`separator-${item.key}`)} />
213213
}
214214

215215
const handleClick = () => {
@@ -289,24 +289,24 @@ const ContextMenuRoot: React.FC<ContextMenuProps> = ({
289289
const { getPopupContainer } = useConfig()
290290
const [visible, setVisible] = useState(false)
291291
const [position, setPosition] = useState<MenuPosition>({ x: 0, y: 0 })
292+
const [resolvedItems, setResolvedItems] = useState<ContextMenuItem[] | undefined>(undefined)
292293
const menuRef = useRef<HTMLUListElement>(null)
293-
const triggerRef = useRef<HTMLDivElement>(null)
294294

295295
const handleContextMenu = useCallback(
296296
(e: React.MouseEvent) => {
297297
if (disabled) return
298298
e.preventDefault()
299299
e.stopPropagation()
300300

301-
// Calculate position, ensuring menu stays within viewport
302-
let x = e.clientX
303-
let y = e.clientY
304-
305-
// We'll adjust after render when we know menu dimensions
306-
setPosition({ x, y })
301+
setPosition({ x: e.clientX, y: e.clientY })
302+
if (typeof items === 'function') {
303+
setResolvedItems(items(e))
304+
} else {
305+
setResolvedItems(items)
306+
}
307307
setVisible(true)
308308
},
309-
[disabled]
309+
[disabled, items]
310310
)
311311

312312
const handleClose = useCallback(() => {
@@ -387,12 +387,11 @@ const ContextMenuRoot: React.FC<ContextMenuProps> = ({
387387
})
388388
}
389389

390-
// Determine if using data-driven or compound pattern
391-
// Find menu content children (not the trigger element)
390+
// Separate trigger (first child) from menu content children
392391
const childArray = React.Children.toArray(children)
393-
const triggerChild = childArray[0]
392+
const trigger = childArray[0]
394393
const menuChildren = cloneChildrenWithKeys(childArray.slice(1))
395-
const useDataDriven = items && items.length > 0
394+
const hasDataItems = resolvedItems && resolvedItems.length > 0
396395

397396
const contextValue: ContextMenuContextValue = {
398397
onSelect: handleSelect,
@@ -401,16 +400,20 @@ const ContextMenuRoot: React.FC<ContextMenuProps> = ({
401400
}
402401
const getTestId = (suffix: string) => (testId ? `${testId}-${suffix}` : undefined)
403402

403+
// Attach onContextMenu directly to trigger via cloneElement — no wrapper div
404+
const triggerWithHandler = React.isValidElement(trigger)
405+
? React.cloneElement(trigger as React.ReactElement<any>, {
406+
onContextMenu: (e: React.MouseEvent) => {
407+
const existing = (trigger as React.ReactElement<any>).props.onContextMenu
408+
if (existing) existing(e)
409+
handleContextMenu(e)
410+
},
411+
})
412+
: trigger
413+
404414
return (
405415
<>
406-
<div
407-
ref={triggerRef}
408-
onContextMenu={handleContextMenu}
409-
className="inline-block"
410-
data-testid={testId}
411-
>
412-
{triggerChild}
413-
</div>
416+
{triggerWithHandler}
414417
{visible &&
415418
createPortal(
416419
<ContextMenuContext.Provider value={contextValue}>
@@ -422,8 +425,8 @@ const ContextMenuRoot: React.FC<ContextMenuProps> = ({
422425
aria-label="Context menu"
423426
data-testid={getTestId('menu')}
424427
>
425-
{useDataDriven
426-
? items!.map((item) => (
428+
{hasDataItems
429+
? resolvedItems.map((item) => (
427430
<MenuItem key={item.key} item={item} onSelect={handleSelect} onClose={handleClose} getTestId={getTestId} />
428431
))
429432
: menuChildren}

packages/asterui/src/components/Terminal.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const Terminal = forwardRef<TerminalRef, TerminalProps>(({
9696
const getTheme = () => ({
9797
background: colors.background,
9898
foreground: colors.foreground,
99-
cursor: colors.primary,
99+
cursor: colors.foreground,
100100
cursorAccent: colors.background,
101101
selectionBackground: colors.primary + '40',
102102
selectionForeground: colors.foreground,
@@ -276,13 +276,14 @@ export const Terminal = forwardRef<TerminalRef, TerminalProps>(({
276276

277277
initialized = true
278278

279+
const { theme: userTheme, ...restOptions } = options
279280
terminal = new XTerm({
280-
theme: getTheme(),
281+
theme: { ...getTheme(), ...userTheme },
281282
cursorBlink: true,
282283
convertEol,
283284
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
284285
fontSize: 14,
285-
...options,
286+
...restOptions,
286287
})
287288
fitAddon = new FitAddon()
288289

@@ -335,7 +336,7 @@ export const Terminal = forwardRef<TerminalRef, TerminalProps>(({
335336
// Update theme when colors change
336337
useEffect(() => {
337338
if (!terminalRef.current) return
338-
terminalRef.current.options.theme = getTheme()
339+
terminalRef.current.options.theme = { ...getTheme(), ...options.theme }
339340
}, [isDark, colors]) // eslint-disable-line react-hooks/exhaustive-deps
340341

341342
return (

packages/docs/public/docs/contextmenu.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ import { ContextMenu } from 'asterui'
106106
| Property | Description | Type | Default |
107107
|----------|-------------|------|---------|
108108
| children | Trigger element (first child) and menu items (compound pattern) | `React.ReactNode` | - |
109-
| items | Menu items (data-driven pattern) | `ContextMenuItem[]` | - |
109+
| items | Menu items (static array or function for dynamic items) | `ContextMenuItem[] \| ((e: React.MouseEvent) => ContextMenuItem[])` | - |
110110
| onSelect | Selection callback | `(key: string) => void` | - |
111111
| disabled | Whether the context menu is disabled | `boolean` | `false` |
112112
| className | Additional CSS classes | `string` | - |

packages/docs/public/llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ npm install asterui
142142
- [Browser](https://asterui.com/docs/browser.md)
143143
- [Code](https://asterui.com/docs/code.md)
144144
- [Phone](https://asterui.com/docs/phone.md)
145-
- [Terminal](https://asterui.com/docs/terminal.md)
145+
- [Terminal](https://asterui.com/docs/terminal.md) (import from `asterui/terminal`)
146146
- [Window](https://asterui.com/docs/window.md)
147147

148148
### Hooks

packages/docs/src/components/ContextMenuDemo.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,34 @@ export function SubmenuDemo() {
126126
)
127127
}
128128

129+
// @example-imports: { ContextMenu, notification } from 'asterui'
130+
export function DynamicItemsDemo() {
131+
return (
132+
<Demo>
133+
{/* @example-return */}
134+
<ContextMenu
135+
items={(e) => {
136+
const target = (e.target as HTMLElement).textContent || ''
137+
return [
138+
{ key: 'info', label: `Clicked on: "${target}"` , disabled: true },
139+
{ key: 'divider', divider: true },
140+
{ key: 'copy', label: 'Copy' },
141+
{ key: 'inspect', label: 'Inspect' },
142+
]
143+
}}
144+
onSelect={(key) => notification.info({ message: `Selected: ${key}` })}
145+
>
146+
<div className="flex gap-2">
147+
<div className="p-4 bg-base-200 rounded-lg cursor-context-menu">Item A</div>
148+
<div className="p-4 bg-base-200 rounded-lg cursor-context-menu">Item B</div>
149+
<div className="p-4 bg-base-200 rounded-lg cursor-context-menu">Item C</div>
150+
</div>
151+
</ContextMenu>
152+
{/* @example-return-end */}
153+
</Demo>
154+
)
155+
}
156+
129157
// @example-imports: { ContextMenu, notification } from 'asterui'
130158
export function DisabledDemo() {
131159
// @example-include

packages/docs/src/content/docs/components/contextmenu.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
IconsDemo,
1111
DividersDemo,
1212
SubmenuDemo,
13+
DynamicItemsDemo,
1314
DisabledDemo,
1415
} from '../../../components/ContextMenuDemo'
1516
import ContextMenuDemoSource from '../../../components/ContextMenuDemo.tsx?raw'
@@ -47,6 +48,10 @@ import { ContextMenu } from 'asterui'
4748
<SubmenuDemo client:load slot="demo" />
4849
</Example>
4950

51+
<Example title="Dynamic Items" description="Use a function for items to generate menu items based on the click event." source={ContextMenuDemoSource} fn="DynamicItemsDemo">
52+
<DynamicItemsDemo client:load slot="demo" />
53+
</Example>
54+
5055
<Example title="Disabled Items" description="Some menu items can be disabled." source={ContextMenuDemoSource} fn="DisabledDemo">
5156
<DisabledDemo client:load slot="demo" />
5257
</Example>
@@ -60,7 +65,7 @@ import { ContextMenu } from 'asterui'
6065
| Property | Description | Type | Default |
6166
|----------|-------------|------|---------|
6267
| `children` | Element that triggers the context menu on right-click | `React.ReactNode` | - |
63-
| `items` | Menu items | `ContextMenuItem[]` | - |
68+
| `items` | Menu items (static array or function for dynamic items) | `ContextMenuItem[] \| ((e: React.MouseEvent) => ContextMenuItem[])` | - |
6469
| `onSelect` | Callback when an item is selected | `(key: string) => void` | - |
6570
| `disabled` | Whether the context menu is disabled | `boolean` | `false` |
6671
| `className` | Additional CSS classes for the menu | `string` | - |

packages/docs/src/content/docs/components/terminal.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import { Terminal } from 'asterui/terminal'
6767
| `onLine` | Callback when user submits a line in readline mode. Return a Promise to defer the next prompt. | `(line: string) => void \| Promise<void>` | - |
6868
| `onData` | Callback for raw input (not used in readline mode) | `(data: string) => void` | - |
6969
| `onReady` | Callback when terminal is initialized | `(terminal: XTerm) => void` | - |
70-
| `options` | xterm.js options (theme is auto-applied) | `ITerminalOptions` | - |
70+
| `options` | xterm.js options (theme is auto-generated and merged with any user theme overrides) | `ITerminalOptions` | - |
7171
| `className` | Additional CSS classes | `string` | - |
7272
| `style` | Container style | `React.CSSProperties` | - |
7373
| `data-testid` | Test ID for testing | `string` | - |
@@ -102,7 +102,7 @@ When `readline` is enabled, the terminal provides:
102102
The Terminal component automatically uses DaisyUI theme colors:
103103

104104
- **Background/Foreground**: Uses `base-100` and `base-content`
105-
- **Cursor**: Uses `primary` color
105+
- **Cursor**: Uses `foreground` color (base-content)
106106
- **ANSI Colors**: Maps to `error`, `success`, `warning`, `info`, `secondary`, `accent`
107107

108108
When the theme changes (light/dark), the terminal colors update automatically.

0 commit comments

Comments
 (0)