@musecat/uikit 0.1.1

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 (180) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +51 -0
  3. package/index.ts +78 -0
  4. package/package.json +103 -0
  5. package/packages/Components/Box/Box.tsx +173 -0
  6. package/packages/Components/Box/Box.types.ts +48 -0
  7. package/packages/Components/Button/Button.tsx +275 -0
  8. package/packages/Components/Button/Button.types.ts +44 -0
  9. package/packages/Components/Card/Card.default.tsx +180 -0
  10. package/packages/Components/Card/Card.foldable.tsx +383 -0
  11. package/packages/Components/Card/Card.module.scss +23 -0
  12. package/packages/Components/Card/Card.tsx +8 -0
  13. package/packages/Components/Card/Card.types.ts +91 -0
  14. package/packages/Components/Card/Card.utils.ts +66 -0
  15. package/packages/Components/Checkbox/Checkbox.module.scss +11 -0
  16. package/packages/Components/Checkbox/Checkbox.tsx +104 -0
  17. package/packages/Components/Checkbox/Checkbox.types.ts +24 -0
  18. package/packages/Components/CodeBox/CodeBox.copy.tsx +43 -0
  19. package/packages/Components/CodeBox/CodeBox.module.scss +72 -0
  20. package/packages/Components/CodeBox/CodeBox.tsx +91 -0
  21. package/packages/Components/CodeBox/CodeBox.types.ts +17 -0
  22. package/packages/Components/ContextMenu/ContextMenu.module.scss +8 -0
  23. package/packages/Components/ContextMenu/ContextMenu.options.tsx +126 -0
  24. package/packages/Components/ContextMenu/ContextMenu.tsx +381 -0
  25. package/packages/Components/ContextMenu/ContextMenu.types.ts +46 -0
  26. package/packages/Components/ContributionGraph/ContributionGraph.module.scss +42 -0
  27. package/packages/Components/ContributionGraph/ContributionGraph.tsx +454 -0
  28. package/packages/Components/ContributionGraph/ContributionGraph.types.ts +27 -0
  29. package/packages/Components/DatePicker/DatePicker.calendar.tsx +161 -0
  30. package/packages/Components/DatePicker/DatePicker.core.tsx +582 -0
  31. package/packages/Components/DatePicker/DatePicker.module.scss +26 -0
  32. package/packages/Components/DatePicker/DatePicker.tsx +786 -0
  33. package/packages/Components/DatePicker/DatePicker.types.ts +36 -0
  34. package/packages/Components/DatePicker/DatePicker.utils.ts +80 -0
  35. package/packages/Components/DatePicker/hooks/useCalendar.ts +61 -0
  36. package/packages/Components/DatePicker/hooks/useSelection.ts +208 -0
  37. package/packages/Components/Divider/Divider.tsx +46 -0
  38. package/packages/Components/Divider/Divider.types.ts +12 -0
  39. package/packages/Components/Header/Header.module.scss +74 -0
  40. package/packages/Components/Header/Header.tsx +47 -0
  41. package/packages/Components/Header/Header.types.ts +8 -0
  42. package/packages/Components/Icon/Icon.group.tsx +122 -0
  43. package/packages/Components/Icon/Icon.group.types.ts +20 -0
  44. package/packages/Components/Icon/Icon.tsx +341 -0
  45. package/packages/Components/Icon/Icon.types.ts +59 -0
  46. package/packages/Components/Icon/SVG/data.tsx +232 -0
  47. package/packages/Components/Input/Input.module.scss +5 -0
  48. package/packages/Components/Input/Input.tsx +87 -0
  49. package/packages/Components/Input/Input.types.ts +14 -0
  50. package/packages/Components/Label/Label.module.scss +19 -0
  51. package/packages/Components/Label/Label.tsx +115 -0
  52. package/packages/Components/Label/Label.types.ts +28 -0
  53. package/packages/Components/Layout/Layout.docs.module.scss +42 -0
  54. package/packages/Components/Layout/Layout.docs.tsx +150 -0
  55. package/packages/Components/Layout/Layout.docs.types.ts +18 -0
  56. package/packages/Components/Layout/Layout.module.scss +91 -0
  57. package/packages/Components/Layout/Layout.tsx +306 -0
  58. package/packages/Components/Layout/Layout.types.ts +71 -0
  59. package/packages/Components/Maps/OSM/MapOSM.module.scss +11 -0
  60. package/packages/Components/Maps/OSM/MapOSM.shared.ts +121 -0
  61. package/packages/Components/Maps/OSM/MapOSM.tsx +279 -0
  62. package/packages/Components/Maps/OSM/MapOSM.types.ts +18 -0
  63. package/packages/Components/Nav/Nav.module.scss +137 -0
  64. package/packages/Components/Nav/Nav.tsx +304 -0
  65. package/packages/Components/Nav/Nav.types.ts +32 -0
  66. package/packages/Components/Nav/hooks/useNavDrag.ts +374 -0
  67. package/packages/Components/Nav/hooks/useNavIndicator.ts +137 -0
  68. package/packages/Components/Pagination/Pagination.tsx +254 -0
  69. package/packages/Components/Pagination/Pagination.types.ts +18 -0
  70. package/packages/Components/Pill/Pill.tsx +127 -0
  71. package/packages/Components/Pill/Pill.types.ts +33 -0
  72. package/packages/Components/Profile/Profile.shared.ts +74 -0
  73. package/packages/Components/Profile/Profile.tsx +382 -0
  74. package/packages/Components/Profile/Profile.types.ts +55 -0
  75. package/packages/Components/Progress/Progress.module.scss +15 -0
  76. package/packages/Components/Progress/Progress.tsx +88 -0
  77. package/packages/Components/Progress/Progress.types.ts +22 -0
  78. package/packages/Components/Radio/Radio.module.scss +11 -0
  79. package/packages/Components/Radio/Radio.tsx +121 -0
  80. package/packages/Components/Radio/Radio.types.ts +20 -0
  81. package/packages/Components/Select/Select.keyboard.tsx +76 -0
  82. package/packages/Components/Select/Select.trigger.tsx +88 -0
  83. package/packages/Components/Select/Select.tsx +11 -0
  84. package/packages/Components/Select/Select.types.ts +38 -0
  85. package/packages/Components/Select/Select.utils.ts +84 -0
  86. package/packages/Components/Select/hooks/useSelectNavigation.ts +461 -0
  87. package/packages/Components/Select/hooks/useSelectSelection.ts +142 -0
  88. package/packages/Components/Select/hooks/useSelectState.ts +314 -0
  89. package/packages/Components/Select/select.control.tsx +228 -0
  90. package/packages/Components/Select/select.inner.tsx +312 -0
  91. package/packages/Components/Skeleton/Skeleton.tsx +30 -0
  92. package/packages/Components/Skeleton/Skeleton.types.ts +12 -0
  93. package/packages/Components/Spinner/Spinner.module.scss +78 -0
  94. package/packages/Components/Spinner/Spinner.tsx +125 -0
  95. package/packages/Components/Spinner/Spinner.types.ts +19 -0
  96. package/packages/Components/Text/Text.module.scss +7 -0
  97. package/packages/Components/Text/Text.tsx +49 -0
  98. package/packages/Components/Text/Text.types.ts +33 -0
  99. package/packages/Components/TimePicker/TimePicker.core.tsx +448 -0
  100. package/packages/Components/TimePicker/TimePicker.module.scss +30 -0
  101. package/packages/Components/TimePicker/TimePicker.tsx +511 -0
  102. package/packages/Components/TimePicker/TimePicker.types.ts +22 -0
  103. package/packages/Components/Timeline/Timeline.tsx +111 -0
  104. package/packages/Components/Timeline/Timeline.types.ts +23 -0
  105. package/packages/Components/Title/Title.tsx +305 -0
  106. package/packages/Components/Title/Title.types.ts +64 -0
  107. package/packages/Components/Toggle/Toggle.module.scss +32 -0
  108. package/packages/Components/Toggle/Toggle.tsx +272 -0
  109. package/packages/Components/Toggle/Toggle.types.ts +17 -0
  110. package/packages/Components/Tooltip/Tooltip.module.scss +44 -0
  111. package/packages/Components/Tooltip/Tooltip.tsx +340 -0
  112. package/packages/Components/Tooltip/Tooltip.types.ts +38 -0
  113. package/packages/Frameworks/Dialog/Dialog.background.tsx +88 -0
  114. package/packages/Frameworks/Dialog/Dialog.boot.tsx +32 -0
  115. package/packages/Frameworks/Dialog/Dialog.store.ts +199 -0
  116. package/packages/Frameworks/Dialog/Dialog.tsx +185 -0
  117. package/packages/Frameworks/Dialog/Dialog.types.ts +264 -0
  118. package/packages/Frameworks/Dialog/Dialog.utils.ts +22 -0
  119. package/packages/Frameworks/Dialog/contents/Dialog.footer.tsx +79 -0
  120. package/packages/Frameworks/Dialog/contents/Dialog.funnel.tsx +260 -0
  121. package/packages/Frameworks/Dialog/contents/Dialog.header.tsx +137 -0
  122. package/packages/Frameworks/Dialog/hooks/useDialogPosition.ts +298 -0
  123. package/packages/Frameworks/Dialog/hooks/useSheetDrag.ts +325 -0
  124. package/packages/Frameworks/Dialog/hooks/useSheetGeometry.ts +201 -0
  125. package/packages/Frameworks/Dialog/hooks/useSheetProgressive.ts +98 -0
  126. package/packages/Frameworks/Dialog/renderers/renderModal.tsx +228 -0
  127. package/packages/Frameworks/Dialog/renderers/renderPopover.tsx +220 -0
  128. package/packages/Frameworks/Dialog/renderers/renderSheet.tsx +571 -0
  129. package/packages/Frameworks/EdgeEffect/EdgeEffect.tsx +52 -0
  130. package/packages/Frameworks/EdgeEffect/EdgeEffect.types.ts +5 -0
  131. package/packages/Frameworks/Motion/Motion.presets.ts +49 -0
  132. package/packages/Frameworks/Pressable/Pressable.module.scss +19 -0
  133. package/packages/Frameworks/Pressable/Pressable.tsx +651 -0
  134. package/packages/Frameworks/Pressable/Pressable.types.ts +73 -0
  135. package/packages/Frameworks/Squircle/Squircle.tsx +263 -0
  136. package/packages/Frameworks/Squircle/Squircle.types.ts +15 -0
  137. package/packages/Frameworks/Theme/Radius.types.ts +55 -0
  138. package/packages/Frameworks/Theme/Theme.types.ts +325 -0
  139. package/packages/Frameworks/Toaster/Toaster.boot.tsx +47 -0
  140. package/packages/Frameworks/Toaster/Toaster.module.scss +168 -0
  141. package/packages/Frameworks/Toaster/Toaster.types.ts +13 -0
  142. package/packages/Frameworks/View/DNDView/DNDView.module.scss +8 -0
  143. package/packages/Frameworks/View/DNDView/DNDView.tsx +470 -0
  144. package/packages/Frameworks/View/DNDView/DNDView.types.ts +61 -0
  145. package/packages/Frameworks/View/HScrollView/HScrollView.module.scss +59 -0
  146. package/packages/Frameworks/View/HScrollView/HScrollView.tsx +386 -0
  147. package/packages/Frameworks/View/HScrollView/HScrollView.types.ts +46 -0
  148. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.counter.tsx +27 -0
  149. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.footer.tsx +76 -0
  150. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.header.tsx +49 -0
  151. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.list.tsx +74 -0
  152. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.slide.tsx +71 -0
  153. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.tsx +175 -0
  154. package/packages/Frameworks/View/ImageView/Image.controls.tsx +39 -0
  155. package/packages/Frameworks/View/ImageView/Image.hooks.ts +82 -0
  156. package/packages/Frameworks/View/ImageView/Image.module.scss +7 -0
  157. package/packages/Frameworks/View/ImageView/Image.tsx +375 -0
  158. package/packages/Frameworks/View/ImageView/Image.types.ts +71 -0
  159. package/packages/Frameworks/View/ImageView/Image.utils.ts +79 -0
  160. package/packages/Frameworks/View/View.module.scss +34 -0
  161. package/packages/Frameworks/View/View.tsx +200 -0
  162. package/packages/Frameworks/View/View.types.ts +36 -0
  163. package/packages/Frameworks/_shared/Padding.types.ts +52 -0
  164. package/packages/Frameworks/_shared/StopParentInteraction.tsx +45 -0
  165. package/packages/Frameworks/_shared/Wind.types.ts +17 -0
  166. package/packages/Frameworks/_shared/bodyScrollLock.ts +127 -0
  167. package/packages/Frameworks/_shared/layer.constants.ts +8 -0
  168. package/packages/Frameworks/_shared/normalize.ts +39 -0
  169. package/packages/Frameworks/_shared/sizing.ts +55 -0
  170. package/packages/Frameworks/_shared/useControllableState.ts +29 -0
  171. package/packages/Styles/_animation.scss +42 -0
  172. package/packages/Styles/_color.scss +131 -0
  173. package/packages/Styles/_font.scss +30 -0
  174. package/packages/Styles/_icon.scss +782 -0
  175. package/packages/Styles/_importer.scss +9 -0
  176. package/packages/Styles/_system.scss +69 -0
  177. package/packages/Styles/_textstyle.scss +111 -0
  178. package/packages/Styles/_theme.scss +452 -0
  179. package/packages/Styles/_viewport-global.scss +99 -0
  180. package/packages/Styles/_viewport.scss +108 -0
