@intlayer/design-system 9.3.1 → 9.3.3

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 (129) hide show
  1. package/dist/esm/api/hooks/auth.mjs +2 -0
  2. package/dist/esm/api/hooks/auth.mjs.map +1 -1
  3. package/dist/esm/api/index.mjs +2 -2
  4. package/dist/esm/components/Browser/Browser.mjs +1 -1
  5. package/dist/esm/components/Browser/Browser.mjs.map +1 -1
  6. package/dist/esm/components/ContentEditor/ContentEditorTextArea.mjs +1 -1
  7. package/dist/esm/components/CopyButton/index.mjs +2 -1
  8. package/dist/esm/components/CopyButton/index.mjs.map +1 -1
  9. package/dist/esm/components/DictionaryFieldEditor/ContentEditor.mjs +4 -5
  10. package/dist/esm/components/DictionaryFieldEditor/ContentEditor.mjs.map +1 -1
  11. package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs +6 -10
  12. package/dist/esm/components/DictionaryFieldEditor/ContentEditorView/TextEditor.mjs.map +1 -1
  13. package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/DictionaryCreationForm.mjs +1 -1
  14. package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/useDictionaryFormSchema.mjs +1 -1
  15. package/dist/esm/components/DictionaryFieldEditor/DictionaryCreationForm/useDictionaryFormSchema.mjs.map +1 -1
  16. package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.mjs +6 -6
  17. package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.mjs.map +1 -1
  18. package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/useDictionaryDetailsSchema.mjs +1 -1
  19. package/dist/esm/components/DictionaryFieldEditor/DictionaryDetails/useDictionaryDetailsSchema.mjs.map +1 -1
  20. package/dist/esm/components/DictionaryFieldEditor/DictionaryFieldEditor.mjs +3 -7
  21. package/dist/esm/components/DictionaryFieldEditor/DictionaryFieldEditor.mjs.map +1 -1
  22. package/dist/esm/components/DictionaryFieldEditor/NavigationView/NavigationViewNode.mjs +1 -1
  23. package/dist/esm/components/DictionaryFieldEditor/SaveForm/SaveForm.mjs +2 -2
  24. package/dist/esm/components/DictionaryFieldEditor/StructureView/StructureView.mjs +1 -1
  25. package/dist/esm/components/ExpandCollapse/ExpandCollapse.mjs +19 -12
  26. package/dist/esm/components/ExpandCollapse/ExpandCollapse.mjs.map +1 -1
  27. package/dist/esm/components/Form/FormBase.mjs.map +1 -1
  28. package/dist/esm/components/Form/elements/FormElementWrapper.mjs +1 -1
  29. package/dist/esm/components/Form/elements/FormElementWrapper.mjs.map +1 -1
  30. package/dist/esm/components/HTMLRender/HTMLRender.mjs +2 -2
  31. package/dist/esm/components/HTMLRender/HTMLRender.mjs.map +1 -1
  32. package/dist/esm/components/IDE/Code.mjs +3 -3
  33. package/dist/esm/components/IDE/Code.mjs.map +1 -1
  34. package/dist/esm/components/IDE/CodeBlockClient.mjs +16 -4
  35. package/dist/esm/components/IDE/CodeBlockClient.mjs.map +1 -1
  36. package/dist/esm/components/IDE/CodeBlockHighlight.mjs +14 -5
  37. package/dist/esm/components/IDE/CodeBlockHighlight.mjs.map +1 -1
  38. package/dist/esm/components/IDE/CodeBlockServer.mjs +5 -3
  39. package/dist/esm/components/IDE/CodeBlockServer.mjs.map +1 -1
  40. package/dist/esm/components/IDE/CodeBlockShiki.mjs +18 -18
  41. package/dist/esm/components/IDE/CodeBlockShiki.mjs.map +1 -1
  42. package/dist/esm/components/IDE/IDE.mjs +2 -5
  43. package/dist/esm/components/IDE/IDE.mjs.map +1 -1
  44. package/dist/esm/components/IDE/MarkDownRender.mjs +1 -2
  45. package/dist/esm/components/IDE/MarkDownRender.mjs.map +1 -1
  46. package/dist/esm/components/IDE/index.mjs +2 -2
  47. package/dist/esm/components/IDE/shikiThemes.mjs +18 -0
  48. package/dist/esm/components/IDE/shikiThemes.mjs.map +1 -0
  49. package/dist/esm/components/MarkDownRender/MarkDownIframe.mjs +105 -4
  50. package/dist/esm/components/MarkDownRender/MarkDownIframe.mjs.map +1 -1
  51. package/dist/esm/components/MarkDownRender/MarkDownRender.mjs +16 -16
  52. package/dist/esm/components/MarkDownRender/MarkDownRender.mjs.map +1 -1
  53. package/dist/esm/components/MaxHeightSmoother/index.mjs +1 -1
  54. package/dist/esm/components/MaxHeightSmoother/index.mjs.map +1 -1
  55. package/dist/esm/components/MaxWidthSmoother/index.mjs +1 -1
  56. package/dist/esm/components/MaxWidthSmoother/index.mjs.map +1 -1
  57. package/dist/esm/components/Pattern/GridDistortionPattern.mjs +100 -15
  58. package/dist/esm/components/Pattern/GridDistortionPattern.mjs.map +1 -1
  59. package/dist/esm/components/Popover/dynamic.mjs +5 -4
  60. package/dist/esm/components/Popover/dynamic.mjs.map +1 -1
  61. package/dist/esm/components/Popover/index.mjs +2 -2
  62. package/dist/esm/components/Popover/static.mjs +47 -18
  63. package/dist/esm/components/Popover/static.mjs.map +1 -1
  64. package/dist/esm/components/Tab/Tab.mjs +37 -16
  65. package/dist/esm/components/Tab/Tab.mjs.map +1 -1
  66. package/dist/esm/components/TextArea/AutoSizeTextArea.mjs +3 -2
  67. package/dist/esm/components/TextArea/AutoSizeTextArea.mjs.map +1 -1
  68. package/dist/esm/components/index.mjs +3 -3
  69. package/dist/esm/hooks/useDevice.mjs +16 -6
  70. package/dist/esm/hooks/useDevice.mjs.map +1 -1
  71. package/dist/esm/hooks/useItemSelector.mjs +107 -112
  72. package/dist/esm/hooks/useItemSelector.mjs.map +1 -1
  73. package/dist/esm/libs/auth.mjs +16 -3
  74. package/dist/esm/libs/auth.mjs.map +1 -1
  75. package/dist/esm/structured-data/buildCreativeWorkJsonLd.mjs +1 -1
  76. package/dist/esm/structured-data/buildCreativeWorkJsonLd.mjs.map +1 -1
  77. package/dist/types/api/hooks/auth.d.ts.map +1 -1
  78. package/dist/types/components/Browser/Browser.d.ts +7 -1
  79. package/dist/types/components/Browser/Browser.d.ts.map +1 -1
  80. package/dist/types/components/CopyButton/index.d.ts +9 -3
  81. package/dist/types/components/CopyButton/index.d.ts.map +1 -1
  82. package/dist/types/components/DictionaryFieldEditor/ContentEditor.d.ts +0 -1
  83. package/dist/types/components/DictionaryFieldEditor/ContentEditor.d.ts.map +1 -1
  84. package/dist/types/components/DictionaryFieldEditor/ContentEditorView/TextEditor.d.ts +0 -1
  85. package/dist/types/components/DictionaryFieldEditor/ContentEditorView/TextEditor.d.ts.map +1 -1
  86. package/dist/types/components/DictionaryFieldEditor/DictionaryCreationForm/useDictionaryFormSchema.d.ts +1 -1
  87. package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.d.ts +0 -1
  88. package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/DictionaryDetailsForm.d.ts.map +1 -1
  89. package/dist/types/components/DictionaryFieldEditor/DictionaryDetails/useDictionaryDetailsSchema.d.ts +1 -1
  90. package/dist/types/components/ExpandCollapse/ExpandCollapse.d.ts.map +1 -1
  91. package/dist/types/components/Form/FormBase.d.ts +1 -1
  92. package/dist/types/components/Form/elements/FormElementWrapper.d.ts +1 -1
  93. package/dist/types/components/HTMLRender/HTMLRender.d.ts +1 -2
  94. package/dist/types/components/HTMLRender/HTMLRender.d.ts.map +1 -1
  95. package/dist/types/components/IDE/Code.d.ts +6 -1
  96. package/dist/types/components/IDE/Code.d.ts.map +1 -1
  97. package/dist/types/components/IDE/CodeBlockClient.d.ts +18 -2
  98. package/dist/types/components/IDE/CodeBlockClient.d.ts.map +1 -1
  99. package/dist/types/components/IDE/CodeBlockHighlight.d.ts +3 -2
  100. package/dist/types/components/IDE/CodeBlockHighlight.d.ts.map +1 -1
  101. package/dist/types/components/IDE/CodeBlockServer.d.ts +0 -1
  102. package/dist/types/components/IDE/CodeBlockServer.d.ts.map +1 -1
  103. package/dist/types/components/IDE/CodeBlockShiki.d.ts +0 -1
  104. package/dist/types/components/IDE/CodeBlockShiki.d.ts.map +1 -1
  105. package/dist/types/components/IDE/IDE.d.ts +0 -1
  106. package/dist/types/components/IDE/IDE.d.ts.map +1 -1
  107. package/dist/types/components/IDE/MarkDownRender.d.ts +0 -1
  108. package/dist/types/components/IDE/MarkDownRender.d.ts.map +1 -1
  109. package/dist/types/components/IDE/index.d.ts +2 -2
  110. package/dist/types/components/IDE/shikiThemes.d.ts +18 -0
  111. package/dist/types/components/IDE/shikiThemes.d.ts.map +1 -0
  112. package/dist/types/components/MarkDownRender/MarkDownIframe.d.ts.map +1 -1
  113. package/dist/types/components/MarkDownRender/MarkDownRender.d.ts +1 -2
  114. package/dist/types/components/MarkDownRender/MarkDownRender.d.ts.map +1 -1
  115. package/dist/types/components/Pattern/GridDistortionPattern.d.ts.map +1 -1
  116. package/dist/types/components/Popover/dynamic.d.ts.map +1 -1
  117. package/dist/types/components/Popover/index.d.ts +2 -2
  118. package/dist/types/components/Popover/static.d.ts +6 -1
  119. package/dist/types/components/Popover/static.d.ts.map +1 -1
  120. package/dist/types/components/Tab/Tab.d.ts +2 -2
  121. package/dist/types/components/Tab/Tab.d.ts.map +1 -1
  122. package/dist/types/components/index.d.ts +3 -3
  123. package/dist/types/hooks/useItemSelector.d.ts.map +1 -1
  124. package/dist/types/libs/auth.d.ts +3 -0
  125. package/dist/types/libs/auth.d.ts.map +1 -1
  126. package/dist/types/structured-data/buildCreativeWorkJsonLd.d.ts +9 -3
  127. package/dist/types/structured-data/buildCreativeWorkJsonLd.d.ts.map +1 -1
  128. package/package.json +43 -38
  129. package/tailwind.css +27 -1
