@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.
Files changed (131) hide show
  1. package/dist/components/ai/PromptInput/PromptInput.js +3 -0
  2. package/dist/components/ai/PromptInput/VoiceRecorder.js +3 -0
  3. package/dist/components/core/Button/Button.theme.js +1 -1
  4. package/dist/components/core/Calendar/Calendar.theme.js +1 -1
  5. package/dist/components/core/CommandPalette/CommandItem.js +4 -5
  6. package/dist/components/core/CommandPalette/CommandPalette.js +1 -1
  7. package/dist/components/core/CommandPalette/CommandPalette.theme.js +11 -13
  8. package/dist/components/core/CommandPalette/CommandPalette.types.d.ts +0 -2
  9. package/dist/components/core/ContextMenu/ContextMenu.theme.js +13 -14
  10. package/dist/components/core/DotSeparator/DotSeparator.d.ts +3 -0
  11. package/dist/components/core/DotSeparator/DotSeparator.js +19 -0
  12. package/dist/components/core/DotSeparator/DotSeparator.theme.d.ts +2 -0
  13. package/dist/components/core/DotSeparator/DotSeparator.theme.js +10 -0
  14. package/dist/components/core/DotSeparator/DotSeparator.types.d.ts +25 -0
  15. package/dist/components/core/DotSeparator/index.d.ts +3 -0
  16. package/dist/components/core/Dropdown/Dropdown.theme.js +2 -3
  17. package/dist/components/core/Dropdown/DropdownItem.js +1 -1
  18. package/dist/components/core/Dropdown/DropdownMenu.js +1 -1
  19. package/dist/components/core/MenuItem/MenuItem.js +5 -2
  20. package/dist/components/core/MenuItem/MenuItem.theme.d.ts +6 -0
  21. package/dist/components/core/MenuItem/MenuItem.theme.js +19 -16
  22. package/dist/components/core/MenuItem/MenuItem.types.d.ts +2 -0
  23. package/dist/components/core/NavItem/NavItem.js +20 -4
  24. package/dist/components/core/NavItem/NavItem.theme.js +13 -2
  25. package/dist/components/core/Popover/Popover.types.d.ts +6 -2
  26. package/dist/components/core/Popover/PopoverBody.js +10 -3
  27. package/dist/components/core/Segmented/Segmented.theme.js +1 -1
  28. package/dist/components/core/Sidebar/Sidebar.d.ts +2 -0
  29. package/dist/components/core/Sidebar/Sidebar.js +15 -7
  30. package/dist/components/core/Sidebar/Sidebar.types.d.ts +92 -4
  31. package/dist/components/core/Sidebar/SidebarCard.d.ts +25 -0
  32. package/dist/components/core/Sidebar/SidebarCard.js +68 -0
  33. package/dist/components/core/Sidebar/SidebarCard.theme.d.ts +8 -0
  34. package/dist/components/core/Sidebar/SidebarCard.theme.js +18 -0
  35. package/dist/components/core/Sidebar/SidebarContent.js +5 -1
  36. package/dist/components/core/Sidebar/SidebarContext.js +1 -1
  37. package/dist/components/core/Sidebar/SidebarHeader.js +10 -2
  38. package/dist/components/core/Sidebar/SidebarLogo.js +38 -3
  39. package/dist/components/core/Sidebar/SidebarSearch.d.ts +22 -0
  40. package/dist/components/core/Sidebar/SidebarSearch.js +135 -0
  41. package/dist/components/core/Sidebar/SidebarToggle.js +1 -1
  42. package/dist/components/core/Sidebar/SidebarUserProfile.js +2 -2
  43. package/dist/components/core/Sidebar/index.d.ts +3 -1
  44. package/dist/components/core/Tag/Tag.js +29 -5
  45. package/dist/components/core/Tag/Tag.theme.js +13 -8
  46. package/dist/components/core/Tag/Tag.types.d.ts +7 -0
  47. package/dist/components/core/Tooltip/Tooltip.js +1 -1
  48. package/dist/components/core/Tooltip/Tooltip.theme.js +1 -2
  49. package/dist/components/core/index.d.ts +1 -0
  50. package/dist/components/core/index.js +4 -0
  51. package/dist/components/data-display/GanttChart/GanttChart.d.ts +1 -1
  52. package/dist/components/data-display/GanttChart/GanttChart.js +31 -4
  53. package/dist/components/data-display/GanttChart/GanttChart.theme.js +19 -17
  54. package/dist/components/data-display/GanttChart/GanttChart.types.d.ts +19 -0
  55. package/dist/components/data-display/GanttChart/GanttChart.utils.js +1 -1
  56. package/dist/components/data-display/GanttChart/GanttChartPanel.d.ts +1 -1
  57. package/dist/components/data-display/GanttChart/GanttChartPanel.js +3 -3
  58. package/dist/components/data-display/GanttChart/GanttDependencyArrows.js +7 -4
  59. package/dist/components/data-display/GanttChart/GanttMilestone.js +5 -5
  60. package/dist/components/data-display/GanttChart/GanttTablePanel.js +1 -1
  61. package/dist/components/data-display/GanttChart/GanttTableTaskRow.js +2 -2
  62. package/dist/components/data-display/GanttChart/GanttTaskBar.js +37 -16
  63. package/dist/components/data-display/GanttChart/GanttTimelineGrid.js +2 -1
  64. package/dist/components/data-display/GanttChart/GanttTimelineHeader.js +35 -11
  65. package/dist/components/data-display/GanttChart/GanttToolbar.js +2 -2
  66. package/dist/components/data-display/GanttChart/hooks/useGanttDrag.js +3 -2
  67. package/dist/components/data-display/GanttChart/index.d.ts +1 -1
  68. package/dist/components/data-display/Table/Table.theme.js +3 -5
  69. package/dist/components/effects/Aurora/Aurora.d.ts +6 -17
  70. package/dist/components/effects/Aurora/Aurora.js +297 -131
  71. package/dist/components/effects/Aurora/Aurora.types.d.ts +6 -0
  72. package/dist/components/forms/Combobox/Combobox.js +1 -1
  73. package/dist/components/forms/Combobox/Combobox.theme.js +2 -4
  74. package/dist/components/forms/CronInput/CronInput.js +1 -1
  75. package/dist/components/forms/CronInput/CronInput.theme.js +14 -23
  76. package/dist/components/forms/DatePicker/DatePicker.theme.js +11 -18
  77. package/dist/components/forms/DatePicker/DatePickerTrigger.js +1 -1
  78. package/dist/components/forms/DateRangePicker/DateRangePicker.theme.js +6 -13
  79. package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +1 -1
  80. package/dist/components/forms/FileUpload/FileUpload.animations.d.ts +29 -0
  81. package/dist/components/forms/FileUpload/FileUpload.animations.js +19 -0
  82. package/dist/components/forms/FileUpload/FileUpload.js +87 -48
  83. package/dist/components/forms/FileUpload/FileUpload.theme.d.ts +2 -10
  84. package/dist/components/forms/FileUpload/FileUpload.theme.js +16 -30
  85. package/dist/components/forms/FileUpload/FileUpload.types.d.ts +26 -10
  86. package/dist/components/forms/Input/Input.theme.js +13 -10
  87. package/dist/components/forms/InputCounter/InputCounter.theme.js +1 -1
  88. package/dist/components/forms/Mention/Mention.js +152 -57
  89. package/dist/components/forms/Mention/Mention.theme.js +16 -23
  90. package/dist/components/forms/Mention/Mention.types.d.ts +12 -12
  91. package/dist/components/forms/Mention/Mention.utils.js +32 -4
  92. package/dist/components/forms/RichTextEditor/RichTextEditor.js +3 -3
  93. package/dist/components/forms/RichTextEditor/RichTextEditor.theme.js +5 -5
  94. package/dist/components/forms/Select/Select.js +2 -2
  95. package/dist/components/forms/Select/Select.theme.js +3 -3
  96. package/dist/components/forms/TimePicker/TimePickerContent.js +3 -0
  97. package/dist/components/forms/TimePicker/TimePickerTrigger.js +1 -1
  98. package/dist/components/forms/forms.theme.js +21 -16
  99. package/dist/components/forms/index.js +3 -0
  100. package/dist/components/layout/AppShell/AppShell.d.ts +10 -0
  101. package/dist/components/layout/AppShell/AppShell.js +84 -0
  102. package/dist/components/layout/AppShell/AppShell.theme.d.ts +3 -0
  103. package/dist/components/layout/AppShell/AppShell.theme.js +42 -0
  104. package/dist/components/layout/AppShell/AppShell.types.d.ts +127 -0
  105. package/dist/components/layout/AppShell/AppShellContext.d.ts +14 -0
  106. package/dist/components/layout/AppShell/AppShellContext.js +19 -0
  107. package/dist/components/layout/AppShell/AppShellFooter.d.ts +3 -0
  108. package/dist/components/layout/AppShell/AppShellFooter.js +12 -0
  109. package/dist/components/layout/AppShell/AppShellHeader.d.ts +3 -0
  110. package/dist/components/layout/AppShell/AppShellHeader.js +12 -0
  111. package/dist/components/layout/AppShell/AppShellMain.d.ts +3 -0
  112. package/dist/components/layout/AppShell/AppShellMain.js +19 -0
  113. package/dist/components/layout/AppShell/AppShellRightPanel.d.ts +3 -0
  114. package/dist/components/layout/AppShell/AppShellRightPanel.js +16 -0
  115. package/dist/components/layout/AppShell/AppShellSidebar.d.ts +3 -0
  116. package/dist/components/layout/AppShell/AppShellSidebar.js +16 -0
  117. package/dist/components/layout/AppShell/AppShellTopbar.d.ts +3 -0
  118. package/dist/components/layout/AppShell/AppShellTopbar.js +12 -0
  119. package/dist/components/layout/AppShell/index.d.ts +3 -0
  120. package/dist/components/layout/FormLayout/FormLayout.js +3 -0
  121. package/dist/components/layout/index.d.ts +1 -0
  122. package/dist/components/layout/index.js +2 -0
  123. package/dist/hooks/useSelectPortal.d.ts +10 -2
  124. package/dist/hooks/useSelectPortal.js +45 -24
  125. package/dist/index.js +6 -0
  126. package/dist/registry.json +10021 -0
  127. package/dist/styles.css +1 -1
  128. package/dist/utils/dateUtils.js +11 -1
  129. package/package.json +4 -2
  130. package/src/global.scss +29 -8
  131. 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
