index.stories.tsx 11 KB

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