@tcn/ui 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/draggable.module-BgelQsuJ.js +5 -0
  2. package/dist/draggable.module-BgelQsuJ.js.map +1 -0
  3. package/dist/form/field/field.js +13 -10
  4. package/dist/form/field/field.js.map +1 -1
  5. package/dist/form/field_presenters/field_presenter.d.ts +2 -2
  6. package/dist/form/field_presenters/field_presenter.d.ts.map +1 -1
  7. package/dist/form/field_presenters/field_presenter.js.map +1 -1
  8. package/dist/inputs/color_input/color_picker.js +5 -3
  9. package/dist/inputs/color_input/color_picker.js.map +1 -1
  10. package/dist/inputs/combo_box/combo_box.js +4 -2
  11. package/dist/inputs/combo_box/combo_box.js.map +1 -1
  12. package/dist/inputs/date_picker/date_picker.js +16 -14
  13. package/dist/inputs/date_picker/date_picker.js.map +1 -1
  14. package/dist/inputs/date_picker/date_picker_input.js +10 -8
  15. package/dist/inputs/date_picker/date_picker_input.js.map +1 -1
  16. package/dist/inputs/date_picker/date_picker_year_selector.js +4 -2
  17. package/dist/inputs/date_picker/date_picker_year_selector.js.map +1 -1
  18. package/dist/inputs/mask_input/key_capture_input.js +15 -12
  19. package/dist/inputs/mask_input/key_capture_input.js.map +1 -1
  20. package/dist/inputs/mask_input/mask_input.js +13 -10
  21. package/dist/inputs/mask_input/mask_input.js.map +1 -1
  22. package/dist/inputs/multiselect/multiselect.js +9 -7
  23. package/dist/inputs/multiselect/multiselect.js.map +1 -1
  24. package/dist/inputs/phone_number_input/phone_number_input.d.ts +1 -0
  25. package/dist/inputs/phone_number_input/phone_number_input.d.ts.map +1 -1
  26. package/dist/inputs/phone_number_input/phone_number_input.js +136 -133
  27. package/dist/inputs/phone_number_input/phone_number_input.js.map +1 -1
  28. package/dist/inputs/select/select.js +4 -2
  29. package/dist/inputs/select/select.js.map +1 -1
  30. package/dist/inputs/slider/slider.js +7 -5
  31. package/dist/inputs/slider/slider.js.map +1 -1
  32. package/dist/inputs/suggestions/suggestion_list.js +4 -2
  33. package/dist/inputs/suggestions/suggestion_list.js.map +1 -1
  34. package/dist/inputs/switch/switch.js +16 -14
  35. package/dist/inputs/switch/switch.js.map +1 -1
  36. package/dist/inputs/unit_input/unit_input.js +16 -14
  37. package/dist/inputs/unit_input/unit_input.js.map +1 -1
  38. package/dist/navigation/tabs/primitives/tabs_list.d.ts.map +1 -1
  39. package/dist/navigation/tabs/primitives/tabs_list.js +61 -21
  40. package/dist/navigation/tabs/primitives/tabs_list.js.map +1 -1
  41. package/dist/navigation/tabs/state/link/tab_link.d.ts.map +1 -1
  42. package/dist/navigation/tabs/state/link/tab_link.js +25 -19
  43. package/dist/navigation/tabs/state/link/tab_link.js.map +1 -1
  44. package/dist/navigation/tabs/state/link/use_tab_link.js +8 -8
  45. package/dist/navigation/tabs/state/link/use_tab_link.js.map +1 -1
  46. package/dist/navigation/tabs/state/tab.d.ts.map +1 -1
  47. package/dist/navigation/tabs/state/tab.js +8 -3
  48. package/dist/navigation/tabs/state/tab.js.map +1 -1
  49. package/dist/overlay/caret/caret.d.ts +8 -0
  50. package/dist/overlay/caret/caret.d.ts.map +1 -0
  51. package/dist/overlay/caret/caret.js +20 -0
  52. package/dist/overlay/caret/caret.js.map +1 -0
  53. package/dist/overlay/menu/menu.js +34 -32
  54. package/dist/overlay/menu/menu.js.map +1 -1
  55. package/dist/overlay/popper/legacy/popper.js +22 -20
  56. package/dist/overlay/popper/legacy/popper.js.map +1 -1
  57. package/dist/overlay/popper/preview_popper.js +12 -9
  58. package/dist/overlay/popper/preview_popper.js.map +1 -1
  59. package/dist/overlay/tethered/hooks/calculate_origin.d.ts +23 -0
  60. package/dist/overlay/tethered/hooks/calculate_origin.d.ts.map +1 -0
  61. package/dist/overlay/tethered/hooks/calculate_origin.js +41 -0
  62. package/dist/overlay/tethered/hooks/calculate_origin.js.map +1 -0
  63. package/dist/overlay/tethered/hooks/useCaretRefDimensions.d.ts +9 -0
  64. package/dist/overlay/tethered/hooks/useCaretRefDimensions.d.ts.map +1 -0
  65. package/dist/overlay/tethered/hooks/useCaretRefDimensions.js +14 -0
  66. package/dist/overlay/tethered/hooks/useCaretRefDimensions.js.map +1 -0
  67. package/dist/overlay/tethered/hooks/useTether.d.ts +1 -1
  68. package/dist/overlay/tethered/hooks/useTether.d.ts.map +1 -1
  69. package/dist/overlay/tethered/hooks/useTether.js +22 -21
  70. package/dist/overlay/tethered/hooks/useTether.js.map +1 -1
  71. package/dist/overlay/tethered/hooks/useTetherContentRect.d.ts +3 -0
  72. package/dist/overlay/tethered/hooks/useTetherContentRect.d.ts.map +1 -0
  73. package/dist/overlay/tethered/hooks/useTetherContentRect.js +36 -0
  74. package/dist/overlay/tethered/hooks/useTetherContentRect.js.map +1 -0
  75. package/dist/overlay/tethered/hooks/useTetherOrigin.d.ts +14 -0
  76. package/dist/overlay/tethered/hooks/useTetherOrigin.d.ts.map +1 -0
  77. package/dist/overlay/tethered/hooks/useTetherOrigin.js +24 -0
  78. package/dist/overlay/tethered/hooks/useTetherOrigin.js.map +1 -0
  79. package/dist/overlay/tethered/tethered.d.ts +2 -1
  80. package/dist/overlay/tethered/tethered.d.ts.map +1 -1
  81. package/dist/overlay/tethered/tethered.js +71 -38
  82. package/dist/overlay/tethered/tethered.js.map +1 -1
  83. package/dist/stacks/box/box.js +29 -27
  84. package/dist/stacks/box/box.js.map +1 -1
  85. package/dist/stacks/h_collapsible_box.js +14 -12
  86. package/dist/stacks/h_collapsible_box.js.map +1 -1
  87. package/dist/stacks/v_collapsible_box.js +8 -6
  88. package/dist/stacks/v_collapsible_box.js.map +1 -1
  89. package/dist/surfaces/pop_confirm/pop_confirm.d.ts.map +1 -1
  90. package/dist/surfaces/pop_confirm/pop_confirm.js +14 -13
  91. package/dist/surfaces/pop_confirm/pop_confirm.js.map +1 -1
  92. package/dist/surfaces/tooltip/tooltip.d.ts.map +1 -1
  93. package/dist/surfaces/tooltip/tooltip.js +12 -11
  94. package/dist/surfaces/tooltip/tooltip.js.map +1 -1
  95. package/dist/tethered.css +1 -1
  96. package/dist/themes/themes/ergo/ergo_theme.css +1 -1
  97. package/dist/themes/themes/ergo/ergo_theme.js +87 -57
  98. package/dist/themes/themes/ergo/ergo_theme.js.map +1 -1
  99. package/dist/themes/themes/windows_98/windows_98.css +1 -1
  100. package/dist/themes/themes/windows_98/windows_98_theme.js +18 -18
  101. package/dist/themes/themes/windows_98/windows_98_theme.js.map +1 -1
  102. package/dist/utils/dnd/draggable/draggable.js +13 -12
  103. package/dist/utils/dnd/draggable/draggable.js.map +1 -1
  104. package/dist/utils/index.d.ts +2 -0
  105. package/dist/utils/index.d.ts.map +1 -1
  106. package/dist/utils/index.js +23 -19
  107. package/dist/utils/index.js.map +1 -1
  108. package/package.json +11 -11
  109. package/src/form/field_presenters/field_presenter.ts +3 -3
  110. package/src/inputs/phone_number_input/phone_number_input.stories.tsx +24 -0
  111. package/src/inputs/phone_number_input/phone_number_input.tsx +8 -6
  112. package/src/navigation/tabs/primitives/tabs_list.tsx +46 -2
  113. package/src/navigation/tabs/state/link/tab_link.tsx +4 -1
  114. package/src/navigation/tabs/state/link/use_tab_link.ts +4 -4
  115. package/src/navigation/tabs/state/tab.tsx +10 -0
  116. package/src/overlay/{carrot/carrot.stories.tsx → caret/caret.stories.tsx} +14 -14
  117. package/src/overlay/caret/caret.tsx +24 -0
  118. package/src/overlay/tethered/__stories__/shared/base_story_config.ts +8 -0
  119. package/src/overlay/tethered/hooks/calculate_origin.ts +74 -0
  120. package/src/overlay/tethered/hooks/useCaretRefDimensions.ts +22 -0
  121. package/src/overlay/tethered/hooks/useTether.ts +4 -3
  122. package/src/overlay/tethered/hooks/useTetherContentRect.ts +49 -0
  123. package/src/overlay/tethered/hooks/useTetherOrigin.ts +49 -0
  124. package/src/overlay/tethered/tethered.module.css +55 -0
  125. package/src/overlay/tethered/tethered.tsx +44 -6
  126. package/src/surfaces/panel/__stories__/panel.stories.tsx +62 -27
  127. package/src/surfaces/panel/__stories__/panel_stories.module.css +14 -1
  128. package/src/surfaces/pop_confirm/pop_confirm.tsx +4 -3
  129. package/src/surfaces/tooltip/tooltip.tsx +1 -0
  130. package/src/themes/themes/ergo/ergo_theme.css +87 -57
  131. package/src/themes/themes/windows_98/windows_98.css +18 -18
  132. package/src/utils/index.ts +3 -0
  133. package/dist/overlay/carrot/base_carrot.d.ts +0 -8
  134. package/dist/overlay/carrot/base_carrot.d.ts.map +0 -1
  135. package/dist/overlay/carrot/base_carrot.js +0 -21
  136. package/dist/overlay/carrot/base_carrot.js.map +0 -1
  137. package/src/overlay/carrot/base_carrot.tsx +0 -24