- // Container wrapper
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-background-secondary)] hover:bg-[var(--color-background-secondary)] hover:border-[var(--color-primary)] transition-colors duration-400 cursor-pointer",
10
- // Dropzone when dragging over
11
- dropzoneActiveStyle: "border-[var(--color-primary)] bg-[var(--color-primary-50)] scale-[1.01]",
12
- // Dropzone when disabled
13
- dropzoneDisabledStyle: "cursor-not-allowed opacity-60 hover:bg-[var(--color-background-secondary)] hover:border-[var(--color-border)]",
14
- // Content wrapper inside dropzone
15
- contentStyle: "flex flex-col items-center text-center pointer-events-none",
16
- // Upload icon
17
- iconStyle: "size-8 text-[var(--color-text-placeholder)]",
18
- // Main text
19
- textStyle: "text-sm font-medium text-[var(--color-text-primary)]",
20
- // Helper text
21
- helperTextStyle: "text-sm text-[var(--color-text-muted)] mt-1",
22
- // Description below dropzone
23
- descriptionStyle: "text-xs text-[var(--color-text-muted)] mt-1",
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
- * Callback when files are selected/dropped
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?: string;
87
+ buttonText?: React.ReactNode;
67
88
  /**
68
89
  * Custom dropzone helper text
69
90
  * @default "or drag and drop"
70
91
  */
