tyrell-react 1.0.0-RC10

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +410 -0
  3. package/dist/components/TyButton.d.ts +52 -0
  4. package/dist/components/TyButton.d.ts.map +1 -0
  5. package/dist/components/TyButton.js +76 -0
  6. package/dist/components/TyButton.js.map +1 -0
  7. package/dist/components/TyCalendar.d.ts +63 -0
  8. package/dist/components/TyCalendar.d.ts.map +1 -0
  9. package/dist/components/TyCalendar.js +128 -0
  10. package/dist/components/TyCalendar.js.map +1 -0
  11. package/dist/components/TyCalendarMonth.d.ts +32 -0
  12. package/dist/components/TyCalendarMonth.d.ts.map +1 -0
  13. package/dist/components/TyCalendarMonth.js +54 -0
  14. package/dist/components/TyCalendarMonth.js.map +1 -0
  15. package/dist/components/TyCalendarNavigation.d.ts +21 -0
  16. package/dist/components/TyCalendarNavigation.d.ts.map +1 -0
  17. package/dist/components/TyCalendarNavigation.js +50 -0
  18. package/dist/components/TyCalendarNavigation.js.map +1 -0
  19. package/dist/components/TyCheckbox.d.ts +39 -0
  20. package/dist/components/TyCheckbox.d.ts.map +1 -0
  21. package/dist/components/TyCheckbox.js +76 -0
  22. package/dist/components/TyCheckbox.js.map +1 -0
  23. package/dist/components/TyCopy.d.ts +21 -0
  24. package/dist/components/TyCopy.d.ts.map +1 -0
  25. package/dist/components/TyCopy.js +46 -0
  26. package/dist/components/TyCopy.js.map +1 -0
  27. package/dist/components/TyDatePicker.d.ts +45 -0
  28. package/dist/components/TyDatePicker.d.ts.map +1 -0
  29. package/dist/components/TyDatePicker.js +122 -0
  30. package/dist/components/TyDatePicker.js.map +1 -0
  31. package/dist/components/TyDropdown.d.ts +62 -0
  32. package/dist/components/TyDropdown.d.ts.map +1 -0
  33. package/dist/components/TyDropdown.js +124 -0
  34. package/dist/components/TyDropdown.js.map +1 -0
  35. package/dist/components/TyFileUpload.d.ts +31 -0
  36. package/dist/components/TyFileUpload.d.ts.map +1 -0
  37. package/dist/components/TyFileUpload.js +56 -0
  38. package/dist/components/TyFileUpload.js.map +1 -0
  39. package/dist/components/TyIcon.d.ts +17 -0
  40. package/dist/components/TyIcon.d.ts.map +1 -0
  41. package/dist/components/TyIcon.js +42 -0
  42. package/dist/components/TyIcon.js.map +1 -0
  43. package/dist/components/TyInput.d.ts +65 -0
  44. package/dist/components/TyInput.d.ts.map +1 -0
  45. package/dist/components/TyInput.js +134 -0
  46. package/dist/components/TyInput.js.map +1 -0
  47. package/dist/components/TyModal.d.ts +48 -0
  48. package/dist/components/TyModal.d.ts.map +1 -0
  49. package/dist/components/TyModal.js +120 -0
  50. package/dist/components/TyModal.js.map +1 -0
  51. package/dist/components/TyMultiselect.d.ts +57 -0
  52. package/dist/components/TyMultiselect.d.ts.map +1 -0
  53. package/dist/components/TyMultiselect.js +111 -0
  54. package/dist/components/TyMultiselect.js.map +1 -0
  55. package/dist/components/TyOption.d.ts +10 -0
  56. package/dist/components/TyOption.d.ts.map +1 -0
  57. package/dist/components/TyOption.js +29 -0
  58. package/dist/components/TyOption.js.map +1 -0
  59. package/dist/components/TyPopup.d.ts +24 -0
  60. package/dist/components/TyPopup.d.ts.map +1 -0
  61. package/dist/components/TyPopup.js +70 -0
  62. package/dist/components/TyPopup.js.map +1 -0
  63. package/dist/components/TyRadio.d.ts +20 -0
  64. package/dist/components/TyRadio.d.ts.map +1 -0
  65. package/dist/components/TyRadio.js +35 -0
  66. package/dist/components/TyRadio.js.map +1 -0
  67. package/dist/components/TyRadioGroup.d.ts +40 -0
  68. package/dist/components/TyRadioGroup.d.ts.map +1 -0
  69. package/dist/components/TyRadioGroup.js +61 -0
  70. package/dist/components/TyRadioGroup.js.map +1 -0
  71. package/dist/components/TyResizeObserver.d.ts +11 -0
  72. package/dist/components/TyResizeObserver.d.ts.map +1 -0
  73. package/dist/components/TyResizeObserver.js +28 -0
  74. package/dist/components/TyResizeObserver.js.map +1 -0
  75. package/dist/components/TyScrollContainer.d.ts +25 -0
  76. package/dist/components/TyScrollContainer.d.ts.map +1 -0
  77. package/dist/components/TyScrollContainer.js +61 -0
  78. package/dist/components/TyScrollContainer.js.map +1 -0
  79. package/dist/components/TyStep.d.ts +17 -0
  80. package/dist/components/TyStep.d.ts.map +1 -0
  81. package/dist/components/TyStep.js +35 -0
  82. package/dist/components/TyStep.js.map +1 -0
  83. package/dist/components/TySwitch.d.ts +35 -0
  84. package/dist/components/TySwitch.d.ts.map +1 -0
  85. package/dist/components/TySwitch.js +59 -0
  86. package/dist/components/TySwitch.js.map +1 -0
  87. package/dist/components/TyTab.d.ts +13 -0
  88. package/dist/components/TyTab.d.ts.map +1 -0
  89. package/dist/components/TyTab.js +34 -0
  90. package/dist/components/TyTab.js.map +1 -0
  91. package/dist/components/TyTabs.d.ts +23 -0
  92. package/dist/components/TyTabs.d.ts.map +1 -0
  93. package/dist/components/TyTabs.js +48 -0
  94. package/dist/components/TyTabs.js.map +1 -0
  95. package/dist/components/TyTag.d.ts +22 -0
  96. package/dist/components/TyTag.d.ts.map +1 -0
  97. package/dist/components/TyTag.js +51 -0
  98. package/dist/components/TyTag.js.map +1 -0
  99. package/dist/components/TyTextarea.d.ts +37 -0
  100. package/dist/components/TyTextarea.d.ts.map +1 -0
  101. package/dist/components/TyTextarea.js +116 -0
  102. package/dist/components/TyTextarea.js.map +1 -0
  103. package/dist/components/TyTooltip.d.ts +17 -0
  104. package/dist/components/TyTooltip.d.ts.map +1 -0
  105. package/dist/components/TyTooltip.js +41 -0
  106. package/dist/components/TyTooltip.js.map +1 -0
  107. package/dist/components/TyWizard.d.ts +26 -0
  108. package/dist/components/TyWizard.d.ts.map +1 -0
  109. package/dist/components/TyWizard.js +50 -0
  110. package/dist/components/TyWizard.js.map +1 -0
  111. package/dist/components/index.d.ts +112 -0
  112. package/dist/components/index.d.ts.map +1 -0
  113. package/dist/components/index.js +127 -0
  114. package/dist/components/index.js.map +1 -0
  115. package/dist/utils/react-version.d.ts +2 -0
  116. package/dist/utils/react-version.d.ts.map +1 -0
  117. package/dist/utils/react-version.js +8 -0
  118. package/dist/utils/react-version.js.map +1 -0
  119. package/dist/utils/use-boolean-prop.d.ts +36 -0
  120. package/dist/utils/use-boolean-prop.d.ts.map +1 -0
  121. package/dist/utils/use-boolean-prop.js +62 -0
  122. package/dist/utils/use-boolean-prop.js.map +1 -0
  123. package/dist/version.d.ts +3 -0
  124. package/dist/version.d.ts.map +1 -0
  125. package/dist/version.js +6 -0
  126. package/dist/version.js.map +1 -0
  127. package/package.json +47 -0
  128. package/src/components/EventConventionTest.tsx +155 -0
  129. package/src/components/TyButton.tsx +157 -0
  130. package/src/components/TyCalendar.tsx +247 -0
  131. package/src/components/TyCalendarMonth.tsx +108 -0
  132. package/src/components/TyCalendarNavigation.tsx +91 -0
  133. package/src/components/TyCheckbox.tsx +147 -0
  134. package/src/components/TyCopy.tsx +83 -0
  135. package/src/components/TyDatePicker.tsx +215 -0
  136. package/src/components/TyDropdown.tsx +240 -0
  137. package/src/components/TyFileUpload.tsx +108 -0
  138. package/src/components/TyIcon.tsx +71 -0
  139. package/src/components/TyInput.tsx +239 -0
  140. package/src/components/TyModal.tsx +195 -0
  141. package/src/components/TyMultiselect.tsx +208 -0
  142. package/src/components/TyOption.tsx +47 -0
  143. package/src/components/TyPopup.tsx +116 -0
  144. package/src/components/TyRadio.tsx +61 -0
  145. package/src/components/TyRadioGroup.tsx +125 -0
  146. package/src/components/TyResizeObserver.tsx +54 -0
  147. package/src/components/TyScrollContainer.tsx +102 -0
  148. package/src/components/TyStep.tsx +71 -0
  149. package/src/components/TySwitch.tsx +114 -0
  150. package/src/components/TyTab.tsx +65 -0
  151. package/src/components/TyTabs.tsx +93 -0
  152. package/src/components/TyTag.tsx +86 -0
  153. package/src/components/TyTextarea.tsx +181 -0
  154. package/src/components/TyTooltip.tsx +83 -0
  155. package/src/components/TyWizard.tsx +99 -0
  156. package/src/components/index.ts +279 -0
  157. package/src/utils/react-version.ts +8 -0
  158. package/src/utils/use-boolean-prop.ts +62 -0
  159. package/src/version.ts +6 -0
