@mihcm/ui 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CheckboxGrid.native.d.ts.map +1 -1
- package/dist/CheckboxGrid.native.js +2 -1
- package/dist/CheckboxGrid.native.js.map +1 -1
- package/dist/Combobox.native.d.ts.map +1 -1
- package/dist/Combobox.native.js +2 -1
- package/dist/Combobox.native.js.map +1 -1
- package/dist/DataTable/column-filter.d.ts +8 -0
- package/dist/DataTable/column-filter.d.ts.map +1 -0
- package/dist/DataTable/column-filter.js +67 -0
- package/dist/DataTable/column-filter.js.map +1 -0
- package/dist/DataTable/column-header.d.ts +16 -0
- package/dist/DataTable/column-header.d.ts.map +1 -0
- package/dist/DataTable/column-header.js +11 -0
- package/dist/DataTable/column-header.js.map +1 -0
- package/dist/DataTable/column-visibility.d.ts +7 -0
- package/dist/DataTable/column-visibility.d.ts.map +1 -0
- package/dist/DataTable/column-visibility.js +35 -0
- package/dist/DataTable/column-visibility.js.map +1 -0
- package/dist/DataTable/index.d.ts +5 -0
- package/dist/DataTable/index.d.ts.map +1 -0
- package/dist/DataTable/index.js +5 -0
- package/dist/DataTable/index.js.map +1 -0
- package/dist/DataTable/pinning.d.ts +13 -0
- package/dist/DataTable/pinning.d.ts.map +1 -0
- package/dist/DataTable/pinning.js +29 -0
- package/dist/DataTable/pinning.js.map +1 -0
- package/dist/DataTable.d.ts +3 -7
- package/dist/DataTable.d.ts.map +1 -1
- package/dist/DataTable.js +7 -126
- package/dist/DataTable.js.map +1 -1
- package/dist/Dialog.native.d.ts +3 -1
- package/dist/Dialog.native.d.ts.map +1 -1
- package/dist/Dialog.native.js +2 -2
- package/dist/Dialog.native.js.map +1 -1
- package/dist/Form/building-blocks.d.ts +26 -0
- package/dist/Form/building-blocks.d.ts.map +1 -0
- package/dist/Form/building-blocks.js +29 -0
- package/dist/Form/building-blocks.js.map +1 -0
- package/dist/Form/fields-choice.d.ts +72 -0
- package/dist/Form/fields-choice.d.ts.map +1 -0
- package/dist/Form/fields-choice.js +69 -0
- package/dist/Form/fields-choice.js.map +1 -0
- package/dist/Form/fields-complex.d.ts +28 -0
- package/dist/Form/fields-complex.d.ts.map +1 -0
- package/dist/Form/fields-complex.js +38 -0
- package/dist/Form/fields-complex.js.map +1 -0
- package/dist/Form/fields-date.d.ts +46 -0
- package/dist/Form/fields-date.d.ts.map +1 -0
- package/dist/Form/fields-date.js +41 -0
- package/dist/Form/fields-date.js.map +1 -0
- package/dist/Form/fields-text.d.ts +47 -0
- package/dist/Form/fields-text.d.ts.map +1 -0
- package/dist/Form/fields-text.js +46 -0
- package/dist/Form/fields-text.js.map +1 -0
- package/dist/Form/fields-toggle.d.ts +24 -0
- package/dist/Form/fields-toggle.d.ts.map +1 -0
- package/dist/Form/fields-toggle.js +32 -0
- package/dist/Form/fields-toggle.js.map +1 -0
- package/dist/Form/helpers.d.ts +66 -0
- package/dist/Form/helpers.d.ts.map +1 -0
- package/dist/Form/helpers.js +44 -0
- package/dist/Form/helpers.js.map +1 -0
- package/dist/Form/types.d.ts +25 -0
- package/dist/Form/types.d.ts.map +1 -0
- package/dist/Form/types.js +8 -0
- package/dist/Form/types.js.map +1 -0
- package/dist/Form.d.ts +24 -298
- package/dist/Form.d.ts.map +1 -1
- package/dist/Form.js +30 -246
- package/dist/Form.js.map +1 -1
- package/dist/IconSidebar.d.ts +6 -46
- package/dist/IconSidebar.d.ts.map +1 -1
- package/dist/IconSidebar.js +6 -116
- package/dist/IconSidebar.js.map +1 -1
- package/dist/MainSidebar/back-button.d.ts +14 -0
- package/dist/MainSidebar/back-button.d.ts.map +1 -0
- package/dist/MainSidebar/back-button.js +14 -0
- package/dist/MainSidebar/back-button.js.map +1 -0
- package/dist/MainSidebar/breadcrumb.d.ts +10 -0
- package/dist/MainSidebar/breadcrumb.d.ts.map +1 -0
- package/dist/MainSidebar/breadcrumb.js +24 -0
- package/dist/MainSidebar/breadcrumb.js.map +1 -0
- package/dist/MainSidebar/columns.d.ts +3 -0
- package/dist/MainSidebar/columns.d.ts.map +1 -0
- package/dist/MainSidebar/columns.js +198 -0
- package/dist/MainSidebar/columns.js.map +1 -0
- package/dist/MainSidebar/command.d.ts +3 -0
- package/dist/MainSidebar/command.d.ts.map +1 -0
- package/dist/MainSidebar/command.js +193 -0
- package/dist/MainSidebar/command.js.map +1 -0
- package/dist/MainSidebar/drilldown.d.ts +3 -0
- package/dist/MainSidebar/drilldown.d.ts.map +1 -0
- package/dist/MainSidebar/drilldown.js +154 -0
- package/dist/MainSidebar/drilldown.js.map +1 -0
- package/dist/MainSidebar/expanded.d.ts +7 -0
- package/dist/MainSidebar/expanded.d.ts.map +1 -0
- package/dist/MainSidebar/expanded.js +102 -0
- package/dist/MainSidebar/expanded.js.map +1 -0
- package/dist/MainSidebar/floating.d.ts +3 -0
- package/dist/MainSidebar/floating.d.ts.map +1 -0
- package/dist/MainSidebar/floating.js +116 -0
- package/dist/MainSidebar/floating.js.map +1 -0
- package/dist/MainSidebar/helpers.d.ts +50 -0
- package/dist/MainSidebar/helpers.d.ts.map +1 -0
- package/dist/MainSidebar/helpers.js +148 -0
- package/dist/MainSidebar/helpers.js.map +1 -0
- package/dist/MainSidebar/hover.d.ts +3 -0
- package/dist/MainSidebar/hover.d.ts.map +1 -0
- package/dist/MainSidebar/hover.js +177 -0
- package/dist/MainSidebar/hover.js.map +1 -0
- package/dist/MainSidebar/index.d.ts +6 -0
- package/dist/MainSidebar/index.d.ts.map +1 -0
- package/dist/MainSidebar/index.js +108 -0
- package/dist/MainSidebar/index.js.map +1 -0
- package/dist/MainSidebar/mobile.d.ts +29 -0
- package/dist/MainSidebar/mobile.d.ts.map +1 -0
- package/dist/MainSidebar/mobile.js +38 -0
- package/dist/MainSidebar/mobile.js.map +1 -0
- package/dist/MainSidebar/motion.d.ts +23 -0
- package/dist/MainSidebar/motion.d.ts.map +1 -0
- package/dist/MainSidebar/motion.js +40 -0
- package/dist/MainSidebar/motion.js.map +1 -0
- package/dist/MainSidebar/rail.d.ts +24 -0
- package/dist/MainSidebar/rail.d.ts.map +1 -0
- package/dist/MainSidebar/rail.js +29 -0
- package/dist/MainSidebar/rail.js.map +1 -0
- package/dist/MainSidebar/search.d.ts +19 -0
- package/dist/MainSidebar/search.d.ts.map +1 -0
- package/dist/MainSidebar/search.js +33 -0
- package/dist/MainSidebar/search.js.map +1 -0
- package/dist/MainSidebar/types.d.ts +161 -0
- package/dist/MainSidebar/types.d.ts.map +1 -0
- package/dist/MainSidebar/types.js +2 -0
- package/dist/MainSidebar/types.js.map +1 -0
- package/dist/MainSidebar.d.ts +6 -1
- package/dist/MainSidebar.d.ts.map +1 -1
- package/dist/MainSidebar.js +6 -1
- package/dist/MainSidebar.js.map +1 -1
- package/dist/NavigationMenu.js +1 -1
- package/dist/NavigationMenu.js.map +1 -1
- package/dist/RichTextEditor/theme.d.ts +44 -0
- package/dist/RichTextEditor/theme.d.ts.map +1 -0
- package/dist/RichTextEditor/theme.js +41 -0
- package/dist/RichTextEditor/theme.js.map +1 -0
- package/dist/RichTextEditor/toolbar-icons.d.ts +21 -0
- package/dist/RichTextEditor/toolbar-icons.d.ts.map +1 -0
- package/dist/RichTextEditor/toolbar-icons.js +21 -0
- package/dist/RichTextEditor/toolbar-icons.js.map +1 -0
- package/dist/RichTextEditor/toolbar.d.ts +5 -0
- package/dist/RichTextEditor/toolbar.d.ts.map +1 -0
- package/dist/RichTextEditor/toolbar.js +116 -0
- package/dist/RichTextEditor/toolbar.js.map +1 -0
- package/dist/RichTextEditor.d.ts +16 -9
- package/dist/RichTextEditor.d.ts.map +1 -1
- package/dist/RichTextEditor.js +18 -164
- package/dist/RichTextEditor.js.map +1 -1
- package/dist/Select/content.d.ts +9 -0
- package/dist/Select/content.d.ts.map +1 -0
- package/dist/Select/content.js +80 -0
- package/dist/Select/content.js.map +1 -0
- package/dist/Select/context.d.ts +27 -0
- package/dist/Select/context.d.ts.map +1 -0
- package/dist/Select/context.js +35 -0
- package/dist/Select/context.js.map +1 -0
- package/dist/Select/item.d.ts +13 -0
- package/dist/Select/item.d.ts.map +1 -0
- package/dist/Select/item.js +39 -0
- package/dist/Select/item.js.map +1 -0
- package/dist/Select/parts.d.ts +14 -0
- package/dist/Select/parts.d.ts.map +1 -0
- package/dist/Select/parts.js +17 -0
- package/dist/Select/parts.js.map +1 -0
- package/dist/Select/react-select.d.ts +25 -0
- package/dist/Select/react-select.d.ts.map +1 -0
- package/dist/Select/react-select.js +66 -0
- package/dist/Select/react-select.js.map +1 -0
- package/dist/Select/root.d.ts +15 -0
- package/dist/Select/root.d.ts.map +1 -0
- package/dist/Select/root.js +41 -0
- package/dist/Select/root.js.map +1 -0
- package/dist/Select/trigger.d.ts +15 -0
- package/dist/Select/trigger.d.ts.map +1 -0
- package/dist/Select/trigger.js +61 -0
- package/dist/Select/trigger.js.map +1 -0
- package/dist/Select.d.ts +14 -62
- package/dist/Select.d.ts.map +1 -1
- package/dist/Select.js +14 -293
- package/dist/Select.js.map +1 -1
- package/dist/Sidebar/context.d.ts +28 -0
- package/dist/Sidebar/context.d.ts.map +1 -0
- package/dist/Sidebar/context.js +37 -0
- package/dist/Sidebar/context.js.map +1 -0
- package/dist/Sidebar/group.d.ts +13 -0
- package/dist/Sidebar/group.d.ts.map +1 -0
- package/dist/Sidebar/group.js +20 -0
- package/dist/Sidebar/group.js.map +1 -0
- package/dist/Sidebar/icons.d.ts +7 -0
- package/dist/Sidebar/icons.d.ts.map +1 -0
- package/dist/Sidebar/icons.js +12 -0
- package/dist/Sidebar/icons.js.map +1 -0
- package/dist/Sidebar/layout.d.ts +9 -0
- package/dist/Sidebar/layout.d.ts.map +1 -0
- package/dist/Sidebar/layout.js +21 -0
- package/dist/Sidebar/layout.js.map +1 -0
- package/dist/Sidebar/menu.d.ts +29 -0
- package/dist/Sidebar/menu.d.ts.map +1 -0
- package/dist/Sidebar/menu.js +55 -0
- package/dist/Sidebar/menu.js.map +1 -0
- package/dist/Sidebar/provider.d.ts +33 -0
- package/dist/Sidebar/provider.d.ts.map +1 -0
- package/dist/Sidebar/provider.js +110 -0
- package/dist/Sidebar/provider.js.map +1 -0
- package/dist/Sidebar/sidebar.d.ts +17 -0
- package/dist/Sidebar/sidebar.d.ts.map +1 -0
- package/dist/Sidebar/sidebar.js +51 -0
- package/dist/Sidebar/sidebar.js.map +1 -0
- package/dist/Sidebar/submenu.d.ts +13 -0
- package/dist/Sidebar/submenu.d.ts.map +1 -0
- package/dist/Sidebar/submenu.js +17 -0
- package/dist/Sidebar/submenu.js.map +1 -0
- package/dist/Sidebar/trigger.d.ts +9 -0
- package/dist/Sidebar/trigger.d.ts.map +1 -0
- package/dist/Sidebar/trigger.js +33 -0
- package/dist/Sidebar/trigger.js.map +1 -0
- package/dist/Sidebar.d.ts +14 -104
- package/dist/Sidebar.d.ts.map +1 -1
- package/dist/Sidebar.js +14 -300
- package/dist/Sidebar.js.map +1 -1
- package/dist/StatCard.d.ts +67 -9
- package/dist/StatCard.d.ts.map +1 -1
- package/dist/StatCard.js +111 -9
- package/dist/StatCard.js.map +1 -1
- package/dist/TransferList.native.d.ts.map +1 -1
- package/dist/TransferList.native.js +2 -1
- package/dist/TransferList.native.js.map +1 -1
- package/package.json +2 -2
- package/src/CheckboxGrid.native.tsx +2 -1
- package/src/Combobox.native.tsx +2 -1
- package/src/DataTable/column-filter.tsx +134 -0
- package/src/DataTable/column-header.tsx +67 -0
- package/src/DataTable/column-visibility.tsx +87 -0
- package/src/DataTable/index.ts +4 -0
- package/src/DataTable/pinning.ts +40 -0
- package/src/DataTable.tsx +14 -297
- package/src/Dialog.native.tsx +4 -2
- package/src/Form/building-blocks.tsx +97 -0
- package/src/Form/fields-choice.tsx +312 -0
- package/src/Form/fields-complex.tsx +195 -0
- package/src/Form/fields-date.tsx +195 -0
- package/src/Form/fields-text.tsx +218 -0
- package/src/Form/fields-toggle.tsx +123 -0
- package/src/Form/helpers.tsx +189 -0
- package/src/Form/types.ts +26 -0
- package/src/Form.tsx +91 -1308
- package/src/IconSidebar.tsx +20 -442
- package/src/MainSidebar/back-button.tsx +58 -0
- package/src/MainSidebar/breadcrumb.tsx +53 -0
- package/src/MainSidebar/columns.tsx +350 -0
- package/src/MainSidebar/command.tsx +404 -0
- package/src/MainSidebar/drilldown.tsx +373 -0
- package/src/MainSidebar/expanded.tsx +414 -0
- package/src/MainSidebar/floating.tsx +268 -0
- package/src/MainSidebar/helpers.ts +164 -0
- package/src/MainSidebar/hover.tsx +334 -0
- package/src/MainSidebar/index.tsx +191 -0
- package/src/MainSidebar/mobile.tsx +117 -0
- package/src/MainSidebar/motion.ts +64 -0
- package/src/MainSidebar/rail.tsx +137 -0
- package/src/MainSidebar/search.tsx +99 -0
- package/src/MainSidebar/types.ts +208 -0
- package/src/MainSidebar.tsx +15 -4
- package/src/NavigationMenu.tsx +1 -1
- package/src/RichTextEditor/theme.ts +43 -0
- package/src/RichTextEditor/toolbar-icons.tsx +40 -0
- package/src/RichTextEditor/toolbar.tsx +271 -0
- package/src/RichTextEditor.tsx +23 -371
- package/src/Select/content.tsx +111 -0
- package/src/Select/context.tsx +66 -0
- package/src/Select/item.tsx +97 -0
- package/src/Select/parts.tsx +43 -0
- package/src/Select/react-select.tsx +216 -0
- package/src/Select/root.tsx +75 -0
- package/src/Select/trigger.tsx +122 -0
- package/src/Select.tsx +34 -692
- package/src/Sidebar/context.tsx +72 -0
- package/src/Sidebar/group.tsx +69 -0
- package/src/Sidebar/icons.tsx +42 -0
- package/src/Sidebar/layout.tsx +64 -0
- package/src/Sidebar/menu.tsx +171 -0
- package/src/Sidebar/provider.tsx +224 -0
- package/src/Sidebar/sidebar.tsx +178 -0
- package/src/Sidebar/submenu.tsx +58 -0
- package/src/Sidebar/trigger.tsx +104 -0
- package/src/Sidebar.tsx +44 -927
- package/src/StatCard.tsx +365 -20
- package/src/TransferList.native.tsx +2 -1
- package/dist/TiptapEditor.d.ts +0 -24
- package/dist/TiptapEditor.d.ts.map +0 -1
- package/dist/TiptapEditor.js +0 -84
- package/dist/TiptapEditor.js.map +0 -1
package/src/RichTextEditor.tsx
CHANGED
|
@@ -6,23 +6,20 @@
|
|
|
6
6
|
* A Lexical-powered rich text editor styled to match the MiHCM design system.
|
|
7
7
|
* Four variants control which toolbar features are shown:
|
|
8
8
|
*
|
|
9
|
-
* minimal
|
|
10
|
-
* default
|
|
11
|
-
* semi
|
|
12
|
-
*
|
|
9
|
+
* minimal — Bold, Italic only
|
|
10
|
+
* default — Bold, Italic, Underline, Strikethrough + Bullet/Numbered lists
|
|
11
|
+
* semi — default + Headings (H1–H3) + Undo/Redo + Code + Link +
|
|
12
|
+
* Indent/Outdent + Markdown shortcuts + Tab indentation
|
|
13
|
+
* full — semi + Text alignment (L/C/R/Justify) + Superscript/Subscript +
|
|
14
|
+
* Check list + Table + Horizontal rule + Block quote +
|
|
15
|
+
* Clear formatting
|
|
13
16
|
*
|
|
14
|
-
*
|
|
17
|
+
* Implementation is split across `./RichTextEditor/*` per CLAUDE.md §6 —
|
|
18
|
+
* theme/variant config, toolbar icons, and toolbar plugin live separately.
|
|
15
19
|
*
|
|
16
20
|
* Wiki: docs/components/RichTextEditor.md
|
|
17
21
|
*/
|
|
18
|
-
import {
|
|
19
|
-
forwardRef,
|
|
20
|
-
useCallback,
|
|
21
|
-
useEffect,
|
|
22
|
-
useState,
|
|
23
|
-
type HTMLAttributes,
|
|
24
|
-
type ReactNode,
|
|
25
|
-
} from 'react';
|
|
22
|
+
import { forwardRef, type HTMLAttributes } from 'react';
|
|
26
23
|
import { cn } from './internal/cn.js';
|
|
27
24
|
|
|
28
25
|
import { LexicalComposer } from '@lexical/react/LexicalComposer.js';
|
|
@@ -36,366 +33,21 @@ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin.js';
|
|
|
36
33
|
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin.js';
|
|
37
34
|
import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin.js';
|
|
38
35
|
import { TablePlugin } from '@lexical/react/LexicalTablePlugin.js';
|
|
39
|
-
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js';
|
|
40
36
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary.js';
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
FORMAT_TEXT_COMMAND,
|
|
46
|
-
FORMAT_ELEMENT_COMMAND,
|
|
47
|
-
INDENT_CONTENT_COMMAND,
|
|
48
|
-
OUTDENT_CONTENT_COMMAND,
|
|
49
|
-
UNDO_COMMAND,
|
|
50
|
-
REDO_COMMAND,
|
|
51
|
-
type EditorState,
|
|
52
|
-
type LexicalEditor,
|
|
53
|
-
type Klass,
|
|
54
|
-
type LexicalNode,
|
|
55
|
-
} from 'lexical';
|
|
56
|
-
import { HeadingNode, $createHeadingNode, QuoteNode, $createQuoteNode, type HeadingTagType } from '@lexical/rich-text';
|
|
57
|
-
import { ListItemNode, ListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list';
|
|
58
|
-
import { INSERT_CHECK_LIST_COMMAND } from '@lexical/list';
|
|
59
|
-
import { LinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
|
37
|
+
import type { EditorState, LexicalEditor, Klass, LexicalNode } from 'lexical';
|
|
38
|
+
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
39
|
+
import { ListItemNode, ListNode } from '@lexical/list';
|
|
40
|
+
import { LinkNode } from '@lexical/link';
|
|
60
41
|
import { CodeNode } from '@lexical/code';
|
|
61
|
-
import {
|
|
62
|
-
import { HorizontalRuleNode, INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode.js';
|
|
42
|
+
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode.js';
|
|
63
43
|
import { HorizontalRulePlugin } from '@lexical/react/LexicalHorizontalRulePlugin.js';
|
|
64
|
-
import { TableNode, TableCellNode, TableRowNode
|
|
44
|
+
import { TableNode, TableCellNode, TableRowNode } from '@lexical/table';
|
|
65
45
|
import { TRANSFORMERS, ELEMENT_TRANSFORMERS, TEXT_FORMAT_TRANSFORMERS, TEXT_MATCH_TRANSFORMERS } from '@lexical/markdown';
|
|
66
46
|
|
|
67
|
-
|
|
47
|
+
import { editorTheme, type RichTextEditorVariant } from './RichTextEditor/theme.js';
|
|
48
|
+
import { ToolbarPlugin } from './RichTextEditor/toolbar.js';
|
|
68
49
|
|
|
69
|
-
export type RichTextEditorVariant
|
|
70
|
-
|
|
71
|
-
/* ── Theme (maps Lexical class names to Tailwind) ─────────────────── */
|
|
72
|
-
|
|
73
|
-
const editorTheme = {
|
|
74
|
-
paragraph: 'mb-2 last:mb-0',
|
|
75
|
-
heading: {
|
|
76
|
-
h1: 'text-2xl font-bold mb-3 text-foreground',
|
|
77
|
-
h2: 'text-xl font-semibold mb-2 text-foreground',
|
|
78
|
-
h3: 'text-lg font-medium mb-2 text-foreground',
|
|
79
|
-
},
|
|
80
|
-
text: {
|
|
81
|
-
bold: 'font-bold',
|
|
82
|
-
italic: 'italic',
|
|
83
|
-
underline: 'underline',
|
|
84
|
-
strikethrough: 'line-through',
|
|
85
|
-
underlineStrikethrough: 'underline line-through',
|
|
86
|
-
code: 'font-mono text-sm bg-muted px-1.5 py-0.5 rounded',
|
|
87
|
-
subscript: 'align-sub',
|
|
88
|
-
superscript: 'align-super',
|
|
89
|
-
},
|
|
90
|
-
list: {
|
|
91
|
-
ul: 'list-disc ml-6 mb-2',
|
|
92
|
-
ol: 'list-decimal ml-6 mb-2',
|
|
93
|
-
listitem: 'mb-0.5',
|
|
94
|
-
listitemChecked: 'line-through opacity-60 relative ml-2 list-none outline-none',
|
|
95
|
-
listitemUnchecked: 'relative ml-2 list-none outline-none',
|
|
96
|
-
nested: { listitem: 'list-none' },
|
|
97
|
-
checklist: 'list-none ml-0',
|
|
98
|
-
},
|
|
99
|
-
link: 'text-primary underline cursor-pointer hover:text-primary/80',
|
|
100
|
-
quote: 'border-l-4 border-primary-200 dark:border-primary-800 pl-4 italic text-muted-foreground mb-2',
|
|
101
|
-
code: 'font-mono text-sm bg-muted p-3 rounded-md mb-2 block overflow-x-auto',
|
|
102
|
-
horizontalRule: 'my-4 border-t border-border',
|
|
103
|
-
table: 'border-collapse border border-border w-full mb-2',
|
|
104
|
-
tableCell: 'border border-border px-2 py-1.5 text-sm min-w-[75px]',
|
|
105
|
-
tableCellHeader: 'border border-border px-2 py-1.5 text-sm font-semibold bg-muted/50',
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/* ── Toolbar primitives ───────────────────────────────────────────── */
|
|
109
|
-
|
|
110
|
-
function TbIcon({ d, className }: { d: string; className?: string }) {
|
|
111
|
-
return (
|
|
112
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={cn('size-3.5', className)}>
|
|
113
|
-
<path d={d} />
|
|
114
|
-
</svg>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function TbMultiIcon({ paths, className }: { paths: string[]; className?: string }) {
|
|
119
|
-
return (
|
|
120
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={cn('size-3.5', className)}>
|
|
121
|
-
{paths.map((d, i) => <path key={i} d={d} />)}
|
|
122
|
-
</svg>
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function TbBtn({ active, onClick, title, children }: { active?: boolean; onClick: () => void; title: string; children: ReactNode }) {
|
|
127
|
-
return (
|
|
128
|
-
<button type="button" onClick={onClick} title={title} aria-label={title} aria-pressed={active} className={cn(
|
|
129
|
-
'flex items-center justify-center size-7 rounded transition-colors duration-150 cursor-pointer',
|
|
130
|
-
'hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
131
|
-
active && 'border border-primary bg-primary text-primary-foreground shadow-mi-card',
|
|
132
|
-
)}>
|
|
133
|
-
{children}
|
|
134
|
-
</button>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function TbSep() {
|
|
139
|
-
return <span className="w-px h-4 bg-border mx-0.5" aria-hidden />;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* ── Toolbar plugin ───────────────────────────────────────────────── */
|
|
143
|
-
|
|
144
|
-
function ToolbarPlugin({ variant }: { variant: RichTextEditorVariant }) {
|
|
145
|
-
const [editor] = useLexicalComposerContext();
|
|
146
|
-
const [isBold, setIsBold] = useState(false);
|
|
147
|
-
const [isItalic, setIsItalic] = useState(false);
|
|
148
|
-
const [isUnderline, setIsUnderline] = useState(false);
|
|
149
|
-
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
|
150
|
-
const [isCode, setIsCode] = useState(false);
|
|
151
|
-
const [isSuperscript, setIsSuperscript] = useState(false);
|
|
152
|
-
const [isSubscript, setIsSubscript] = useState(false);
|
|
153
|
-
const [isLink, setIsLink] = useState(false);
|
|
154
|
-
|
|
155
|
-
const updateToolbar = useCallback(() => {
|
|
156
|
-
const selection = $getSelection();
|
|
157
|
-
if ($isRangeSelection(selection)) {
|
|
158
|
-
setIsBold(selection.hasFormat('bold'));
|
|
159
|
-
setIsItalic(selection.hasFormat('italic'));
|
|
160
|
-
setIsUnderline(selection.hasFormat('underline'));
|
|
161
|
-
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
|
162
|
-
setIsCode(selection.hasFormat('code'));
|
|
163
|
-
setIsSuperscript(selection.hasFormat('superscript'));
|
|
164
|
-
setIsSubscript(selection.hasFormat('subscript'));
|
|
165
|
-
const node = selection.anchor.getNode();
|
|
166
|
-
const parent = node.getParent();
|
|
167
|
-
setIsLink($isLinkNode(parent) || $isLinkNode(node));
|
|
168
|
-
}
|
|
169
|
-
}, []);
|
|
170
|
-
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
return editor.registerUpdateListener(({ editorState }) => {
|
|
173
|
-
editorState.read(() => updateToolbar());
|
|
174
|
-
});
|
|
175
|
-
}, [editor, updateToolbar]);
|
|
176
|
-
|
|
177
|
-
const formatHeading = (tag: HeadingTagType) => {
|
|
178
|
-
editor.update(() => {
|
|
179
|
-
const selection = $getSelection();
|
|
180
|
-
if ($isRangeSelection(selection)) $setBlocksType(selection, () => $createHeadingNode(tag));
|
|
181
|
-
});
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const formatParagraph = () => {
|
|
185
|
-
editor.update(() => {
|
|
186
|
-
const selection = $getSelection();
|
|
187
|
-
if ($isRangeSelection(selection)) $setBlocksType(selection, () => $createParagraphNode());
|
|
188
|
-
});
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const formatBlockQuote = () => {
|
|
192
|
-
editor.update(() => {
|
|
193
|
-
const selection = $getSelection();
|
|
194
|
-
if ($isRangeSelection(selection)) $setBlocksType(selection, () => $createQuoteNode());
|
|
195
|
-
});
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const insertLink = () => {
|
|
199
|
-
if (isLink) {
|
|
200
|
-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
201
|
-
} else {
|
|
202
|
-
const url = prompt('Enter URL:');
|
|
203
|
-
if (url) editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const insertTable = () => {
|
|
208
|
-
editor.update(() => {
|
|
209
|
-
const table = $createTableNodeWithDimensions(3, 3, false);
|
|
210
|
-
const selection = $getSelection();
|
|
211
|
-
if ($isRangeSelection(selection)) {
|
|
212
|
-
const anchor = selection.anchor.getNode();
|
|
213
|
-
const parent = anchor.getTopLevelElementOrThrow();
|
|
214
|
-
parent.insertAfter(table);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const clearFormatting = () => {
|
|
220
|
-
editor.update(() => {
|
|
221
|
-
const selection = $getSelection();
|
|
222
|
-
if ($isRangeSelection(selection)) {
|
|
223
|
-
for (const format of ['bold', 'italic', 'underline', 'strikethrough', 'code', 'superscript', 'subscript'] as const) {
|
|
224
|
-
if (selection.hasFormat(format)) {
|
|
225
|
-
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const showHistory = variant === 'semi' || variant === 'full';
|
|
233
|
-
const showHeadings = variant === 'semi' || variant === 'full';
|
|
234
|
-
const showUnderlineStrike = variant !== 'minimal';
|
|
235
|
-
const showLists = variant !== 'minimal';
|
|
236
|
-
const showCode = variant === 'semi' || variant === 'full';
|
|
237
|
-
const showLink = variant === 'semi' || variant === 'full';
|
|
238
|
-
const showIndent = variant === 'semi' || variant === 'full';
|
|
239
|
-
const showAlignment = variant === 'full';
|
|
240
|
-
const showSuperSub = variant === 'full';
|
|
241
|
-
const showCheckList = variant === 'full';
|
|
242
|
-
const showTable = variant === 'full';
|
|
243
|
-
const showExtras = variant === 'full';
|
|
244
|
-
|
|
245
|
-
return (
|
|
246
|
-
<div className="flex items-center gap-0.5 px-2 py-1.5 border-b border-border bg-muted/50 rounded-t-lg flex-wrap">
|
|
247
|
-
{/* Undo / Redo */}
|
|
248
|
-
{showHistory && (
|
|
249
|
-
<>
|
|
250
|
-
<TbBtn title="Undo (Ctrl+Z)" onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}>
|
|
251
|
-
<TbIcon d="M3 7v6h6M3 13a9 9 0 1 0 3-7.7" />
|
|
252
|
-
</TbBtn>
|
|
253
|
-
<TbBtn title="Redo (Ctrl+Shift+Z)" onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}>
|
|
254
|
-
<TbIcon d="M21 7v6h-6M21 13a9 9 0 1 1-3-7.7" />
|
|
255
|
-
</TbBtn>
|
|
256
|
-
<TbSep />
|
|
257
|
-
</>
|
|
258
|
-
)}
|
|
259
|
-
|
|
260
|
-
{/* Headings */}
|
|
261
|
-
{showHeadings && (
|
|
262
|
-
<>
|
|
263
|
-
<TbBtn title="Heading 1" onClick={() => formatHeading('h1')}>
|
|
264
|
-
<span className="text-xs font-bold leading-none">H1</span>
|
|
265
|
-
</TbBtn>
|
|
266
|
-
<TbBtn title="Heading 2" onClick={() => formatHeading('h2')}>
|
|
267
|
-
<span className="text-xs font-bold leading-none">H2</span>
|
|
268
|
-
</TbBtn>
|
|
269
|
-
<TbBtn title="Heading 3" onClick={() => formatHeading('h3')}>
|
|
270
|
-
<span className="text-xs font-bold leading-none">H3</span>
|
|
271
|
-
</TbBtn>
|
|
272
|
-
<TbBtn title="Paragraph" onClick={formatParagraph}>
|
|
273
|
-
<span className="text-xs font-medium leading-none">¶</span>
|
|
274
|
-
</TbBtn>
|
|
275
|
-
<TbSep />
|
|
276
|
-
</>
|
|
277
|
-
)}
|
|
278
|
-
|
|
279
|
-
{/* Text formatting */}
|
|
280
|
-
<TbBtn title="Bold (Ctrl+B)" active={isBold} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}>
|
|
281
|
-
<span className="text-xs font-bold">B</span>
|
|
282
|
-
</TbBtn>
|
|
283
|
-
<TbBtn title="Italic (Ctrl+I)" active={isItalic} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}>
|
|
284
|
-
<span className="text-xs italic">I</span>
|
|
285
|
-
</TbBtn>
|
|
286
|
-
{showUnderlineStrike && (
|
|
287
|
-
<>
|
|
288
|
-
<TbBtn title="Underline (Ctrl+U)" active={isUnderline} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')}>
|
|
289
|
-
<span className="text-xs underline">U</span>
|
|
290
|
-
</TbBtn>
|
|
291
|
-
<TbBtn title="Strikethrough" active={isStrikethrough} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')}>
|
|
292
|
-
<span className="text-xs line-through">S</span>
|
|
293
|
-
</TbBtn>
|
|
294
|
-
</>
|
|
295
|
-
)}
|
|
296
|
-
|
|
297
|
-
{/* Code */}
|
|
298
|
-
{showCode && (
|
|
299
|
-
<TbBtn title="Inline code" active={isCode} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code')}>
|
|
300
|
-
<TbMultiIcon paths={['M16 18l6-6-6-6', 'M8 6l-6 6 6 6']} />
|
|
301
|
-
</TbBtn>
|
|
302
|
-
)}
|
|
303
|
-
|
|
304
|
-
{/* Super/sub */}
|
|
305
|
-
{showSuperSub && (
|
|
306
|
-
<>
|
|
307
|
-
<TbBtn title="Superscript" active={isSuperscript} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript')}>
|
|
308
|
-
<span className="text-xs leading-none">X<sup>2</sup></span>
|
|
309
|
-
</TbBtn>
|
|
310
|
-
<TbBtn title="Subscript" active={isSubscript} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript')}>
|
|
311
|
-
<span className="text-xs leading-none">X<sub>2</sub></span>
|
|
312
|
-
</TbBtn>
|
|
313
|
-
</>
|
|
314
|
-
)}
|
|
315
|
-
|
|
316
|
-
{(showLists || showLink) && <TbSep />}
|
|
317
|
-
|
|
318
|
-
{/* Lists */}
|
|
319
|
-
{showLists && (
|
|
320
|
-
<>
|
|
321
|
-
<TbBtn title="Bullet list" onClick={() => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)}>
|
|
322
|
-
<TbIcon d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
|
|
323
|
-
</TbBtn>
|
|
324
|
-
<TbBtn title="Numbered list" onClick={() => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)}>
|
|
325
|
-
<TbIcon d="M10 6h11M10 12h11M10 18h11M3 5l2 1V4M3 11h2l-1 2h2M3 17l2 1-2 1" />
|
|
326
|
-
</TbBtn>
|
|
327
|
-
</>
|
|
328
|
-
)}
|
|
329
|
-
|
|
330
|
-
{/* Check list */}
|
|
331
|
-
{showCheckList && (
|
|
332
|
-
<TbBtn title="Check list" onClick={() => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)}>
|
|
333
|
-
<TbMultiIcon paths={['M9 11l3 3L22 4', 'M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11']} />
|
|
334
|
-
</TbBtn>
|
|
335
|
-
)}
|
|
336
|
-
|
|
337
|
-
{/* Link */}
|
|
338
|
-
{showLink && (
|
|
339
|
-
<TbBtn title="Insert link" active={isLink} onClick={insertLink}>
|
|
340
|
-
<TbMultiIcon paths={['M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71', 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71']} />
|
|
341
|
-
</TbBtn>
|
|
342
|
-
)}
|
|
343
|
-
|
|
344
|
-
{/* Indent / Outdent */}
|
|
345
|
-
{showIndent && (
|
|
346
|
-
<>
|
|
347
|
-
<TbSep />
|
|
348
|
-
<TbBtn title="Indent" onClick={() => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)}>
|
|
349
|
-
<TbMultiIcon paths={['M3 8l4 4-4 4', 'M11 6h10', 'M11 12h10', 'M11 18h10']} />
|
|
350
|
-
</TbBtn>
|
|
351
|
-
<TbBtn title="Outdent" onClick={() => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)}>
|
|
352
|
-
<TbMultiIcon paths={['M7 8l-4 4 4 4', 'M11 6h10', 'M11 12h10', 'M11 18h10']} />
|
|
353
|
-
</TbBtn>
|
|
354
|
-
</>
|
|
355
|
-
)}
|
|
356
|
-
|
|
357
|
-
{/* Alignment */}
|
|
358
|
-
{showAlignment && (
|
|
359
|
-
<>
|
|
360
|
-
<TbSep />
|
|
361
|
-
<TbBtn title="Align left" onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')}>
|
|
362
|
-
<TbIcon d="M17 10H3M21 6H3M21 14H3M17 18H3" />
|
|
363
|
-
</TbBtn>
|
|
364
|
-
<TbBtn title="Align center" onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')}>
|
|
365
|
-
<TbIcon d="M18 10H6M21 6H3M21 14H3M18 18H6" />
|
|
366
|
-
</TbBtn>
|
|
367
|
-
<TbBtn title="Align right" onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')}>
|
|
368
|
-
<TbIcon d="M21 10H7M21 6H3M21 14H3M21 18H7" />
|
|
369
|
-
</TbBtn>
|
|
370
|
-
<TbBtn title="Justify" onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify')}>
|
|
371
|
-
<TbIcon d="M3 6h18M3 12h18M3 18h18" />
|
|
372
|
-
</TbBtn>
|
|
373
|
-
</>
|
|
374
|
-
)}
|
|
375
|
-
|
|
376
|
-
{/* Table + Extras (full only) */}
|
|
377
|
-
{showExtras && (
|
|
378
|
-
<>
|
|
379
|
-
<TbSep />
|
|
380
|
-
<TbBtn title="Insert table (3×3)" onClick={insertTable}>
|
|
381
|
-
<TbMultiIcon paths={['M3 3h18v18H3V3z', 'M3 9h18', 'M3 15h18', 'M9 3v18', 'M15 3v18']} />
|
|
382
|
-
</TbBtn>
|
|
383
|
-
<TbBtn title="Block quote" onClick={formatBlockQuote}>
|
|
384
|
-
<TbMultiIcon paths={['M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1', 'M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1']} />
|
|
385
|
-
</TbBtn>
|
|
386
|
-
<TbBtn title="Horizontal rule" onClick={() => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined)}>
|
|
387
|
-
<TbIcon d="M3 12h18" />
|
|
388
|
-
</TbBtn>
|
|
389
|
-
<TbBtn title="Clear formatting" onClick={clearFormatting}>
|
|
390
|
-
<TbMultiIcon paths={['M4 7h7l-2 9h7', 'M16 4l4 4-4 4', 'M7 20l5-5']} />
|
|
391
|
-
</TbBtn>
|
|
392
|
-
</>
|
|
393
|
-
)}
|
|
394
|
-
</div>
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/* ── Placeholder ──────────────────────────────────────────────────── */
|
|
50
|
+
export type { RichTextEditorVariant };
|
|
399
51
|
|
|
400
52
|
function Placeholder({ text }: { text: string }) {
|
|
401
53
|
return (
|
|
@@ -405,16 +57,16 @@ function Placeholder({ text }: { text: string }) {
|
|
|
405
57
|
);
|
|
406
58
|
}
|
|
407
59
|
|
|
408
|
-
/* ── RichTextEditor ───────────────────────────────────────────────── */
|
|
409
|
-
|
|
410
60
|
export interface RichTextEditorProps
|
|
411
61
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
412
62
|
/**
|
|
413
63
|
* Controls which toolbar features are shown.
|
|
414
64
|
* - `minimal` — Bold, Italic
|
|
415
65
|
* - `default` — Bold, Italic, Underline, Strikethrough, Lists
|
|
416
|
-
* - `semi` — default + Headings, Undo/Redo, Code, Link, Indent/Outdent,
|
|
417
|
-
*
|
|
66
|
+
* - `semi` — default + Headings, Undo/Redo, Code, Link, Indent/Outdent,
|
|
67
|
+
* Markdown shortcuts, Tab indentation
|
|
68
|
+
* - `full` — semi + Alignment (L/C/R/Justify), Super/Subscript, Check list,
|
|
69
|
+
* Table, Block quote, HR, Clear formatting
|
|
418
70
|
*/
|
|
419
71
|
variant?: RichTextEditorVariant;
|
|
420
72
|
/** Placeholder text for the editor. */
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SelectContent — floating listbox panel with outside-click dismissal,
|
|
5
|
+
* arrow-key navigation, and focus management.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
forwardRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
useEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
type HTMLAttributes,
|
|
13
|
+
type MutableRefObject,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import { cn } from '../internal/cn.js';
|
|
16
|
+
import { useSelectContext } from './context.js';
|
|
17
|
+
|
|
18
|
+
export interface SelectContentProps extends HTMLAttributes<HTMLDivElement> {}
|
|
19
|
+
|
|
20
|
+
export const SelectContent = forwardRef<HTMLDivElement, SelectContentProps>(
|
|
21
|
+
function SelectContent({ className, children, ...props }, ref) {
|
|
22
|
+
const ctx = useSelectContext();
|
|
23
|
+
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
24
|
+
const focusedIndexRef = useRef(-1);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!ctx.open) return;
|
|
28
|
+
function handleClickOutside(e: MouseEvent) {
|
|
29
|
+
const target = e.target as Node;
|
|
30
|
+
if (
|
|
31
|
+
contentRef.current &&
|
|
32
|
+
!contentRef.current.contains(target) &&
|
|
33
|
+
ctx.triggerRef.current &&
|
|
34
|
+
!ctx.triggerRef.current.contains(target)
|
|
35
|
+
) {
|
|
36
|
+
ctx.setOpen(false);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
40
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
41
|
+
}, [ctx]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!ctx.open) return;
|
|
45
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
46
|
+
const options = contentRef.current?.querySelectorAll<HTMLElement>('[role="option"]');
|
|
47
|
+
if (!options?.length) return;
|
|
48
|
+
|
|
49
|
+
if (e.key === 'Escape') {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
ctx.setOpen(false);
|
|
52
|
+
ctx.triggerRef.current?.focus();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (e.key === 'ArrowDown') {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
focusedIndexRef.current = Math.min(focusedIndexRef.current + 1, options.length - 1);
|
|
59
|
+
options[focusedIndexRef.current]?.focus();
|
|
60
|
+
} else if (e.key === 'ArrowUp') {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
focusedIndexRef.current = Math.max(focusedIndexRef.current - 1, 0);
|
|
63
|
+
options[focusedIndexRef.current]?.focus();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
67
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
68
|
+
}, [ctx]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!ctx.open || !contentRef.current) return;
|
|
72
|
+
focusedIndexRef.current = -1;
|
|
73
|
+
const selected = contentRef.current.querySelector<HTMLElement>('[aria-selected="true"]');
|
|
74
|
+
if (selected) {
|
|
75
|
+
selected.focus();
|
|
76
|
+
} else {
|
|
77
|
+
const first = contentRef.current.querySelector<HTMLElement>('[role="option"]');
|
|
78
|
+
first?.focus();
|
|
79
|
+
}
|
|
80
|
+
}, [ctx.open]);
|
|
81
|
+
|
|
82
|
+
const setRef = useCallback(
|
|
83
|
+
(el: HTMLDivElement | null) => {
|
|
84
|
+
contentRef.current = el;
|
|
85
|
+
if (typeof ref === 'function') ref(el);
|
|
86
|
+
else if (ref) (ref as MutableRefObject<HTMLDivElement | null>).current = el;
|
|
87
|
+
},
|
|
88
|
+
[ref],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!ctx.open) return null;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
ref={setRef}
|
|
96
|
+
id={ctx.triggerRef.current?.id ? `${ctx.triggerRef.current.id}-listbox` : undefined}
|
|
97
|
+
role="listbox"
|
|
98
|
+
aria-labelledby={ctx.triggerRef.current?.id || undefined}
|
|
99
|
+
className={cn(
|
|
100
|
+
'absolute left-0 top-full z-50 mt-1 w-full min-w-[8rem] overflow-hidden rounded-lg',
|
|
101
|
+
'border border-border bg-card text-card-foreground shadow-mi-input',
|
|
102
|
+
'animate-dropdown-in',
|
|
103
|
+
className,
|
|
104
|
+
)}
|
|
105
|
+
{...props}
|
|
106
|
+
>
|
|
107
|
+
<div className="max-h-60 overflow-y-auto p-1">{children}</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal context for the composable Select.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
Children,
|
|
8
|
+
createContext,
|
|
9
|
+
isValidElement,
|
|
10
|
+
useContext,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
type RefObject,
|
|
13
|
+
} from 'react';
|
|
14
|
+
|
|
15
|
+
export interface SelectContextValue {
|
|
16
|
+
value: string | undefined;
|
|
17
|
+
onValueChange: (value: string) => void;
|
|
18
|
+
open: boolean;
|
|
19
|
+
setOpen: (open: boolean) => void;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
22
|
+
setTriggerRef: (el: HTMLButtonElement | null) => void;
|
|
23
|
+
/** Label text of the currently selected item (set by SelectItem). */
|
|
24
|
+
selectedLabel: { value: string; label: string } | undefined;
|
|
25
|
+
setSelectedLabel: (selected: { value: string; label: string } | undefined) => void;
|
|
26
|
+
getLabelForValue: (value: string | undefined) => string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const SelectContext = createContext<SelectContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
export function useSelectContext() {
|
|
32
|
+
const ctx = useContext(SelectContext);
|
|
33
|
+
if (!ctx) throw new Error('Select sub-components must be used within <Select>.');
|
|
34
|
+
return ctx;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getTextLabel(children: ReactNode): string | undefined {
|
|
38
|
+
if (typeof children === 'string' || typeof children === 'number') {
|
|
39
|
+
return String(children);
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function findSelectItemLabel(children: ReactNode, value: string | undefined): string | undefined {
|
|
45
|
+
if (!value) return undefined;
|
|
46
|
+
|
|
47
|
+
let match: string | undefined;
|
|
48
|
+
|
|
49
|
+
Children.forEach(children, (child) => {
|
|
50
|
+
if (match || !isValidElement(child)) return;
|
|
51
|
+
|
|
52
|
+
const props = child.props as {
|
|
53
|
+
value?: unknown;
|
|
54
|
+
children?: ReactNode;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (props.value === value) {
|
|
58
|
+
match = getTextLabel(props.children);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
match = findSelectItemLabel(props.children, value);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return match;
|
|
66
|
+
}
|