71
- helperText?: string;
92
+ helperText?: React.ReactNode;
72
93
  /**
73
94
  * Helper text displayed below the upload area
74
95
  */
75
- description?: string;
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-white transition-all " +
13
- "dark:shadow-[inset_0_0_0_1px_var(--color-neutral-700)] dark:bg-[var(--color-neutral-900)]",
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-[inset_0_0_0_2px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10 " +
16
- "dark:shadow-[inset_0_0_0_2px_var(--color-primary-500)] dark:ring-[var(--color-primary-500)]/20",
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-[inset_0_0_0_2px_var(--color-danger)] ring-4 ring-[var(--color-danger)]/10 " +
19
- "dark:shadow-[inset_0_0_0_2px_var(--color-danger-500)] dark:ring-[var(--color-danger-500)]/20",
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
- "dark:bg-[var(--color-danger-950)]/30 dark:shadow-[inset_0_0_0_1px_var(--color-danger-500)]",
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
- if (!isControlled) {
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
- }, [isControlled, onChange, triggerChars, notifyMentionsChange]);
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, _b, _c, _d, _e;
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
- e.preventDefault();
412
- (_b = prevSibling.parentNode) === null || _b === void 0 ? void 0 : _b.removeChild(prevSibling);
413
- updateFromDOM();
414
- return;
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
- e.preventDefault();
426
- (_c = prevChild.parentNode) === null || _c === void 0 ? void 0 : _c.removeChild(prevChild);
427
- updateFromDOM();
428
- return;
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
- e.preventDefault();
449
- (_d = nextSibling.parentNode) === null || _d === void 0 ? void 0 : _d.removeChild(nextSibling);
450
- updateFromDOM();
451
- return;
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
- e.preventDefault();
462
- (_e = nextChild.parentNode) === null || _e === void 0 ? void 0 : _e.removeChild(nextChild);
463
- updateFromDOM();
464
- return;
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
- updateFromDOM,
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
- // Find which DOM mention this is (nth occurrence)
517
- const allMentions = editableRef.current.querySelectorAll(`[${MENTION_ATTR}]`);
518
- const domIdx = Array.from(allMentions).indexOf(mentionEl);
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 || (i === 0 ? "" : "") }, `text-${i}`));
629
+ return (jsx("span", { "data-segment": "text", children: seg.text }, `text-${i}`));
556
630
  }