@@ -1,33 +1,37 @@
1
1
  import { ClickAwayListener as o, isEventWithinElement as t } from "./click_away_listener.js";
2
- import { FocusRedirect as m } from "./focus_redirect.js";
3
- import { ScrollAwayListener as s } from "./scroll_away_listener.js";
4
- import { useDraggable as a } from "./dnd/hooks/use_draggable.js";
2
+ import { FocusRedirect as a } from "./focus_redirect.js";
3
+ import { ScrollAwayListener as p } from "./scroll_away_listener.js";
4
+ import { useDraggable as s } from "./dnd/hooks/use_draggable.js";
5
5
  import { makeContextHook as i } from "./hooks/make_context_hook.js";
6
- import { useForkRef as u } from "./hooks/use_fork_ref.js";
6
+ import { useForkRef as l } from "./hooks/use_fork_ref.js";
7
7
  import { useMediaQuery as d } from "./hooks/use_media_query.js";
8
- import { TriggerConfig as R, useResizeObserver as c } from "./hooks/use_resize_observer.js";
9
- import { defaultValue as C } from "./default_value.js";
10
- import { CalendarDatesGenerator as v } from "./calendar/calendar_dates_generator.js";
11
- import { getDaysOfWeek as M } from "./calendar/get_days_of_week.js";
12
- import { getMonthsOfYear as b } from "./calendar/get_months_of_year.js";
8
+ import { TriggerConfig as D, useResizeObserver as R } from "./hooks/use_resize_observer.js";
9
+ import { defaultValue as y } from "./default_value.js";
10
+ import { CalendarDatesGenerator as b } from "./calendar/calendar_dates_generator.js";
11
+ import { getDaysOfWeek as v } from "./calendar/get_days_of_week.js";
12
+ import { getMonthsOfYear as O } from "./calendar/get_months_of_year.js";
13
13
  import { Month as A } from "./calendar/month.js";