@@ -0,0 +1,305 @@
1
+ import clsx from "clsx";
2
+ import IconGroup from "@/packages/Components/Icon/Icon.group";
3
+ import { IconInner } from "@/packages/Components/Icon/Icon";
4
+ import Pill from "@/packages/Components/Pill/Pill";
5
+ import Text from "@/packages/Components/Text/Text";
6
+ import type {
7
+ TitleContextItem,
8
+ TitleContextProps,
9
+ TitleProps,
10
+ } from "@/packages/Components/Title/Title.types";
11
+ import Pressable from "@/packages/Frameworks/Pressable/Pressable";
12
+ import View from "@/packages/Frameworks/View/View";
13
+
14
+ const buildRenderKeys = <T,>(
15
+ items: T[],
16
+ resolveBase: (item: T) => string,
17
+ ): string[] => {
18
+ const counts = new Map<string, number>();
19
+
20
+ return items.map((item) => {
21
+ const base = resolveBase(item) || "item";
22
+ const nextCount = (counts.get(base) ?? 0) + 1;
23
+ counts.set(base, nextCount);
24
+ return `${base}-${nextCount}`;
25
+ });
26
+ };
27
+
28
+ function TitleRoot({
29
+ title,
30
+ titleType,
31
+ titleClassName,
32
+ titleGap,
33
+ leftGap,
34
+ rightGap,
35
+ fontType,
36
+ titleLineHeight,
37
+ titleVerticalTrim,
38
+ caption,
39
+ suffix,
40
+ meta,
41
+ actions,
42
+ captionOpacity,
43
+ context,
44
+ className,
45
+ style,
46
+ "data-color-mode": dataTheme,
47
+ }: TitleProps) {
48
+ const titleItems = Array.isArray(title) ? title : title ? [title] : [];
49
+ const hasActive = titleItems.some((item) => item?.active);
50
+ const hasTitleCluster = titleItems.length > 0 || Boolean(suffix);
51
+ const hasRightCluster = Boolean(meta || actions);
52
+
53
+ const titleKeys = buildRenderKeys(
54
+ titleItems,
55
+ (item) => item?.text?.toString() ?? "",
56
+ );
57
+
58
+ return (
59
+ <View
60
+ column
61
+ gap={16}
62
+ className={className}
63
+ data-color-mode={dataTheme}
64
+ style={style}
65
+ >
66
+ <View column gap={4}>
67
+ <View
68
+ alignItems="center"
69
+ justifyContent={
70
+ hasTitleCluster && hasRightCluster ? "space-between" : undefined
71
+ }
72
+ gap={6}
73
+ >
74
+ {hasTitleCluster && (
75
+ <View gap={leftGap ?? 8} alignItems="center">
76
+ {titleItems.length > 0 && (
77
+ <View gap={titleGap ?? 8}>
78
+ {titleItems.map((item, index) => (
79
+ <Pressable
80
+ radius="Regular"
81
+ key={titleKeys[index]}
82
+ data-color-mode={item?.["data-color-mode"] ?? dataTheme}
83
+ padding={[2, 4]}
84
+ style={{
85
+ margin: "-.2rem -.4rem",
86
+ maxWidth: "max-content",
87
+ }}
88
+ {...item?.pressable}
89
+ >
90
+ <Text
91
+ type={titleType || "Title3"}
92
+ className={titleClassName}
93
+ fontType={fontType}
94
+ lineHeight={titleLineHeight}
95
+ verticalTrim={titleVerticalTrim}
96
+ opacity={hasActive && !item?.active ? 0.4 : 1}
97
+ >
98
+ {item?.text}
99
+ </Text>
100
+ </Pressable>
101
+ ))}
102
+ </View>
103
+ )}
104
+ {suffix && (
105
+ <View alignItems="center">
106
+ <Pill padding={6} {...suffix} />
107
+ </View>
108
+ )}
109
+ </View>
110
+ )}
111
+ {hasRightCluster && (
112
+ <View
113
+ alignItems="center"
114
+ gap={rightGap ?? 8}
115
+ justifyContent="flex-end"
116
+ >
117
+ {meta && <Text type="Footnote">{meta}</Text>}
118
+ {actions && (
119
+ <IconGroup
120
+ iconSize={20}
121
+ iconBoxOptions={{
122
+ padding: 4,
123
+ }}
124
+ icons={actions}
125
+ />
126
+ )}
127
+ </View>
128
+ )}
129
+ </View>
130
+ {caption && (
131
+ <Text
132
+ type="Footnote"
133
+ fontType={fontType}
134
+ opacity={captionOpacity ?? 0.6}
135
+ >
136
+ {caption}
137
+ </Text>
138
+ )}
139
+ </View>
140
+ {context && <View fullWidth>{context}</View>}
141
+ </View>
142
+ );
143
+ }
144
+
145
+ function toContextItems(items?: TitleContextItem | TitleContextItem[]) {
146
+ if (!items) return [];
147
+ return Array.isArray(items) ? items : [items];
148
+ }
149
+
150
+ function getContextItemKey(
151
+ item: TitleContextItem,
152
+ side: "start" | "end",
153
+ index: number,
154
+ ) {
155
+ return item.key ?? `${side}-${index}`;
156
+ }
157
+
158
+ function TitleContextItemView({ item }: { item: TitleContextItem }) {
159
+ const {
160
+ content,
161
+ leadingIcon,
162
+ trailingIcon,
163
+ leadingIconProps,
164
+ trailingIconProps,
165
+ pressable,
166
+ key: _key,
167
+ className: itemDesignClassName,
168
+ style: itemDesignStyle,
169
+ ...designProps
170
+ } = item;
171
+ const {
172
+ className: pressableClassName,
173
+ style: pressableStyle,
174
+ type,
175
+ ...pressableProps
176
+ } = pressable ?? {};
177
+ const itemClassName = clsx(itemDesignClassName, pressableClassName);
178
+ const itemStyle = {
179
+ display: "inline-flex",
180
+ alignItems: "center",
181
+ gap: ".4rem",
182
+ minWidth: 0,
183
+ minHeight: "2.6rem",
184
+ padding: ".4rem .6rem",
185
+ margin: "-.4rem -.6rem",
186
+ whiteSpace: "nowrap",
187
+ transition: "var(--transition-surface)",
188
+ ...itemDesignStyle,
189
+ ...pressableStyle,
190
+ } as React.CSSProperties;
191
+ const itemContent = (
192
+ <>
193
+ {leadingIcon && (
194
+ <IconInner
195
+ {...leadingIconProps}
196
+ icon={leadingIcon}
197
+ size={leadingIconProps?.size ?? 18}
198
+ />
199
+ )}
200
+ <Text
201
+ type="Footnote"
202
+ style={{
203
+ display: "inline-flex",
204
+ alignItems: "center",
205
+ whiteSpace: "nowrap",
206
+ }}
207
+ >
208
+ {content}
209
+ </Text>
210
+ {trailingIcon && (
211
+ <IconInner
212
+ {...trailingIconProps}
213
+ icon={trailingIcon}
214
+ size={trailingIconProps?.size ?? 18}
215
+ />
216
+ )}
217
+ </>
218
+ );
219
+
220
+ if (!pressable) {
221
+ return (
222
+ <Text className={itemClassName} style={itemStyle}>
223
+ {itemContent}
224
+ </Text>
225
+ );
226
+ }
227
+
228
+ return (
229
+ <Pressable
230
+ {...designProps}
231
+ {...pressableProps}
232
+ className={itemClassName}
233
+ radius="Circle"
234
+ alignItems="center"
235
+ gap={4}
236
+ padding={[4, 6]}
237
+ style={{
238
+ display: "inline-flex",
239
+ minWidth: 0,
240
+ minHeight: "2.6rem",
241
+ margin: "-.4rem -.6rem",
242
+ whiteSpace: "nowrap",
243
+ transition: "var(--transition-surface)",
244
+ ...itemDesignStyle,
245
+ ...pressableStyle,
246
+ }}
247
+ >
248
+ {itemContent}
249
+ </Pressable>
250
+ );
251
+ }
252
+
253
+ export function TitleContext({
254
+ start,
255
+ end,
256
+ startGap,
257
+ endGap,
258
+ className,
259
+ style,
260
+ }: TitleContextProps) {
261
+ const startItems = toContextItems(start);
262
+ const endItems = toContextItems(end);
263
+
264
+ if (startItems.length === 0 && endItems.length === 0) {
265
+ return null;
266
+ }
267
+
268
+ return (
269
+ <View
270
+ gap={6}
271
+ className={className}
272
+ fullWidth
273
+ style={{ minHeight: "2.8rem", ...style }}
274
+ alignItems="center"
275
+ justifyContent="space-between"
276
+ >
277
+ {startItems.length > 0 && (
278
+ <View gap={startGap ?? 16} alignItems="center">
279
+ {startItems.map((item, index) => (
280
+ <TitleContextItemView
281
+ key={getContextItemKey(item, "start", index)}
282
+ item={item}
283
+ />
284
+ ))}
285
+ </View>
286
+ )}
287
+ {endItems.length > 0 && (
288
+ <View gap={endGap ?? 16} justifyContent="flex-end" alignItems="center">
289
+ {endItems.map((item, index) => (
290
+ <TitleContextItemView
291
+ key={getContextItemKey(item, "end", index)}
292
+ item={item}
293
+ />
294
+ ))}
295
+ </View>
296
+ )}
297
+ </View>
298
+ );
299
+ }
300
+
301
+ const Title = Object.assign(TitleRoot, {
302
+ ContextBar: TitleContext,
303
+ });
304
+
305
+ export default Title;
@@ -0,0 +1,64 @@
1
+ import type { IconGroupProps } from "@/packages/Components/Icon/Icon.group.types";
2
+ import type { IconProps } from "@/packages/Components/Icon/Icon.types";
3
+ import type { PillProps } from "@/packages/Components/Pill/Pill.types";
4
+ import type { TextProps } from "@/packages/Components/Text/Text.types";
5
+ import type { PressableProps } from "@/packages/Frameworks/Pressable/Pressable.types";
6
+ import type { ViewProps } from "@/packages/Frameworks/View/View.types";
7
+
8
+ interface TitleItemProps {
9
+ "data-color-mode"?: string;
10
+ text?: React.ReactNode;
11
+ active?: boolean;
12
+ pressable?: PressableProps;
13
+ }
14
+
15
+ export interface TitleProps {
16
+ "data-color-mode"?: string;
17
+ className?: string;
18
+ style?: React.CSSProperties;
19
+ title?: TitleItemProps[] | TitleItemProps;
20
+ titleType?: TextProps["type"];
21
+ titleClassName?: TextProps["className"];
22
+ titleGap?: ViewProps["gap"];
23
+ leftGap?: ViewProps["gap"];
24
+ rightGap?: ViewProps["gap"];
25
+ fontType?: TextProps["fontType"];
26
+ titleLineHeight?: number;
27
+ titleVerticalTrim?: boolean;
28
+ suffix?: PillProps;
29
+ meta?: React.ReactNode;
30
+ actions?: IconGroupProps["icons"];
31
+ caption?: React.ReactNode;
32
+ captionOpacity?: TextProps["opacity"];
33
+ context?: React.ReactNode;
34
+ }
35
+
36
+ export type TitleContextItem = Pick<
37
+ PressableProps,
38
+ | "className"
39
+ | "style"
40
+ | "themePreset"
41
+ | "background"
42
+ | "color"
43
+ | "themeInteractive"
44
+ | "selected"
45
+ | "border"
46
+ | "data-color-mode"
47
+ > & {
48
+ key?: React.Key;
49
+ content: React.ReactNode;
50
+ leadingIcon?: IconProps["icon"];
51
+ trailingIcon?: IconProps["icon"];
52
+ leadingIconProps?: Omit<IconProps, "icon" | "pressable">;
53
+ trailingIconProps?: Omit<IconProps, "icon" | "pressable">;
54
+ pressable?: PressableProps;
55
+ };
56
+
57
+ export interface TitleContextProps {
58
+ className?: string;
59
+ style?: React.CSSProperties;
60
+ start?: TitleContextItem | TitleContextItem[];
61
+ end?: TitleContextItem | TitleContextItem[];
62
+ startGap?: ViewProps["gap"];
63
+ endGap?: ViewProps["gap"];
64
+ }
@@ -0,0 +1,32 @@
1
+ .ToggleWrapper {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 0.8rem;
5
+ width: max-content;
6
+ background: transparent;
7
+
8
+ .Input {
9
+ position: absolute;
10
+ visibility: hidden;
11
+ pointer-events: none;
12
+ }
13
+ }
14
+
15
+ .ToggleCircle {
16
+ position: absolute;
17
+ top: var(--toggle-padding, 0.2rem);
18
+ left: var(--toggle-padding, 0.2rem);
19
+ width: calc(
20
+ (var(--toggle-circle-size, 2rem) - var(--toggle-padding, 0.2rem)) * 1.5
21
+ );
22
+ height: calc(var(--toggle-circle-size, 2rem));
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ z-index: 1;
27
+ transform: scale(1);
28
+ transform-origin: center;
29
+ will-change: transform, background;
30
+ cursor: default;
31
+ touch-action: pan-x;
32
+ }
@@ -0,0 +1,272 @@
1
+ "use client";
2
+ import clsx from "clsx";
3
+ import { motion } from "motion/react";
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import Spinner from "@/packages/Components/Spinner/Spinner";
12
+ import Text from "@/packages/Components/Text/Text";
13
+ import styles from "@/packages/Components/Toggle/Toggle.module.scss";
14
+ import type { ToggleProps } from "@/packages/Components/Toggle/Toggle.types";
15
+ import { Size, SizePX } from "@/packages/Frameworks/_shared/sizing";
16
+ import { useControllableState } from "@/packages/Frameworks/_shared/useControllableState";
17
+ import { Radius } from "@/packages/Frameworks/Theme/Radius.types";
18
+ import { Border } from "@/packages/Frameworks/Theme/Theme.types";
19
+ import View from "@/packages/Frameworks/View/View";
20
+
21
+ type ToggleTrackStyle = React.CSSProperties & {
22
+ "--toggle-padding"?: string;
23
+ "--toggle-circle-size"?: string;
24
+ };
25
+
26
+ export default function Toggle({
27
+ title,
28
+ titleType,
29
+ titleSpaceBetween,
30
+ reversed,
31
+ readOnly,
32
+ loading,
33
+ size,
34
+ width,
35
+ height,
36
+ checked: checkedProp,
37
+ defaultChecked,
38
+ disabled,
39
+ onChange,
40
+ themePreset,
41
+ background,
42
+ color,
43
+ ...props
44
+ }: ToggleProps) {
45
+ const resolvedHeight = size ?? height;
46
+ const heightPX = SizePX(resolvedHeight, 24, 10);
47
+ const resolvedWidth =
48
+ width ?? (heightPX !== undefined ? heightPX * 1.75 : undefined);
49
+ const togglePadding =
50
+ heightPX !== undefined ? `${heightPX / 12}px` : undefined;
51
+ const toggleCircleSize =
52
+ heightPX !== undefined
53
+ ? `${Math.max(0, heightPX - (heightPX / 12) * 2)}px`
54
+ : undefined;
55
+ const toggleTrackStyle: ToggleTrackStyle = {
56
+ order: reversed ? 1 : undefined,
57
+ cursor: "default",
58
+ position: "relative",
59
+ "--toggle-padding": togglePadding,
60
+ "--toggle-circle-size": toggleCircleSize,
61
+ };
62
+ const [checked, setChecked, isControlled] = useControllableState({
63
+ value: checkedProp,
64
+ defaultValue: Boolean(defaultChecked),
65
+ });
66
+ const [dragRange, setDragRange] = useState(0);
67
+ const [dragging, setDragging] = useState(false);
68
+ const [pressed, setPressed] = useState(false);
69
+ const [visualLabelChecked, setVisualLabelChecked] = useState(() => checked);
70
+ const inputRef = useRef<HTMLInputElement>(null);
71
+ const trackRef = useRef<HTMLDivElement>(null);
72
+ const knobRef = useRef<HTMLDivElement>(null);
73
+ const suppressClickRef = useRef(false);
74
+ const dragStartXRef = useRef(0);
75
+ const visualLabelCheckedRef = useRef(checked);
76
+ const emitChange = useCallback(
77
+ (nextChecked: boolean) => {
78
+ if (disabled || readOnly || loading) return;
79
+ if (!isControlled) setChecked(nextChecked);
80
+ if (inputRef.current) inputRef.current.checked = nextChecked;
81
+ onChange?.({
82
+ target: (inputRef.current ?? {
83
+ checked: nextChecked,
84
+ }) as HTMLInputElement,
85
+ currentTarget: (inputRef.current ?? {
86
+ checked: nextChecked,
87
+ }) as HTMLInputElement,
88
+ } as React.ChangeEvent<HTMLInputElement>);
89
+ },
90
+ [disabled, isControlled, loading, onChange, readOnly, setChecked],
91
+ );
92
+ useLayoutEffect(() => {
93
+ const updateRange = () => {
94
+ if (!trackRef.current || !knobRef.current) return;
95
+ const trackWidth = trackRef.current.getBoundingClientRect().width;
96
+ const knobWidth = knobRef.current.getBoundingClientRect().width;
97
+ const knobStyle = window.getComputedStyle(knobRef.current);
98
+ const knobLeft = Number.parseFloat(knobStyle.left) || 0;
99
+ setDragRange(Math.max(0, trackWidth - knobWidth - knobLeft * 2));
100
+ };
101
+ updateRange();
102
+ window.addEventListener("resize", updateRange);
103
+ const resizeObserver =
104
+ typeof ResizeObserver !== "undefined"
105
+ ? new ResizeObserver(() => {
106
+ updateRange();
107
+ })
108
+ : null;
109
+ if (resizeObserver) {
110
+ if (trackRef.current) {
111
+ resizeObserver.observe(trackRef.current);
112
+ }
113
+ if (knobRef.current) {
114
+ resizeObserver.observe(knobRef.current);
115
+ }
116
+ }
117
+ return () => {
118
+ window.removeEventListener("resize", updateRange);
119
+ resizeObserver?.disconnect();
120
+ };
121
+ }, []);
122
+ const handleToggle = useCallback(() => {
123
+ emitChange(!checked);
124
+ }, [checked, emitChange]);
125
+
126
+ const applyVisualProgress = useCallback((nextProgress: number) => {
127
+ const clampedProgress = Math.max(0, Math.min(1, nextProgress));
128
+ const progressPercent = `${Math.round(clampedProgress * 100)}%`;
129
+ const nextLabelChecked = clampedProgress >= 0.5;
130
+
131
+ if (trackRef.current) {
132
+ trackRef.current.style.background = `color-mix(in srgb, var(--color-base-dark-1) ${progressPercent}, color-mix(in srgb, var(--color-base-light-4) 55%, transparent))`;
133
+ }
134
+
135
+ if (knobRef.current) {
136
+ knobRef.current.style.background = `color-mix(in srgb, var(--color-base-light-1) ${progressPercent}, var(--color-base-light-1))`;
137
+ }
138
+
139
+ if (visualLabelCheckedRef.current !== nextLabelChecked) {
140
+ visualLabelCheckedRef.current = nextLabelChecked;
141
+ setVisualLabelChecked(nextLabelChecked);
142
+ }
143
+ }, []);
144
+
145
+ useEffect(() => {
146
+ visualLabelCheckedRef.current = checked;
147
+ setVisualLabelChecked(checked);
148
+ applyVisualProgress(checked ? 1 : 0);
149
+ }, [applyVisualProgress, checked]);
150
+
151
+ return (
152
+ <label
153
+ className={styles.ToggleWrapper}
154
+ title={title}
155
+ data-disabled={disabled ? "true" : undefined}
156
+ data-readonly={readOnly ? "true" : undefined}
157
+ style={{
158
+ justifyContent: titleSpaceBetween ? "space-between" : undefined,
159
+ width: titleSpaceBetween ? "100%" : undefined,
160
+ }}
161
+ >
162
+ <input
163
+ ref={inputRef}
164
+ type="checkbox"
165
+ role="switch"
166
+ aria-checked={checked}
167
+ readOnly
168
+ className={styles.Input}
169
+ checked={checked}
170
+ disabled={disabled || loading}
171
+ {...props}
172
+ onKeyDown={(e) => {
173
+ if (disabled || readOnly || loading) return;
174
+ if (e.key === "Enter" || e.key === " ") {
175
+ e.preventDefault();
176
+ handleToggle();
177
+ }
178
+ props.onKeyDown?.(e);
179
+ }}
180
+ />
181
+ <View
182
+ ref={trackRef}
183
+ width={Size(resolvedWidth) ?? "4.4rem"}
184
+ height={Size(resolvedHeight) ?? "2.4rem"}
185
+ data-checked={checked || undefined}
186
+ data-loading={loading || undefined}
187
+ data-dragging={dragging || undefined}
188
+ data-pressed={pressed || undefined}
189
+ onClick={(e) => {
190
+ e.preventDefault();
191
+ if (suppressClickRef.current) {
192
+ suppressClickRef.current = false;
193
+ return;
194
+ }
195
+ handleToggle();
196
+ }}
197
+ onPointerDown={() => {
198
+ if (disabled || readOnly || loading) return;
199
+ setPressed(true);
200
+ }}
201
+ onPointerUp={() => {
202
+ setPressed(false);
203
+ }}
204
+ onPointerCancel={() => {
205
+ setPressed(false);
206
+ }}
207
+ onPointerLeave={() => {
208
+ if (!dragging) {
209
+ setPressed(false);
210
+ }
211
+ }}
212
+ style={toggleTrackStyle}
213
+ radius="Circle"
214
+ >
215
+ <motion.div
216
+ ref={knobRef}
217
+ className={clsx(styles.ToggleCircle, Border("Base3TP1"))}
218
+ style={{ borderRadius: Radius("Circle") }}
219
+ drag={disabled || readOnly || loading ? false : "x"}
220
+ dragConstraints={{ left: 0, right: dragRange }}
221
+ dragElastic={0}
222
+ dragMomentum={false}
223
+ animate={{
224
+ x: checked ? dragRange : 0,
225
+ scale: pressed || dragging ? 1.4 : 1,
226
+ }}
227
+ transition={{
228
+ x: { type: "spring", stiffness: 700, damping: 40 },
229
+ scale: { type: "spring", stiffness: 420, damping: 22 },
230
+ }}
231
+ onDragStart={() => {
232
+ setDragging(true);
233
+ setPressed(true);
234
+ suppressClickRef.current = false;
235
+ dragStartXRef.current = checked ? dragRange : 0;
236
+ }}
237
+ onDrag={(_, info) => {
238
+ const currentX = Math.max(
239
+ 0,
240
+ Math.min(dragRange, dragStartXRef.current + info.offset.x),
241
+ );
242
+ applyVisualProgress(dragRange ? currentX / dragRange : 0);
243
+ }}
244
+ onDragEnd={(_, info) => {
245
+ setDragging(false);
246
+ setPressed(false);
247
+ suppressClickRef.current = Math.abs(info.offset.x) > 3;
248
+ const startX = checked ? dragRange : 0;
249
+ const endX = Math.max(
250
+ 0,
251
+ Math.min(dragRange, startX + info.offset.x),
252
+ );
253
+ const nextChecked = endX >= dragRange / 2;
254
+ applyVisualProgress(nextChecked ? 1 : 0);
255
+ emitChange(nextChecked);
256
+ }}
257
+ >
258
+ {loading ? (
259
+ <View opacity={0.4}>
260
+ <Spinner
261
+ size={12}
262
+ strokeWidth={6}
263
+ color={visualLabelChecked ? "Reversed1" : "Base1"}
264
+ />
265
+ </View>
266
+ ) : null}
267
+ </motion.div>
268
+ </View>
269
+ {title && <Text type={titleType ?? "Subheadline"}>{title}</Text>}
270
+ </label>
271
+ );
272
+ }
@@ -0,0 +1,17 @@
1
+ import type { TextProps } from "@/packages/Components/Text/Text.types";
2
+ import type { UIKitSizeValue } from "@/packages/Frameworks/_shared/sizing";
3
+ import type { ThemeSystemProps } from "@/packages/Frameworks/Theme/Theme.types";
4
+
5
+ export interface ToggleProps
6
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "color">,
7
+ ThemeSystemProps {
8
+ title?: string;
9
+ titleType?: TextProps["type"];
10
+ titleSpaceBetween?: boolean;
11
+ reversed?: boolean;
12
+ readOnly?: boolean;
13
+ loading?: boolean;
14
+ size?: UIKitSizeValue;
15
+ width?: UIKitSizeValue;
16
+ height?: UIKitSizeValue;
17
+ }