index.stories.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. import type { Meta, StoryObj } from "@storybook/react";
  2. import { fn } from "@storybook/test";
  3. import { useState } from "react";
  4. import { Switch as SwitchComponent } from "./index.jsx";
  5. import styles from "./index.stories.module.scss";
  6. const meta = {
  7. component: SwitchComponent,
  8. argTypes: {
  9. isDisabled: {
  10. control: "boolean",
  11. table: {
  12. category: "State",
  13. },
  14. },
  15. isPending: {
  16. control: "boolean",
  17. table: {
  18. category: "State",
  19. },
  20. },
  21. isSelected: {
  22. control: "boolean",
  23. table: {
  24. category: "State",
  25. },
  26. },
  27. defaultSelected: {
  28. control: "boolean",
  29. table: {
  30. category: "State",
  31. },
  32. },
  33. onChange: {
  34. action: "changed",
  35. table: {
  36. category: "Behavior",
  37. },
  38. },
  39. children: {
  40. control: "text",
  41. table: {
  42. category: "Label",
  43. },
  44. },
  45. },
  46. tags: ["autodocs"],
  47. } satisfies Meta<typeof SwitchComponent>;
  48. export default meta;
  49. type Story = StoryObj<typeof SwitchComponent>;
  50. export const Default: Story = {
  51. args: {
  52. children: "Enable feature",
  53. onChange: fn(),
  54. },
  55. };
  56. export const WithLabel: Story = {
  57. args: {
  58. children: "Enable notifications",
  59. onChange: fn(),
  60. },
  61. };
  62. export const DefaultSelected: Story = {
  63. args: {
  64. children: "Already enabled",
  65. defaultSelected: true,
  66. onChange: fn(),
  67. },
  68. };
  69. export const Disabled: Story = {
  70. args: {
  71. children: "Disabled switch",
  72. isDisabled: true,
  73. onChange: fn(),
  74. },
  75. };
  76. export const DisabledSelected: Story = {
  77. args: {
  78. children: "Disabled but selected",
  79. isDisabled: true,
  80. defaultSelected: true,
  81. onChange: fn(),
  82. },
  83. };
  84. export const Pending: Story = {
  85. args: {
  86. children: "Loading...",
  87. isPending: true,
  88. onChange: fn(),
  89. },
  90. };
  91. export const PendingSelected: Story = {
  92. args: {
  93. children: "Saving changes...",
  94. isPending: true,
  95. defaultSelected: true,
  96. onChange: fn(),
  97. },
  98. };
  99. export const AllStates: Story = {
  100. render: () => (
  101. <div className={styles.statesGrid}>
  102. <div className={styles.stateRow}>
  103. <SwitchComponent onChange={fn()}>Normal</SwitchComponent>
  104. <span className={styles.description}>Default state</span>
  105. </div>
  106. <div className={styles.stateRow}>
  107. <SwitchComponent defaultSelected onChange={fn()}>Selected</SwitchComponent>
  108. <span className={styles.description}>Selected state</span>
  109. </div>
  110. <div className={styles.stateRow}>
  111. <SwitchComponent isDisabled onChange={fn()}>Disabled</SwitchComponent>
  112. <span className={styles.description}>Disabled state</span>
  113. </div>
  114. <div className={styles.stateRow}>
  115. <SwitchComponent isDisabled defaultSelected onChange={fn()}>Disabled Selected</SwitchComponent>
  116. <span className={styles.description}>Disabled & selected</span>
  117. </div>
  118. <div className={styles.stateRow}>
  119. <SwitchComponent isPending onChange={fn()}>Pending</SwitchComponent>
  120. <span className={styles.description}>Loading state</span>
  121. </div>
  122. <div className={styles.stateRow}>
  123. <SwitchComponent isPending defaultSelected onChange={fn()}>Pending Selected</SwitchComponent>
  124. <span className={styles.description}>Loading & selected</span>
  125. </div>
  126. </div>
  127. ),
  128. };
  129. export const ControlledExample: Story = {
  130. render: () => {
  131. const [isSelected, setIsSelected] = useState(false);
  132. const handleChange = fn((value: boolean) => {
  133. setIsSelected(value);
  134. });
  135. return (
  136. <div className={styles.controlledContainer}>
  137. <SwitchComponent
  138. isSelected={isSelected}
  139. onChange={handleChange}
  140. >
  141. Controlled switch
  142. </SwitchComponent>
  143. <p>Switch is {isSelected ? "ON" : "OFF"}</p>
  144. </div>
  145. );
  146. },
  147. };
  148. export const WithAsyncAction: Story = {
  149. render: () => {
  150. const [isSelected, setIsSelected] = useState(false);
  151. const [isPending, setIsPending] = useState(false);
  152. const handleChange = fn(async (value: boolean) => {
  153. setIsPending(true);
  154. // Simulate async operation
  155. await new Promise(resolve => setTimeout(resolve, 2000));
  156. setIsSelected(value);
  157. setIsPending(false);
  158. });
  159. return (
  160. <div className={styles.controlledContainer}>
  161. <SwitchComponent
  162. isSelected={isSelected}
  163. onChange={handleChange}
  164. isPending={isPending}
  165. >
  166. Save to server
  167. </SwitchComponent>
  168. <p>{isPending ? "Saving..." : `Saved state: ${isSelected ? "ON" : "OFF"}`}</p>
  169. </div>
  170. );
  171. },
  172. };
  173. export const SettingsExample: Story = {
  174. render: () => {
  175. const [settings, setSettings] = useState({
  176. notifications: true,
  177. darkMode: false,
  178. autoSave: true,
  179. analytics: false,
  180. });
  181. const handleSettingChange = (setting: keyof typeof settings) =>
  182. fn((value: boolean) => {
  183. setSettings(prev => ({ ...prev, [setting]: value }));
  184. });
  185. return (
  186. <div className={styles.settingsList}>
  187. <div className={styles.settingItem}>
  188. <SwitchComponent
  189. isSelected={settings.notifications}
  190. onChange={handleSettingChange('notifications')}
  191. >
  192. Push notifications
  193. </SwitchComponent>
  194. <span className={styles.settingDescription}>
  195. Receive alerts for important updates
  196. </span>
  197. </div>
  198. <div className={styles.settingItem}>
  199. <SwitchComponent
  200. isSelected={settings.darkMode}
  201. onChange={handleSettingChange('darkMode')}
  202. >
  203. Dark mode
  204. </SwitchComponent>
  205. <span className={styles.settingDescription}>
  206. Use dark theme for better night viewing
  207. </span>
  208. </div>
  209. <div className={styles.settingItem}>
  210. <SwitchComponent
  211. isSelected={settings.autoSave}
  212. onChange={handleSettingChange('autoSave')}
  213. >
  214. Auto-save
  215. </SwitchComponent>
  216. <span className={styles.settingDescription}>
  217. Automatically save your work
  218. </span>
  219. </div>
  220. <div className={styles.settingItem}>
  221. <SwitchComponent
  222. isSelected={settings.analytics}
  223. onChange={handleSettingChange('analytics')}
  224. isDisabled
  225. >
  226. Analytics (Pro only)
  227. </SwitchComponent>
  228. <span className={styles.settingDescription}>
  229. Advanced usage analytics
  230. </span>
  231. </div>
  232. </div>
  233. );
  234. },
  235. };
  236. export const FeatureFlags: Story = {
  237. render: () => {
  238. const [flags, setFlags] = useState({
  239. betaFeatures: false,
  240. experimentalApi: false,
  241. debugMode: false,
  242. });
  243. const [pendingFlags, setPendingFlags] = useState<string[]>([]);
  244. const handleFlagChange = (flag: keyof typeof flags) =>
  245. fn(async (value: boolean) => {
  246. setPendingFlags(prev => [...prev, flag]);
  247. // Simulate API call
  248. await new Promise(resolve => setTimeout(resolve, 1500));
  249. setFlags(prev => ({ ...prev, [flag]: value }));
  250. setPendingFlags(prev => prev.filter(f => f !== flag));
  251. });
  252. return (
  253. <div className={styles.featureFlags}>
  254. <h3>Feature Flags</h3>
  255. <div className={styles.flagItem}>
  256. <SwitchComponent
  257. isSelected={flags.betaFeatures}
  258. onChange={handleFlagChange('betaFeatures')}
  259. isPending={pendingFlags.includes('betaFeatures')}
  260. >
  261. Enable beta features
  262. </SwitchComponent>
  263. </div>
  264. <div className={styles.flagItem}>
  265. <SwitchComponent
  266. isSelected={flags.experimentalApi}
  267. onChange={handleFlagChange('experimentalApi')}
  268. isPending={pendingFlags.includes('experimentalApi')}
  269. >
  270. Use experimental API
  271. </SwitchComponent>
  272. </div>
  273. <div className={styles.flagItem}>
  274. <SwitchComponent
  275. isSelected={flags.debugMode}
  276. onChange={handleFlagChange('debugMode')}
  277. isPending={pendingFlags.includes('debugMode')}
  278. >
  279. Debug mode
  280. </SwitchComponent>
  281. </div>
  282. </div>
  283. );
  284. },
  285. };
  286. export const PermissionsExample: Story = {
  287. render: () => {
  288. const permissions = [
  289. { id: 'read', label: 'Read access', enabled: true, locked: false },
  290. { id: 'write', label: 'Write access', enabled: false, locked: false },
  291. { id: 'delete', label: 'Delete access', enabled: false, locked: true },
  292. { id: 'admin', label: 'Admin access', enabled: false, locked: true },
  293. ];
  294. return (
  295. <div className={styles.permissionsList}>
  296. <h3>User Permissions</h3>
  297. {permissions.map(permission => (
  298. <div key={permission.id} className={styles.permissionItem}>
  299. <SwitchComponent
  300. defaultSelected={permission.enabled}
  301. isDisabled={permission.locked}
  302. onChange={fn()}
  303. >
  304. {permission.label}
  305. </SwitchComponent>
  306. {permission.locked && (
  307. <span className={styles.lockedBadge}>Requires upgrade</span>
  308. )}
  309. </div>
  310. ))}
  311. </div>
  312. );
  313. },
  314. };
  315. export const WithCustomLabels: Story = {
  316. render: () => (
  317. <div className={styles.customLabels}>
  318. <SwitchComponent onChange={fn()}>
  319. {({ isSelected }) => (
  320. <span className={styles.dynamicLabel}>
  321. {isSelected ? "🌙 Night mode" : "☀️ Day mode"}
  322. </span>
  323. )}
  324. </SwitchComponent>
  325. <SwitchComponent onChange={fn()}>
  326. {({ isSelected }) => (
  327. <span className={styles.dynamicLabel}>
  328. Status: <strong>{isSelected ? "Active" : "Inactive"}</strong>
  329. </span>
  330. )}
  331. </SwitchComponent>
  332. <SwitchComponent onChange={fn()}>
  333. {({ isSelected }) => (
  334. <span className={styles.dynamicLabel}>
  335. {isSelected ? "✅ Subscribed" : "❌ Unsubscribed"}
  336. </span>
  337. )}
  338. </SwitchComponent>
  339. </div>
  340. ),
  341. };
  342. export const ErrorHandling: Story = {
  343. render: () => {
  344. const [isSelected, setIsSelected] = useState(false);
  345. const [error, setError] = useState<string | null>(null);
  346. const [isPending, setIsPending] = useState(false);
  347. const handleChange = fn(async (value: boolean) => {
  348. setError(null);
  349. setIsPending(true);
  350. try {
  351. // Simulate API call that might fail
  352. await new Promise((resolve, reject) => {
  353. setTimeout(() => {
  354. if (Math.random() > 0.5) {
  355. resolve(true);
  356. } else {
  357. reject(new Error("Failed to update setting"));
  358. }
  359. }, 1000);
  360. });
  361. setIsSelected(value);
  362. } catch (err) {
  363. setError((err as Error).message);
  364. } finally {
  365. setIsPending(false);
  366. }
  367. });
  368. return (
  369. <div className={styles.errorExample}>
  370. <SwitchComponent
  371. isSelected={isSelected}
  372. onChange={handleChange}
  373. isPending={isPending}
  374. >
  375. Risky operation (50% failure rate)
  376. </SwitchComponent>
  377. {error && (
  378. <div className={styles.errorMessage}>
  379. ⚠️ {error}
  380. </div>
  381. )}
  382. </div>
  383. );
  384. },
  385. };
  386. // Legacy export for backwards compatibility
  387. export const Switch = Default;