14
14
  import { ResponsiveRenderer as F } from "./responsive/responsive_renderer.js";
15
+ import { Draggable as L } from "./dnd/draggable/draggable.js";
16
+ import { DragHandle as z } from "./dnd/handle.js";
15
17
  export {
16
- v as CalendarDatesGenerator,
18
+ b as CalendarDatesGenerator,
17
19
  o as ClickAwayListener,
18
- m as FocusRedirect,
20
+ z as DragHandle,
21
+ L as Draggable,
22
+ a as FocusRedirect,
19
23
  A as Month,
20
24
  F as ResponsiveRenderer,
21
- s as ScrollAwayListener,
22
- R as TriggerConfig,
23
- C as defaultValue,
24
- M as getDaysOfWeek,
25
- b as getMonthsOfYear,
25
+ p as ScrollAwayListener,
26
+ D as TriggerConfig,
27
+ y as defaultValue,
28
+ v as getDaysOfWeek,
29
+ O as getMonthsOfYear,
26
30
  t as isEventWithinElement,
27
31
  i as makeContextHook,
28
- a as useDraggable,
29
- u as useForkRef,
32
+ s as useDraggable,
33
+ l as useForkRef,
30
34
  d as useMediaQuery,
31
- c as useResizeObserver
35
+ R as useResizeObserver
32
36
  };
33
37
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tcn/ui",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "author": "TCN",
@@ -141,8 +141,8 @@
141
141
  "@fontsource/lato": "^5.2.7",