@@ -9,6 +9,14 @@ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import { cva } from "class-variance-authority";
10
10
 
11
11
  //#region src/components/Tab/Tab.tsx
12
+ /**
13
+ * Builds an id fragment usable in `id` / `aria-controls`.
14
+ *
15
+ * Tab values come from markdown (`<Tab value="Extract command">`), so they can
16
+ * hold spaces — which makes `aria-controls` reference a list of ids rather than
17
+ * the one panel, and silently breaks the association for screen readers.
18
+ */
19
+ const toIdFragment = (value) => value.trim().replace(/\s+/g, "-").replace(/[^\w-]/g, "");
12
20
  const TabContext = createContext(void 0);
13
21
  const tabContainerVariant = cva("relative w-full rounded-lg", {
14
22
  variants: {
@@ -25,6 +33,18 @@ const tabContainerVariant = cva("relative w-full rounded-lg", {
25
33
  defaultVariants: { variant: "default" }
26
34
  });
27
35
  /**
36
+ * Resolves the identifier of a tab.
37
+ *
38
+ * Both `label` and `value` are optional for markdown-authored tabs
39
+ * (`<Tab label="next-intl">`), so the identifier falls back to the label, then
40
+ * to the position, keeping every tab addressable instead of rendering ids from
41
+ * `undefined`.
42
+ */
43
+ const resolveTabValue = ({ value, label }, index) => {
44
+ const identifier = value ?? label;
45
+ return typeof identifier === "string" ? identifier : String(index);
46
+ };
47
+ /**
28
48
  * TabItem component that represents a single tab
29
49
  * Must be used as a child of the Tab component
30
50
  */
@@ -51,24 +71,27 @@ TabItem.displayName = "TabItem";
51
71
  const TabComponent = ({ defaultTab, group, variant, children, className, headerClassName, fullHeight, ...props }) => {
52
72
  const tabItems = Children.toArray(children).filter((child) => {
53
73
  return isValidElement(child) && child.type === TabItem;
54
- });
55
- const firstTabValue = tabItems[0]?.props?.value;
74
+ }).map((child, index) => ({
75
+ ...child.props,
76
+ value: resolveTabValue(child.props, index)
77
+ }));
78
+ const firstTabValue = tabItems[0]?.value;
56
79
  const { tabsValues, setTabsValues } = useTabContext();
57
80
  const [activeTab, setActiveTab] = useState(defaultTab ?? firstTabValue ?? "");
58
81
  const currentTabValue = (group && typeof tabsValues === "object" ? tabsValues?.[group] : activeTab) ?? defaultTab ?? firstTabValue;
59
- const activeTabIndex = tabItems.findIndex((tab) => tab.props.value === currentTabValue);
82
+ const activeTabIndex = tabItems.findIndex((tab) => tab.value === currentTabValue);
60
83
  const tabsCount = tabItems.length;
61
84
  const { containerProps, dragDeltaPct, isDragging } = useHorizontalSwipe({
62
85
  itemIndex: activeTabIndex,
63
86
  itemCount: tabsCount,
64
87
  onSwipeLeft: () => {
65
88
  const targetIndex = Math.min(tabsCount - 1, activeTabIndex + 1);
66
- const nextValue = tabItems[targetIndex]?.props?.value;
89
+ const nextValue = tabItems[targetIndex]?.value;
67
90
  if (nextValue) handleSetActiveTab(nextValue);
68
91
  },
69
92
  onSwipeRight: () => {
70
93
  const targetIndex = Math.max(0, activeTabIndex - 1);
71
- const nextValue = tabItems[targetIndex]?.props?.value;
94
+ const nextValue = tabItems[targetIndex]?.value;
72
95
  if (nextValue) handleSetActiveTab(nextValue);
73
96
  }
74
97
  });
@@ -92,9 +115,9 @@ const TabComponent = ({ defaultTab, group, variant, children, className, headerC
92
115
  className: cn("flex shrink-0 gap-3 p-3", headerClassName),
93
116
  children: /* @__PURE__ */ jsx(TabSelector, {
94
117
  selectedChoice: currentTabValue,
95
- tabs: tabItems.map((child) => {
96
- const { label, value, disabled } = child.props;
118
+ tabs: tabItems.map(({ label, value, disabled }) => {
97
119
  const isActive = currentTabValue === value;
120
+ const idFragment = toIdFragment(value);
98
121
  return /* @__PURE__ */ jsx("button", {
99
122
  className: cn("cursor-pointer whitespace-nowrap rounded-md px-4 py-1 font-medium text-sm transition-colors focus:outline-none", !isActive && "text-neutral/70"),
100
123
  "data-active": isActive,
@@ -102,8 +125,8 @@ const TabComponent = ({ defaultTab, group, variant, children, className, headerC
102
125
  onClick: () => !disabled && handleSetActiveTab(value),
103
126
  role: "tab",
104
127
  "aria-selected": isActive,
105
- "aria-controls": `tabpanel-${value}`,
106
- id: `tab-${value}`,
128
+ "aria-controls": `tabpanel-${idFragment}`,
129
+ id: `tab-${idFragment}`,
107
130
  type: "button",
108
131
  children: label ?? value
109
132
  }, value);
@@ -115,21 +138,19 @@ const TabComponent = ({ defaultTab, group, variant, children, className, headerC
115
138
  className: cn("relative w-full min-w-0 overflow-x-clip [-webkit-clip-path:inset(0)] [clip-path:inset(0)]", fullHeight && "min-h-0 flex-1"),
116
139
  ...containerProps,
117
140
  children: /* @__PURE__ */ jsx("div", {
118
- role: "tablist",
119
- "aria-orientation": "horizontal",
120
141
  className: cn("grid w-full min-w-0", fullHeight && "h-full", isDragging ? "transition-none" : "transition-transform duration-300 ease-in-out"),
121
142
  style: {
122
143
  gridTemplateColumns: `repeat(${tabItems.length}, 100%)`,
123
144
  transform: `translateX(-${activeTabIndex * 100 - (isDragging ? dragDeltaPct : 0)}%)`
124
145
  },
125
- children: tabItems.map(({ props }, index) => {
126
- const { value, children, className: itemClassName } = props;
146
+ children: tabItems.map(({ value, children, className: itemClassName }, index) => {
127
147
  const isActive = index === activeTabIndex;
148
+ const idFragment = toIdFragment(value);
128
149
  return /* @__PURE__ */ jsx("div", {
129
150
  role: "tabpanel",
130
- "aria-labelledby": `tab-${value}`,
131
- id: `tabpanel-${value}`,
132
- "aria-hidden": !isActive,
151
+ "aria-labelledby": `tab-${idFragment}`,
152
+ id: `tabpanel-${idFragment}`,
153
+ inert: !isActive,
133
154
  tabIndex: isActive ? 0 : -1,
134
155
  "data-active": isActive,
135
156
  className: cn("w-full min-w-0 p-3 opacity-100 transition-opacity duration-300 ease-in-out", fullHeight && "h-full overflow-y-auto", !isActive && "pointer-events-none opacity-0", itemClassName),
@@ -1 +1 @@
1
- {"version":3,"file":"Tab.mjs","names":[],"sources":["../../../../src/components/Tab/Tab.tsx"],"sourcesContent":["'use client';\n\nimport { useHorizontalSwipe } from '@hooks/useHorizontalSwipe';\nimport { cn } from '@utils/cn';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n Children,\n createContext,\n type HTMLAttributes,\n isValidElement,\n type ReactElement,\n type ReactNode,\n useState,\n} from 'react';\nimport { TabSelector } from '../TabSelector';\nimport { useTabContext } from './TabContext';\n\n// Context for managing tab state\ntype TabContextType = {\n activeTab: string;\n setActiveTab: (tab: string) => void;\n};\n\nconst TabContext = createContext<TabContextType | undefined>(undefined);\n\n// Tab container variants\nconst tabContainerVariant = cva('relative w-full rounded-lg', {\n variants: {\n background: {\n with: 'border border-neutral/20 bg-background/2 shadow-[0_0_10px_-15px_rgba(0,0,0,0.3)] backdrop-blur',\n without: '',\n },\n variant: {\n default: '',\n bordered: 'border-2',\n ghost: 'border-0 bg-transparent shadow-none',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n});\n\nexport type TabProps = HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof tabContainerVariant> & {\n defaultTab?: string;\n group?: string;\n children: ReactNode;\n headerClassName?: string;\n fullHeight?: boolean;\n };\n\nexport type TabItemProps = HTMLAttributes<HTMLDivElement> & {\n label: string;\n value: string;\n disabled?: boolean;\n children: ReactNode;\n};\n\n/**\n * TabItem component that represents a single tab\n * Must be used as a child of the Tab component\n */\nconst TabItem = ({ children, ...props }: TabItemProps) => (\n // This component is primarily used for its props by the parent Tab component\n // The actual rendering is handled by the Tab component\n <div {...props}>{children}</div>\n);\n\n// Add display name for better debugging\nTabItem.displayName = 'TabItem';\n\n/**\n * Tab container component that manages tab state and renders tab headers and content\n *\n * Example:\n * ```jsx\n * <Tab defaultTab=\"tab1\">\n * <Tab.Item label=\"First Tab\" value=\"tab1\">\n * Content for first tab\n * </Tab.Item>\n * <Tab.Item label=\"Second Tab\" value=\"tab2\">\n * Content for second tab\n * </Tab.Item>\n * </Tab>\n * ```\n */\nconst TabComponent = ({\n defaultTab,\n group,\n variant,\n children,\n className,\n headerClassName,\n fullHeight,\n ...props\n}: TabProps) => {\n // Extract TabItem children to get their props\n const tabItems = Children.toArray(children).filter((child) => {\n return isValidElement(child) && child.type === TabItem;\n }) as ReactElement<TabItemProps>[];\n\n const firstTabValue = tabItems[0]?.props?.value;\n const { tabsValues, setTabsValues } = useTabContext();\n const [activeTab, setActiveTab] = useState(defaultTab ?? firstTabValue ?? '');\n const hasGroup = group && typeof tabsValues === 'object';\n const currentTabValue =\n (hasGroup ? tabsValues?.[group] : activeTab) ?? defaultTab ?? firstTabValue;\n const activeTabIndex = tabItems.findIndex(\n (tab) => tab.props.value === currentTabValue\n );\n\n const tabsCount = tabItems.length;\n\n const { containerProps, dragDeltaPct, isDragging } = useHorizontalSwipe({\n itemIndex: activeTabIndex,\n itemCount: tabsCount,\n onSwipeLeft: () => {\n const targetIndex = Math.min(tabsCount - 1, activeTabIndex + 1);\n const nextValue = tabItems[targetIndex]?.props?.value;\n if (nextValue) handleSetActiveTab(nextValue);\n },\n onSwipeRight: () => {\n const targetIndex = Math.max(0, activeTabIndex - 1);\n const nextValue = tabItems[targetIndex]?.props?.value;\n if (nextValue) handleSetActiveTab(nextValue);\n },\n });\n\n const handleSetActiveTab = (tab: string) => {\n setActiveTab(tab);\n\n if (typeof setTabsValues === 'function') {\n setTabsValues((prev) => ({ ...prev, [group!]: tab }));\n }\n };\n\n const contextValue: TabContextType = {\n activeTab: activeTab ?? firstTabValue ?? '',\n setActiveTab: handleSetActiveTab,\n };\n\n return (\n <TabContext.Provider value={contextValue}>\n <div\n className={cn(\n tabContainerVariant({ variant }),\n fullHeight && 'flex h-full flex-col overflow-hidden',\n className\n )}\n {...props}\n >\n {/* Tab Headers */}\n <div className={cn('flex shrink-0 gap-3 p-3', headerClassName)}>\n <TabSelector\n selectedChoice={currentTabValue}\n tabs={tabItems.map((child) => {\n const { label, value, disabled } = child.props;\n const isActive = currentTabValue === value;\n\n return (\n <button\n key={value}\n className={cn(\n 'cursor-pointer whitespace-nowrap rounded-md px-4 py-1 font-medium text-sm transition-colors focus:outline-none',\n !isActive && 'text-neutral/70'\n )}\n data-active={isActive}\n disabled={disabled}\n onClick={() => !disabled && handleSetActiveTab(value)}\n role=\"tab\"\n aria-selected={isActive}\n aria-controls={`tabpanel-${value}`}\n id={`tab-${value}`}\n type=\"button\"\n >\n {label ?? value}\n </button>\n );\n })}\n hoverable\n color=\"text\"\n />\n </div>\n {/* Tab Content */}\n {/* Clipper: no overflow; uses clip-path */}\n <div\n className={cn(\n 'relative w-full min-w-0 overflow-x-clip [-webkit-clip-path:inset(0)] [clip-path:inset(0)]',\n fullHeight && 'min-h-0 flex-1'\n )}\n {...containerProps}\n >\n {/* Track */}\n <div\n role=\"tablist\"\n aria-orientation=\"horizontal\"\n className={cn(\n 'grid w-full min-w-0',\n fullHeight && 'h-full',\n isDragging\n ? 'transition-none'\n : 'transition-transform duration-300 ease-in-out'\n )}\n style={{\n gridTemplateColumns: `repeat(${tabItems.length}, 100%)`,\n transform: `translateX(-${activeTabIndex * 100 - (isDragging ? dragDeltaPct : 0)}%)`,\n }}\n >\n {tabItems.map(({ props }, index) => {\n const { value, children, className: itemClassName } = props;\n const isActive = index === activeTabIndex;\n\n return (\n <div\n key={value}\n role=\"tabpanel\"\n aria-labelledby={`tab-${value}`}\n id={`tabpanel-${value}`}\n aria-hidden={!isActive}\n tabIndex={isActive ? 0 : -1}\n data-active={isActive}\n className={cn(\n 'w-full min-w-0 p-3 opacity-100 transition-opacity duration-300 ease-in-out',\n fullHeight && 'h-full overflow-y-auto',\n !isActive && 'pointer-events-none opacity-0', // prevent offscreen interaction\n itemClassName\n )}\n >\n <div\n className={cn(\n 'flex w-full min-w-0 flex-col items-stretch gap-6',\n fullHeight && 'min-h-full'\n )}\n >\n {children}\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n </TabContext.Provider>\n );\n};\n\n// Create the compound component\nexport const Tab = Object.assign(TabComponent, {\n Item: TabItem,\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"Tab.mjs","names":[],"sources":["../../../../src/components/Tab/Tab.tsx"],"sourcesContent":["'use client';\n\nimport { useHorizontalSwipe } from '@hooks/useHorizontalSwipe';\nimport { cn } from '@utils/cn';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n Children,\n createContext,\n type HTMLAttributes,\n isValidElement,\n type ReactElement,\n type ReactNode,\n useState,\n} from 'react';\nimport { TabSelector } from '../TabSelector';\nimport { useTabContext } from './TabContext';\n\n/**\n * Builds an id fragment usable in `id` / `aria-controls`.\n *\n * Tab values come from markdown (`<Tab value=\"Extract command\">`), so they can\n * hold spaces — which makes `aria-controls` reference a list of ids rather than\n * the one panel, and silently breaks the association for screen readers.\n */\nconst toIdFragment = (value: string) =>\n value\n .trim()\n .replace(/\\s+/g, '-')\n .replace(/[^\\w-]/g, '');\n\n// Context for managing tab state\ntype TabContextType = {\n activeTab: string;\n setActiveTab: (tab: string) => void;\n};\n\nconst TabContext = createContext<TabContextType | undefined>(undefined);\n\n// Tab container variants\nconst tabContainerVariant = cva('relative w-full rounded-lg', {\n variants: {\n background: {\n with: 'border border-neutral/20 bg-background/2 shadow-[0_0_10px_-15px_rgba(0,0,0,0.3)] backdrop-blur',\n without: '',\n },\n variant: {\n default: '',\n bordered: 'border-2',\n ghost: 'border-0 bg-transparent shadow-none',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n});\n\nexport type TabProps = HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof tabContainerVariant> & {\n defaultTab?: string;\n group?: string;\n children: ReactNode;\n headerClassName?: string;\n fullHeight?: boolean;\n };\n\nexport type TabItemProps = HTMLAttributes<HTMLDivElement> & {\n label?: string;\n value?: string;\n disabled?: boolean;\n children: ReactNode;\n};\n\n/** A tab item whose identifier has been resolved to a usable string. */\ntype ResolvedTabItem = TabItemProps & { value: string };\n\n/**\n * Resolves the identifier of a tab.\n *\n * Both `label` and `value` are optional for markdown-authored tabs\n * (`<Tab label=\"next-intl\">`), so the identifier falls back to the label, then\n * to the position, keeping every tab addressable instead of rendering ids from\n * `undefined`.\n */\nconst resolveTabValue = (\n { value, label }: TabItemProps,\n index: number\n): string => {\n const identifier = value ?? label;\n\n return typeof identifier === 'string' ? identifier : String(index);\n};\n\n/**\n * TabItem component that represents a single tab\n * Must be used as a child of the Tab component\n */\nconst TabItem = ({ children, ...props }: TabItemProps) => (\n // This component is primarily used for its props by the parent Tab component\n // The actual rendering is handled by the Tab component\n <div {...props}>{children}</div>\n);\n\n// Add display name for better debugging\nTabItem.displayName = 'TabItem';\n\n/**\n * Tab container component that manages tab state and renders tab headers and content\n *\n * Example:\n * ```jsx\n * <Tab defaultTab=\"tab1\">\n * <Tab.Item label=\"First Tab\" value=\"tab1\">\n * Content for first tab\n * </Tab.Item>\n * <Tab.Item label=\"Second Tab\" value=\"tab2\">\n * Content for second tab\n * </Tab.Item>\n * </Tab>\n * ```\n */\nconst TabComponent = ({\n defaultTab,\n group,\n variant,\n children,\n className,\n headerClassName,\n fullHeight,\n ...props\n}: TabProps) => {\n // Extract TabItem children to get their props\n const tabItems: ResolvedTabItem[] = (\n Children.toArray(children).filter((child) => {\n return isValidElement(child) && child.type === TabItem;\n }) as ReactElement<TabItemProps>[]\n ).map((child, index) => ({\n ...child.props,\n value: resolveTabValue(child.props, index),\n }));\n\n const firstTabValue = tabItems[0]?.value;\n const { tabsValues, setTabsValues } = useTabContext();\n const [activeTab, setActiveTab] = useState(defaultTab ?? firstTabValue ?? '');\n const hasGroup = group && typeof tabsValues === 'object';\n const currentTabValue =\n (hasGroup ? tabsValues?.[group] : activeTab) ?? defaultTab ?? firstTabValue;\n const activeTabIndex = tabItems.findIndex(\n (tab) => tab.value === currentTabValue\n );\n\n const tabsCount = tabItems.length;\n\n const { containerProps, dragDeltaPct, isDragging } = useHorizontalSwipe({\n itemIndex: activeTabIndex,\n itemCount: tabsCount,\n onSwipeLeft: () => {\n const targetIndex = Math.min(tabsCount - 1, activeTabIndex + 1);\n const nextValue = tabItems[targetIndex]?.value;\n if (nextValue) handleSetActiveTab(nextValue);\n },\n onSwipeRight: () => {\n const targetIndex = Math.max(0, activeTabIndex - 1);\n const nextValue = tabItems[targetIndex]?.value;\n if (nextValue) handleSetActiveTab(nextValue);\n },\n });\n\n const handleSetActiveTab = (tab: string) => {\n setActiveTab(tab);\n\n if (typeof setTabsValues === 'function') {\n setTabsValues((prev) => ({ ...prev, [group!]: tab }));\n }\n };\n\n const contextValue: TabContextType = {\n activeTab: activeTab ?? firstTabValue ?? '',\n setActiveTab: handleSetActiveTab,\n };\n\n return (\n <TabContext.Provider value={contextValue}>\n <div\n className={cn(\n tabContainerVariant({ variant }),\n fullHeight && 'flex h-full flex-col overflow-hidden',\n className\n )}\n {...props}\n >\n {/* Tab Headers — `TabSelector` is the tablist; wrapping it in another\n one would nest two, leaving the outer without any `tab` child. */}\n <div className={cn('flex shrink-0 gap-3 p-3', headerClassName)}>\n <TabSelector\n selectedChoice={currentTabValue}\n tabs={tabItems.map(({ label, value, disabled }) => {\n const isActive = currentTabValue === value;\n const idFragment = toIdFragment(value);\n\n return (\n <button\n key={value}\n className={cn(\n 'cursor-pointer whitespace-nowrap rounded-md px-4 py-1 font-medium text-sm transition-colors focus:outline-none',\n !isActive && 'text-neutral/70'\n )}\n data-active={isActive}\n disabled={disabled}\n onClick={() => !disabled && handleSetActiveTab(value)}\n role=\"tab\"\n aria-selected={isActive}\n aria-controls={`tabpanel-${idFragment}`}\n id={`tab-${idFragment}`}\n type=\"button\"\n >\n {label ?? value}\n </button>\n );\n })}\n hoverable\n color=\"text\"\n />\n </div>\n {/* Tab Content */}\n {/* Clipper: no overflow; uses clip-path */}\n <div\n className={cn(\n 'relative w-full min-w-0 overflow-x-clip [-webkit-clip-path:inset(0)] [clip-path:inset(0)]',\n fullHeight && 'min-h-0 flex-1'\n )}\n {...containerProps}\n >\n {/* Track — presentational: it carries the sliding transform, while\n the panels it holds are the tablist's controlled regions. */}\n <div\n className={cn(\n 'grid w-full min-w-0',\n fullHeight && 'h-full',\n isDragging\n ? 'transition-none'\n : 'transition-transform duration-300 ease-in-out'\n )}\n style={{\n gridTemplateColumns: `repeat(${tabItems.length}, 100%)`,\n transform: `translateX(-${activeTabIndex * 100 - (isDragging ? dragDeltaPct : 0)}%)`,\n }}\n >\n {tabItems.map(\n ({ value, children, className: itemClassName }, index) => {\n const isActive = index === activeTabIndex;\n const idFragment = toIdFragment(value);\n\n return (\n <div\n key={value}\n role=\"tabpanel\"\n aria-labelledby={`tab-${idFragment}`}\n id={`tabpanel-${idFragment}`}\n // `inert` takes the whole subtree out of the tab order and\n // the accessibility tree at once. `aria-hidden` alone leaves\n // the buttons and links of an off-screen panel focusable,\n // which strands keyboard users on invisible controls.\n inert={!isActive}\n tabIndex={isActive ? 0 : -1}\n data-active={isActive}\n className={cn(\n 'w-full min-w-0 p-3 opacity-100 transition-opacity duration-300 ease-in-out',\n fullHeight && 'h-full overflow-y-auto',\n !isActive && 'pointer-events-none opacity-0', // prevent offscreen interaction\n itemClassName\n )}\n >\n <div\n className={cn(\n 'flex w-full min-w-0 flex-col items-stretch gap-6',\n fullHeight && 'min-h-full'\n )}\n >\n {children}\n </div>\n </div>\n );\n }\n )}\n </div>\n </div>\n </div>\n </TabContext.Provider>\n );\n};\n\n// Create the compound component\nexport const Tab = Object.assign(TabComponent, {\n Item: TabItem,\n});\n"],"mappings":"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"}
@@ -79,11 +79,13 @@ const AutoSizedTextArea = ({ className, autoSize = true, onChange, maxRows = 999
79
79
  textAreaStyle.height = `${Math.max(Math.min(scrollHeight, maxHeight), 36)}px`;
80
80
  };
81
81
  useLayoutEffect(() => {
82
+ if (!Boolean(props.value ?? props.defaultValue)) return;
82
83
  adjustHeight();
83
84
  }, [
84
85
  props.value,
85
86
  props.defaultValue,
86
- adjustHeight
87
+ autoSize,
88
+ maxRows
87
89
  ]);
88
90
  const handleChange = (e) => {
89
91
  onChange?.(e);
@@ -91,7 +93,6 @@ const AutoSizedTextArea = ({ className, autoSize = true, onChange, maxRows = 999
91
93
  };
92
94
  const setRef = (el) => {
93
95
  textAreaRef.current = el;
94
- if (el) adjustHeight();
95
96
  };
96
97
  return /* @__PURE__ */ jsx(TextArea, {
97
98
  ref: setRef,
@@ -1 +1 @@
1
- {"version":3,"file":"AutoSizeTextArea.mjs","names":[],"sources":["../../../../src/components/TextArea/AutoSizeTextArea.tsx"],"sourcesContent":["'use client';\n\nimport { cn } from '@utils/cn';\nimport {\n type ChangeEventHandler,\n type FC,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n} from 'react';\nimport { TextArea, type TextAreaProps } from './TextArea';\n\n/**\n * Props for the AutoSizedTextArea component.\n *\n * Extends TextAreaProps with auto-sizing functionality and row limitations.\n *\n * @example\n * ```tsx\n * // Auto-sizing textarea that grows with content\n * <AutoSizedTextArea\n * placeholder=\"Start typing and watch it grow...\"\n * autoSize={true}\n * maxRows={10}\n * />\n *\n * // Limited height with scroll when exceeded\n * <AutoSizedTextArea\n * value={longText}\n * onChange={handleChange}\n * autoSize={true}\n * maxRows={5}\n * className=\"max-h-30\"\n * />\n *\n * // Disable auto-sizing for fixed height\n * <AutoSizedTextArea\n * autoSize={false}\n * rows={3}\n * placeholder=\"Fixed height textarea\"\n * />\n * ```\n */\nexport type AutoSizedTextAreaProps = TextAreaProps & {\n /** Whether to automatically adjust height based on content */\n autoSize?: boolean;\n /** Maximum number of rows before scrolling is enabled */\n maxRows?: number;\n};\n\nconst LINE_HEIGHT = 24; // px\nconst LINE_PADDING = 12; // px\n\n/**\n * AutoSizedTextArea Component\n *\n * An enhanced textarea that automatically adjusts its height based on content,\n * providing a smooth user experience for variable-length text input.\n *\n * ## Features\n * - **Auto-Sizing**: Dynamically grows and shrinks based on content\n * - **Row Limits**: Configurable maximum rows before scrolling\n * - **Smooth Transitions**: Seamless height adjustments as user types\n * - **Scroll Management**: Automatic overflow handling when max height reached\n * - **Performance Optimized**: Efficient height calculations and updates\n *\n * ## Technical Details\n * - Line height: 24px with 12px padding\n * - Height calculation: `scrollHeight` vs `maxRows * lineHeight + padding`\n * - Resize: Disabled when auto-sizing is active for smooth experience\n * - Ref forwarding: Supports imperative access to textarea element\n *\n * ## Use Cases\n * - Chat message composition with dynamic sizing\n * - Comment forms that expand with content\n * - Note-taking interfaces with variable length\n * - Social media post creation\n * - Code snippet input with growth limits\n *\n * @example\n * ```tsx\n * // Chat-style auto-expanding textarea\n * const [message, setMessage] = useState('');\n *\n * <AutoSizedTextArea\n * value={message}\n * onChange={(e) => setMessage(e.target.value)}\n * placeholder=\"Type your message...\"\n * autoSize={true}\n * maxRows={8}\n * className=\"min-h-10\"\n * onKeyDown={(e) => {\n * if (e.key === 'Enter' && !e.shiftKey) {\n * e.preventDefault();\n * sendMessage(message);\n * setMessage('');\n * }\n * }}\n * />\n *\n * // Note-taking with generous height limits\n * <AutoSizedTextArea\n * defaultValue={note.content}\n * onChange={handleNoteChange}\n * placeholder=\"Write your notes here...\"\n * autoSize={true}\n * maxRows={20}\n * variant=\"default\"\n * />\n * ```\n */\nexport const AutoSizedTextArea: FC<AutoSizedTextAreaProps> = ({\n className,\n autoSize = true,\n onChange,\n maxRows = 999,\n ref,\n ...props\n}) => {\n const textAreaRef = useRef<HTMLTextAreaElement | null>(null);\n\n useImperativeHandle(ref, () => textAreaRef.current!);\n\n const adjustHeight = () => {\n const textAreaEl = textAreaRef.current;\n\n if (!textAreaEl || !autoSize) return;\n\n const textAreaStyle = textAreaEl.style;\n\n // Reset height to get accurate scrollHeight\n textAreaStyle.height = 'auto';\n const scrollHeight = textAreaEl.scrollHeight;\n const maxHeight = LINE_HEIGHT * maxRows + LINE_PADDING;\n const minHeight = LINE_HEIGHT + LINE_PADDING;\n\n // Set the new height\n textAreaStyle.height = `${Math.max(Math.min(scrollHeight, maxHeight), minHeight)}px`;\n };\n\n useLayoutEffect(() => {\n adjustHeight();\n }, [props.value, props.defaultValue, adjustHeight]);\n\n const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (e) => {\n onChange?.(e);\n adjustHeight();\n };\n\n const setRef = (el: HTMLTextAreaElement | null) => {\n textAreaRef.current = el;\n if (el) {\n adjustHeight();\n }\n };\n\n return (\n <TextArea\n ref={setRef}\n onChange={handleChange}\n className={cn('overflow-y-auto', autoSize && 'resize-none', className)}\n {...props}\n />\n );\n};\n"],"mappings":";;;;;;;;AAkDA,MAAM,cAAc;AACpB,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DrB,MAAa,qBAAiD,EAC5D,WACA,WAAW,MACX,UACA,UAAU,KACV,KACA,GAAG,YACC;CACJ,MAAM,cAAc,OAAmC,IAAI;CAE3D,oBAAoB,WAAW,YAAY,OAAQ;CAEnD,MAAM,qBAAqB;EACzB,MAAM,aAAa,YAAY;EAE/B,IAAI,CAAC,cAAc,CAAC,UAAU;EAE9B,MAAM,gBAAgB,WAAW;EAGjC,cAAc,SAAS;EACvB,MAAM,eAAe,WAAW;EAChC,MAAM,YAAY,cAAc,UAAU;EAI1C,cAAc,SAAS,GAAG,KAAK,IAAI,KAAK,IAAI,cAAc,SAAS,GAAG,EAAS,EAAE;CACnF;CAEA,sBAAsB;EACpB,aAAa;CACf,GAAG;EAAC,MAAM;EAAO,MAAM;EAAc;CAAY,CAAC;CAElD,MAAM,gBAAyD,MAAM;EACnE,WAAW,CAAC;EACZ,aAAa;CACf;CAEA,MAAM,UAAU,OAAmC;EACjD,YAAY,UAAU;EACtB,IAAI,IACF,aAAa;CAEjB;CAEA,OACE,oBAAC,UAAD;EACE,KAAK;EACL,UAAU;EACV,WAAW,GAAG,mBAAmB,YAAY,eAAe,SAAS;EACrE,GAAI;CACL;AAEL"}
1
+ {"version":3,"file":"AutoSizeTextArea.mjs","names":[],"sources":["../../../../src/components/TextArea/AutoSizeTextArea.tsx"],"sourcesContent":["'use client';\n\nimport { cn } from '@utils/cn';\nimport {\n type ChangeEventHandler,\n type FC,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n} from 'react';\nimport { TextArea, type TextAreaProps } from './TextArea';\n\n/**\n * Props for the AutoSizedTextArea component.\n *\n * Extends TextAreaProps with auto-sizing functionality and row limitations.\n *\n * @example\n * ```tsx\n * // Auto-sizing textarea that grows with content\n * <AutoSizedTextArea\n * placeholder=\"Start typing and watch it grow...\"\n * autoSize={true}\n * maxRows={10}\n * />\n *\n * // Limited height with scroll when exceeded\n * <AutoSizedTextArea\n * value={longText}\n * onChange={handleChange}\n * autoSize={true}\n * maxRows={5}\n * className=\"max-h-30\"\n * />\n *\n * // Disable auto-sizing for fixed height\n * <AutoSizedTextArea\n * autoSize={false}\n * rows={3}\n * placeholder=\"Fixed height textarea\"\n * />\n * ```\n */\nexport type AutoSizedTextAreaProps = TextAreaProps & {\n /** Whether to automatically adjust height based on content */\n autoSize?: boolean;\n /** Maximum number of rows before scrolling is enabled */\n maxRows?: number;\n};\n\nconst LINE_HEIGHT = 24; // px\nconst LINE_PADDING = 12; // px\n\n/**\n * AutoSizedTextArea Component\n *\n * An enhanced textarea that automatically adjusts its height based on content,\n * providing a smooth user experience for variable-length text input.\n *\n * ## Features\n * - **Auto-Sizing**: Dynamically grows and shrinks based on content\n * - **Row Limits**: Configurable maximum rows before scrolling\n * - **Smooth Transitions**: Seamless height adjustments as user types\n * - **Scroll Management**: Automatic overflow handling when max height reached\n * - **Performance Optimized**: Efficient height calculations and updates\n *\n * ## Technical Details\n * - Line height: 24px with 12px padding\n * - Height calculation: `scrollHeight` vs `maxRows * lineHeight + padding`\n * - Resize: Disabled when auto-sizing is active for smooth experience\n * - Ref forwarding: Supports imperative access to textarea element\n *\n * ## Use Cases\n * - Chat message composition with dynamic sizing\n * - Comment forms that expand with content\n * - Note-taking interfaces with variable length\n * - Social media post creation\n * - Code snippet input with growth limits\n *\n * @example\n * ```tsx\n * // Chat-style auto-expanding textarea\n * const [message, setMessage] = useState('');\n *\n * <AutoSizedTextArea\n * value={message}\n * onChange={(e) => setMessage(e.target.value)}\n * placeholder=\"Type your message...\"\n * autoSize={true}\n * maxRows={8}\n * className=\"min-h-10\"\n * onKeyDown={(e) => {\n * if (e.key === 'Enter' && !e.shiftKey) {\n * e.preventDefault();\n * sendMessage(message);\n * setMessage('');\n * }\n * }}\n * />\n *\n * // Note-taking with generous height limits\n * <AutoSizedTextArea\n * defaultValue={note.content}\n * onChange={handleNoteChange}\n * placeholder=\"Write your notes here...\"\n * autoSize={true}\n * maxRows={20}\n * variant=\"default\"\n * />\n * ```\n */\nexport const AutoSizedTextArea: FC<AutoSizedTextAreaProps> = ({\n className,\n autoSize = true,\n onChange,\n maxRows = 999,\n ref,\n ...props\n}) => {\n const textAreaRef = useRef<HTMLTextAreaElement | null>(null);\n\n useImperativeHandle(ref, () => textAreaRef.current!);\n\n const adjustHeight = () => {\n const textAreaEl = textAreaRef.current;\n\n if (!textAreaEl || !autoSize) return;\n\n const textAreaStyle = textAreaEl.style;\n\n // Reset height to get accurate scrollHeight\n textAreaStyle.height = 'auto';\n const scrollHeight = textAreaEl.scrollHeight;\n const maxHeight = LINE_HEIGHT * maxRows + LINE_PADDING;\n const minHeight = LINE_HEIGHT + LINE_PADDING;\n\n // Set the new height\n textAreaStyle.height = `${Math.max(Math.min(scrollHeight, maxHeight), minHeight)}px`;\n };\n\n /*\n * `adjustHeight` writes a style and then reads `scrollHeight`, which forces\n * the browser to lay the page out there and then. Depending on the function\n * identity — recreated every render — repeated that reflow on every render of\n * every ancestor; the height only ever changes with the content, so that is\n * what this watches.\n *\n * An empty field is skipped entirely: its height is already the one line the\n * CSS gives it, so measuring only buys a reflow. That is the common case for\n * a chat composer sitting unopened on a large page.\n */\n useLayoutEffect(() => {\n const hasContent = Boolean(props.value ?? props.defaultValue);\n if (!hasContent) return;\n\n adjustHeight();\n }, [props.value, props.defaultValue, autoSize, maxRows]);\n\n const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (e) => {\n onChange?.(e);\n adjustHeight();\n };\n\n const setRef = (el: HTMLTextAreaElement | null) => {\n textAreaRef.current = el;\n };\n\n return (\n <TextArea\n ref={setRef}\n onChange={handleChange}\n className={cn('overflow-y-auto', autoSize && 'resize-none', className)}\n {...props}\n />\n );\n};\n"],"mappings":";;;;;;;;AAkDA,MAAM,cAAc;AACpB,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DrB,MAAa,qBAAiD,EAC5D,WACA,WAAW,MACX,UACA,UAAU,KACV,KACA,GAAG,YACC;CACJ,MAAM,cAAc,OAAmC,IAAI;CAE3D,oBAAoB,WAAW,YAAY,OAAQ;CAEnD,MAAM,qBAAqB;EACzB,MAAM,aAAa,YAAY;EAE/B,IAAI,CAAC,cAAc,CAAC,UAAU;EAE9B,MAAM,gBAAgB,WAAW;EAGjC,cAAc,SAAS;EACvB,MAAM,eAAe,WAAW;EAChC,MAAM,YAAY,cAAc,UAAU;EAI1C,cAAc,SAAS,GAAG,KAAK,IAAI,KAAK,IAAI,cAAc,SAAS,GAAG,EAAS,EAAE;CACnF;CAaA,sBAAsB;EAEpB,IAAI,CADe,QAAQ,MAAM,SAAS,MAAM,YAClC,GAAG;EAEjB,aAAa;CACf,GAAG;EAAC,MAAM;EAAO,MAAM;EAAc;EAAU;CAAO,CAAC;CAEvD,MAAM,gBAAyD,MAAM;EACnE,WAAW,CAAC;EACZ,aAAa;CACf;CAEA,MAAM,UAAU,OAAmC;EACjD,YAAY,UAAU;CACxB;CAEA,OACE,oBAAC,UAAD;EACE,KAAK;EACL,UAAU;EACV,WAAW,GAAG,mBAAmB,YAAY,eAAe,SAAS;EACrE,GAAI;CACL;AAEL"}
@@ -20,7 +20,7 @@ import { Radio, radioVariants } from "./Input/Radio.mjs";
20
20
  import { SearchInput } from "./Input/SearchInput.mjs";
21
21
  import { Browser } from "./Browser/Browser.mjs";
22
22
  import { KeyboardShortcut } from "./KeyboardShortcut/KeyboardShortcut.mjs";
23
- import { Detail, PopoverStatic } from "./Popover/static.mjs";
23
+ import { Detail, PopoverStatic, usePopoverIds } from "./Popover/static.mjs";
24
24
  import { Popover } from "./Popover/dynamic.mjs";
25
25
  import { Carousel } from "./Carousel/index.mjs";
26
26
  import { CollapsibleTable } from "./CollapsibleTable/CollapsibleTable.mjs";
@@ -105,7 +105,7 @@ import { Footer } from "./Footer/index.mjs";
105
105
  import { HeightResizer } from "./HeightResizer/index.mjs";
106
106
  import { HideShow } from "./HideShow/index.mjs";
107
107
  import { HTMLRenderer, getIntlayerHTMLOptions } from "./HTMLRender/HTMLRender.mjs";
108
- import { CodeBlock, CodeDefault } from "./IDE/CodeBlockClient.mjs";
108
+ import { CodeBlock, CodeDefault, CodeHighlighted, shikiWrapperClassName } from "./IDE/CodeBlockClient.mjs";
109
109
  import { Code } from "./IDE/Code.mjs";
110
110
  import { FileList } from "./IDE/FileList.mjs";
111
111
  import { WithResizer } from "./WithResizer/index.mjs";
@@ -141,4 +141,4 @@ import { Terminal } from "./Terminal/Terminal.mjs";
141
141
  import { DesktopThemeSwitcher } from "./ThemeSwitcherDropDown/DesktopThemeSwitcher.mjs";
142
142
  import { MobileThemeSwitcher } from "./ThemeSwitcherDropDown/MobileThemeSwitcher.mjs";
143
143
 
144
- export { Accordion, AutoCompleteTextarea, AutoSizedTextArea, Avatar, Badge, BitbucketLogo, Breadcrumb, Browser, Burger, Button, Carousel, Checkbox, ClickOutsideDiv, Code, CodeBlock, CodeDefault, CollapsibleTable, Command, CommandRoot, Container, ContentEditableTextArea, ContentEditorInput, ContentEditorTextArea, ContentSelector, CopyButton, CopyToClipboard, DesktopNavbar, DesktopThemeSwitcher, Detail, DictionaryCreationForm, DictionaryEditor, DictionaryFieldEditor, DiscordLogo, DotPattern, DropDown, EditableFieldInput, EditableFieldTextArea, ExpandCollapse, FacebookLogo, FileList, Flag, flags_exports as Flags, Footer, Form, AutoSizedTextAreaElement as FormAutoSizedTextArea, Button as FormButton, CheckboxElement as FormCheckbox, FormControl, FormDescription, EditableFieldInputElement as FormEditableFieldInput, EditableFieldTextAreaElement as FormEditableFieldTextArea, FormElement, FormField, InputElement as FormInput, InputPasswordElement as FormInputPassword, FormItem, FormLabelLayout as FormLabel, FormMessage, MultiSelectElement as FormMultiSelect, OTPElement as FormOTP, SearchInputElement as FormSearchInput, SelectElement as FormSelect, SwitchSelectorElement as FormSwitchSelector, TextAreaElement as FormTextArea, GitHubLogo, GitLabLogo, GoogleLogo, GridDistortionPattern, GridPattern, H1, H2, H3, H4, H5, H6, HTMLRenderer, HeightResizer, HideShow, Hr, IDE, InformationTag, Input, InputIndicator, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, InputPassword, InstagramLogo, KeyPathBreadcrumb, KeyboardScreenAdapter, KeyboardShortcut, Label, LanguageBackground, LanguageSection, Link, LinkedInLogo, Loader, LocaleSwitcher, LocaleSwitcherContent, LocaleSwitcherContentProvider, Logo, LogoTextOnly, LogoWithText, LogoWithTextBelow, MarkDownIframe, MarkdownEditor, MarkdownRenderer, MaxHeightSmoother, MaxWidthSmoother, MobileNavbar, MobileThemeSwitcher, Modal, MultiSelect, Navbar, NumberItemsSelector, OTPInput, OTPInputContext, Pagination, Popover, PopoverStatic, PressableSpan, ProductHuntLogo, Radio, RightDrawer, SaveForm, SearchInput, Select, SelectContent, SelectLabel, SelectSeparator, ShowingResultsNumberItems, SmartTable, SocialNetworks, Spotlight, Step, Steps, SwitchSelector, Tab, TabSelector, Table, Tag, Td, TechLogo, techLogos_exports as TechLogos, Terminal, TextArea, TextEditor, TextEditorContainer, Th, TiktokLogo, Toast, ToastAction, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport, Toaster, Tr, VersionSwitcher, VersionSwitcherProvider, VerticalSwitchSelector, WithResizer, XLogo, YoutubeLogo, badgeVariants, baseMarkdownComponents, buttonVariants, checkIsExternalLink, checkboxVariants, choiceVariant, containerVariants, createEditorUploadFn, defaultChoices, drawerManager, getCapitals, getIntlayerHTMLOptions, getIntlayerMarkdownOptions, indicatorVariant, inputSlotVariants, inputVariants, isTextChildren, linkVariants, paginationVariants, radioVariants, reducer, switchSelectorVariant, toast, toastVariants, traceKeys, useContentEditable, useCopyToClipboard, useForm, useFormField, useLocaleSwitcherContent, usePasswordManagerBadge, usePrevious, useRightDrawer, useToast, useVersionSwitcher };
144
+ export { Accordion, AutoCompleteTextarea, AutoSizedTextArea, Avatar, Badge, BitbucketLogo, Breadcrumb, Browser, Burger, Button, Carousel, Checkbox, ClickOutsideDiv, Code, CodeBlock, CodeDefault, CodeHighlighted, CollapsibleTable, Command, CommandRoot, Container, ContentEditableTextArea, ContentEditorInput, ContentEditorTextArea, ContentSelector, CopyButton, CopyToClipboard, DesktopNavbar, DesktopThemeSwitcher, Detail, DictionaryCreationForm, DictionaryEditor, DictionaryFieldEditor, DiscordLogo, DotPattern, DropDown, EditableFieldInput, EditableFieldTextArea, ExpandCollapse, FacebookLogo, FileList, Flag, flags_exports as Flags, Footer, Form, AutoSizedTextAreaElement as FormAutoSizedTextArea, Button as FormButton, CheckboxElement as FormCheckbox, FormControl, FormDescription, EditableFieldInputElement as FormEditableFieldInput, EditableFieldTextAreaElement as FormEditableFieldTextArea, FormElement, FormField, InputElement as FormInput, InputPasswordElement as FormInputPassword, FormItem, FormLabelLayout as FormLabel, FormMessage, MultiSelectElement as FormMultiSelect, OTPElement as FormOTP, SearchInputElement as FormSearchInput, SelectElement as FormSelect, SwitchSelectorElement as FormSwitchSelector, TextAreaElement as FormTextArea, GitHubLogo, GitLabLogo, GoogleLogo, GridDistortionPattern, GridPattern, H1, H2, H3, H4, H5, H6, HTMLRenderer, HeightResizer, HideShow, Hr, IDE, InformationTag, Input, InputIndicator, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, InputPassword, InstagramLogo, KeyPathBreadcrumb, KeyboardScreenAdapter, KeyboardShortcut, Label, LanguageBackground, LanguageSection, Link, LinkedInLogo, Loader, LocaleSwitcher, LocaleSwitcherContent, LocaleSwitcherContentProvider, Logo, LogoTextOnly, LogoWithText, LogoWithTextBelow, MarkDownIframe, MarkdownEditor, MarkdownRenderer, MaxHeightSmoother, MaxWidthSmoother, MobileNavbar, MobileThemeSwitcher, Modal, MultiSelect, Navbar, NumberItemsSelector, OTPInput, OTPInputContext, Pagination, Popover, PopoverStatic, PressableSpan, ProductHuntLogo, Radio, RightDrawer, SaveForm, SearchInput, Select, SelectContent, SelectLabel, SelectSeparator, ShowingResultsNumberItems, SmartTable, SocialNetworks, Spotlight, Step, Steps, SwitchSelector, Tab, TabSelector, Table, Tag, Td, TechLogo, techLogos_exports as TechLogos, Terminal, TextArea, TextEditor, TextEditorContainer, Th, TiktokLogo, Toast, ToastAction, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport, Toaster, Tr, VersionSwitcher, VersionSwitcherProvider, VerticalSwitchSelector, WithResizer, XLogo, YoutubeLogo, badgeVariants, baseMarkdownComponents, buttonVariants, checkIsExternalLink, checkboxVariants, choiceVariant, containerVariants, createEditorUploadFn, defaultChoices, drawerManager, getCapitals, getIntlayerHTMLOptions, getIntlayerMarkdownOptions, indicatorVariant, inputSlotVariants, inputVariants, isTextChildren, linkVariants, paginationVariants, radioVariants, reducer, shikiWrapperClassName, switchSelectorVariant, toast, toastVariants, traceKeys, useContentEditable, useCopyToClipboard, useForm, useFormField, useLocaleSwitcherContent, usePasswordManagerBadge, usePopoverIds, usePrevious, useRightDrawer, useToast, useVersionSwitcher };
@@ -64,12 +64,22 @@ const useDevice = (breakpoint = "md") => {
64
64
  isMac: void 0
65
65
  });
66
66
  useEffect(() => {
67
- setResult(calculateIsMobile(breakpoint));
68
- const handleResize = () => {
69
- setResult(calculateIsMobile(breakpoint));
70
- };
71
- window.addEventListener("resize", handleResize, { passive: true });
72
- return () => window.removeEventListener("resize", handleResize);
67
+ const breakpointValue = getBreakpointFromSize(breakpoint);
68
+ const mobileScreenQuery = window.matchMedia(`(max-width: ${breakpointValue}px)`);
69
+ const isMobileUserAgent = checkIsMobileUserAgent();
70
+ const isIOS = checkIsIOS();
71
+ const isMac = checkIsMac();
72
+ const applyMatches = (isMobileScreen) => setResult({
73
+ isMobileScreen,
74
+ isMobileUserAgent,
75
+ isMobile: isMobileScreen,
76
+ isIOS,
77
+ isMac
78
+ });
79
+ applyMatches(mobileScreenQuery.matches);
80
+ const handleChange = (event) => applyMatches(event.matches);
81
+ mobileScreenQuery.addEventListener("change", handleChange);
82
+ return () => mobileScreenQuery.removeEventListener("change", handleChange);
73
83
  }, [breakpoint]);
74
84
  return result;
75
85
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useDevice.mjs","names":[],"sources":["../../../src/hooks/useDevice.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nexport type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n\nexport const getBreakpointFromSize = (breakpoint: SizeType | number) => {\n switch (breakpoint) {\n case 'xs':\n return 480;\n case 'sm':\n return 640;\n case 'md':\n return 768;\n case 'lg':\n return 1024;\n case 'xl':\n return 1280;\n case '2xl':\n return 1536;\n default:\n return breakpoint;\n }\n};\n\nexport const checkIsMobileUserAgent = (): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n const userAgent = window.navigator?.userAgent;\n\n if (typeof userAgent === 'undefined') return;\n\n return /android|bb\\d+|meego|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(?:hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(?:ob|in)i|palm(?: os)?|phone|p(?:ixi|re)\\/|plucker|pocket|psp|series(?:4|6)0|symbian|treo|up\\.(?:browser|link)|vodafone|wap|windows ce|windows phone|xda|xiino|zte-/i.test(\n userAgent\n );\n};\n\nexport const checkIsIphoneOrSafariDevice = (): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n const userAgent = window.navigator?.userAgent;\n\n /* 1 . is it Safari? (Chrome & co. also contain “Safari/…”, so exclude them) */\n const isSafari =\n /Safari\\/\\d/i.test(userAgent) && // has “Safari/xxx”\n !/Chrome|CriOS|FxiOS|Edg|OPR/i.test(userAgent); // …but not the other browsers\n\n /* 2. is it an Apple device? (macOS or iOS/iPadOS) */\n const isAppleDevice = /Macintosh|iP(?:hone|ad|od)/.test(userAgent);\n\n return isSafari && isAppleDevice; // true for mac-Safari & iOS-Safari\n};\n\nexport const checkIsIOS = (): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n const userAgent = window.navigator?.userAgent;\n\n if (typeof userAgent === 'undefined') return;\n\n // Check for iOS devices: iPhone, iPad, iPod\n return /iP(?:hone|ad|od)/.test(userAgent);\n};\n\nexport const checkIsMac = (): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n const userAgent = window.navigator?.userAgent;\n\n if (typeof userAgent === 'undefined') return;\n\n // Check for macOS/Mac devices\n return /Macintosh|MacIntel|Mac OS X/.test(userAgent);\n};\n\nexport const checkIsMobileScreen = (\n breakpoint: number\n): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n return (window?.innerWidth ?? 0) <= breakpoint;\n};\n\ntype UseDeviceState = {\n isMobileScreen: boolean | undefined;\n isMobileUserAgent: boolean | undefined;\n isMobile: boolean | undefined;\n isIOS: boolean | undefined;\n isMac: boolean | undefined;\n};\n\nexport const calculateIsMobile = (breakpoint: SizeType | number = 'md') => {\n const breakpointValue = getBreakpointFromSize(breakpoint);\n\n const isMobileUserAgent = checkIsMobileUserAgent();\n const isMobileScreen = checkIsMobileScreen(breakpointValue);\n const isMobile = isMobileScreen ?? isMobileUserAgent;\n const isIOS = checkIsIOS();\n const isMac = checkIsMac();\n\n return {\n isMobileScreen, // Is the screen width within a mobile breakpoint.\n isMobileUserAgent, // Is the user agent indicative of a mobile device.\n isMobile, // Combines both checks to determine if the device is mobile.\n isIOS, // Is the device running iOS (iPhone, iPad, iPod).\n isMac, // Is the device a Mac computer (macOS).\n };\n};\n\nexport const useDevice = (breakpoint: SizeType | number = 'md') => {\n const [result, setResult] = useState<UseDeviceState>({\n isMobileScreen: undefined,\n isMobileUserAgent: undefined,\n isMobile: undefined,\n isIOS: undefined,\n isMac: undefined,\n });\n\n useEffect(() => {\n setResult(calculateIsMobile(breakpoint));\n\n const handleResize = () => {\n setResult(calculateIsMobile(breakpoint));\n };\n\n window.addEventListener('resize', handleResize, { passive: true });\n\n // Cleanup listener on unmount\n return () => window.removeEventListener('resize', handleResize);\n }, [breakpoint]);\n\n return result;\n};\n"],"mappings":";;;;;AAMA,MAAa,yBAAyB,eAAkC;CACtE,QAAQ,YAAR;EACE,KAAK,MACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,OACH,OAAO;EACT,SACE,OAAO;CACX;AACF;AAEA,MAAa,+BAAoD;CAC/D,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,YAAY,OAAO,WAAW;CAEpC,IAAI,OAAO,cAAc,aAAa;CAEtC,OAAO,mVAAmV,KACxV,SACF;AACF;AAEA,MAAa,oCAAyD;CACpE,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,YAAY,OAAO,WAAW;CAGpC,MAAM,WACJ,cAAc,KAAK,SAAS,KAC5B,CAAC,8BAA8B,KAAK,SAAS;CAG/C,MAAM,gBAAgB,6BAA6B,KAAK,SAAS;CAEjE,OAAO,YAAY;AACrB;AAEA,MAAa,mBAAwC;CACnD,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,YAAY,OAAO,WAAW;CAEpC,IAAI,OAAO,cAAc,aAAa;CAGtC,OAAO,mBAAmB,KAAK,SAAS;AAC1C;AAEA,MAAa,mBAAwC;CACnD,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,YAAY,OAAO,WAAW;CAEpC,IAAI,OAAO,cAAc,aAAa;CAGtC,OAAO,8BAA8B,KAAK,SAAS;AACrD;AAEA,MAAa,uBACX,eACwB;CACxB,IAAI,OAAO,WAAW,aAAa;CAEnC,QAAQ,QAAQ,cAAc,MAAM;AACtC;AAUA,MAAa,qBAAqB,aAAgC,SAAS;CACzE,MAAM,kBAAkB,sBAAsB,UAAU;CAExD,MAAM,oBAAoB,uBAAuB;CACjD,MAAM,iBAAiB,oBAAoB,eAAe;CAK1D,OAAO;EACL;EACA;EACA,UAPe,kBAAkB;EAQjC,OAPY,WAOR;EACJ,OAPY,WAOR;CACN;AACF;AAEA,MAAa,aAAa,aAAgC,SAAS;CACjE,MAAM,CAAC,QAAQ,aAAa,SAAyB;EACnD,gBAAgB;EAChB,mBAAmB;EACnB,UAAU;EACV,OAAO;EACP,OAAO;CACT,CAAC;CAED,gBAAgB;EACd,UAAU,kBAAkB,UAAU,CAAC;EAEvC,MAAM,qBAAqB;GACzB,UAAU,kBAAkB,UAAU,CAAC;EACzC;EAEA,OAAO,iBAAiB,UAAU,cAAc,EAAE,SAAS,KAAK,CAAC;EAGjE,aAAa,OAAO,oBAAoB,UAAU,YAAY;CAChE,GAAG,CAAC,UAAU,CAAC;CAEf,OAAO;AACT"}
1
+ {"version":3,"file":"useDevice.mjs","names":[],"sources":["../../../src/hooks/useDevice.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nexport type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n\nexport const getBreakpointFromSize = (breakpoint: SizeType | number) => {\n switch (breakpoint) {\n case 'xs':\n return 480;\n case 'sm':\n return 640;\n case 'md':\n return 768;\n case 'lg':\n return 1024;\n case 'xl':\n return 1280;\n case '2xl':\n return 1536;\n default:\n return breakpoint;\n }\n};\n\nexport const checkIsMobileUserAgent = (): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n const userAgent = window.navigator?.userAgent;\n\n if (typeof userAgent === 'undefined') return;\n\n return /android|bb\\d+|meego|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(?:hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(?:ob|in)i|palm(?: os)?|phone|p(?:ixi|re)\\/|plucker|pocket|psp|series(?:4|6)0|symbian|treo|up\\.(?:browser|link)|vodafone|wap|windows ce|windows phone|xda|xiino|zte-/i.test(\n userAgent\n );\n};\n\nexport const checkIsIphoneOrSafariDevice = (): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n const userAgent = window.navigator?.userAgent;\n\n /* 1 . is it Safari? (Chrome & co. also contain “Safari/…”, so exclude them) */\n const isSafari =\n /Safari\\/\\d/i.test(userAgent) && // has “Safari/xxx”\n !/Chrome|CriOS|FxiOS|Edg|OPR/i.test(userAgent); // …but not the other browsers\n\n /* 2. is it an Apple device? (macOS or iOS/iPadOS) */\n const isAppleDevice = /Macintosh|iP(?:hone|ad|od)/.test(userAgent);\n\n return isSafari && isAppleDevice; // true for mac-Safari & iOS-Safari\n};\n\nexport const checkIsIOS = (): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n const userAgent = window.navigator?.userAgent;\n\n if (typeof userAgent === 'undefined') return;\n\n // Check for iOS devices: iPhone, iPad, iPod\n return /iP(?:hone|ad|od)/.test(userAgent);\n};\n\nexport const checkIsMac = (): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n const userAgent = window.navigator?.userAgent;\n\n if (typeof userAgent === 'undefined') return;\n\n // Check for macOS/Mac devices\n return /Macintosh|MacIntel|Mac OS X/.test(userAgent);\n};\n\nexport const checkIsMobileScreen = (\n breakpoint: number\n): boolean | undefined => {\n if (typeof window === 'undefined') return;\n\n return (window?.innerWidth ?? 0) <= breakpoint;\n};\n\ntype UseDeviceState = {\n isMobileScreen: boolean | undefined;\n isMobileUserAgent: boolean | undefined;\n isMobile: boolean | undefined;\n isIOS: boolean | undefined;\n isMac: boolean | undefined;\n};\n\nexport const calculateIsMobile = (breakpoint: SizeType | number = 'md') => {\n const breakpointValue = getBreakpointFromSize(breakpoint);\n\n const isMobileUserAgent = checkIsMobileUserAgent();\n const isMobileScreen = checkIsMobileScreen(breakpointValue);\n const isMobile = isMobileScreen ?? isMobileUserAgent;\n const isIOS = checkIsIOS();\n const isMac = checkIsMac();\n\n return {\n isMobileScreen, // Is the screen width within a mobile breakpoint.\n isMobileUserAgent, // Is the user agent indicative of a mobile device.\n isMobile, // Combines both checks to determine if the device is mobile.\n isIOS, // Is the device running iOS (iPhone, iPad, iPod).\n isMac, // Is the device a Mac computer (macOS).\n };\n};\n\nexport const useDevice = (breakpoint: SizeType | number = 'md') => {\n const [result, setResult] = useState<UseDeviceState>({\n isMobileScreen: undefined,\n isMobileUserAgent: undefined,\n isMobile: undefined,\n isIOS: undefined,\n isMac: undefined,\n });\n\n useEffect(() => {\n const breakpointValue = getBreakpointFromSize(breakpoint);\n const mobileScreenQuery = window.matchMedia(\n `(max-width: ${breakpointValue}px)`\n );\n\n const isMobileUserAgent = checkIsMobileUserAgent();\n const isIOS = checkIsIOS();\n const isMac = checkIsMac();\n\n const applyMatches = (isMobileScreen: boolean) =>\n setResult({\n isMobileScreen,\n isMobileUserAgent,\n isMobile: isMobileScreen,\n isIOS,\n isMac,\n });\n\n applyMatches(mobileScreenQuery.matches);\n\n const handleChange = (event: MediaQueryListEvent) =>\n applyMatches(event.matches);\n\n mobileScreenQuery.addEventListener('change', handleChange);\n\n return () => mobileScreenQuery.removeEventListener('change', handleChange);\n }, [breakpoint]);\n\n return result;\n};\n"],"mappings":";;;;;AAMA,MAAa,yBAAyB,eAAkC;CACtE,QAAQ,YAAR;EACE,KAAK,MACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,OACH,OAAO;EACT,SACE,OAAO;CACX;AACF;AAEA,MAAa,+BAAoD;CAC/D,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,YAAY,OAAO,WAAW;CAEpC,IAAI,OAAO,cAAc,aAAa;CAEtC,OAAO,mVAAmV,KACxV,SACF;AACF;AAEA,MAAa,oCAAyD;CACpE,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,YAAY,OAAO,WAAW;CAGpC,MAAM,WACJ,cAAc,KAAK,SAAS,KAC5B,CAAC,8BAA8B,KAAK,SAAS;CAG/C,MAAM,gBAAgB,6BAA6B,KAAK,SAAS;CAEjE,OAAO,YAAY;AACrB;AAEA,MAAa,mBAAwC;CACnD,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,YAAY,OAAO,WAAW;CAEpC,IAAI,OAAO,cAAc,aAAa;CAGtC,OAAO,mBAAmB,KAAK,SAAS;AAC1C;AAEA,MAAa,mBAAwC;CACnD,IAAI,OAAO,WAAW,aAAa;CAEnC,MAAM,YAAY,OAAO,WAAW;CAEpC,IAAI,OAAO,cAAc,aAAa;CAGtC,OAAO,8BAA8B,KAAK,SAAS;AACrD;AAEA,MAAa,uBACX,eACwB;CACxB,IAAI,OAAO,WAAW,aAAa;CAEnC,QAAQ,QAAQ,cAAc,MAAM;AACtC;AAUA,MAAa,qBAAqB,aAAgC,SAAS;CACzE,MAAM,kBAAkB,sBAAsB,UAAU;CAExD,MAAM,oBAAoB,uBAAuB;CACjD,MAAM,iBAAiB,oBAAoB,eAAe;CAK1D,OAAO;EACL;EACA;EACA,UAPe,kBAAkB;EAQjC,OAPY,WAOR;EACJ,OAPY,WAOR;CACN;AACF;AAEA,MAAa,aAAa,aAAgC,SAAS;CACjE,MAAM,CAAC,QAAQ,aAAa,SAAyB;EACnD,gBAAgB;EAChB,mBAAmB;EACnB,UAAU;EACV,OAAO;EACP,OAAO;CACT,CAAC;CAED,gBAAgB;EACd,MAAM,kBAAkB,sBAAsB,UAAU;EACxD,MAAM,oBAAoB,OAAO,WAC/B,eAAe,gBAAgB,IACjC;EAEA,MAAM,oBAAoB,uBAAuB;EACjD,MAAM,QAAQ,WAAW;EACzB,MAAM,QAAQ,WAAW;EAEzB,MAAM,gBAAgB,mBACpB,UAAU;GACR;GACA;GACA,UAAU;GACV;GACA;EACF,CAAC;EAEH,aAAa,kBAAkB,OAAO;EAEtC,MAAM,gBAAgB,UACpB,aAAa,MAAM,OAAO;EAE5B,kBAAkB,iBAAiB,UAAU,YAAY;EAEzD,aAAa,kBAAkB,oBAAoB,UAAU,YAAY;CAC3E,GAAG,CAAC,UAAU,CAAC;CAEf,OAAO;AACT"}
@@ -1,157 +1,152 @@
1
1
  'use client';
2
2
 
3
- import { useLayoutEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
4
 
5
5
  //#region src/hooks/useItemSelector.ts
6
6
  const selectorDefault = (option) => option?.getAttribute("aria-selected") === "true";
7
+ /** Attributes whose change moves the indicator to another option. */
8
+ const WATCHED_ATTRIBUTES = [
9
+ "aria-selected",
10
+ "data-active",
11
+ "data-indicator"
12
+ ];
13
+ /** Delay before the indicator leaves the option the pointer just left. */
14
+ const HOVER_RELEASE_DELAY = 150;
7
15
  const useItemSelector = (optionsRefs, { selector = selectorDefault, isHoverable = false, orientation = "horizontal" } = {
8
16
  selector: selectorDefault,
9
17
  isHoverable: false,
10
18
  orientation: "horizontal"
11
19
  }) => {
12
20
  const [choiceIndicatorPosition, setChoiceIndicatorPosition] = useState(null);
13
- const [hoveredItem, setHoveredItem] = useState(null);
21
+ const hoveredItemRef = useRef(null);
14
22
  const hideTimeoutRef = useRef(null);
15
23
  const lastPositionRef = useRef(null);
24
+ const measurementFrameRef = useRef(null);
25
+ const selectorRef = useRef(selector);
26
+ selectorRef.current = selector;
27
+ const orientationRef = useRef(orientation);
28
+ orientationRef.current = orientation;
16
29
  const itemsLength = optionsRefs.current.length;
17
- const calculatePosition = () => {
18
- let targetElement = null;
19
- if (hoveredItem) {
20
- targetElement = hoveredItem;
21
- if (hideTimeoutRef.current) {
22
- clearTimeout(hideTimeoutRef.current);
23
- hideTimeoutRef.current = null;
24
- }
25
- } else targetElement = optionsRefs.current.find(selector) ?? null;
30
+ const calculatePosition = useCallback(() => {
31
+ const targetElement = hoveredItemRef.current ?? optionsRefs.current.find(selectorRef.current) ?? null;
32
+ if (hoveredItemRef.current && hideTimeoutRef.current) {
33
+ clearTimeout(hideTimeoutRef.current);
34
+ hideTimeoutRef.current = null;
35
+ }
36
+ const isVertical = orientationRef.current === "vertical";
26
37
  if (!targetElement) {
27
- if (orientation === "vertical") {
28
- const verticalPrev = lastPositionRef.current;
29
- const newPosition = {
30
- top: verticalPrev?.top ?? 0,
31
- height: verticalPrev?.height ?? 0,
32
- opacity: 0
33
- };
34
- if (verticalPrev?.opacity !== 0) {
35
- setChoiceIndicatorPosition(newPosition);
36
- lastPositionRef.current = newPosition;
37
- }
38
- } else {
39
- const horizontalPrev = lastPositionRef.current;
40
- const newPosition = {
41
- left: horizontalPrev?.left ?? 0,
42
- width: horizontalPrev?.width ?? 0,
43
- opacity: 0
44
- };
45
- if (horizontalPrev?.opacity !== 0) {
46
- setChoiceIndicatorPosition(newPosition);
47
- lastPositionRef.current = newPosition;
48
- }
49
- }
38
+ if (lastPositionRef.current?.opacity === 0) return;
39
+ const newPosition = isVertical ? {
40
+ top: lastPositionRef.current?.top ?? 0,
41
+ height: lastPositionRef.current?.height ?? 0,
42
+ opacity: 0
43
+ } : {
44
+ left: lastPositionRef.current?.left ?? 0,
45
+ width: lastPositionRef.current?.width ?? 0,
46
+ opacity: 0
47
+ };
48
+ setChoiceIndicatorPosition(newPosition);
49
+ lastPositionRef.current = newPosition;
50
50
  return;
51
51
  }
52
- if (orientation === "vertical") {
52
+ if (isVertical) {
53
53
  const top = targetElement.offsetTop;
54
54
  const height = targetElement.offsetHeight;
55
+ const previous = lastPositionRef.current;
56
+ if (previous?.top === top && previous.height === height && previous.opacity === 1) return;
55
57
  const newPosition = {
56
58
  top,
57
59
  height,
58
60
  opacity: 1
59
61
  };
60
- const prev = lastPositionRef.current;
61
- if (!prev || prev.top !== top || prev.height !== height || prev.opacity !== 1) {
62
- setChoiceIndicatorPosition(newPosition);
63
- lastPositionRef.current = newPosition;
64
- }
65
- } else {
66
- const left = targetElement.offsetLeft;
67
- const width = targetElement.offsetWidth;
68
- const newPosition = {
69
- left,
70
- width,
71
- opacity: 1
72
- };
73
- const prev = lastPositionRef.current;
74
- if (!prev || prev.left !== left || prev.width !== width || prev.opacity !== 1) {
75
- setChoiceIndicatorPosition(newPosition);
76
- lastPositionRef.current = newPosition;
77
- }
62
+ setChoiceIndicatorPosition(newPosition);
63
+ lastPositionRef.current = newPosition;
64
+ return;
78
65
  }
79
- };
80
- useLayoutEffect(() => {
81
- calculatePosition();
82
- window.addEventListener("resize", calculatePosition);
83
- window.addEventListener("DOMContentLoaded", calculatePosition);
84
- const mutationObservers = [];
85
- optionsRefs.current.forEach((option) => {
86
- if (option) {
87
- const observer = new MutationObserver((mutations) => {
88
- for (const mutation of mutations) if (mutation.type === "attributes" && (mutation.attributeName === "aria-selected" || mutation.attributeName === "data-active" || mutation.attributeName === "data-indicator")) {
89
- calculatePosition();
90
- break;
91
- }
92
- });
93
- observer.observe(option, {
94
- attributes: true,
95
- attributeFilter: [
96
- "aria-selected",
97
- "data-active",
98
- "data-indicator"
99
- ]
100
- });
101
- mutationObservers.push(observer);
102
- }
103
- });
104
- const resizeObservers = [];
105
- const observeSize = (element) => {
106
- if (!element) return;
107
- const resizeObserver = new ResizeObserver(() => {
108
- calculatePosition();
109
- });
110
- resizeObserver.observe(element);
111
- resizeObservers.push(resizeObserver);
66
+ const left = targetElement.offsetLeft;
67
+ const width = targetElement.offsetWidth;
68
+ const previous = lastPositionRef.current;
69
+ if (previous?.left === left && previous.width === width && previous.opacity === 1) return;
70
+ const newPosition = {
71
+ left,
72
+ width,
73
+ opacity: 1
112
74
  };
113
- const selectedItem = optionsRefs.current.find(selector) ?? null;
114
- if (selectedItem) observeSize(selectedItem);
115
- if (hoveredItem) observeSize(hoveredItem);
75
+ setChoiceIndicatorPosition(newPosition);
76
+ lastPositionRef.current = newPosition;
77
+ }, [optionsRefs]);
78
+ /**
79
+ * Defers the measurement to the next frame, and to a single one however many
80
+ * observers fire.
81
+ *
82
+ * Reading `offsetLeft` straight from a mutation, resize or scroll callback
83
+ * forces a synchronous reflow, and a documentation page mounts one selector
84
+ * per tab group and one per code block.
85
+ */
86
+ const scheduleMeasurement = useCallback(() => {
87
+ if (measurementFrameRef.current !== null) return;
88
+ measurementFrameRef.current = requestAnimationFrame(() => {
89
+ measurementFrameRef.current = null;
90
+ calculatePosition();
91
+ });
92
+ }, [calculatePosition]);
93
+ useEffect(() => {
94
+ scheduleMeasurement();
95
+ window.addEventListener("resize", scheduleMeasurement, { passive: true });
96
+ window.addEventListener("DOMContentLoaded", scheduleMeasurement);
97
+ const options = optionsRefs.current.filter(Boolean);
98
+ const mutationObserver = new MutationObserver(scheduleMeasurement);
99
+ const resizeObserver = new ResizeObserver(scheduleMeasurement);
116
100
  const handleMouseEnter = (event) => {
117
101
  if (hideTimeoutRef.current) {
118
102
  clearTimeout(hideTimeoutRef.current);
119
103
  hideTimeoutRef.current = null;
120
104
  }
121
- setHoveredItem(event.currentTarget);
105
+ hoveredItemRef.current = event.currentTarget;
106
+ scheduleMeasurement();
122
107
  };
123
108
  const handleMouseLeave = () => {
124
109
  if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
125
110
  hideTimeoutRef.current = setTimeout(() => {
126
- setHoveredItem(null);
127
- }, 150);
111
+ hoveredItemRef.current = null;
112
+ scheduleMeasurement();
113
+ }, HOVER_RELEASE_DELAY);
128
114
  };
129
- if (isHoverable) optionsRefs.current.forEach((option) => {
130
- option?.addEventListener("mouseenter", handleMouseEnter, { passive: true });
131
- option?.addEventListener("mouseleave", handleMouseLeave, { passive: true });
132
- });
133
- return () => {
134
- if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
135
- window.removeEventListener("resize", calculatePosition);
136
- window.removeEventListener("DOMContentLoaded", calculatePosition);
137
- mutationObservers.forEach((observer) => {
138
- observer.disconnect();
139
- });
140
- resizeObservers.forEach((observer) => {
141
- observer.disconnect();
142
- });
143
- optionsRefs.current.forEach((option) => {
144
- option?.removeEventListener("mouseenter", handleMouseEnter);
145
- option?.removeEventListener("mouseleave", handleMouseLeave);
115
+ for (const option of options) {
116
+ mutationObserver.observe(option, {
117
+ attributes: true,
118
+ attributeFilter: WATCHED_ATTRIBUTES
146
119
  });
120
+ resizeObserver.observe(option);
121
+ if (isHoverable) {
122
+ option.addEventListener("mouseenter", handleMouseEnter, { passive: true });
123
+ option.addEventListener("mouseleave", handleMouseLeave, { passive: true });
124
+ }
125
+ }
126
+ return () => {
127
+ if (measurementFrameRef.current !== null) {
128
+ cancelAnimationFrame(measurementFrameRef.current);
129
+ measurementFrameRef.current = null;
130
+ }
131
+ if (hideTimeoutRef.current) {
132
+ clearTimeout(hideTimeoutRef.current);
133
+ hideTimeoutRef.current = null;
134
+ }
135
+ window.removeEventListener("resize", scheduleMeasurement);
136
+ window.removeEventListener("DOMContentLoaded", scheduleMeasurement);
137
+ mutationObserver.disconnect();
138
+ resizeObserver.disconnect();
139
+ if (isHoverable) for (const option of options) {
140
+ option.removeEventListener("mouseenter", handleMouseEnter);
141
+ option.removeEventListener("mouseleave", handleMouseLeave);
142
+ }
147
143
  };
148
144
  }, [
149
145
  optionsRefs,
150
- selector,
151
- hoveredItem,
152
146
  itemsLength,
147
+ isHoverable,
153
148
  orientation,
154
- isHoverable
149
+ scheduleMeasurement
155
150
  ]);
156
151
  return {
157
152
  choiceIndicatorPosition,