557
- return (jsx(Tag, { size: mentionTagSize, removable: false, avatar: seg.data.avatar
558
- ? { src: seg.data.avatar, name: seg.data.label }
559
- : undefined, contentEditable: false, className: cn(theme.mentionTagStyle, i === selectedMentionIndex &&
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
- theme.mentionTagStyle,
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 (jsxs("div", { role: "option", "aria-selected": isHighlighted, "data-suggestion-index": index, className: cn(theme.suggestionStyle, isHighlighted && theme.suggestionHighlightedStyle), onMouseEnter: () => setHighlightedIndex(index), onMouseDown: (e) => {
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: [jsx(Avatar, { src: suggestion.avatar, name: suggestion.label, size: "xs", className: "ring-0 border-0" }), jsxs("div", { className: "flex flex-col min-w-0", children: [jsx("span", { className: theme.suggestionLabelStyle, children: suggestion.label }), suggestion.description && (jsx("span", { className: theme.suggestionDescriptionStyle, children: suggestion.description }))] })] }, suggestion.id));
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, isFocused && theme.inputGroupFocusStyle, isInvalid && "border-[var(--color-danger)] ring-1 ring-[var(--color-danger)]"), children: jsx("div", { 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 &&
605
- "before:content-[attr(data-placeholder)] before:text-[var(--color-text-placeholder)] before:pointer-events-none before:absolute dark:before:text-[var(--color-neutral-500)]", isDisabled && "cursor-not-allowed opacity-60", className), style: {
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
- }, ...props, children: renderedSegments }) }), helperText && (jsx("div", { id: helperId, className: cn("text-sm mt-1", isInvalid
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: "shadow-[inset_0_0_0_2px_var(--color-primary-600)] " +
14
- "ring-4 ring-[var(--color-primary)]/10 " +
15
- "dark:shadow-[inset_0_0_0_2px_var(--color-primary-500)] dark:ring-[var(--color-primary-500)]/20",
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
- dropdownStyle: formsBaseTheme.dropdownStyles.container + " py-1",
28
- // Individual suggestion item
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 };