142
142
  "clsx": "^2.1.1",
143
143
  "react-color": "^2.19.3",
144
- "@tcn/state": "1.1.1",
145
- "@tcn/icons": "2.2.1"
144
+ "@tcn/icons": "2.2.1",
145
+ "@tcn/state": "1.2.0"
146
146
  },
147
147
  "scripts": {
148
148
  "build": "vite build",
@@ -154,15 +154,15 @@
154
154
  "start": "pnpm storybook",
155
155
  "test": "vitest run",
156
156
  "test-coverage": "vitest run --coverage",
157
- "check-all": "concurrently 'pnpm check-types' 'pnpm dlx @biomejs/biome check .'",
157
+ "check-all": "concurrently 'pnpm check-types' 'pnpm exec biome check .'",
158
158
  "check-types": "tsc --project tsconfig.typecheck.json --noEmit",
159
- "check-format": "pnpm dlx @biomejs/biome format .",
160
- "check-lint": "pnpm dlx @biomejs/biome lint .",
161
- "check-imports": "pnpm dlx @biomejs/biome check --formatter-enabled=false --linter-enabled=false --assist-enabled=true",
162
- "fix-all": "pnpm dlx @biomejs/biome check --write",
163
- "fix-format": "pnpm dlx @biomejs/biome format --write",
164
- "fix-lint": "pnpm dlx @biomejs/biome lint --write",
165
- "fix-imports": "pnpm dlx @biomejs/biome check --write --unsafe --formatter-enabled=false --linter-enabled=false --assist-enabled=true",
159
+ "check-format": "pnpm exec biome format .",
160
+ "check-lint": "pnpm exec biome lint .",
161
+ "check-imports": "pnpm exec biome check --formatter-enabled=false --linter-enabled=false --assist-enabled=true",
162
+ "fix-all": "pnpm exec biome check --write",
163
+ "fix-format": "pnpm exec biome format --write",
164
+ "fix-lint": "pnpm exec biome lint --write",
165
+ "fix-imports": "pnpm exec biome check --write --unsafe --formatter-enabled=false --linter-enabled=false --assist-enabled=true",
166
166
  "publish-dry-run": "pnpm build && pnpm publish --dry-run --force --no-git-checks"
167
167
  }
168
168
  }
@@ -1,4 +1,4 @@
1
- import { ISubscription, Runner, Signal, WeakPromise } from '@tcn/state';
1
+ import { ISubscription, Runner, Signal, IWeakPromise } from '@tcn/state';
2
2
 
