solid-tom-ui 1.0.10 → 1.0.14
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 +246 -246
- package/dist/README.md +246 -246
- package/dist/components/avatar/avatar.js.map +1 -1
- package/dist/components/badge/badge.js.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/carousel/carousel.js.map +1 -1
- package/dist/components/chat-bubble/chatBubble.js.map +1 -1
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/collapse/collapse.js.map +1 -1
- package/dist/components/context-menu/context-menu.js.map +1 -1
- package/dist/components/context-menu/context-menu.store.js.map +1 -1
- package/dist/components/divider/divider.js.map +1 -1
- package/dist/components/dropdown/dropdown.js.map +1 -1
- package/dist/components/dropdown/dropdown.store.js.map +1 -1
- package/dist/components/float-button/float-button.js.map +1 -1
- package/dist/components/hover-3d-image/hover-3d-image.js.map +1 -1
- package/dist/components/image-preview/image-preview.js.map +1 -1
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/input/input.utils.js.map +1 -1
- package/dist/components/input/variants/input-color.js.map +1 -1
- package/dist/components/input/variants/input-date.js.map +1 -1
- package/dist/components/input/variants/input-number.d.ts.map +1 -1
- package/dist/components/input/variants/input-number.js +1 -1
- package/dist/components/input/variants/input-number.js.map +1 -1
- package/dist/components/input/variants/input-otp.js.map +1 -1
- package/dist/components/input/variants/input-password.js.map +1 -1
- package/dist/components/input/variants/input-radio.js.map +1 -1
- package/dist/components/input/variants/input-range.js.map +1 -1
- package/dist/components/input/variants/input-text.d.ts.map +1 -1
- package/dist/components/input/variants/input-text.js +1 -1
- package/dist/components/input/variants/input-text.js.map +1 -1
- package/dist/components/input/variants/input-textarea.js.map +1 -1
- package/dist/components/loading/loading.js.map +1 -1
- package/dist/components/mansory/mansory.js.map +1 -1
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/menu/menu.types.d.ts +2 -3
- package/dist/components/menu/menu.types.d.ts.map +1 -1
- package/dist/components/modal/modal.js.map +1 -1
- package/dist/components/modal/modalContext.js.map +1 -1
- package/dist/components/pagination/pagination.js.map +1 -1
- package/dist/components/progress-bar/progress-bar.js.map +1 -1
- package/dist/components/qr-code/qr-code.js.map +1 -1
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select-zone/select-zone.js.map +1 -1
- package/dist/components/skeleton/skeleton.js.map +1 -1
- package/dist/components/slider/slider.js.map +1 -1
- package/dist/components/splitter/splitter.js.map +1 -1
- package/dist/components/steps/steps.js.map +1 -1
- package/dist/components/swap/swap.js.map +1 -1
- package/dist/components/switch/switch.js.map +1 -1
- package/dist/components/tab/tab.js.map +1 -1
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/timeline/timeline.js.map +1 -1
- package/dist/components/toast/icons/ErrorIcon.js.map +1 -1
- package/dist/components/toast/icons/IconCircle.js.map +1 -1
- package/dist/components/toast/icons/InfoIcon.js.map +1 -1
- package/dist/components/toast/icons/LoaderIcon.js.map +1 -1
- package/dist/components/toast/icons/SuccessIcon.js.map +1 -1
- package/dist/components/toast/icons/WarningIcon.js.map +1 -1
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/toast/toast.store.js.map +1 -1
- package/dist/components/tooltip/tooltip.js.map +1 -1
- package/dist/components/tour/tour.js.map +1 -1
- package/dist/components/upload/upload.js.map +1 -1
- package/dist/components/z-index/z-index.context.js.map +1 -1
- package/dist/components/z-index/z-index.js.map +1 -1
- package/dist/components/z-index/z-index.store.js.map +1 -1
- package/dist/components/z-index/z-index.types.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/skill/avatar.skill.md.txt +255 -255
- package/dist/skill/badge.skill.md.txt +223 -223
- package/dist/skill/breadcrumb.skill.md.txt +177 -177
- package/dist/skill/button.skill.md.txt +198 -198
- package/dist/skill/carousel.skill.md.txt +406 -406
- package/dist/skill/chat-bubble.skill.md.txt +342 -342
- package/dist/skill/checkbox.skill.md.txt +326 -326
- package/dist/skill/code-preview.skill.md.txt +240 -240
- package/dist/skill/collapse.skill.md.txt +329 -329
- package/dist/skill/context-menu.skill.md.txt +233 -233
- package/dist/skill/diff.skill.md.txt +244 -244
- package/dist/skill/divider.skill.md.txt +151 -151
- package/dist/skill/doc.skill.md.txt +191 -191
- package/dist/skill/drawer.skill.md.txt +157 -157
- package/dist/skill/dropdown.skill.md.txt +198 -198
- package/dist/skill/float-button.skill.md.txt +315 -315
- package/dist/skill/hover-3d-image.skill.md.txt +120 -120
- package/dist/skill/iframe.skill.md.txt +114 -114
- package/dist/skill/image-preview.skill.md.txt +162 -162
- package/dist/skill/indicator.skill.md.txt +60 -60
- package/dist/skill/input.skill.md.txt +489 -489
- package/dist/skill/loading.skill.md.txt +127 -127
- package/dist/skill/menu.skill.md.txt +476 -476
- package/dist/skill/modal.skill.md.txt +359 -359
- package/dist/skill/pagination.skill.md.txt +405 -405
- package/dist/skill/progress-bar.skill.md.txt +207 -207
- package/dist/skill/qr-code.skill.md.txt +136 -136
- package/dist/skill/rating.skill.md.txt +167 -167
- package/dist/skill/select-zone.skill.md.txt +93 -93
- package/dist/skill/select.skill.md.txt +663 -663
- package/dist/skill/skeleton.skill.md.txt +192 -192
- package/dist/skill/slider.skill.md.txt +404 -404
- package/dist/skill/splitter.skill.md.txt +411 -411
- package/dist/skill/steps.skill.md.txt +264 -264
- package/dist/skill/swap.skill.md.txt +139 -139
- package/dist/skill/switch.skill.md.txt +191 -191
- package/dist/skill/tab.skill.md.txt +484 -484
- package/dist/skill/table.example.header.md.txt +666 -666
- package/dist/skill/table.skill.md.txt +1407 -1407
- package/dist/skill/text-rotate.skill.md.txt +186 -186
- package/dist/skill/timeline.skill.md.txt +247 -247
- package/dist/skill/toast.skill.md.txt +531 -531
- package/dist/skill/tooltip.skill.md.txt +222 -222
- package/dist/skill/tour.skill.md.txt +156 -156
- package/dist/skill/upload.skill.md.txt +358 -358
- package/dist/utils/cn.js.map +1 -1
- package/dist/utils/element-tracker.js.map +1 -1
- package/dist/utils/helper.js.map +1 -1
- package/dist/utils/hoc.js.map +1 -1
- package/package.json +132 -133
|
@@ -1,233 +1,233 @@
|
|
|
1
|
-
## COMPONENT IDENTITY
|
|
2
|
-
- **Import**: `import { ContextMenu, createContextMenuHandle } from 'solid-tom-ui';`
|
|
3
|
-
- **Exports**: `ContextMenu` (renderer), `createContextMenuHandle` (factory)
|
|
4
|
-
- **Framework**: SolidJS
|
|
5
|
-
- **Purpose**: Context-aware right-click menu with a registry pattern. Each UI zone registers its own items independently; the menu builds the visible list from the current context at trigger time. No coupling between zones — each feature owns its items.
|
|
6
|
-
|
|
7
|
-
## TYPE SIGNATURES
|
|
8
|
-
|
|
9
|
-
### createContextMenuHandle
|
|
10
|
-
```typescript
|
|
11
|
-
import { createContextMenuHandle } from 'solid-tom-ui';
|
|
12
|
-
|
|
13
|
-
function createContextMenuHandle<TContext extends Record<string, unknown>>(): ContextMenuHandle<TContext>;
|
|
14
|
-
|
|
15
|
-
type ContextMenuHandle<TContext> = {
|
|
16
|
-
register: (item: RegistryItem<TContext>) => void;
|
|
17
|
-
unregister: (key: string) => void;
|
|
18
|
-
trigger: (e: MouseEvent, ctx: TContext) => void;
|
|
19
|
-
};
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### ContextMenu
|
|
23
|
-
```typescript
|
|
24
|
-
import { ContextMenu } from 'solid-tom-ui';
|
|
25
|
-
|
|
26
|
-
type ContextMenuProps<TContext> = {
|
|
27
|
-
handle: ContextMenuHandle<TContext>; // REQUIRED: handle from createContextMenuHandle()
|
|
28
|
-
class?: Partial<Record<
|
|
29
|
-
'content' | 'item' | 'separator' | 'submenu',
|
|
30
|
-
string
|
|
31
|
-
>>; // CSS slot overrides
|
|
32
|
-
};
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### RegistryItem (discriminated union)
|
|
36
|
-
```typescript
|
|
37
|
-
// Shared base fields
|
|
38
|
-
type BaseRegistryItem<TContext> = {
|
|
39
|
-
key: string; // REQUIRED: unique ID for dedup / unregister
|
|
40
|
-
label: string; // REQUIRED: display text
|
|
41
|
-
when?: (ctx: TContext) => boolean; // visibility predicate — omit = always visible
|
|
42
|
-
disabled?: (ctx: TContext) => boolean; // disable predicate — omit = always enabled
|
|
43
|
-
shortcut?: string; // display-only hint (e.g. "Ctrl+C"), not bound
|
|
44
|
-
prefixIcon?: JSXElement; // icon element rendered to the left of label
|
|
45
|
-
order?: number; // sort order, lower = higher up (default: 100)
|
|
46
|
-
group?: string; // items in the same group are visually grouped;
|
|
47
|
-
// separators are auto-inserted between groups
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Action item
|
|
51
|
-
type ActionRegistryItem<TContext> = BaseRegistryItem<TContext> & {
|
|
52
|
-
type: 'item';
|
|
53
|
-
onClick: (ctx: TContext) => void; // REQUIRED for 'item'
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Submenu item
|
|
57
|
-
type SubmenuRegistryItem<TContext> = BaseRegistryItem<TContext> & {
|
|
58
|
-
type: 'submenu';
|
|
59
|
-
items: RegistryItem<TContext>[]; // REQUIRED for 'submenu'; one level deep
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
type RegistryItem<TContext> = ActionRegistryItem<TContext> | SubmenuRegistryItem<TContext>;
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## KEY RULES
|
|
66
|
-
- `createContextMenuHandle<TContext>()` is generic — define your own context type that describes UI state at the moment of right-click
|
|
67
|
-
- Only **one** context menu may be open at a time (shared singleton open/close state) — multiple handles still share this state
|
|
68
|
-
- `<ContextMenu>` **must** be rendered in the JSX tree, typically near the app root to avoid z-index conflicts
|
|
69
|
-
- `prefixIcon` accepts a `JSXElement` (rendered JSX), not a function component
|
|
70
|
-
- Submenus are **one level deep** only — nested `items` arrays inside a submenu are not supported
|
|
71
|
-
- `shortcut` is display-only and does **not** bind keyboard events — wire those separately
|
|
72
|
-
|
|
73
|
-
## MINIMAL SETUP
|
|
74
|
-
|
|
75
|
-
```tsx
|
|
76
|
-
import { createContextMenuHandle, ContextMenu } from 'solid-tom-ui';
|
|
77
|
-
|
|
78
|
-
// 1. Define context shape
|
|
79
|
-
type MyCtx = { zone: 'editor' | 'sidebar'; hasSelection: boolean };
|
|
80
|
-
|
|
81
|
-
// 2. Create handle (component level or module scope)
|
|
82
|
-
const handle = createContextMenuHandle<MyCtx>();
|
|
83
|
-
|
|
84
|
-
// 3. Register items
|
|
85
|
-
handle.register({
|
|
86
|
-
key: 'copy',
|
|
87
|
-
type: 'item',
|
|
88
|
-
label: 'Copy',
|
|
89
|
-
when: ctx => ctx.hasSelection,
|
|
90
|
-
onClick: ctx => console.log('copy in', ctx.zone),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// 4. Render once near app root
|
|
94
|
-
<ContextMenu handle={handle} />
|
|
95
|
-
|
|
96
|
-
// 5. Trigger on right-click
|
|
97
|
-
<div onContextMenu={e => handle.trigger(e, { zone: 'editor', hasSelection: true })}>
|
|
98
|
-
Right-click here
|
|
99
|
-
</div>
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## USAGE PATTERNS
|
|
103
|
-
|
|
104
|
-
### Module-scoped handle (shared across components)
|
|
105
|
-
```ts
|
|
106
|
-
// context-menu.handle.ts
|
|
107
|
-
export type AppCtx = { zone: string; isFile: boolean };
|
|
108
|
-
export const appMenuHandle = createContextMenuHandle<AppCtx>();
|
|
109
|
-
|
|
110
|
-
// FileExplorer.tsx — registers its own items
|
|
111
|
-
appMenuHandle.register({ key: 'rename', type: 'item', label: 'Rename', when: ctx => ctx.isFile, onClick: ctx => rename(ctx) });
|
|
112
|
-
|
|
113
|
-
// Editor.tsx — registers independent items
|
|
114
|
-
appMenuHandle.register({ key: 'copy', type: 'item', label: 'Copy', when: ctx => ctx.zone === 'editor', onClick: () => copy() });
|
|
115
|
-
|
|
116
|
-
// App.tsx — render once
|
|
117
|
-
<ContextMenu handle={appMenuHandle} />
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Dynamic registration / unregistration
|
|
121
|
-
```ts
|
|
122
|
-
handle.register({ key: 'admin-delete', type: 'item', label: 'Delete', onClick: () => deleteItem() });
|
|
123
|
-
|
|
124
|
-
// Later (e.g. user logs out):
|
|
125
|
-
handle.unregister('admin-delete');
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Grouped items with separators
|
|
129
|
-
```ts
|
|
130
|
-
handle.register({ key: 'cut', type: 'item', label: 'Cut', group: 'clipboard', order: 10, onClick: () => cut() });
|
|
131
|
-
handle.register({ key: 'copy', type: 'item', label: 'Copy', group: 'clipboard', order: 11, onClick: () => copy() });
|
|
132
|
-
handle.register({ key: 'paste', type: 'item', label: 'Paste', group: 'clipboard', order: 12, onClick: () => paste() });
|
|
133
|
-
handle.register({ key: 'delete', type: 'item', label: 'Delete', group: 'danger', order: 20, onClick: () => del() });
|
|
134
|
-
// Separator is auto-inserted between 'clipboard' and 'danger' groups
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Submenu
|
|
138
|
-
```ts
|
|
139
|
-
handle.register({
|
|
140
|
-
key: 'export',
|
|
141
|
-
type: 'submenu',
|
|
142
|
-
label: 'Export as',
|
|
143
|
-
items: [
|
|
144
|
-
{ key: 'export-png', type: 'item', label: 'PNG', onClick: () => exportPng() },
|
|
145
|
-
{ key: 'export-svg', type: 'item', label: 'SVG', onClick: () => exportSvg() },
|
|
146
|
-
{ key: 'export-pdf', type: 'item', label: 'PDF', onClick: () => exportPdf() },
|
|
147
|
-
],
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Icon and keyboard shortcut hints
|
|
152
|
-
```ts
|
|
153
|
-
import Copy from 'lucide-solid/icons/copy';
|
|
154
|
-
|
|
155
|
-
handle.register({
|
|
156
|
-
key: 'copy',
|
|
157
|
-
type: 'item',
|
|
158
|
-
label: 'Copy',
|
|
159
|
-
prefixIcon: <Copy size={14} />,
|
|
160
|
-
shortcut: 'Ctrl+C',
|
|
161
|
-
onClick: () => copy(),
|
|
162
|
-
});
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Multiple independent menus (separate context types)
|
|
166
|
-
```ts
|
|
167
|
-
const editorMenu = createContextMenuHandle<EditorCtx>();
|
|
168
|
-
const tableMenu = createContextMenuHandle<TableCtx>();
|
|
169
|
-
|
|
170
|
-
<ContextMenu handle={editorMenu} />
|
|
171
|
-
<ContextMenu handle={tableMenu} />
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### CSS slot override
|
|
175
|
-
```tsx
|
|
176
|
-
<ContextMenu
|
|
177
|
-
handle={handle}
|
|
178
|
-
class={{
|
|
179
|
-
content: 'rounded-xl shadow-2xl',
|
|
180
|
-
item: 'text-sm',
|
|
181
|
-
separator: 'my-1 opacity-30',
|
|
182
|
-
submenu: 'font-medium',
|
|
183
|
-
}}
|
|
184
|
-
/>
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
## DECISION RULES
|
|
188
|
-
|
|
189
|
-
**Single handle vs multiple handles:**
|
|
190
|
-
- One shared context type across the entire app → single module-scoped handle
|
|
191
|
-
- Completely separate feature areas with different context shapes → separate handles per area
|
|
192
|
-
|
|
193
|
-
**`when` vs `disabled`:**
|
|
194
|
-
- Item doesn't belong to this context at all → use `when` (hides the item)
|
|
195
|
-
- Item belongs but action is not available right now → use `disabled` (shows grayed-out)
|
|
196
|
-
|
|
197
|
-
**`order` and `group`:**
|
|
198
|
-
- Use `group` to visually separate categories (separators are automatic)
|
|
199
|
-
- Use `order` to control position within a group (lower number = higher position)
|
|
200
|
-
- If order is not specified, items are rendered in registration order (default: 100)
|
|
201
|
-
|
|
202
|
-
**`prefixIcon`:**
|
|
203
|
-
- Pass a rendered JSX element: `prefixIcon={<Copy size={14} />}`
|
|
204
|
-
- Import icons by direct path to avoid bundling the entire lucide library
|
|
205
|
-
|
|
206
|
-
## ANTI-PATTERNS
|
|
207
|
-
```tsx
|
|
208
|
-
// ❌ Rendering ContextMenu deep in a child component — may cause z-index issues
|
|
209
|
-
<Modal>
|
|
210
|
-
<ContextMenu handle={handle} /> // wrong — render near app root instead
|
|
211
|
-
</Modal>
|
|
212
|
-
|
|
213
|
-
// ❌ Passing a function component as prefixIcon (it won't render)
|
|
214
|
-
prefixIcon={Copy} // wrong
|
|
215
|
-
prefixIcon={<Copy size={14} />} // ✅ pass JSX element
|
|
216
|
-
|
|
217
|
-
// ❌ Nesting submenus more than one level deep (not supported)
|
|
218
|
-
{
|
|
219
|
-
type: 'submenu',
|
|
220
|
-
items: [{ type: 'submenu', items: [...] }] // second level ignored
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ❌ Expecting shortcut to bind a keyboard event
|
|
224
|
-
shortcut: 'Ctrl+Z' // display only — wire onKeyDown separately
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
## Component Conventions
|
|
230
|
-
|
|
231
|
-
> **CSS encoding**: internal CSS classes use short encoded names (e.g. `cm01`, `cm02`) per project convention.
|
|
232
|
-
|
|
233
|
-
> **Unique IDs**: if this component needs to generate HTML `id` attributes, always use `createUniqueId()` from `solid-js` — never `Math.random()` or `Date.now()`.
|
|
1
|
+
## COMPONENT IDENTITY
|
|
2
|
+
- **Import**: `import { ContextMenu, createContextMenuHandle } from 'solid-tom-ui';`
|
|
3
|
+
- **Exports**: `ContextMenu` (renderer), `createContextMenuHandle` (factory)
|
|
4
|
+
- **Framework**: SolidJS
|
|
5
|
+
- **Purpose**: Context-aware right-click menu with a registry pattern. Each UI zone registers its own items independently; the menu builds the visible list from the current context at trigger time. No coupling between zones — each feature owns its items.
|
|
6
|
+
|
|
7
|
+
## TYPE SIGNATURES
|
|
8
|
+
|
|
9
|
+
### createContextMenuHandle
|
|
10
|
+
```typescript
|
|
11
|
+
import { createContextMenuHandle } from 'solid-tom-ui';
|
|
12
|
+
|
|
13
|
+
function createContextMenuHandle<TContext extends Record<string, unknown>>(): ContextMenuHandle<TContext>;
|
|
14
|
+
|
|
15
|
+
type ContextMenuHandle<TContext> = {
|
|
16
|
+
register: (item: RegistryItem<TContext>) => void;
|
|
17
|
+
unregister: (key: string) => void;
|
|
18
|
+
trigger: (e: MouseEvent, ctx: TContext) => void;
|
|
19
|
+
};
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### ContextMenu
|
|
23
|
+
```typescript
|
|
24
|
+
import { ContextMenu } from 'solid-tom-ui';
|
|
25
|
+
|
|
26
|
+
type ContextMenuProps<TContext> = {
|
|
27
|
+
handle: ContextMenuHandle<TContext>; // REQUIRED: handle from createContextMenuHandle()
|
|
28
|
+
class?: Partial<Record<
|
|
29
|
+
'content' | 'item' | 'separator' | 'submenu',
|
|
30
|
+
string
|
|
31
|
+
>>; // CSS slot overrides
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### RegistryItem (discriminated union)
|
|
36
|
+
```typescript
|
|
37
|
+
// Shared base fields
|
|
38
|
+
type BaseRegistryItem<TContext> = {
|
|
39
|
+
key: string; // REQUIRED: unique ID for dedup / unregister
|
|
40
|
+
label: string; // REQUIRED: display text
|
|
41
|
+
when?: (ctx: TContext) => boolean; // visibility predicate — omit = always visible
|
|
42
|
+
disabled?: (ctx: TContext) => boolean; // disable predicate — omit = always enabled
|
|
43
|
+
shortcut?: string; // display-only hint (e.g. "Ctrl+C"), not bound
|
|
44
|
+
prefixIcon?: JSXElement; // icon element rendered to the left of label
|
|
45
|
+
order?: number; // sort order, lower = higher up (default: 100)
|
|
46
|
+
group?: string; // items in the same group are visually grouped;
|
|
47
|
+
// separators are auto-inserted between groups
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Action item
|
|
51
|
+
type ActionRegistryItem<TContext> = BaseRegistryItem<TContext> & {
|
|
52
|
+
type: 'item';
|
|
53
|
+
onClick: (ctx: TContext) => void; // REQUIRED for 'item'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Submenu item
|
|
57
|
+
type SubmenuRegistryItem<TContext> = BaseRegistryItem<TContext> & {
|
|
58
|
+
type: 'submenu';
|
|
59
|
+
items: RegistryItem<TContext>[]; // REQUIRED for 'submenu'; one level deep
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type RegistryItem<TContext> = ActionRegistryItem<TContext> | SubmenuRegistryItem<TContext>;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## KEY RULES
|
|
66
|
+
- `createContextMenuHandle<TContext>()` is generic — define your own context type that describes UI state at the moment of right-click
|
|
67
|
+
- Only **one** context menu may be open at a time (shared singleton open/close state) — multiple handles still share this state
|
|
68
|
+
- `<ContextMenu>` **must** be rendered in the JSX tree, typically near the app root to avoid z-index conflicts
|
|
69
|
+
- `prefixIcon` accepts a `JSXElement` (rendered JSX), not a function component
|
|
70
|
+
- Submenus are **one level deep** only — nested `items` arrays inside a submenu are not supported
|
|
71
|
+
- `shortcut` is display-only and does **not** bind keyboard events — wire those separately
|
|
72
|
+
|
|
73
|
+
## MINIMAL SETUP
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { createContextMenuHandle, ContextMenu } from 'solid-tom-ui';
|
|
77
|
+
|
|
78
|
+
// 1. Define context shape
|
|
79
|
+
type MyCtx = { zone: 'editor' | 'sidebar'; hasSelection: boolean };
|
|
80
|
+
|
|
81
|
+
// 2. Create handle (component level or module scope)
|
|
82
|
+
const handle = createContextMenuHandle<MyCtx>();
|
|
83
|
+
|
|
84
|
+
// 3. Register items
|
|
85
|
+
handle.register({
|
|
86
|
+
key: 'copy',
|
|
87
|
+
type: 'item',
|
|
88
|
+
label: 'Copy',
|
|
89
|
+
when: ctx => ctx.hasSelection,
|
|
90
|
+
onClick: ctx => console.log('copy in', ctx.zone),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// 4. Render once near app root
|
|
94
|
+
<ContextMenu handle={handle} />
|
|
95
|
+
|
|
96
|
+
// 5. Trigger on right-click
|
|
97
|
+
<div onContextMenu={e => handle.trigger(e, { zone: 'editor', hasSelection: true })}>
|
|
98
|
+
Right-click here
|
|
99
|
+
</div>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## USAGE PATTERNS
|
|
103
|
+
|
|
104
|
+
### Module-scoped handle (shared across components)
|
|
105
|
+
```ts
|
|
106
|
+
// context-menu.handle.ts
|
|
107
|
+
export type AppCtx = { zone: string; isFile: boolean };
|
|
108
|
+
export const appMenuHandle = createContextMenuHandle<AppCtx>();
|
|
109
|
+
|
|
110
|
+
// FileExplorer.tsx — registers its own items
|
|
111
|
+
appMenuHandle.register({ key: 'rename', type: 'item', label: 'Rename', when: ctx => ctx.isFile, onClick: ctx => rename(ctx) });
|
|
112
|
+
|
|
113
|
+
// Editor.tsx — registers independent items
|
|
114
|
+
appMenuHandle.register({ key: 'copy', type: 'item', label: 'Copy', when: ctx => ctx.zone === 'editor', onClick: () => copy() });
|
|
115
|
+
|
|
116
|
+
// App.tsx — render once
|
|
117
|
+
<ContextMenu handle={appMenuHandle} />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Dynamic registration / unregistration
|
|
121
|
+
```ts
|
|
122
|
+
handle.register({ key: 'admin-delete', type: 'item', label: 'Delete', onClick: () => deleteItem() });
|
|
123
|
+
|
|
124
|
+
// Later (e.g. user logs out):
|
|
125
|
+
handle.unregister('admin-delete');
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Grouped items with separators
|
|
129
|
+
```ts
|
|
130
|
+
handle.register({ key: 'cut', type: 'item', label: 'Cut', group: 'clipboard', order: 10, onClick: () => cut() });
|
|
131
|
+
handle.register({ key: 'copy', type: 'item', label: 'Copy', group: 'clipboard', order: 11, onClick: () => copy() });
|
|
132
|
+
handle.register({ key: 'paste', type: 'item', label: 'Paste', group: 'clipboard', order: 12, onClick: () => paste() });
|
|
133
|
+
handle.register({ key: 'delete', type: 'item', label: 'Delete', group: 'danger', order: 20, onClick: () => del() });
|
|
134
|
+
// Separator is auto-inserted between 'clipboard' and 'danger' groups
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Submenu
|
|
138
|
+
```ts
|
|
139
|
+
handle.register({
|
|
140
|
+
key: 'export',
|
|
141
|
+
type: 'submenu',
|
|
142
|
+
label: 'Export as',
|
|
143
|
+
items: [
|
|
144
|
+
{ key: 'export-png', type: 'item', label: 'PNG', onClick: () => exportPng() },
|
|
145
|
+
{ key: 'export-svg', type: 'item', label: 'SVG', onClick: () => exportSvg() },
|
|
146
|
+
{ key: 'export-pdf', type: 'item', label: 'PDF', onClick: () => exportPdf() },
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Icon and keyboard shortcut hints
|
|
152
|
+
```ts
|
|
153
|
+
import Copy from 'lucide-solid/icons/copy';
|
|
154
|
+
|
|
155
|
+
handle.register({
|
|
156
|
+
key: 'copy',
|
|
157
|
+
type: 'item',
|
|
158
|
+
label: 'Copy',
|
|
159
|
+
prefixIcon: <Copy size={14} />,
|
|
160
|
+
shortcut: 'Ctrl+C',
|
|
161
|
+
onClick: () => copy(),
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Multiple independent menus (separate context types)
|
|
166
|
+
```ts
|
|
167
|
+
const editorMenu = createContextMenuHandle<EditorCtx>();
|
|
168
|
+
const tableMenu = createContextMenuHandle<TableCtx>();
|
|
169
|
+
|
|
170
|
+
<ContextMenu handle={editorMenu} />
|
|
171
|
+
<ContextMenu handle={tableMenu} />
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### CSS slot override
|
|
175
|
+
```tsx
|
|
176
|
+
<ContextMenu
|
|
177
|
+
handle={handle}
|
|
178
|
+
class={{
|
|
179
|
+
content: 'rounded-xl shadow-2xl',
|
|
180
|
+
item: 'text-sm',
|
|
181
|
+
separator: 'my-1 opacity-30',
|
|
182
|
+
submenu: 'font-medium',
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## DECISION RULES
|
|
188
|
+
|
|
189
|
+
**Single handle vs multiple handles:**
|
|
190
|
+
- One shared context type across the entire app → single module-scoped handle
|
|
191
|
+
- Completely separate feature areas with different context shapes → separate handles per area
|
|
192
|
+
|
|
193
|
+
**`when` vs `disabled`:**
|
|
194
|
+
- Item doesn't belong to this context at all → use `when` (hides the item)
|
|
195
|
+
- Item belongs but action is not available right now → use `disabled` (shows grayed-out)
|
|
196
|
+
|
|
197
|
+
**`order` and `group`:**
|
|
198
|
+
- Use `group` to visually separate categories (separators are automatic)
|
|
199
|
+
- Use `order` to control position within a group (lower number = higher position)
|
|
200
|
+
- If order is not specified, items are rendered in registration order (default: 100)
|
|
201
|
+
|
|
202
|
+
**`prefixIcon`:**
|
|
203
|
+
- Pass a rendered JSX element: `prefixIcon={<Copy size={14} />}`
|
|
204
|
+
- Import icons by direct path to avoid bundling the entire lucide library
|
|
205
|
+
|
|
206
|
+
## ANTI-PATTERNS
|
|
207
|
+
```tsx
|
|
208
|
+
// ❌ Rendering ContextMenu deep in a child component — may cause z-index issues
|
|
209
|
+
<Modal>
|
|
210
|
+
<ContextMenu handle={handle} /> // wrong — render near app root instead
|
|
211
|
+
</Modal>
|
|
212
|
+
|
|
213
|
+
// ❌ Passing a function component as prefixIcon (it won't render)
|
|
214
|
+
prefixIcon={Copy} // wrong
|
|
215
|
+
prefixIcon={<Copy size={14} />} // ✅ pass JSX element
|
|
216
|
+
|
|
217
|
+
// ❌ Nesting submenus more than one level deep (not supported)
|
|
218
|
+
{
|
|
219
|
+
type: 'submenu',
|
|
220
|
+
items: [{ type: 'submenu', items: [...] }] // second level ignored
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ❌ Expecting shortcut to bind a keyboard event
|
|
224
|
+
shortcut: 'Ctrl+Z' // display only — wire onKeyDown separately
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Component Conventions
|
|
230
|
+
|
|
231
|
+
> **CSS encoding**: internal CSS classes use short encoded names (e.g. `cm01`, `cm02`) per project convention.
|
|
232
|
+
|
|
233
|
+
> **Unique IDs**: if this component needs to generate HTML `id` attributes, always use `createUniqueId()` from `solid-js` — never `Math.random()` or `Date.now()`.
|