@flikk/ui 1.0.0-beta.26 → 1.0.0-beta.27
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/components/ai/PromptInput/PromptInput.js +3 -0
- package/dist/components/ai/PromptInput/VoiceRecorder.js +3 -0
- package/dist/components/core/Button/Button.theme.js +1 -1
- package/dist/components/core/Calendar/Calendar.theme.js +1 -1
- package/dist/components/core/CommandPalette/CommandItem.js +4 -5
- package/dist/components/core/CommandPalette/CommandPalette.js +1 -1
- package/dist/components/core/CommandPalette/CommandPalette.theme.js +11 -13
- package/dist/components/core/CommandPalette/CommandPalette.types.d.ts +0 -2
- package/dist/components/core/ContextMenu/ContextMenu.theme.js +13 -14
- package/dist/components/core/DotSeparator/DotSeparator.d.ts +3 -0
- package/dist/components/core/DotSeparator/DotSeparator.js +19 -0
- package/dist/components/core/DotSeparator/DotSeparator.theme.d.ts +2 -0
- package/dist/components/core/DotSeparator/DotSeparator.theme.js +10 -0
- package/dist/components/core/DotSeparator/DotSeparator.types.d.ts +25 -0
- package/dist/components/core/DotSeparator/index.d.ts +3 -0
- package/dist/components/core/Dropdown/Dropdown.theme.js +2 -3
- package/dist/components/core/Dropdown/DropdownItem.js +1 -1
- package/dist/components/core/Dropdown/DropdownMenu.js +1 -1
- package/dist/components/core/MenuItem/MenuItem.js +5 -2
- package/dist/components/core/MenuItem/MenuItem.theme.d.ts +6 -0
- package/dist/components/core/MenuItem/MenuItem.theme.js +19 -16
- package/dist/components/core/MenuItem/MenuItem.types.d.ts +2 -0
- package/dist/components/core/NavItem/NavItem.js +20 -4
- package/dist/components/core/NavItem/NavItem.theme.js +13 -2
- package/dist/components/core/Popover/Popover.types.d.ts +6 -2
- package/dist/components/core/Popover/PopoverBody.js +10 -3
- package/dist/components/core/Segmented/Segmented.theme.js +1 -1
- package/dist/components/core/Sidebar/Sidebar.d.ts +2 -0
- package/dist/components/core/Sidebar/Sidebar.js +15 -7
- package/dist/components/core/Sidebar/Sidebar.types.d.ts +92 -4
- package/dist/components/core/Sidebar/SidebarCard.d.ts +25 -0
- package/dist/components/core/Sidebar/SidebarCard.js +68 -0
- package/dist/components/core/Sidebar/SidebarCard.theme.d.ts +8 -0
- package/dist/components/core/Sidebar/SidebarCard.theme.js +18 -0
- package/dist/components/core/Sidebar/SidebarContent.js +5 -1
- package/dist/components/core/Sidebar/SidebarContext.js +1 -1
- package/dist/components/core/Sidebar/SidebarHeader.js +10 -2
- package/dist/components/core/Sidebar/SidebarLogo.js +38 -3
- package/dist/components/core/Sidebar/SidebarSearch.d.ts +22 -0
- package/dist/components/core/Sidebar/SidebarSearch.js +135 -0
- package/dist/components/core/Sidebar/SidebarToggle.js +1 -1
- package/dist/components/core/Sidebar/SidebarUserProfile.js +2 -2
- package/dist/components/core/Sidebar/index.d.ts +3 -1
- package/dist/components/core/Tag/Tag.js +29 -5
- package/dist/components/core/Tag/Tag.theme.js +13 -8
- package/dist/components/core/Tag/Tag.types.d.ts +7 -0
- package/dist/components/core/Tooltip/Tooltip.js +1 -1
- package/dist/components/core/Tooltip/Tooltip.theme.js +1 -2
- package/dist/components/core/index.d.ts +1 -0
- package/dist/components/core/index.js +4 -0
- package/dist/components/data-display/GanttChart/GanttChart.d.ts +1 -1
- package/dist/components/data-display/GanttChart/GanttChart.js +31 -4
- package/dist/components/data-display/GanttChart/GanttChart.theme.js +19 -17
- package/dist/components/data-display/GanttChart/GanttChart.types.d.ts +19 -0
- package/dist/components/data-display/GanttChart/GanttChart.utils.js +1 -1
- package/dist/components/data-display/GanttChart/GanttChartPanel.d.ts +1 -1
- package/dist/components/data-display/GanttChart/GanttChartPanel.js +3 -3
- package/dist/components/data-display/GanttChart/GanttDependencyArrows.js +7 -4
- package/dist/components/data-display/GanttChart/GanttMilestone.js +5 -5
- package/dist/components/data-display/GanttChart/GanttTablePanel.js +1 -1
- package/dist/components/data-display/GanttChart/GanttTableTaskRow.js +2 -2
- package/dist/components/data-display/GanttChart/GanttTaskBar.js +37 -16
- package/dist/components/data-display/GanttChart/GanttTimelineGrid.js +2 -1
- package/dist/components/data-display/GanttChart/GanttTimelineHeader.js +35 -11
- package/dist/components/data-display/GanttChart/GanttToolbar.js +2 -2
- package/dist/components/data-display/GanttChart/hooks/useGanttDrag.js +3 -2
- package/dist/components/data-display/GanttChart/index.d.ts +1 -1
- package/dist/components/data-display/Table/Table.theme.js +3 -5
- package/dist/components/effects/Aurora/Aurora.d.ts +6 -17
- package/dist/components/effects/Aurora/Aurora.js +297 -131
- package/dist/components/effects/Aurora/Aurora.types.d.ts +6 -0
- package/dist/components/forms/Combobox/Combobox.js +1 -1
- package/dist/components/forms/Combobox/Combobox.theme.js +2 -4
- package/dist/components/forms/CronInput/CronInput.js +1 -1
- package/dist/components/forms/CronInput/CronInput.theme.js +14 -23
- package/dist/components/forms/DatePicker/DatePicker.theme.js +11 -18
- package/dist/components/forms/DatePicker/DatePickerTrigger.js +1 -1
- package/dist/components/forms/DateRangePicker/DateRangePicker.theme.js +6 -13
- package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +1 -1
- package/dist/components/forms/FileUpload/FileUpload.animations.d.ts +29 -0
- package/dist/components/forms/FileUpload/FileUpload.animations.js +19 -0
- package/dist/components/forms/FileUpload/FileUpload.js +87 -48
- package/dist/components/forms/FileUpload/FileUpload.theme.d.ts +2 -10
- package/dist/components/forms/FileUpload/FileUpload.theme.js +16 -30
- package/dist/components/forms/FileUpload/FileUpload.types.d.ts +26 -10
- package/dist/components/forms/Input/Input.theme.js +13 -10
- package/dist/components/forms/InputCounter/InputCounter.theme.js +1 -1
- package/dist/components/forms/Mention/Mention.js +152 -57
- package/dist/components/forms/Mention/Mention.theme.js +16 -23
- package/dist/components/forms/Mention/Mention.types.d.ts +12 -12
- package/dist/components/forms/Mention/Mention.utils.js +32 -4
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +3 -3
- package/dist/components/forms/RichTextEditor/RichTextEditor.theme.js +5 -5
- package/dist/components/forms/Select/Select.js +2 -2
- package/dist/components/forms/Select/Select.theme.js +3 -3
- package/dist/components/forms/TimePicker/TimePickerContent.js +3 -0
- package/dist/components/forms/TimePicker/TimePickerTrigger.js +1 -1
- package/dist/components/forms/forms.theme.js +21 -16
- package/dist/components/forms/index.js +3 -0
- package/dist/components/layout/AppShell/AppShell.d.ts +10 -0
- package/dist/components/layout/AppShell/AppShell.js +84 -0
- package/dist/components/layout/AppShell/AppShell.theme.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShell.theme.js +42 -0
- package/dist/components/layout/AppShell/AppShell.types.d.ts +127 -0
- package/dist/components/layout/AppShell/AppShellContext.d.ts +14 -0
- package/dist/components/layout/AppShell/AppShellContext.js +19 -0
- package/dist/components/layout/AppShell/AppShellFooter.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellFooter.js +12 -0
- package/dist/components/layout/AppShell/AppShellHeader.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellHeader.js +12 -0
- package/dist/components/layout/AppShell/AppShellMain.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellMain.js +19 -0
- package/dist/components/layout/AppShell/AppShellRightPanel.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellRightPanel.js +16 -0
- package/dist/components/layout/AppShell/AppShellSidebar.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellSidebar.js +16 -0
- package/dist/components/layout/AppShell/AppShellTopbar.d.ts +3 -0
- package/dist/components/layout/AppShell/AppShellTopbar.js +12 -0
- package/dist/components/layout/AppShell/index.d.ts +3 -0
- package/dist/components/layout/FormLayout/FormLayout.js +3 -0
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/hooks/useSelectPortal.d.ts +10 -2
- package/dist/hooks/useSelectPortal.js +45 -24
- package/dist/index.js +6 -0
- package/dist/registry.json +10021 -0
- package/dist/styles.css +1 -1
- package/dist/utils/dateUtils.js +11 -1
- package/package.json +4 -2
- package/src/global.scss +29 -8
- package/src/styles/theme.css +51 -12
|
@@ -1,36 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default theme for the FileUpload component
|
|
3
|
-
* Uses production-ready CSS variables from global.scss
|
|
4
|
-
*/
|
|
5
1
|
const fileUploadTheme = {
|
|
6
|
-
//
|
|
2
|
+
// Root container
|
|
7
3
|
containerStyle: "w-full",
|
|
8
4
|
// Dropzone area (default state)
|
|
9
|
-
dropzoneStyle: "relative flex flex-col items-center justify-center w-full min-h-[160px] p-6 border border-dashed border-[var(--color-border)] rounded-[var(--form-radius)] bg-[var(--color-
|
|
10
|
-
//
|
|
11
|
-
dropzoneActiveStyle: "border-[var(--color-primary)] bg-[var(--color-primary
|
|
12
|
-
//
|
|
13
|
-
dropzoneDisabledStyle: "cursor-not-allowed opacity-60 hover:bg-[var(--color-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
// Preview container
|
|
25
|
-
previewContainerStyle: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 mt-4",
|
|
26
|
-
// Individual preview item
|
|
27
|
-
previewItemStyle: "relative group rounded-[var(--radius-base)] border border-[var(--color-border)] overflow-hidden bg-[var(--color-background-secondary)]",
|
|
28
|
-
// Preview image
|
|
29
|
-
previewImageStyle: "w-full h-32 object-cover",
|
|
30
|
-
// Preview file (non-image)
|
|
31
|
-
previewFileStyle: "flex items-center justify-center w-full h-32 p-3 text-sm text-[var(--color-text-secondary)] text-center break-all",
|
|
32
|
-
// Remove button
|
|
33
|
-
removeButtonStyle: "absolute top-1 right-1 -translate-y-1 opacity-0 group-hover:opacity-100 transition-all duration-400 group-hover:-translate-y-0",
|
|
5
|
+
dropzoneStyle: "relative flex flex-col items-center justify-center w-full min-h-[160px] p-6 border border-dashed border-[var(--color-border)] rounded-[var(--form-radius)] bg-[var(--color-surface)] hover:border-[var(--color-border-hover)] transition-colors duration-400 cursor-pointer",
|
|
6
|
+
// Dragging over — translucent primary tint composites over the surface in both modes
|
|
7
|
+
dropzoneActiveStyle: "border-solid border-[var(--color-primary)] bg-[var(--color-primary)]/5",
|
|
8
|
+
// Disabled
|
|
9
|
+
dropzoneDisabledStyle: "cursor-not-allowed opacity-60 hover:bg-[var(--color-surface)] hover:border-[var(--color-border)]",
|
|
10
|
+
// Wrapping row of fixed-width thumbnails (not a stretch grid)
|
|
11
|
+
previewContainerStyle: "flex flex-wrap gap-1 mt-2",
|
|
12
|
+
// Fixed responsive thumbnail — stays small on large containers, wraps to more-per-row
|
|
13
|
+
previewItemStyle: "relative group shrink-0 size-10 sm:size-12 md:size-16 rounded-[var(--radius-base)] border border-[var(--color-border)] overflow-hidden bg-[var(--color-surface)]",
|
|
14
|
+
// Image fills the thumbnail box
|
|
15
|
+
previewImageStyle: "size-full object-cover",
|
|
16
|
+
// Non-image tile — centered document icon (filename surfaced via title tooltip)
|
|
17
|
+
previewFileStyle: "flex items-center justify-center size-full text-[var(--color-text-muted)]",
|
|
18
|
+
// Remove button (revealed on item hover)
|
|
19
|
+
removeButtonStyle: "absolute top-1 right-1 -translate-y-1 opacity-0 group-hover:opacity-100 transition-all duration-400 group-hover:-translate-y-0",
|
|
34
20
|
};
|
|
35
21
|
|
|
36
22
|
export { fileUploadTheme };
|
|
@@ -4,7 +4,7 @@ export type FileUploadState = "default" | "disabled" | "invalid";
|
|
|
4
4
|
* FileUpload component props
|
|
5
5
|
* Supports both click-to-upload and drag-and-drop
|
|
6
6
|
*/
|
|
7
|
-
export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError'> {
|
|
7
|
+
export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError' | 'onChange'> {
|
|
8
8
|
/**
|
|
9
9
|
* Accepted file types (MIME types or file extensions)
|
|
10
10
|
* @example ["image/*", ".pdf", ".doc"]
|
|
@@ -43,7 +43,22 @@ export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElemen
|
|
|
43
43
|
*/
|
|
44
44
|
errorMessage?: React.ReactNode;
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Controlled file list. When provided, the component is controlled and the
|
|
47
|
+
* parent owns the list — pair with `onChange`. Leave undefined for the
|
|
48
|
+
* uncontrolled behavior (the component tracks its own list internally).
|
|
49
|
+
*/
|
|
50
|
+
value?: File[];
|
|
51
|
+
/**
|
|
52
|
+
* Initial file list for the uncontrolled case. Ignored when `value` is set.
|
|
53
|
+
*/
|
|
54
|
+
defaultFiles?: File[];
|
|
55
|
+
/**
|
|
56
|
+
* Callback fired whenever the full file list changes (add or remove),
|
|
57
|
+
* with the complete next list. Required to read state in controlled mode.
|
|
58
|
+
*/
|
|
59
|
+
onChange?: (files: File[]) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Callback when files are selected/dropped, with only the newly added files.
|
|
47
62
|
*/
|
|
48
63
|
onUpload?: (files: File[]) => void;
|
|
49
64
|
/**
|
|
@@ -59,20 +74,26 @@ export interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElemen
|
|
|
59
74
|
* @default true
|
|
60
75
|
*/
|
|
61
76
|
showPreview?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Show the upload icon inside the dropzone. Set `false` for slim/compact
|
|
79
|
+
* containers where the icon would crowd the text.
|
|
80
|
+
* @default true
|
|
81
|
+
*/
|
|
82
|
+
showIcon?: boolean;
|
|
62
83
|
/**
|
|
63
84
|
* Custom upload button text
|
|
64
85
|
* @default "Choose files"
|
|
65
86
|
*/
|
|
66
|
-
buttonText?:
|
|
87
|
+
buttonText?: React.ReactNode;
|
|
67
88
|
/**
|
|
68
89
|
* Custom dropzone helper text
|
|
69
90
|
* @default "or drag and drop"
|
|
70
91
|
*/
|
|
71
|
-
helperText?:
|
|
92
|
+
helperText?: React.ReactNode;
|
|
72
93
|
/**
|
|
73
94
|
* Helper text displayed below the upload area
|
|
74
95
|
*/
|
|
75
|
-
description?:
|
|
96
|
+
description?: React.ReactNode;
|
|
76
97
|
/**
|
|
77
98
|
* Custom CSS class for the container
|
|
78
99
|
*/
|
|
@@ -96,11 +117,6 @@ export interface FileUploadThemeOverrides {
|
|
|
96
117
|
dropzoneStyle?: string;
|
|
97
118
|
dropzoneActiveStyle?: string;
|
|
98
119
|
dropzoneDisabledStyle?: string;
|
|
99
|
-
contentStyle?: string;
|
|
100
|
-
iconStyle?: string;
|
|
101
|
-
textStyle?: string;
|
|
102
|
-
helperTextStyle?: string;
|
|
103
|
-
descriptionStyle?: string;
|
|
104
120
|
previewContainerStyle?: string;
|
|
105
121
|
previewItemStyle?: string;
|
|
106
122
|
previewImageStyle?: string;
|
|
@@ -9,14 +9,16 @@ const inputTheme = {
|
|
|
9
9
|
wrapperStyle: formsBaseTheme.wrapperStyle,
|
|
10
10
|
// Input group styles - no focus-within (focus is managed via JS for contentStart/contentEnd compatibility)
|
|
11
11
|
inputGroupStyle: "relative flex rounded-[var(--form-radius)] shadow-[inset_0_0_0_1px_var(--color-border)] " +
|
|
12
|
-
"bg-
|
|
13
|
-
|
|
12
|
+
"bg-[var(--color-surface)] transition-all " +
|
|
13
|
+
// Hover edge: strengthen the border to --color-border-secondary on hover. Scoped with
|
|
14
|
+
// not-focus-within so the JS-driven focus border (primary) always wins.
|
|
15
|
+
"not-focus-within:hover:shadow-[inset_0_0_0_1px_var(--color-border-secondary)]",
|
|
14
16
|
// JS-driven focus ring (applied when isFocused is true)
|
|
15
|
-
inputGroupFocusStyle: "shadow-[
|
|
16
|
-
"dark:shadow-[
|
|
17
|
+
inputGroupFocusStyle: "shadow-[inset_0_0_0_1px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10 " +
|
|
18
|
+
"dark:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:ring-[var(--color-primary-500)]/20",
|
|
17
19
|
// JS-driven focus ring for invalid state
|
|
18
|
-
inputGroupFocusInvalidStyle: "shadow-[
|
|
19
|
-
"dark:shadow-[
|
|
20
|
+
inputGroupFocusInvalidStyle: "shadow-[inset_0_0_0_1px_var(--color-danger)] ring-4 ring-[var(--color-danger)]/10 " +
|
|
21
|
+
"dark:shadow-[inset_0_0_0_1px_var(--color-danger-500)] dark:ring-[var(--color-danger-500)]/20",
|
|
20
22
|
// Base input styles - extends common form control with design tokens
|
|
21
23
|
baseStyle: `${formsBaseTheme.controlBaseStyle} py-2 px-3`,
|
|
22
24
|
// Size variants - composed from atomic tokens
|
|
@@ -30,7 +32,10 @@ const inputTheme = {
|
|
|
30
32
|
default: formsBaseTheme.states.default,
|
|
31
33
|
disabled: formsBaseTheme.states.disabled,
|
|
32
34
|
invalid: "shadow-[inset_0_0_0_1px_var(--color-danger)] bg-[var(--color-danger-50)] " +
|
|
33
|
-
|
|
35
|
+
// Keep the danger border on hover (don't let the hover edge override it)
|
|
36
|
+
"not-focus-within:hover:shadow-[inset_0_0_0_1px_var(--color-danger)] " +
|
|
37
|
+
"dark:bg-[var(--color-danger-950)]/30 dark:shadow-[inset_0_0_0_1px_var(--color-danger-500)] " +
|
|
38
|
+
"dark:not-focus-within:hover:shadow-[inset_0_0_0_1px_var(--color-danger-500)]",
|
|
34
39
|
},
|
|
35
40
|
// Helper text styles with state handling
|
|
36
41
|
helperText: formsBaseTheme.helperText,
|
|
@@ -66,9 +71,7 @@ const inputTheme = {
|
|
|
66
71
|
numberButtonsContainerStyle: "absolute inset-y-0 right-0 flex flex-row items-center pr-2 z-10 gap-1",
|
|
67
72
|
numberButtonStyle: "bg-[var(--color-background-secondary)] flex items-center justify-center " +
|
|
68
73
|
"text-[var(--color-text-muted)] hover:bg-[var(--color-surface-hover)] " +
|
|
69
|
-
"hover:text-[var(--color-text-primary)] cursor-pointer size-6 rounded-sm transition-colors
|
|
70
|
-
"dark:bg-[var(--color-neutral-800)] dark:text-[var(--color-neutral-500)] " +
|
|
71
|
-
"dark:hover:bg-[var(--color-neutral-700)] dark:hover:text-[var(--color-neutral-200)]",
|
|
74
|
+
"hover:text-[var(--color-text-primary)] cursor-pointer size-6 rounded-sm transition-colors",
|
|
72
75
|
// Type-specific styles organized by input type
|
|
73
76
|
typeStyles: {
|
|
74
77
|
password: {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Follows Flikkui design system with CSS variables
|
|
4
4
|
*/
|
|
5
5
|
const inputCounterTheme = {
|
|
6
|
-
baseStyle: "flex items-center gap-3 transition-all p-1 border border-[var(--color-border)] rounded-[var(--form-radius)] w-fit",
|
|
6
|
+
baseStyle: "flex items-center gap-3 transition-all p-1 bg-[var(--color-surface)] border border-[var(--color-border)] rounded-[var(--form-radius)] w-fit",
|
|
7
7
|
buttonWrapperStyle: "flex-shrink-0",
|
|
8
8
|
numberStyle: "min-w-fit text-center text-sm font-medium text-[var(--color-text-primary)] tabular-nums select-none",
|
|
9
9
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import React__default, { useMemo, useId, useState, useRef, useEffect, useCallback, useLayoutEffect, createElement } from 'react';
|
|
1
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import React__default, { useMemo, useId, useState, useRef, useEffect, useCallback, useLayoutEffect } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { AnimatePresence, motion } from 'motion/react';
|
|
5
5
|
import { mentionTheme } from './Mention.theme.js';
|
|
6
6
|
import { cn } from '../../../utils/cn.js';
|
|
7
7
|
import { Avatar } from '../../core/Avatar/Avatar.js';
|
|
8
8
|
import { Tag } from '../../core/Tag/Tag.js';
|
|
9
|
+
import { MenuItem } from '../../core/MenuItem/MenuItem.js';
|
|
9
10
|
import { FormLabel } from '../FormLabel/FormLabel.js';
|
|
10
11
|
import { useIsClient } from '../../../hooks/useIsClient.js';
|
|
11
12
|
import { normalizeSegments, setCaretAtOffset, getPlainTextOffset, parseDOMToSegments, serializeSegments, serializeSegmentsMasked, insertMentionIntoSegments, MENTION_ATTR } from './Mention.utils.js';
|
|
@@ -74,7 +75,7 @@ const dropdownVariants = {
|
|
|
74
75
|
// ---------------------------------------------------------------------------
|
|
75
76
|
// Mention component
|
|
76
77
|
// ---------------------------------------------------------------------------
|
|
77
|
-
const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions: suggestionsProp, onSearch: onSearchProp, value: controlledValue, defaultValue = "", onChange, onMentionSelect, onMentionsChange, placeholder, size = "md", maxSuggestions = 8, className, wrapperClassName, dropdownClassName, theme: themeOverrides, state = "default", disabled = false, helperText, required, label, renderSuggestion, emptyMessage = "No results found", loading = false, mentionTagSize = "sm", mentionTagClassName, rows = 3, ...props }, ref) => {
|
|
78
|
+
const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions: suggestionsProp, onSearch: onSearchProp, value: controlledValue, defaultValue = "", onChange, onMentionSelect, onMentionsChange, placeholder, size = "md", maxSuggestions = 8, className, wrapperClassName, dropdownClassName, theme: themeOverrides, state = "default", disabled = false, helperText, required, label, renderSuggestion, emptyMessage = "No results found", loading = false, mentionTagSize = "sm", mentionTagClassName, mentionTagRemovable = true, rows = 3, style: styleProp, ...props }, ref) => {
|
|
78
79
|
const theme = useMemo(() => ({ ...mentionTheme, ...(themeOverrides || {}) }), [themeOverrides]);
|
|
79
80
|
const generatedId = useId();
|
|
80
81
|
const isDisabled = state === "disabled" || disabled;
|
|
@@ -125,6 +126,25 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
125
126
|
const dropdownRef = useRef(null);
|
|
126
127
|
const composingRef = useRef(false);
|
|
127
128
|
const caretRestoreRef = useRef(null);
|
|
129
|
+
// Bumped on programmatic structural mutations (mention insert/remove) to
|
|
130
|
+
// remount the contentEditable so React rebuilds it from `segments` instead
|
|
131
|
+
// of diffing against a DOM the browser has already restructured. Editing a
|
|
132
|
+
// contentEditable lets the browser delete/merge/move React-owned nodes
|
|
133
|
+
// (e.g. backspacing the space before a chip removes its text span); the
|
|
134
|
+
// next structural reconcile then calls removeChild on a node that is no
|
|
135
|
+
// longer where React's fiber expects it and throws "removeChild ... not a
|
|
136
|
+
// child of this node". A full remount sidesteps the partial diff entirely.
|
|
137
|
+
// NEVER bump on typing — that path reconciles content-only changes safely
|
|
138
|
+
// and must keep the caret/IME composition stable.
|
|
139
|
+
const [editorKey, setEditorKey] = useState(0);
|
|
140
|
+
const lastEditorKeyRef = useRef(0);
|
|
141
|
+
const remountEditor = useCallback(() => setEditorKey((k) => k + 1), []);
|
|
142
|
+
// Mirrors the mention count React currently has committed to the DOM. Used
|
|
143
|
+
// by updateFromDOM to detect when a *browser-handled* edit (range-selection
|
|
144
|
+
// delete, cut, drag) structurally removed a chip — that desyncs React's
|
|
145
|
+
// fiber from the DOM, so the next reconcile must be a full remount, not a
|
|
146
|
+
// partial diff. Synced from `segments` after each commit (below).
|
|
147
|
+
const renderedMentionCountRef = useRef(0);
|
|
128
148
|
// Track which segment index is "selected" when clicking a mention tag
|
|
129
149
|
const [selectedMentionIndex, setSelectedMentionIndex] = useState(null);
|
|
130
150
|
const isClient = useIsClient();
|
|
@@ -244,10 +264,22 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
244
264
|
}
|
|
245
265
|
}
|
|
246
266
|
if (caretRestoreRef.current !== null) {
|
|
267
|
+
// A structural mutation remounts the editable div (see `editorKey`),
|
|
268
|
+
// which drops DOM focus. Re-focus before restoring the caret so the
|
|
269
|
+
// user can keep typing seamlessly after inserting/removing a mention.
|
|
270
|
+
if (editorKey !== lastEditorKeyRef.current && document.activeElement !== el) {
|
|
271
|
+
el.focus({ preventScroll: true });
|
|
272
|
+
}
|
|
247
273
|
setCaretAtOffset(el, caretRestoreRef.current);
|
|
248
274
|
caretRestoreRef.current = null;
|
|
249
275
|
}
|
|
276
|
+
lastEditorKeyRef.current = editorKey;
|
|
250
277
|
});
|
|
278
|
+
// Keep the committed-mention-count ref in sync so updateFromDOM can detect
|
|
279
|
+
// a browser-removed chip on the next edit (see renderedMentionCountRef).
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
renderedMentionCountRef.current = segments.filter((s) => s.type === "mention").length;
|
|
282
|
+
}, [segments]);
|
|
251
283
|
// --- Handlers ---
|
|
252
284
|
const updateFromDOM = useCallback(() => {
|
|
253
285
|
if (!editableRef.current || composingRef.current)
|
|
@@ -260,14 +292,18 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
260
292
|
if (offset !== null) {
|
|
261
293
|
caretRestoreRef.current = offset;
|
|
262
294
|
}
|
|
295
|
+
// If a browser-handled edit (range-selection delete, cut, drag) removed a
|
|
296
|
+
// chip, the DOM no longer matches React's fiber. A content-only reconcile
|
|
297
|
+
// would then call removeChild on an already-gone node and throw, so force
|
|
298
|
+
// a full remount instead. Pure text edits keep the count stable and
|
|
299
|
+
// reconcile normally — preserving caret/IME.
|
|
300
|
+
const newMentionCount = newSegs.filter((s) => s.type === "mention").length;
|
|
301
|
+
if (newMentionCount !== renderedMentionCountRef.current) {
|
|
302
|
+
remountEditor();
|
|
303
|
+
}
|
|
263
304
|
setSegments(newSegs);
|
|
264
305
|
lastSerializedRef.current = serialized;
|
|
265
|
-
|
|
266
|
-
onChange === null || onChange === void 0 ? void 0 : onChange(serialized);
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
onChange === null || onChange === void 0 ? void 0 : onChange(serialized);
|
|
270
|
-
}
|
|
306
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(serialized);
|
|
271
307
|
notifyMentionsChange(newSegs);
|
|
272
308
|
// Detect active mention using masked text (hides @ from existing mentions)
|
|
273
309
|
if (offset !== null) {
|
|
@@ -275,7 +311,7 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
275
311
|
const mention = detectActiveMention(maskedText, offset, triggerChars);
|
|
276
312
|
setActiveMention(mention);
|
|
277
313
|
}
|
|
278
|
-
}, [
|
|
314
|
+
}, [onChange, triggerChars, notifyMentionsChange, remountEditor]);
|
|
279
315
|
const handleInput = useCallback(() => {
|
|
280
316
|
setSelectedMentionIndex(null);
|
|
281
317
|
updateFromDOM();
|
|
@@ -312,6 +348,7 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
312
348
|
setSegments(newSegs);
|
|
313
349
|
setActiveMention(null);
|
|
314
350
|
setHighlightedIndex(-1);
|
|
351
|
+
remountEditor();
|
|
315
352
|
onChange === null || onChange === void 0 ? void 0 : onChange(serialized);
|
|
316
353
|
notifyMentionsChange(newSegs);
|
|
317
354
|
onMentionSelect === null || onMentionSelect === void 0 ? void 0 : onMentionSelect(suggestion, trigger);
|
|
@@ -321,9 +358,51 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
321
358
|
onChange,
|
|
322
359
|
onMentionSelect,
|
|
323
360
|
notifyMentionsChange,
|
|
361
|
+
remountEditor,
|
|
324
362
|
]);
|
|
363
|
+
// Map a mention DOM element back to its segment index. The chips are
|
|
364
|
+
// React-rendered, so we locate the element among all mention nodes and
|
|
365
|
+
// translate that position into the nth "mention" segment.
|
|
366
|
+
const segmentIndexForMentionEl = useCallback((mentionEl) => {
|
|
367
|
+
if (!editableRef.current)
|
|
368
|
+
return -1;
|
|
369
|
+
const allMentions = editableRef.current.querySelectorAll(`[${MENTION_ATTR}]`);
|
|
370
|
+
const domIdx = Array.from(allMentions).indexOf(mentionEl);
|
|
371
|
+
if (domIdx === -1)
|
|
372
|
+
return -1;
|
|
373
|
+
let count = 0;
|
|
374
|
+
for (let i = 0; i < segments.length; i++) {
|
|
375
|
+
if (segments[i].type === "mention") {
|
|
376
|
+
if (count === domIdx)
|
|
377
|
+
return i;
|
|
378
|
+
count++;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return -1;
|
|
382
|
+
}, [segments]);
|
|
383
|
+
// Remove a mention via the segment model — NEVER by mutating the DOM.
|
|
384
|
+
// The chips are React-owned nodes; calling removeChild() on them directly
|
|
385
|
+
// desyncs React's fiber tree from the DOM and makes the next reconcile
|
|
386
|
+
// throw "removeChild ... not a child of this node". Splicing the segment
|
|
387
|
+
// and letting React drop the node keeps React the sole DOM owner. The
|
|
388
|
+
// caret lands at the gap the mention left (its segment start offset).
|
|
389
|
+
const removeMentionSegment = useCallback((segIndex) => {
|
|
390
|
+
const newSegs = normalizeSegments([
|
|
391
|
+
...segments.slice(0, segIndex),
|
|
392
|
+
...segments.slice(segIndex + 1),
|
|
393
|
+
]);
|
|
394
|
+
caretRestoreRef.current = getOffsetAtSegment(segments, segIndex);
|
|
395
|
+
const serialized = serializeSegments(newSegs);
|
|
396
|
+
lastSerializedRef.current = serialized;
|
|
397
|
+
setSegments(newSegs);
|
|
398
|
+
setSelectedMentionIndex(null);
|
|
399
|
+
setActiveMention(null);
|
|
400
|
+
remountEditor();
|
|
401
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(serialized);
|
|
402
|
+
notifyMentionsChange(newSegs);
|
|
403
|
+
}, [segments, onChange, notifyMentionsChange, remountEditor]);
|
|
325
404
|
const handleKeyDown = useCallback((e) => {
|
|
326
|
-
var _a
|
|
405
|
+
var _a;
|
|
327
406
|
// --- Selected mention: delete / replace / deselect ---
|
|
328
407
|
if (selectedMentionIndex !== null &&
|
|
329
408
|
((_a = segments[selectedMentionIndex]) === null || _a === void 0 ? void 0 : _a.type) === "mention") {
|
|
@@ -338,6 +417,7 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
338
417
|
lastSerializedRef.current = serialized;
|
|
339
418
|
setSegments(newSegs);
|
|
340
419
|
setSelectedMentionIndex(null);
|
|
420
|
+
remountEditor();
|
|
341
421
|
onChange === null || onChange === void 0 ? void 0 : onChange(serialized);
|
|
342
422
|
notifyMentionsChange(newSegs);
|
|
343
423
|
return;
|
|
@@ -356,6 +436,7 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
356
436
|
lastSerializedRef.current = serialized;
|
|
357
437
|
setSegments(newSegs);
|
|
358
438
|
setSelectedMentionIndex(null);
|
|
439
|
+
remountEditor();
|
|
359
440
|
onChange === null || onChange === void 0 ? void 0 : onChange(serialized);
|
|
360
441
|
notifyMentionsChange(newSegs);
|
|
361
442
|
return;
|
|
@@ -408,10 +489,12 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
408
489
|
if (prevSibling &&
|
|
409
490
|
prevSibling.nodeType === Node.ELEMENT_NODE &&
|
|
410
491
|
prevSibling.hasAttribute(MENTION_ATTR)) {
|
|
411
|
-
|
|
412
|
-
(
|
|
413
|
-
|
|
414
|
-
|
|
492
|
+
const segIdx = segmentIndexForMentionEl(prevSibling);
|
|
493
|
+
if (segIdx !== -1) {
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
removeMentionSegment(segIdx);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
415
498
|
}
|
|
416
499
|
}
|
|
417
500
|
// Case 2: Caret in parent element, child before offset is a mention
|
|
@@ -422,10 +505,12 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
422
505
|
if (prevChild &&
|
|
423
506
|
prevChild.nodeType === Node.ELEMENT_NODE &&
|
|
424
507
|
prevChild.hasAttribute(MENTION_ATTR)) {
|
|
425
|
-
|
|
426
|
-
(
|
|
427
|
-
|
|
428
|
-
|
|
508
|
+
const segIdx = segmentIndexForMentionEl(prevChild);
|
|
509
|
+
if (segIdx !== -1) {
|
|
510
|
+
e.preventDefault();
|
|
511
|
+
removeMentionSegment(segIdx);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
429
514
|
}
|
|
430
515
|
}
|
|
431
516
|
}
|
|
@@ -445,10 +530,12 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
445
530
|
if (nextSibling &&
|
|
446
531
|
nextSibling.nodeType === Node.ELEMENT_NODE &&
|
|
447
532
|
nextSibling.hasAttribute(MENTION_ATTR)) {
|
|
448
|
-
|
|
449
|
-
(
|
|
450
|
-
|
|
451
|
-
|
|
533
|
+
const segIdx = segmentIndexForMentionEl(nextSibling);
|
|
534
|
+
if (segIdx !== -1) {
|
|
535
|
+
e.preventDefault();
|
|
536
|
+
removeMentionSegment(segIdx);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
452
539
|
}
|
|
453
540
|
}
|
|
454
541
|
}
|
|
@@ -458,10 +545,12 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
458
545
|
if (nextChild &&
|
|
459
546
|
nextChild.nodeType === Node.ELEMENT_NODE &&
|
|
460
547
|
nextChild.hasAttribute(MENTION_ATTR)) {
|
|
461
|
-
|
|
462
|
-
(
|
|
463
|
-
|
|
464
|
-
|
|
548
|
+
const segIdx = segmentIndexForMentionEl(nextChild);
|
|
549
|
+
if (segIdx !== -1) {
|
|
550
|
+
e.preventDefault();
|
|
551
|
+
removeMentionSegment(segIdx);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
465
554
|
}
|
|
466
555
|
}
|
|
467
556
|
}
|
|
@@ -475,7 +564,9 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
475
564
|
highlightedIndex,
|
|
476
565
|
filteredSuggestions,
|
|
477
566
|
insertMention,
|
|
478
|
-
|
|
567
|
+
segmentIndexForMentionEl,
|
|
568
|
+
removeMentionSegment,
|
|
569
|
+
remountEditor,
|
|
479
570
|
selectedMentionIndex,
|
|
480
571
|
segments,
|
|
481
572
|
onChange,
|
|
@@ -513,26 +604,9 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
513
604
|
setSelectedMentionIndex(null);
|
|
514
605
|
return;
|
|
515
606
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (domIdx === -1) {
|
|
520
|
-
setSelectedMentionIndex(null);
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
// Map nth DOM mention → segment index
|
|
524
|
-
let count = 0;
|
|
525
|
-
for (let i = 0; i < segments.length; i++) {
|
|
526
|
-
if (segments[i].type === "mention") {
|
|
527
|
-
if (count === domIdx) {
|
|
528
|
-
setSelectedMentionIndex(i);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
count++;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
setSelectedMentionIndex(null);
|
|
535
|
-
}, [segments]);
|
|
607
|
+
const segIdx = segmentIndexForMentionEl(mentionEl);
|
|
608
|
+
setSelectedMentionIndex(segIdx === -1 ? null : segIdx);
|
|
609
|
+
}, [segmentIndexForMentionEl]);
|
|
536
610
|
// Scroll highlighted suggestion into view
|
|
537
611
|
useEffect(() => {
|
|
538
612
|
if (highlightedIndex >= 0 && dropdownRef.current) {
|
|
@@ -552,11 +626,22 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
552
626
|
const renderedSegments = useMemo(() => {
|
|
553
627
|
return segments.map((seg, i) => {
|
|
554
628
|
if (seg.type === "text") {
|
|
555
|
-
return (jsx("span", { "data-segment": "text", children: seg.text
|
|
629
|
+
return (jsx("span", { "data-segment": "text", children: seg.text }, `text-${i}`));
|
|
556
630
|
}
|
|
557
|
-
return (jsx(Tag, { size: mentionTagSize, removable:
|
|
558
|
-
|
|
559
|
-
|
|
631
|
+
return (jsx(Tag, { size: mentionTagSize, removable: mentionTagRemovable,
|
|
632
|
+
// Route removal through the segment model (same path as keyboard
|
|
633
|
+
// delete) so React stays the DOM owner and the editor remounts
|
|
634
|
+
// cleanly. stopPropagation in Tag keeps this click from also
|
|
635
|
+
// selecting the chip via the editable's onClick.
|
|
636
|
+
onRemove: () => removeMentionSegment(i), animated: false,
|
|
637
|
+
// Always pass the avatar object (src may be undefined) so the chip
|
|
638
|
+
// shows the initials fallback when no photo is available — matching
|
|
639
|
+
// the suggestion dropdown. Avatar renders the first initial at xs.
|
|
640
|
+
avatar: { src: seg.data.avatar, name: seg.data.label }, contentEditable: false, className: cn(
|
|
641
|
+
// align-middle: avatar vs text-only chips have differing intrinsic
|
|
642
|
+
// heights and an inline-flex baseline is ill-defined, so center on
|
|
643
|
+
// the line's middle to keep every chip level when used inline.
|
|
644
|
+
"align-middle", i === selectedMentionIndex &&
|
|
560
645
|
"ring-2 ring-[var(--color-primary)] ring-offset-1", mentionTagClassName), [MENTION_ATTR]: seg.data.id,
|
|
561
646
|
"data-mention-label": seg.data.label,
|
|
562
647
|
"data-mention-trigger": seg.data.trigger, children: seg.data.label }, `mention-${seg.data.id}-${i}`));
|
|
@@ -565,7 +650,8 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
565
650
|
segments,
|
|
566
651
|
mentionTagSize,
|
|
567
652
|
mentionTagClassName,
|
|
568
|
-
|
|
653
|
+
mentionTagRemovable,
|
|
654
|
+
removeMentionSegment,
|
|
569
655
|
selectedMentionIndex,
|
|
570
656
|
]);
|
|
571
657
|
// -----------------------------------------------------------------------
|
|
@@ -580,10 +666,17 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
580
666
|
insertMention(suggestion);
|
|
581
667
|
}, children: renderSuggestion(suggestion, isHighlighted, triggerChar) }, suggestion.id));
|
|
582
668
|
}
|
|
583
|
-
return (
|
|
669
|
+
return (jsx(MenuItem, { role: "option", "aria-selected": isHighlighted, "data-suggestion-index": index,
|
|
670
|
+
// Drives MenuItem's selected styling; highlight follows keyboard nav
|
|
671
|
+
// (and hover, via onMouseEnter setting highlightedIndex).
|
|
672
|
+
selected: isHighlighted, startContent: jsx(Avatar, { src: suggestion.avatar, name: suggestion.label, size: "xs", className: "ring-0 border-0" }), subtitle: suggestion.description, onMouseEnter: () => setHighlightedIndex(index),
|
|
673
|
+
// mousedown + preventDefault keeps focus in the contentEditable so
|
|
674
|
+
// selecting a suggestion never blurs the editor (MenuItem's onClick/
|
|
675
|
+
// onActivate path would fire too late, after focus moves).
|
|
676
|
+
onMouseDown: (e) => {
|
|
584
677
|
e.preventDefault();
|
|
585
678
|
insertMention(suggestion);
|
|
586
|
-
}, children:
|
|
679
|
+
}, children: suggestion.label }, suggestion.id));
|
|
587
680
|
};
|
|
588
681
|
// -----------------------------------------------------------------------
|
|
589
682
|
// Dropdown portal content
|
|
@@ -601,11 +694,13 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
|
|
|
601
694
|
// Render
|
|
602
695
|
// -----------------------------------------------------------------------
|
|
603
696
|
const helperId = `${generatedId}-helper`;
|
|
604
|
-
return (jsxs("div", { className: cn(theme.wrapperStyle, wrapperClassName), children: [label && (jsx(FormLabel, { htmlFor: generatedId, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", required: required, className: "mb-1.5", children: label })), jsx("div", { ref: wrapperRef, className: cn(theme.inputGroupStyle,
|
|
605
|
-
|
|
697
|
+
return (jsxs("div", { className: cn(theme.wrapperStyle, wrapperClassName), children: [label && (jsx(FormLabel, { htmlFor: generatedId, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", required: required, className: "mb-1.5", children: label })), jsx("div", { ref: wrapperRef, className: cn(theme.inputGroupStyle, isInvalid && theme.invalidStyle, isFocused &&
|
|
698
|
+
(isInvalid ? theme.invalidFocusStyle : theme.inputGroupFocusStyle)), children: createElement("div", { ...props, key: editorKey, ref: setEditableRef, id: generatedId, contentEditable: !isDisabled, suppressContentEditableWarning: true, onInput: handleInput, onKeyDown: handleKeyDown, onClick: handleClick, onFocus: handleFocus, onBlur: handleBlur, onPaste: handlePaste, onSelect: handleSelect, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, role: "textbox", "aria-multiline": "true", "aria-autocomplete": "list", "aria-expanded": isDropdownOpen, "aria-haspopup": "listbox", "aria-placeholder": placeholder, "aria-required": required || undefined, "data-placeholder": placeholder, "aria-disabled": isDisabled || undefined, "aria-invalid": isInvalid || undefined, "aria-describedby": helperText ? helperId : undefined, className: cn(theme.editableStyle, theme.sizes[size], isContentEmpty &&
|
|
699
|
+
"before:content-[attr(data-placeholder)] before:text-[var(--color-text-placeholder)] before:pointer-events-none before:absolute", isDisabled && "cursor-not-allowed opacity-60", className), style: {
|
|
606
700
|
minHeight: rows ? `${rows * 1.5}em` : undefined,
|
|
701
|
+
...styleProp,
|
|
607
702
|
position: "relative",
|
|
608
|
-
}
|
|
703
|
+
} }, renderedSegments) }), helperText && (jsx("div", { id: helperId, className: cn("text-sm mt-1", isInvalid
|
|
609
704
|
? "text-[var(--color-danger)]"
|
|
610
705
|
: "text-[var(--color-text-muted)]"), children: helperText })), isClient && createPortal(dropdownContent, document.body)] }));
|
|
611
706
|
});
|
|
@@ -10,9 +10,11 @@ const mentionTheme = {
|
|
|
10
10
|
// Input group container with outline and focus ring
|
|
11
11
|
inputGroupStyle: formsBaseTheme.inputGroupBaseStyle,
|
|
12
12
|
// JS-driven focus ring (applied when editable div is focused)
|
|
13
|
-
inputGroupFocusStyle:
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
inputGroupFocusStyle: formsBaseTheme.focusStates.default,
|
|
14
|
+
// Invalid state — inset box-shadow + danger fill (NOT border/ring; see CLAUDE.md §9)
|
|
15
|
+
invalidStyle: formsBaseTheme.states.invalid,
|
|
16
|
+
// Invalid + focused — danger inset shadow + danger ring glow
|
|
17
|
+
invalidFocusStyle: formsBaseTheme.focusStates.invalid,
|
|
16
18
|
// Base editable div styles
|
|
17
19
|
editableStyle: `${formsBaseTheme.controlBaseStyle} whitespace-pre-wrap break-words overflow-y-auto cursor-text`,
|
|
18
20
|
// Size variants
|
|
@@ -21,35 +23,26 @@ const mentionTheme = {
|
|
|
21
23
|
md: `${formsBaseTheme.sizes.md.text} ${formsBaseTheme.sizes.md.padding} ${formsBaseTheme.sizes.md.textareaHeight}`,
|
|
22
24
|
lg: `${formsBaseTheme.sizes.lg.text} ${formsBaseTheme.sizes.lg.padding} ${formsBaseTheme.sizes.lg.textareaHeight}`,
|
|
23
25
|
},
|
|
24
|
-
// Inline mention Tag chip styles
|
|
25
|
-
mentionTagStyle: "mx-0.5 align-baseline cursor-default select-none",
|
|
26
|
+
// Inline mention Tag chip styles.
|
|
26
27
|
// Dropdown container - reuses the shared dropdown styles
|
|
27
|
-
|
|
28
|
-
//
|
|
28
|
+
// p-1 (not py-1) so MenuItem rows' rounded highlight insets evenly on all
|
|
29
|
+
// sides instead of running flush to the container's left/right edges.
|
|
30
|
+
dropdownStyle: formsBaseTheme.dropdownStyles.container + " p-1",
|
|
31
|
+
// Suggestion rows render via the shared MenuItem primitive (avatar →
|
|
32
|
+
// startContent, label → children, description → subtitle). These two styles
|
|
33
|
+
// are only used by the custom `renderSuggestion` escape hatch, which wraps
|
|
34
|
+
// the consumer's node in a plain row that still tracks hover/highlight.
|
|
29
35
|
suggestionStyle: "relative flex items-center gap-2.5 cursor-pointer select-none py-2 px-3 " +
|
|
30
36
|
"text-[var(--color-text-primary)] " +
|
|
31
37
|
"hover:text-[var(--color-primary)] hover:bg-[var(--color-primary-50)]/50 " +
|
|
32
38
|
"dark:hover:text-[var(--color-primary-400)] dark:hover:bg-[var(--color-primary-400)]/10",
|
|
33
|
-
// Highlighted suggestion (keyboard navigation)
|
|
39
|
+
// Highlighted suggestion (keyboard navigation) — custom renderSuggestion path
|
|
34
40
|
suggestionHighlightedStyle: "text-[var(--color-primary)] bg-[var(--color-primary-50)]/50 " +
|
|
35
41
|
"dark:text-[var(--color-primary-400)] dark:bg-[var(--color-primary-400)]/10",
|
|
36
|
-
// Avatar image
|
|
37
|
-
suggestionAvatarStyle: "size-7 rounded-full object-cover shrink-0 " +
|
|
38
|
-
"bg-[var(--color-surface-sunken)] dark:bg-[var(--color-neutral-700)]",
|
|
39
|
-
// Avatar fallback (initials circle)
|
|
40
|
-
suggestionAvatarFallbackStyle: "size-7 rounded-full shrink-0 flex items-center justify-center text-xs font-medium " +
|
|
41
|
-
"bg-[var(--color-primary-100)] text-[var(--color-primary-700)] " +
|
|
42
|
-
"dark:bg-[var(--color-primary-900)] dark:text-[var(--color-primary-300)]",
|
|
43
|
-
// Suggestion label text
|
|
44
|
-
suggestionLabelStyle: "text-sm font-medium truncate",
|
|
45
|
-
// Suggestion description text
|
|
46
|
-
suggestionDescriptionStyle: "text-xs text-[var(--color-text-muted)] truncate " +
|
|
47
|
-
"dark:text-[var(--color-neutral-500)]",
|
|
48
42
|
// Empty state
|
|
49
43
|
emptyMessageStyle: formsBaseTheme.dropdownStyles.emptyMessage,
|
|
50
|
-
// Loading state
|
|
51
|
-
loadingStyle: "py-3 px-3 text-center text-[var(--color-text-muted)]
|
|
52
|
-
"dark:text-[var(--color-neutral-500)]",
|
|
44
|
+
// Loading state (--color-text-muted auto-flips for dark mode)
|
|
45
|
+
loadingStyle: "py-3 px-3 text-center text-[var(--color-text-muted)]",
|
|
53
46
|
};
|
|
54
47
|
|
|
55
48
|
export { mentionTheme };
|