3
3
  function fastCopy<T>(value: T): T {
4
4
  return JSON.parse(JSON.stringify(value));
@@ -19,7 +19,7 @@ export interface FieldState<T> {
19
19
 
20
20
  export interface FieldOptions<T> {
21
21
  description?: string;
22
- validate?: (value: T) => Promise<void> | void;
22
+ validate?: (value: T) => Promise<void> | void | IWeakPromise<void>;
23
23
  validateOnChange?: boolean;
24
24
  equalityCheck?: (a: T, b: T) => boolean;
25
25
  copyInitialValue?: (value: T) => T;
@@ -35,7 +35,7 @@ export class FieldPresenter<T> {
35
35
  private _cloneInitialValue: (v: T) => T;
36
36
 
37
37
  private _validationRunner: Runner<void>;
38
- private _validate: (value: T) => Promise<void> | void | WeakPromise<void>;
38
+ private _validate: (value: T) => Promise<void> | void | IWeakPromise<void>;
39
39
  private _subscriptions: ISubscription<any>[];
40
40
  private _validateOnChange: boolean;
41
41
  private _cancelToken = 'field_presenter_cancel';
@@ -57,6 +57,30 @@ export function PhoneNumberInput() {
57
57
  <Base disabled />
58
58
  </td>
59
59
  </tr>
60
+ <tr>
61
+ <td>Disabled Phone Number</td>
62
+ <td>
63
+ <Base disabledPhoneNumber>
64
+ <Option value="+14355865953" label="John Doe" keywords={['john', 'doe']}>
65
+ John Doe - +1 (435) 586-5953
66
+ </Option>
67
+ <Option
68
+ value="+984355865954"
69
+ label="Jane Smith"
70
+ keywords={['jane', 'smith']}
71
+ >
72
+ Jane Smith - +98 (435) 586-5954
73
+ </Option>
74
+ <Option
75
+ value="+14355865955"
76
+ label="Bob Johnson"
77
+ keywords={['bob', 'johnson']}
78
+ >
79
+ Bob Johnson - +1 (435) 586-5955
80
+ </Option>
81
+ </Base>
82
+ </td>
83
+ </tr>
60
84
  <tr>
61
85
  <td>With Value</td>
62
86
  <td>
@@ -130,6 +130,7 @@ export interface PhoneNumberInputProps
130
130
  countrySelectRef?: React.Ref<HTMLButtonElement>;
131
131
  phoneNumberInputRef?: React.Ref<HTMLInputElement>;
132
132
  disabled?: boolean;
133
+ disabledPhoneNumber?: boolean;
133
134
  allowedCountryCodes?: string[];
134
135
  children?: React.ReactElement<OptionProps> | React.ReactElement<OptionProps>[];
135
136
  }
@@ -144,6 +145,7 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
144
145
  countrySelectRef: countryRef,
145
146
  phoneNumberInputRef: numberRef,
146
147
  disabled = false,
148
+ disabledPhoneNumber = false,
147
149
  allowedCountryCodes,
148
150
  children,
149
151
  ...props
@@ -341,8 +343,8 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
341
343
  width="auto"
342
344
  value={obfuscateValue ? '' : countryCode}
343
345
  onChange={changeCountry}
344
- disabled={disabled || obfuscateValue}
345
- data-is-disabled={disabled || obfuscateValue}
346
+ disabled={disabled || obfuscateValue || disabledPhoneNumber}
347
+ data-is-disabled={disabled || obfuscateValue || disabledPhoneNumber}
346
348
  data-is-obfuscated={obfuscateValue}
347
349
  placeholder={obfuscateValue ? '––' : undefined}
348
350
  >
@@ -364,8 +366,8 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
364
366
  value=""
365
367
  mask={createObfuscatedMasks(currentMasks)}
366
368
  onChange={handleObfuscatedInputChange}
367
- disabled={disabled}
368
- data-is-disabled={disabled}
369
+ disabled={disabled || disabledPhoneNumber}
370
+ data-is-disabled={disabled || disabledPhoneNumber}
369
371
  data-has-phone-book={showPhoneBook}
370
372
  data-is-obfuscated={true}
371
373
  className={clsx(
@@ -386,8 +388,8 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
386
388
  value={phoneNumber}
387
389
  mask={currentMasks}
388
390
  onChange={transformValue}
389
- disabled={disabled}
390
- data-is-disabled={disabled}
391
+ disabled={disabled || disabledPhoneNumber}
392
+ data-is-disabled={disabled || disabledPhoneNumber}
391
393
  data-has-phone-book={showPhoneBook}
392
394
  data-is-obfuscated={false}
393
395
  className={clsx(styles['phone-number-input'], 'tcn-phone-number-input')}
@@ -1,4 +1,4 @@
1
- import { forwardRef, type FC, type PropsWithChildren } from 'react';
1
+ import { forwardRef, useCallback, type FC, type PropsWithChildren } from 'react';
2
2
  import { type BaseButtonProps } from '../../../actions/index.js';
3
3
  import { HStack, type HStackProps } from '../../../stacks/h_stack.js';
4
4
  import clsx from 'clsx';
@@ -6,15 +6,59 @@ import { Toggle } from '../../../actions/toggle/toggle.js';
6
6
 
7
7
  export type TabsListProps = HStackProps;
8
8
 
9
+ const navigateTabs = (event: React.KeyboardEvent<HTMLDivElement>) => {
10
+ const tabs = Array.from(event.currentTarget.querySelectorAll(':scope > [role="tab"]'));
11
+ const currentTab = event.currentTarget.querySelector(':focus');
12
+ const currentIndex = currentTab ? tabs.indexOf(currentTab) : -1;
13
+ if (currentIndex === -1) return; // Exit if the focused element is not a tab
14
+ let newIndex = 0;
15
+
16
+ switch (event.key) {
17
+ case 'ArrowRight':
18
+ newIndex = (currentIndex + 1) % tabs.length;
19
+ break;
20
+ case 'ArrowLeft':
21
+ newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
22
+ break;
23
+ case 'Home':
24
+ newIndex = 0;
25
+ break;
26
+ case 'End':
27
+ newIndex = tabs.length - 1;
28
+ break;
29
+ default:
30
+ return; // Exit if the key is not recognized
31
+ }
32
+
33
+ event.preventDefault();
34
+ event.stopPropagation();
35
+ tabs[newIndex]['focus']();
36
+ };
37
+
9
38
  export const TabsList: FC<PropsWithChildren<TabsListProps>> = ({
10
39
  children,
11
40
  className,
12
41
  role = 'tablist',
13
42
  as = 'menu',
43
+ onKeyDown,
14
44
  ...props
15
45
  }) => {
46
+ const handleKeyDown = useCallback(
47
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
48
+ navigateTabs(event);
49
+ onKeyDown && onKeyDown(event);
50
+ },
51
+ [onKeyDown]
52
+ );
53
+
16
54
  return (
17
- <HStack as={as} role={role} className={clsx('tcn-tabs-list', className)} {...props}>
55
+ <HStack
56
+ onKeyDown={handleKeyDown}
57
+ as={as}
58
+ role={role}
59
+ className={clsx('tcn-tabs-list', className)}
60
+ {...props}
61
+ >
18
62
  {children}
19
63
  </HStack>
20
64
  );
@@ -13,7 +13,7 @@ export interface TabLinkProps
13
13
  TabLinkOwnProps {}
14
14
 
15
15
  export const TabLink = forwardRef<HTMLButtonElement, PropsWithChildren<TabLinkProps>>(
16
- ({ children, value, onClick, minWidth, maxWidth, ...props }, forwardedRef) => {
16
+ ({ children, value, onClick, minWidth, maxWidth, id, ...props }, forwardedRef) => {
17
17
  const { ref: internalRef, isMatch } = useTabLink(value);
18
18
  const state = useTabs();
19
19
  const ref = useForkRef(internalRef, forwardedRef);
@@ -36,6 +36,9 @@ export const TabLink = forwardRef<HTMLButtonElement, PropsWithChildren<TabLinkPr
36
36
  onClick={handleClick}
37
37
  minWidth={pickMinWidth}
38
38
  maxWidth={pickMaxWidth}
39
+ id={`tab-${value}`}
40
+ aria-controls={`tabpanel-${value}`}
41
+ tabIndex={isMatch ? 0 : -1}
39
42
  {...props}
40
43
  >
41
44
  {children}
@@ -3,15 +3,15 @@ import { useTabs } from '../context.js';
3
3
  import { useTrackActiveItemRectangle } from '../../../../utils/css_utils.js';
4
4
 
5
5
  export function useTabLink(value: string) {
6
- const state = useTabs();
7
- const isMatch = state.value === value;
6
+ const { setActiveTrigger, value: currentValue } = useTabs();
7
+ const isMatch = currentValue === value;
8
8
  const { ref, rectangle } = useTrackActiveItemRectangle(isMatch);
9
9
 
10
10
  useLayoutEffect(() => {
11
11
  if (rectangle) {
12
- state.setActiveTrigger(rectangle);
12
+ setActiveTrigger(rectangle);
13
13
  }
14
- }, [rectangle, state.setActiveTrigger]);
14
+ }, [rectangle, setActiveTrigger]);
15
15
 
16
16
  return { ref, isMatch };
17
17
  }
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import type { FC, PropsWithChildren } from 'react';
2
3
  import { useTabs } from './context.js';
3
4
 
@@ -8,5 +9,14 @@ export interface TabProps {
8
9
  export const Tab: FC<PropsWithChildren<TabProps>> = ({ value, children }) => {
9
10
  const state = useTabs();
10
11
  if (state.value !== value) return null;
12
+
13
+ if (children && React.isValidElement(children)) {
14
+ return React.cloneElement(children, {
15
+ ...children.props,
16
+ id: children.props.id ?? `tabpanel-${value}`,
17
+ role: children.props.role ?? 'tabpanel',
18
+ });
19
+ }
20
+
11
21
  return children;
12
22
  };
@@ -1,19 +1,19 @@
1
1
  import type { StoryObj } from '@storybook/react-vite';
2
2
  import { Box } from '../../stacks/index.js';
3
3
  import { VStack } from '../../stacks/v_stack.js';
4
- import { BaseCarrot } from './base_carrot.js';
4
+ import { Caret } from './caret.js';
5
5
 
6
6
  export default {
7
- title: 'Overlays/Carrot',
8
- component: BaseCarrot,
7
+ title: 'Overlays/Caret',
8
+ component: Caret,
9
9
  tags: ['autodocs'],
10
10
  };
11
11
 
12
- interface CarrotStoryProps {
12
+ interface CaretStoryProps {
13
13
  direction: 'top' | 'bottom' | 'start' | 'end';
14
14
  }
15
15
 
16
- const CarrotStory = ({ direction }: CarrotStoryProps) => {
16
+ const CaretStory = ({ direction }: CaretStoryProps) => {
17
17
  return (
18
18
  <VStack
19
19
  minWidth="100px"
@@ -22,32 +22,32 @@ const CarrotStory = ({ direction }: CarrotStoryProps) => {
22
22
  style={{ backgroundColor: 'gray' }}
23
23
  >
24
24
  <Box width="100px" height="100px" padding="24px" style={{ backgroundColor: 'red' }}>
25
- <BaseCarrot direction={direction} />
25
+ <Caret direction={direction} />
26
26
  </Box>
27
27
  </VStack>
28
28
  );
29
29
  };
30
30
 
31
- export const Top: StoryObj<CarrotStoryProps> = {
32
- render: args => <CarrotStory {...args} />,
31
+ export const Top: StoryObj<CaretStoryProps> = {
32
+ render: args => <CaretStory {...args} />,
33
33
  args: {
34
34
  direction: 'top',
35
35
  },
36
36
  };
37
- export const Bottom: StoryObj<CarrotStoryProps> = {
38
- render: args => <CarrotStory {...args} />,
37
+ export const Bottom: StoryObj<CaretStoryProps> = {
38
+ render: args => <CaretStory {...args} />,
39
39
  args: {
40
40
  direction: 'bottom',
41
41
  },
42
42
  };
43
- export const Start: StoryObj<CarrotStoryProps> = {
44
- render: args => <CarrotStory {...args} />,
43
+ export const Start: StoryObj<CaretStoryProps> = {
44
+ render: args => <CaretStory {...args} />,
45
45
  args: {
46
46
  direction: 'start',
47
47
  },
48
48
  };
49
- export const End: StoryObj<CarrotStoryProps> = {
50
- render: args => <CarrotStory {...args} />,
49
+ export const End: StoryObj<CaretStoryProps> = {
50
+ render: args => <CaretStory {...args} />,
51
51
  args: {
52
52
  direction: 'end',
53
53
  },
@@ -0,0 +1,24 @@
1
+ import clsx from 'clsx';
2
+ import { forwardRef } from 'react';
3
+ import { Box, type BoxProps } from '../../stacks/box/box.js';
4
+
5
+ export interface CaretOwnProps {
6
+ direction: 'top' | 'bottom' | 'start' | 'end';
7
+ }
8
+
9
+ export interface CaretProps extends CaretOwnProps, BoxProps {}
10
+
11
+ export const Caret = forwardRef<HTMLElement, CaretProps>(function Caret(
12
+ { direction, className, as = 'span', ...rest },
13
+ ref
14
+ ) {
15
+ return (
16
+ <Box
17
+ ref={ref}
18
+ as="span"
19
+ data-direction={direction}
20
+ className={clsx('tcn-caret', className)}
21
+ {...rest}
22
+ />
23
+ );
24
+ });
@@ -9,6 +9,7 @@ type TetheredStoryArgs = Pick<
9
9
  | 'horizontalOrigin'
10
10
  | 'verticalOffset'
11
11
  | 'horizontalOffset'
12
+ | 'precision'
12
13
  >;
13
14
 
14
15
  export const tetheredArgTypes: ArgTypes<TetheredStoryArgs> = {
@@ -40,6 +41,12 @@ export const tetheredArgTypes: ArgTypes<TetheredStoryArgs> = {
40
41
  control: { type: 'number' },
41
42
  description: 'The offset to position the popper horizontally.',
42
43
  },
44
+ precision: {
45
+ options: ['high', 'low'],
46
+ control: { type: 'radio' },
47
+ description:
48
+ 'The precision level for positioning. High precision includes caret offset calculations.',
49
+ },
43
50
  };
44
51
 
45
52
  export const tetheredArgs: TetheredStoryArgs = {
@@ -49,4 +56,5 @@ export const tetheredArgs: TetheredStoryArgs = {
49
56
  horizontalOrigin: 'center',
50
57
  verticalOffset: 0,
51
58
  horizontalOffset: 0,
59
+ precision: 'low',
52
60
  };
@@ -0,0 +1,74 @@
1
+ import type { Position, Rectangle } from '../../../utils/index.js';
2
+ import type { HorizontalTether, VerticalTether } from '../types.js';
3
+
4
+ export type CaretDirection = 'top' | 'bottom' | 'start' | 'end' | 'none';
5
+
6
+ /**
7
+ * Calculates the offset from the tethered element's top-left corner to the origin point
8
+ * based on tether dimensions and origin alignment.
9
+ */
10
+ export function getOriginOffset(
11
+ tether: Rectangle,
12
+ hOrigin: HorizontalTether,
13
+ vOrigin: VerticalTether
14
+ ): { yOffset: number; xOffset: number } {
15
+ let yOffset = 0;
16
+ let xOffset = 0;
17
+
18
+ // Calculate vertical offset
19
+ switch (vOrigin) {
20
+ case 'top':
21
+ yOffset = 0;
22
+ break;
23
+ case 'center':
24
+ yOffset = tether.dimensions.height / 2;
25
+ break;
26
+ case 'bottom':
27
+ yOffset = tether.dimensions.height;
28
+ break;
29
+ }
30
+
31
+ // Calculate horizontal offset
32
+ switch (hOrigin) {
33
+ case 'start':
34
+ xOffset = 0;
35
+ break;
36
+ case 'center':
37
+ xOffset = tether.dimensions.width / 2;
38
+ break;
39
+ case 'end':
40
+ xOffset = tether.dimensions.width;
41
+ break;
42
+ }
43
+
44
+ return { yOffset, xOffset };
45
+ }
46
+
47
+ /**
48
+ * Adds offset to baseline position to get absolute origin position.
49
+ */
50
+ export function getOriginPosition(
51
+ baselinePosition: Position,
52
+ offset: { yOffset: number; xOffset: number }
53
+ ): Position {
54
+ return {
55
+ x: baselinePosition.x + offset.xOffset,
56
+ y: baselinePosition.y + offset.yOffset,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Determines caret direction based on origin values.
62
+ */
63
+ export function getOriginDirection(
64
+ vOrigin: VerticalTether,
65
+ hOrigin: HorizontalTether
66
+ ): CaretDirection {
67
+ if (vOrigin !== 'center') {
68
+ return vOrigin;
69
+ } else if (hOrigin !== 'center') {
70
+ return hOrigin;
71
+ } else {
72
+ return 'none';
73
+ }
74
+ }
@@ -0,0 +1,22 @@
1
+ import { useLayoutEffect, useState } from 'react';
2
+ import type { CaretDirection } from './calculate_origin.js';
3
+
4
+ export function useCaretRefDimensions(
5
+ precision: 'high' | 'low',
6
+ originDirection: CaretDirection
7
+ ): {
8
+ caretElementRef: (element: HTMLElement | null) => void;
9
+ caretSize: { width: number; height: number };
10
+ } {
11
+ const [caretElement, setCaretElement] = useState<HTMLElement | null>(null);
12
+ const [caretSize, setCaretSize] = useState({ width: 0, height: 0 });
13
+
14
+ useLayoutEffect(() => {
15
+ if (precision === 'high' && originDirection !== 'none' && caretElement) {
16
+ const rect = caretElement.getBoundingClientRect();
17
+ setCaretSize({ width: rect.width, height: rect.height });
18
+ }
19
+ }, [precision, originDirection, caretElement]);
20
+
21
+ return { caretElementRef: setCaretElement, caretSize };
22
+ }
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useLayoutEffect, useRef, useState } from 'react';
2
- import type { HorizontalTether, VerticalTether } from '../types.js';
3
2
  import { type Rectangle } from '../../../utils/index.js';
3
+ import type { HorizontalTether, VerticalTether } from '../types.js';
4
4
  import { calculateTetheredPosition } from './calculate_position.js';
5
5
 
6
6
  export interface UseTetherParams {
@@ -65,12 +65,13 @@ export function useTether({
65
65
  horizontalOffset,
66
66
  ]);
67
67
 
68
- // Update the position when the window is resized
69
68
  useLayoutEffect(() => {
70
69
  const update = () => {
71
70
  const newPosition = getPosition();
72
71
  if (!newPosition) return;
73
- if (position.top !== newPosition.top || position.left !== newPosition.left) {
72
+ const hasChanged =
73
+ position.top !== newPosition.top || position.left !== newPosition.left;
74
+ if (hasChanged) {
74
75
  setPosition(newPosition);
75
76
  }
76
77
  };
@@ -0,0 +1,49 @@
1
+ import { useLayoutEffect, useState } from 'react';
2
+ import type { Rectangle } from '../../../utils/index.js';
3
+
4
+ function getTetherContentRect(element: HTMLElement | null): Rectangle {
5
+ if (!element) {
6
+ return {
7
+ dimensions: { width: 0, height: 0 },
8
+ position: { x: 0, y: 0 },
9
+ };
10
+ }
11
+
12
+ const rect = element.getBoundingClientRect();
13
+ return {
14
+ dimensions: {
15
+ width: rect.width,
16
+ height: rect.height,
17
+ },
18
+ position: {
19
+ x: rect.left,
20
+ y: rect.top,
21
+ },
22
+ };
23
+ }
24
+
25
+ export function useTetherContentRect(ref: React.RefObject<HTMLElement>): Rectangle {
26
+ const [contentRect, setContentRect] = useState<Rectangle>(() =>
27
+ getTetherContentRect(ref.current)
28
+ );
29
+
30
+ useLayoutEffect(() => {
31
+ const update = () => {
32
+ setContentRect(getTetherContentRect(ref.current));
33
+ };
34
+
35
+ update();
36
+
37
+ // Track resize events
38
+ window.addEventListener('resize', update);
39
+ // Track scroll events
40
+ window.addEventListener('scroll', update, true);
41
+
42
+ return () => {
43
+ window.removeEventListener('resize', update);
44
+ window.removeEventListener('scroll', update, true);
45
+ };
46
+ }, [ref]);
47
+
48
+ return contentRect;
49
+ }