@@ -0,0 +1,239 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
4
+
5
+ // Event detail structure for ty-input events
6
+ export interface TyInputEventDetail {
7
+ value: any; // shadow value (processed/parsed)
8
+ formattedValue: string; // user-visible formatted value
9
+ rawValue: string; // raw input value
10
+ originalEvent: Event; // original DOM event
11
+ }
12
+
13
+ export interface TyInputCSSProperties extends React.CSSProperties {
14
+ '--input-bg'?: string;
15
+ '--input-color'?: string;
16
+ '--input-border'?: string;
17
+ '--input-border-hover'?: string;
18
+ '--input-border-focus'?: string;
19
+ '--input-shadow-focus'?: string;
20
+ '--input-placeholder'?: string;
21
+ '--input-disabled-bg'?: string;
22
+ '--input-disabled-border'?: string;
23
+ '--input-disabled-color'?: string;
24
+ }
25
+
26
+ // Type definitions for Ty Input component
27
+ export interface TyInputProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onFocus' | 'onBlur' | 'style'> {
28
+ style?: TyInputCSSProperties;
29
+ /** Input type */
30
+ type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search'
31
+ | 'currency' | 'percent' | 'compact';
32
+
33
+ /** Semantic styling variant */
34
+ flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
35
+
36
+ /** Input size */
37
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
38
+
39
+ /** Input value */
40
+ value?: string;
41
+
42
+ /** Placeholder text */
43
+ placeholder?: string;
44
+
45
+ /** Input label */
46
+ label?: string;
47
+
48
+ /** Error message */
49
+ error?: string;
50
+
51
+ /** Disable the input */
52
+ disabled?: boolean;
53
+
54
+ /** Required field */
55
+ required?: boolean;
56
+
57
+ /** Form field name for form submission */
58
+ name?: string;
59
+
60
+ /** Checked state for checkbox inputs */
61
+ checked?: boolean;
62
+
63
+ // Numeric formatting props
64
+ currency?: string;
65
+ locale?: string;
66
+ precision?: string | number;
67
+
68
+ /** Debounce in milliseconds (0-5000) */
69
+ debounce?: number;
70
+
71
+ // React event handlers - override with our custom types
72
+ /**
73
+ * Fires on every keystroke (React convention)
74
+ * Maps to native 'input' event from ty-input
75
+ */
76
+ onChange?: (event: CustomEvent<TyInputEventDetail>) => void;
77
+
78
+ /**
79
+ * Fires on blur if value changed (native DOM behavior)
80
+ * Maps to native 'change' event from ty-input
81
+ */
82
+ onChangeCommit?: (event: CustomEvent<TyInputEventDetail>) => void;
83
+
84
+ /** Standard focus event */
85
+ onFocus?: (event: FocusEvent) => void;
86
+
87
+ /** Standard blur event */
88
+ onBlur?: (event: FocusEvent) => void;
89
+ }
90
+
91
+ // One-time global warning flags so we don't spam the console.
92
+ let _warnedOnInputProp = false;
93
+
94
+ // React wrapper for ty-input web component
95
+ export const TyInput = React.forwardRef<HTMLElement, TyInputProps>(
96
+ ({ onChange, onChangeCommit, onFocus, onBlur, disabled, required, name, checked, debounce, ...props }, ref) => {
97
+ const elementRef = useRef<HTMLElement>(null);
98
+
99
+ // Catch the most common mistake: passing `onInput` (React's prop) instead
100
+ // of `onChange` (our wrapper's prop). React's synthetic-event system
101
+ // strips event.detail, so the user's handler crashes on `e.detail.value`.
102
+ // Forward to onChange and log a one-shot warning.
103
+ const onInputProp = (props as any).onInput as ((e: any) => void) | undefined;
104
+ if (onInputProp && !onChange) {
105
+ if (!_warnedOnInputProp) {
106
+ _warnedOnInputProp = true;
107
+ console.warn(
108
+ '[tyrell-react] <TyInput> received `onInput`. ' +
109
+ 'React strips event.detail; use `onChange` instead — it receives the raw CustomEvent. ' +
110
+ 'Forwarding for now, but please rename the prop.'
111
+ );
112
+ }
113
+ onChange = onInputProp;
114
+ }
115
+ // Either way, drop onInput from spread so React doesn't double-wire it.
116
+ delete (props as any).onInput;
117
+
118
+ // Map onChange to input event (React convention)
119
+ const handleInput = useCallback((event: CustomEvent<TyInputEventDetail>) => {
120
+ if (onChange) {
121
+ onChange(event);
122
+ }
123
+ }, [onChange]);
124
+
125
+ // Map onChangeCommit to change event (blur behavior)
126
+ const handleChangeCommit = useCallback((event: CustomEvent<TyInputEventDetail>) => {
127
+ if (onChangeCommit) {
128
+ onChangeCommit(event);
129
+ }
130
+ }, [onChangeCommit]);
131
+
132
+ const handleFocus = useCallback((event: FocusEvent) => {
133
+ if (onFocus) {
134
+ onFocus(event);
135
+ }
136
+ }, [onFocus]);
137
+
138
+ const handleBlur = useCallback((event: FocusEvent) => {
139
+ if (onBlur) {
140
+ onBlur(event);
141
+ }
142
+ }, [onBlur]);
143
+
144
+ useEffect(() => {
145
+ const element = elementRef.current;
146
+ if (!element) return;
147
+
148
+ // Listen for custom input/change events from ty-input
149
+ // Map onChange → input event (React convention)
150
+ if (onChange) {
151
+ element.addEventListener('input', handleInput as EventListener);
152
+ }
153
+
154
+ // Map onChangeCommit → change event (blur behavior)
155
+ if (onChangeCommit) {
156
+ element.addEventListener('change', handleChangeCommit as EventListener);
157
+ }
158
+
159
+ // Listen for standard focus/blur events
160
+ if (onFocus) {
161
+ element.addEventListener('focus', handleFocus as EventListener);
162
+ }
163
+
164
+ if (onBlur) {
165
+ element.addEventListener('blur', handleBlur as EventListener);
166
+ }
167
+
168
+ return () => {
169
+ if (onChange) {
170
+ element.removeEventListener('input', handleInput as EventListener);
171
+ }
172
+ if (onChangeCommit) {
173
+ element.removeEventListener('change', handleChangeCommit as EventListener);
174
+ }
175
+ if (onFocus) {
176
+ element.removeEventListener('focus', handleFocus as EventListener);
177
+ }
178
+ if (onBlur) {
179
+ element.removeEventListener('blur', handleBlur as EventListener);
180
+ }
181
+ };
182
+ }, [handleInput, handleChangeCommit, handleFocus, handleBlur, onChange, onChangeCommit, onFocus, onBlur]);
183
+
184
+ // Handle ref forwarding
185
+ useEffect(() => {
186
+ if (ref && elementRef.current) {
187
+ if (typeof ref === 'function') {
188
+ ref(elementRef.current);
189
+ } else {
190
+ ref.current = elementRef.current;
191
+ }
192
+ }
193
+ }, [ref]);
194
+
195
+ // Imperatively sync `value` to the underlying element's property whenever
196
+ // the React prop changes. React 18's prop-to-property bridging for custom
197
+ // elements is unreliable for empty strings, so we set the property directly
198
+ // to guarantee resets (`value=""`) clear the visible content. React 19+
199
+ // handles this natively, so the effect short-circuits there.
200
+ useEffect(() => {
201
+ if (!needsPropertyBridge) return;
202
+ const element = elementRef.current as any;
203
+ if (!element) return;
204
+ const next = (props as any).value ?? '';
205
+ if (element.value !== next) {
206
+ element.value = next;
207
+ }
208
+ }, [(props as any).value]);
209
+
210
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
211
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
212
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
213
+ const isChecked = useBooleanProperty(elementRef, 'checked', checked);
214
+
215
+ // Convert React props to web component attributes
216
+ const webComponentProps: Record<string, any> = {
217
+ ...props,
218
+ ref: elementRef,
219
+ };
220
+
221
+ // Add conditional attributes
222
+ if (isDisabled) webComponentProps.disabled = '';
223
+ if (isRequired) webComponentProps.required = '';
224
+ if (isChecked) webComponentProps.checked = '';
225
+
226
+ // Add string attributes
227
+ if (name) webComponentProps.name = name;
228
+
229
+ // Add debounce attribute
230
+ if (debounce !== undefined) webComponentProps.debounce = debounce;
231
+
232
+ return React.createElement(
233
+ 'ty-input',
234
+ webComponentProps
235
+ );
236
+ }
237
+ );
238
+
239
+ TyInput.displayName = 'TyInput';
@@ -0,0 +1,195 @@
1
+ import React, { useEffect, useRef, useImperativeHandle } from 'react';
2
+ import { useBooleanProperty, coerceBool } from '../utils/use-boolean-prop';
3
+ import { needsPropertyBridge } from '../utils/react-version';
4
+
5
+ // Event detail structure for modal events
6
+ export interface TyModalEventDetail {
7
+ reason?: 'programmatic' | 'native' | 'backdrop' | 'escape' | 'close-button';
8
+ returnValue?: string;
9
+ }
10
+
11
+ /**
12
+ * Detail for the `beforeclose` event — fired *before* the modal closes,
13
+ * cancellable. Call `event.preventDefault()` to abort the close and render
14
+ * your own confirm UI; once the user consents, call `ref.current?.hide({ force: true })`
15
+ * to actually close (or just toggle your controlled `open` state to `false`).
16
+ */
17
+ export interface TyModalBeforeCloseDetail {
18
+ reason: 'programmatic' | 'backdrop' | 'escape' | 'close-button' | 'native';
19
+ }
20
+
21
+ // Type definitions for Ty Modal component
22
+ export interface TyModalProps extends React.HTMLAttributes<HTMLElement> {
23
+ /** Controls modal visibility */
24
+ open?: boolean;
25
+
26
+ /** Show backdrop behind modal (default: true) */
27
+ backdrop?: boolean;
28
+
29
+ /** Allow closing modal by clicking backdrop (default: true) */
30
+ closeOnOutsideClick?: boolean;
31
+
32
+ /** Allow closing modal with Escape key (default: true) */
33
+ closeOnEscape?: boolean;
34
+
35
+ /** React event handlers */
36
+ onOpen?: (event: CustomEvent<TyModalEventDetail>) => void;
37
+ onClose?: (event: CustomEvent<TyModalEventDetail>) => void;
38
+ /**
39
+ * Fires before the modal closes. Cancellable — call `event.preventDefault()`
40
+ * to abort. Use this for "discard changes?" flows with custom UI.
41
+ */
42
+ onBeforeClose?: (event: CustomEvent<TyModalBeforeCloseDetail>) => void;
43
+
44
+ /** Modal content */
45
+ children?: React.ReactNode;
46
+ }
47
+
48
+ // Ref interface for imperative methods
49
+ export interface TyModalRef {
50
+ show: () => void;
51
+ /**
52
+ * Close the modal. Without `force`, fires `beforeclose` first (consumer can
53
+ * cancel). With `force: true`, bypasses `beforeclose` — call this once your
54
+ * custom confirm UI has captured user consent.
55
+ */
56
+ hide: (opts?: { force?: boolean }) => void;
57
+ element: HTMLElement | null;
58
+ }
59
+
60
+ // React wrapper for ty-modal web component
61
+ export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
62
+ ({
63
+ open,
64
+ backdrop,
65
+ closeOnOutsideClick,
66
+ closeOnEscape,
67
+ onOpen,
68
+ onClose,
69
+ onBeforeClose,
70
+ children,
71
+ ...props
72
+ }, ref) => {
73
+ const elementRef = useRef<HTMLElement>(null);
74
+
75
+ // Expose imperative methods through ref
76
+ useImperativeHandle(ref, () => ({
77
+ show: () => {
78
+ if (elementRef.current && typeof (elementRef.current as any).show === 'function') {
79
+ (elementRef.current as any).show();
80
+ }
81
+ },
82
+ hide: (opts?: { force?: boolean }) => {
83
+ if (elementRef.current && typeof (elementRef.current as any).hide === 'function') {
84
+ (elementRef.current as any).hide(opts);
85
+ }
86
+ },
87
+ element: elementRef.current,
88
+ }), []);
89
+
90
+ // Handle modal events.
91
+ //
92
+ // ty-dropdown, ty-multiselect, ty-date-picker etc. dispatch their own
93
+ // `open`/`close` custom events with `bubbles: true, composed: true` when
94
+ // their internal popups toggle. Those events bubble up through the
95
+ // modal's slotted content and land on the ty-modal host — same event
96
+ // type, same listener. Without the `event.target === element` guard, an
97
+ // `onClose={() => setIsOpen(false)}` callback would fire every time the
98
+ // user closes a dropdown inside the modal, and the React state flip
99
+ // would close the modal. Core's modal.ts already applies the same guard
100
+ // on its internal <dialog>.onclose; this is the React-layer mirror.
101
+ useEffect(() => {
102
+ const element = elementRef.current;
103
+ if (!element) return;
104
+
105
+ const handleOpen = (event: CustomEvent<TyModalEventDetail>) => {
106
+ if (event.target !== element) return;
107
+ if (onOpen) onOpen(event);
108
+ };
109
+
110
+ const handleClose = (event: CustomEvent<TyModalEventDetail>) => {
111
+ if (event.target !== element) return;
112
+ if (onClose) onClose(event);
113
+ };
114
+
115
+ const handleBeforeClose = (event: CustomEvent<TyModalBeforeCloseDetail>) => {
116
+ if (event.target !== element) return;
117
+ if (onBeforeClose) onBeforeClose(event);
118
+ };
119
+
120
+ // Listen for custom modal events
121
+ if (onOpen) {
122
+ element.addEventListener('open', handleOpen as EventListener);
123
+ }
124
+
125
+ if (onClose) {
126
+ element.addEventListener('close', handleClose as EventListener);
127
+ }
128
+
129
+ if (onBeforeClose) {
130
+ element.addEventListener('beforeclose', handleBeforeClose as EventListener);
131
+ }
132
+
133
+ return () => {
134
+ if (onOpen) {
135
+ element.removeEventListener('open', handleOpen as EventListener);
136
+ }
137
+ if (onClose) {
138
+ element.removeEventListener('close', handleClose as EventListener);
139
+ }
140
+ if (onBeforeClose) {
141
+ element.removeEventListener('beforeclose', handleBeforeClose as EventListener);
142
+ }
143
+ };
144
+ }, [onOpen, onClose, onBeforeClose]);
145
+
146
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
147
+ // Without this, flipping `open` from `true` to `false` on React 18 leaves
148
+ // the `open` attribute on the element and the modal stays open.
149
+ const isOpen = useBooleanProperty(elementRef, 'open', open);
150
+
151
+ // For default-true booleans (backdrop, closeOn*), only the explicit-false
152
+ // case is interesting — bridge it imperatively too so it propagates.
153
+ useEffect(() => {
154
+ if (!needsPropertyBridge) return;
155
+ const el = elementRef.current as any;
156
+ if (!el) return;
157
+ const setIf = (prop: string, raw: unknown) => {
158
+ if (raw === undefined) return;
159
+ const next = coerceBool(raw);
160
+ if (Boolean(el[prop]) !== next) el[prop] = next;
161
+ };
162
+ setIf('backdrop', backdrop);
163
+ setIf('closeOnOutsideClick', closeOnOutsideClick);
164
+ setIf('closeOnEscape', closeOnEscape);
165
+ }, [backdrop, closeOnOutsideClick, closeOnEscape]);
166
+
167
+ // Convert React props to web component attributes
168
+ const webComponentProps: Record<string, any> = {
169
+ ...props,
170
+ ref: elementRef,
171
+ };
172
+
173
+ if (isOpen) webComponentProps.open = '';
174
+
175
+ // Default-true booleans use "false" string on the attribute side; the
176
+ // core's parseBoolAttr handles it correctly.
177
+ if (backdrop !== undefined && !coerceBool(backdrop)) {
178
+ webComponentProps.backdrop = 'false';
179
+ }
180
+ if (closeOnOutsideClick !== undefined && !coerceBool(closeOnOutsideClick)) {
181
+ webComponentProps['close-on-outside-click'] = 'false';
182
+ }
183
+ if (closeOnEscape !== undefined && !coerceBool(closeOnEscape)) {
184
+ webComponentProps['close-on-escape'] = 'false';
185
+ }
186
+
187
+ return React.createElement(
188
+ 'ty-modal',
189
+ webComponentProps,
190
+ children
191
+ );
192
+ }
193
+ );
194
+
195
+ TyModal.displayName = 'TyModal';
@@ -0,0 +1,208 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
4
+
5
+ // Type definitions for Ty Multiselect component
6
+ export interface TyMultiselectEventDetail {
7
+ /** Array of currently selected values */
8
+ values: string[];
9
+ /** Action that triggered the change: "add" | "remove" */
10
+ action: 'add' | 'remove';
11
+ /** The specific item that was added or removed */
12
+ item: string;
13
+ }
14
+
15
+ export interface TyMultiselectProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'style'> {
16
+ style?: import('./TyInput').TyInputCSSProperties;
17
+ /** Current selected values as comma-separated string or array */
18
+ value?: string | string[];
19
+
20
+ /** Placeholder text when no items are selected */
21
+ placeholder?: string;
22
+
23
+ /** Disable the multiselect component */
24
+ disabled?: boolean;
25
+
26
+ /**
27
+ * Loading state — replaces the available-options area with a centered
28
+ * spinner. Search input stays usable. Pair with `externalSearch` while
29
+ * fetching results from a parent-owned data source.
30
+ */
31
+ loading?: boolean;
32
+
33
+ /** Make the multiselect read-only */
34
+ readonly?: boolean;
35
+
36
+ /** Semantic styling variant */
37
+ flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
38
+
39
+ /** Label text for the multiselect */
40
+ label?: string;
41
+
42
+ /** Mark the field as required */
43
+ required?: boolean;
44
+
45
+ /**
46
+ * Switch to external (remote) search mode. Default is `false` — the
47
+ * multiselect filters its own children client-side. Set to `true` when you
48
+ * want to handle filtering yourself (e.g. server-side): the component will
49
+ * dispatch `search` events on each keystroke, and you must update the children
50
+ * in response. The `onSearch` prop receives those events.
51
+ */
52
+ externalSearch?: boolean;
53
+
54
+ /** Debounce in milliseconds (0-5000) */
55
+ debounce?: number;
56
+
57
+ /** Mobile section label for selected items */
58
+ selectedLabel?: string;
59
+
60
+ /** Form field name for form submission */
61
+ name?: string;
62
+
63
+ /** Callback when selection changes */
64
+ onChange?: (event: CustomEvent<TyMultiselectEventDetail>) => void;
65
+
66
+ /** Callback fired on each search input change (debounced by `debounce`). Use for external/server-side filtering. */
67
+ onSearch?: (event: CustomEvent<{ query: string; element: HTMLElement }>) => void;
68
+
69
+ /** Children should be TyTag components, not TyOption */
70
+ children?: React.ReactNode;
71
+ }
72
+
73
+ // React wrapper for ty-multiselect web component
74
+ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
75
+ ({
76
+ value,
77
+ placeholder,
78
+ disabled,
79
+ loading,
80
+ readonly,
81
+ flavor,
82
+ label,
83
+ required,
84
+ externalSearch,
85
+ debounce,
86
+ selectedLabel,
87
+ name,
88
+ onChange,
89
+ onSearch,
90
+ children,
91
+ ...props
92
+ }, ref) => {
93
+ const elementRef = useRef<HTMLElement>(null);
94
+
95
+ // Handle ref forwarding
96
+ useEffect(() => {
97
+ if (ref && elementRef.current) {
98
+ if (typeof ref === 'function') {
99
+ ref(elementRef.current);
100
+ } else {
101
+ ref.current = elementRef.current;
102
+ }
103
+ }
104
+ }, [ref]);
105
+
106
+ // Imperatively sync `value` to the underlying property so resets
107
+ // (`value=""` or null) reliably clear the visible selection.
108
+ // React 18 workaround; React 19+ handles this natively.
109
+ useEffect(() => {
110
+ if (!needsPropertyBridge) return;
111
+ const element = elementRef.current as any;
112
+ if (!element) return;
113
+ const next = (props as any).value ?? '';
114
+ if (element.value !== next) {
115
+ element.value = next;
116
+ }
117
+ }, [(props as any).value]);
118
+
119
+ // Handle change events
120
+ const handleChange = useCallback((event: Event) => {
121
+ const customEvent = event as CustomEvent<TyMultiselectEventDetail>;
122
+ if (onChange) {
123
+ onChange(customEvent);
124
+ }
125
+ }, [onChange]);
126
+
127
+ const handleSearch = useCallback((event: Event) => {
128
+ const customEvent = event as CustomEvent<{ query: string; element: HTMLElement }>;
129
+ if (onSearch) {
130
+ onSearch(customEvent);
131
+ }
132
+ }, [onSearch]);
133
+
134
+ // Set up event listeners
135
+ useEffect(() => {
136
+ const element = elementRef.current;
137
+ if (!element) return;
138
+
139
+ if (onChange) element.addEventListener('change', handleChange);
140
+ if (onSearch) element.addEventListener('search', handleSearch);
141
+
142
+ return () => {
143
+ if (onChange) element.removeEventListener('change', handleChange);
144
+ if (onSearch) element.removeEventListener('search', handleSearch);
145
+ };
146
+ }, [handleChange, handleSearch, onChange, onSearch]);
147
+
148
+ // Imperative property sync for boolean props (see use-boolean-prop.ts).
149
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
150
+ const isLoading = useBooleanProperty(elementRef, 'loading', loading);
151
+ const isReadonly = useBooleanProperty(elementRef, 'readonly', readonly);
152
+ const isRequired = useBooleanProperty(elementRef, 'required', required);
153
+ const isExternalSearch = useBooleanProperty(elementRef, 'externalSearch', externalSearch);
154
+
155
+ // Convert React props to web component attributes
156
+ const webComponentProps: Record<string, any> = {
157
+ ...props,
158
+ ref: elementRef,
159
+ };
160
+
161
+ // Handle value conversion (array to comma-separated string)
162
+ if (value !== undefined) {
163
+ const valueString = Array.isArray(value) ? value.join(',') : value;
164
+ webComponentProps.value = valueString;
165
+ }
166
+
167
+ // Add optional attributes only if they have values
168
+ if (placeholder) {
169
+ webComponentProps.placeholder = placeholder;
170
+ }
171
+
172
+ if (isLoading) webComponentProps.loading = '';
173
+ if (isDisabled) webComponentProps.disabled = '';
174
+ if (isReadonly) webComponentProps.readonly = '';
175
+ if (isRequired) webComponentProps.required = '';
176
+ if (isExternalSearch) webComponentProps['external-search'] = '';
177
+
178
+ if (flavor) {
179
+ webComponentProps.flavor = flavor;
180
+ }
181
+
182
+ if (label) {
183
+ webComponentProps.label = label;
184
+ }
185
+
186
+ if (name) {
187
+ webComponentProps.name = name;
188
+ }
189
+
190
+ // Add debounce attribute
191
+ if (debounce !== undefined) {
192
+ webComponentProps.debounce = debounce;
193
+ }
194
+
195
+ // Add selectedLabel attribute
196
+ if (selectedLabel) {
197
+ webComponentProps['selected-label'] = selectedLabel;
198
+ }
199
+
200
+ return React.createElement(
201
+ 'ty-multiselect',
202
+ webComponentProps,
203
+ children
204
+ );
205
+ }
206
+ );
207
+
208
+ TyMultiselect.displayName = 'TyMultiselect';
@@ -0,0 +1,47 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { useBooleanProperty } from '../utils/use-boolean-prop';
3
+
4
+ // Type definitions for Ty Option component
5
+ export interface TyOptionProps extends React.HTMLAttributes<HTMLElement> {
6
+ value?: string;
7
+ disabled?: boolean;
8
+ selected?: boolean;
9
+ hidden?: boolean;
10
+ children?: React.ReactNode;
11
+ }
12
+
13
+ // React wrapper for ty-option web component
14
+ export const TyOption = React.forwardRef<HTMLElement, TyOptionProps>(
15
+ ({ children, disabled, selected, hidden, ...props }, ref) => {
16
+ const elementRef = useRef<HTMLElement>(null);
17
+
18
+ // Handle ref forwarding
19
+ useEffect(() => {
20
+ if (ref && elementRef.current) {
21
+ if (typeof ref === 'function') {
22
+ ref(elementRef.current);
23
+ } else {
24
+ ref.current = elementRef.current;
25
+ }
26
+ }
27
+ }, [ref]);
28
+
29
+ const isDisabled = useBooleanProperty(elementRef, 'disabled', disabled);
30
+ const isSelected = useBooleanProperty(elementRef, 'selected', selected);
31
+ const isHidden = useBooleanProperty(elementRef, 'hidden', hidden);
32
+
33
+ return React.createElement(
34
+ 'ty-option',
35
+ {
36
+ ...props,
37
+ ...(isDisabled && { disabled: "" }),
38
+ ...(isSelected && { selected: "" }),
39
+ ...(isHidden && { hidden: "" }),
40
+ ref: elementRef,
41
+ },
42
+ children
43
+ );
44
+ }
45
+ );
46
+
47
+ TyOption.displayName = 'TyOption';