@cerberus-design/react 0.6.1 → 0.7.0-next-e86fd39

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 (213) hide show
  1. package/build/legacy/_tsup-dts-rollup.d.ts +336 -4
  2. package/build/legacy/aria-helpers/tabs.aria.js +2 -2
  3. package/build/legacy/aria-helpers/trap-focus.aria.js +7 -0
  4. package/build/legacy/aria-helpers/trap-focus.aria.js.map +1 -0
  5. package/build/legacy/chunk-2F5TB2EV.js +25 -0
  6. package/build/legacy/chunk-2F5TB2EV.js.map +1 -0
  7. package/build/legacy/chunk-4CAT3FHV.js +11 -0
  8. package/build/legacy/chunk-4CAT3FHV.js.map +1 -0
  9. package/build/legacy/chunk-4M3EUP57.js +22 -0
  10. package/build/legacy/chunk-4M3EUP57.js.map +1 -0
  11. package/build/{modern/chunk-X4YQ27D5.js → legacy/chunk-5GEC53G7.js} +5 -5
  12. package/build/legacy/{chunk-YJCWUN33.js → chunk-67S42J4B.js} +5 -16
  13. package/build/legacy/chunk-67S42J4B.js.map +1 -0
  14. package/build/legacy/{chunk-3CBN7U25.js → chunk-6TXQZ3PB.js} +6 -3
  15. package/build/legacy/chunk-6TXQZ3PB.js.map +1 -0
  16. package/build/legacy/{chunk-DQOYTLGB.js → chunk-7KJIZIAU.js} +9 -5
  17. package/build/legacy/chunk-7KJIZIAU.js.map +1 -0
  18. package/build/legacy/chunk-C45DY4VE.js +17 -0
  19. package/build/legacy/chunk-C45DY4VE.js.map +1 -0
  20. package/build/{modern/chunk-HE3HFKYU.js → legacy/chunk-CU7HXAKM.js} +5 -5
  21. package/build/legacy/{chunk-HE3HFKYU.js.map → chunk-CU7HXAKM.js.map} +1 -1
  22. package/build/legacy/chunk-D3ZXZA3U.js +155 -0
  23. package/build/legacy/chunk-D3ZXZA3U.js.map +1 -0
  24. package/build/legacy/chunk-DGPLSWFJ.js +208 -0
  25. package/build/legacy/chunk-DGPLSWFJ.js.map +1 -0
  26. package/build/legacy/{chunk-5XNLWIZO.js → chunk-EVEEQRH6.js} +2 -2
  27. package/build/legacy/chunk-EVEEQRH6.js.map +1 -0
  28. package/build/legacy/chunk-G3JEWPLM.js +29 -0
  29. package/build/legacy/chunk-G3JEWPLM.js.map +1 -0
  30. package/build/legacy/chunk-JI4YTPEJ.js +47 -0
  31. package/build/legacy/chunk-JI4YTPEJ.js.map +1 -0
  32. package/build/legacy/chunk-KESKDLX6.js +30 -0
  33. package/build/legacy/chunk-KESKDLX6.js.map +1 -0
  34. package/build/legacy/{chunk-S7HBD2A7.js → chunk-KFUXGX33.js} +2 -2
  35. package/build/legacy/chunk-OGSAAB6K.js +12 -0
  36. package/build/legacy/chunk-OGSAAB6K.js.map +1 -0
  37. package/build/{modern/chunk-G2QMBSK5.js → legacy/chunk-PMCYXRAH.js} +2 -2
  38. package/build/legacy/chunk-PMCYXRAH.js.map +1 -0
  39. package/build/legacy/{chunk-734PGVLT.js → chunk-TAZI77TP.js} +2 -2
  40. package/build/legacy/chunk-TPFNVGYA.js +21 -0
  41. package/build/legacy/chunk-TPFNVGYA.js.map +1 -0
  42. package/build/legacy/chunk-TZNYJ3G7.js +25 -0
  43. package/build/legacy/chunk-TZNYJ3G7.js.map +1 -0
  44. package/build/legacy/chunk-UPODPCRD.js +12 -0
  45. package/build/legacy/chunk-UPODPCRD.js.map +1 -0
  46. package/build/legacy/chunk-VULPMZUW.js +18 -0
  47. package/build/legacy/chunk-VULPMZUW.js.map +1 -0
  48. package/build/{modern/chunk-5GSXIYD3.js → legacy/chunk-X2JMXTBH.js} +6 -8
  49. package/build/{modern/chunk-5GSXIYD3.js.map → legacy/chunk-X2JMXTBH.js.map} +1 -1
  50. package/build/legacy/components/FeatureFlag.js +10 -0
  51. package/build/legacy/components/FeatureFlag.js.map +1 -0
  52. package/build/legacy/components/Input.js +4 -4
  53. package/build/legacy/components/Label.js +2 -2
  54. package/build/legacy/components/Modal.js +7 -0
  55. package/build/legacy/components/Modal.js.map +1 -0
  56. package/build/legacy/components/ModalDescription.js +7 -0
  57. package/build/legacy/components/ModalDescription.js.map +1 -0
  58. package/build/legacy/components/ModalHeader.js +7 -0
  59. package/build/legacy/components/ModalHeader.js.map +1 -0
  60. package/build/legacy/components/ModalHeading.js +7 -0
  61. package/build/legacy/components/ModalHeading.js.map +1 -0
  62. package/build/legacy/components/ModalIcon.js +7 -0
  63. package/build/legacy/components/ModalIcon.js.map +1 -0
  64. package/build/legacy/components/NavMenuList.js +1 -1
  65. package/build/legacy/components/Portal.js +7 -0
  66. package/build/legacy/components/Portal.js.map +1 -0
  67. package/build/legacy/components/Tab.js +3 -3
  68. package/build/legacy/components/TabList.js +2 -2
  69. package/build/legacy/components/TabPanel.js +2 -2
  70. package/build/legacy/components/Tag.js +1 -1
  71. package/build/legacy/components/Toggle.js +3 -3
  72. package/build/legacy/config/cerbIcons.js +1 -1
  73. package/build/legacy/config/defineIcons.js +2 -2
  74. package/build/legacy/context/confirm-modal.js +22 -0
  75. package/build/legacy/context/confirm-modal.js.map +1 -0
  76. package/build/legacy/context/feature-flags.js +10 -0
  77. package/build/legacy/context/feature-flags.js.map +1 -0
  78. package/build/legacy/context/prompt-modal.js +25 -0
  79. package/build/legacy/context/prompt-modal.js.map +1 -0
  80. package/build/legacy/context/tabs.js +1 -1
  81. package/build/legacy/hooks/useModal.js +8 -0
  82. package/build/legacy/hooks/useModal.js.map +1 -0
  83. package/build/legacy/index.js +89 -35
  84. package/build/modern/_tsup-dts-rollup.d.ts +336 -4
  85. package/build/modern/aria-helpers/tabs.aria.js +2 -2
  86. package/build/modern/aria-helpers/trap-focus.aria.js +7 -0
  87. package/build/modern/aria-helpers/trap-focus.aria.js.map +1 -0
  88. package/build/modern/chunk-2F5TB2EV.js +25 -0
  89. package/build/modern/chunk-2F5TB2EV.js.map +1 -0
  90. package/build/modern/chunk-4CAT3FHV.js +11 -0
  91. package/build/modern/chunk-4CAT3FHV.js.map +1 -0
  92. package/build/modern/chunk-4M3EUP57.js +22 -0
  93. package/build/modern/chunk-4M3EUP57.js.map +1 -0
  94. package/build/{legacy/chunk-X4YQ27D5.js → modern/chunk-5GEC53G7.js} +5 -5
  95. package/build/modern/{chunk-YJCWUN33.js → chunk-67S42J4B.js} +5 -16
  96. package/build/modern/chunk-67S42J4B.js.map +1 -0
  97. package/build/modern/{chunk-3CBN7U25.js → chunk-6TXQZ3PB.js} +6 -3
  98. package/build/modern/chunk-6TXQZ3PB.js.map +1 -0
  99. package/build/modern/{chunk-DQOYTLGB.js → chunk-7KJIZIAU.js} +9 -5
  100. package/build/modern/chunk-7KJIZIAU.js.map +1 -0
  101. package/build/modern/chunk-C45DY4VE.js +17 -0
  102. package/build/modern/chunk-C45DY4VE.js.map +1 -0
  103. package/build/modern/chunk-C5HLLGME.js +23 -0
  104. package/build/modern/chunk-C5HLLGME.js.map +1 -0
  105. package/build/{legacy/chunk-HE3HFKYU.js → modern/chunk-CU7HXAKM.js} +5 -5
  106. package/build/modern/{chunk-HE3HFKYU.js.map → chunk-CU7HXAKM.js.map} +1 -1
  107. package/build/modern/chunk-G3JEWPLM.js +29 -0
  108. package/build/modern/chunk-G3JEWPLM.js.map +1 -0
  109. package/build/modern/chunk-HBEEHHON.js +46 -0
  110. package/build/modern/chunk-HBEEHHON.js.map +1 -0
  111. package/build/modern/chunk-JIZQFTW6.js +29 -0
  112. package/build/modern/chunk-JIZQFTW6.js.map +1 -0
  113. package/build/modern/{chunk-S7HBD2A7.js → chunk-KFUXGX33.js} +2 -2
  114. package/build/modern/chunk-OGSAAB6K.js +12 -0
  115. package/build/modern/chunk-OGSAAB6K.js.map +1 -0
  116. package/build/{legacy/chunk-G2QMBSK5.js → modern/chunk-PMCYXRAH.js} +2 -2
  117. package/build/modern/chunk-PMCYXRAH.js.map +1 -0
  118. package/build/modern/chunk-TAVCJ54A.js +154 -0
  119. package/build/modern/chunk-TAVCJ54A.js.map +1 -0
  120. package/build/modern/{chunk-734PGVLT.js → chunk-TAZI77TP.js} +2 -2
  121. package/build/modern/chunk-TPFNVGYA.js +21 -0
  122. package/build/modern/chunk-TPFNVGYA.js.map +1 -0
  123. package/build/modern/chunk-UPODPCRD.js +12 -0
  124. package/build/modern/chunk-UPODPCRD.js.map +1 -0
  125. package/build/modern/chunk-VULPMZUW.js +18 -0
  126. package/build/modern/chunk-VULPMZUW.js.map +1 -0
  127. package/build/modern/chunk-WWG5QWXY.js +207 -0
  128. package/build/modern/chunk-WWG5QWXY.js.map +1 -0
  129. package/build/{legacy/chunk-5GSXIYD3.js → modern/chunk-X2JMXTBH.js} +6 -8
  130. package/build/{legacy/chunk-5GSXIYD3.js.map → modern/chunk-X2JMXTBH.js.map} +1 -1
  131. package/build/modern/{chunk-SLIOCQBZ.js → chunk-Z6IWNVPN.js} +2 -2
  132. package/build/modern/chunk-Z6IWNVPN.js.map +1 -0
  133. package/build/modern/components/FeatureFlag.js +10 -0
  134. package/build/modern/components/FeatureFlag.js.map +1 -0
  135. package/build/modern/components/Input.js +4 -4
  136. package/build/modern/components/Label.js +2 -2
  137. package/build/modern/components/Modal.js +7 -0
  138. package/build/modern/components/Modal.js.map +1 -0
  139. package/build/modern/components/ModalDescription.js +7 -0
  140. package/build/modern/components/ModalDescription.js.map +1 -0
  141. package/build/modern/components/ModalHeader.js +7 -0
  142. package/build/modern/components/ModalHeader.js.map +1 -0
  143. package/build/modern/components/ModalHeading.js +7 -0
  144. package/build/modern/components/ModalHeading.js.map +1 -0
  145. package/build/modern/components/ModalIcon.js +7 -0
  146. package/build/modern/components/ModalIcon.js.map +1 -0
  147. package/build/modern/components/NavMenuList.js +1 -1
  148. package/build/modern/components/Portal.js +7 -0
  149. package/build/modern/components/Portal.js.map +1 -0
  150. package/build/modern/components/Tab.js +3 -3
  151. package/build/modern/components/TabList.js +2 -2
  152. package/build/modern/components/TabPanel.js +2 -2
  153. package/build/modern/components/Tag.js +1 -1
  154. package/build/modern/components/Toggle.js +3 -3
  155. package/build/modern/config/cerbIcons.js +1 -1
  156. package/build/modern/config/defineIcons.js +2 -2
  157. package/build/modern/context/confirm-modal.js +22 -0
  158. package/build/modern/context/confirm-modal.js.map +1 -0
  159. package/build/modern/context/feature-flags.js +10 -0
  160. package/build/modern/context/feature-flags.js.map +1 -0
  161. package/build/modern/context/prompt-modal.js +25 -0
  162. package/build/modern/context/prompt-modal.js.map +1 -0
  163. package/build/modern/context/tabs.js +1 -1
  164. package/build/modern/hooks/useModal.js +8 -0
  165. package/build/modern/hooks/useModal.js.map +1 -0
  166. package/build/modern/index.js +89 -35
  167. package/package.json +3 -2
  168. package/src/aria-helpers/trap-focus.aria.ts +29 -0
  169. package/src/components/FeatureFlag.tsx +14 -0
  170. package/src/components/Label.tsx +1 -1
  171. package/src/components/Modal.tsx +37 -0
  172. package/src/components/ModalDescription.tsx +23 -0
  173. package/src/components/ModalHeader.tsx +37 -0
  174. package/src/components/ModalHeading.tsx +23 -0
  175. package/src/components/ModalIcon.tsx +28 -0
  176. package/src/components/NavMenuList.tsx +1 -1
  177. package/src/components/Portal.tsx +22 -0
  178. package/src/components/Tab.tsx +3 -84
  179. package/src/components/TabList.tsx +2 -4
  180. package/src/components/TabPanel.tsx +3 -14
  181. package/src/components/Tag.tsx +1 -1
  182. package/src/config/cerbIcons.ts +11 -2
  183. package/src/config/defineIcons.ts +6 -3
  184. package/src/context/confirm-modal.tsx +185 -0
  185. package/src/context/feature-flags.tsx +60 -0
  186. package/src/context/prompt-modal.tsx +232 -0
  187. package/src/context/tabs.tsx +15 -5
  188. package/src/hooks/useModal.ts +34 -0
  189. package/src/index.ts +12 -0
  190. package/build/legacy/chunk-3CBN7U25.js.map +0 -1
  191. package/build/legacy/chunk-5MNCW677.js +0 -11
  192. package/build/legacy/chunk-5MNCW677.js.map +0 -1
  193. package/build/legacy/chunk-5XNLWIZO.js.map +0 -1
  194. package/build/legacy/chunk-DQOYTLGB.js.map +0 -1
  195. package/build/legacy/chunk-G2QMBSK5.js.map +0 -1
  196. package/build/legacy/chunk-YA2UV2AB.js +0 -126
  197. package/build/legacy/chunk-YA2UV2AB.js.map +0 -1
  198. package/build/legacy/chunk-YJCWUN33.js.map +0 -1
  199. package/build/modern/chunk-3CBN7U25.js.map +0 -1
  200. package/build/modern/chunk-5MNCW677.js +0 -11
  201. package/build/modern/chunk-5MNCW677.js.map +0 -1
  202. package/build/modern/chunk-DQOYTLGB.js.map +0 -1
  203. package/build/modern/chunk-G2QMBSK5.js.map +0 -1
  204. package/build/modern/chunk-SLIOCQBZ.js.map +0 -1
  205. package/build/modern/chunk-SUP7U42W.js +0 -125
  206. package/build/modern/chunk-SUP7U42W.js.map +0 -1
  207. package/build/modern/chunk-YJCWUN33.js.map +0 -1
  208. /package/build/legacy/{chunk-X4YQ27D5.js.map → chunk-5GEC53G7.js.map} +0 -0
  209. /package/build/legacy/{chunk-S7HBD2A7.js.map → chunk-KFUXGX33.js.map} +0 -0
  210. /package/build/legacy/{chunk-734PGVLT.js.map → chunk-TAZI77TP.js.map} +0 -0
  211. /package/build/modern/{chunk-X4YQ27D5.js.map → chunk-5GEC53G7.js.map} +0 -0
  212. /package/build/modern/{chunk-S7HBD2A7.js.map → chunk-KFUXGX33.js.map} +0 -0
  213. /package/build/modern/{chunk-734PGVLT.js.map → chunk-TAZI77TP.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { css, cx } from '@cerberus/styled-system/css'
3
+ import { cx } from '@cerberus/styled-system/css'
4
4
  import { useMemo, type HTMLAttributes } from 'react'
5
5
  import { useTabsContext } from '../context/tabs'
6
6
  import { Show } from './Show'
@@ -26,7 +26,7 @@ export interface TabPanelProps extends HTMLAttributes<HTMLDivElement> {
26
26
  */
27
27
  export function TabPanel(props: TabPanelProps) {
28
28
  const { tab, ...nativeProps } = props
29
- const { active } = useTabsContext()
29
+ const { active, styles } = useTabsContext()
30
30
  const isActive = useMemo(() => active === tab, [active, tab])
31
31
 
32
32
  return (
@@ -35,18 +35,7 @@ export function TabPanel(props: TabPanelProps) {
35
35
  {...nativeProps}
36
36
  {...(isActive && { tabIndex: 0 })}
37
37
  aria-labelledby={tab}
38
- className={cx(
39
- nativeProps.className,
40
- css({
41
- rounded: 'md',
42
- _focusVisible: {
43
- boxShadow: 'none',
44
- outline: '3px solid',
45
- outlineColor: 'action.border.focus',
46
- outlineOffset: '2px',
47
- },
48
- }),
49
- )}
38
+ className={cx(nativeProps.className, styles.tabPanel)}
50
39
  id={`panel:${tab}`}
51
40
  role="tabpanel"
52
41
  />
@@ -37,7 +37,7 @@ export type TagProps = StaticTagProps | ClickableTagProps
37
37
  */
38
38
  export function Tag(props: PropsWithChildren<TagProps>): JSX.Element {
39
39
  const { shape: initShape, gradient, onClick, usage, ...nativeProps } = props
40
- const palette = props?.palette ?? 'neutral'
40
+ const palette = props?.palette ?? 'page'
41
41
  const isClosable = Boolean(onClick)
42
42
  const shape = isClosable ? 'pill' : initShape
43
43
  const closableStyles = isClosable ? closableCss : ''
@@ -1,12 +1,21 @@
1
- import { Checkmark, WarningFilled, type CarbonIconType } from '@cerberus/icons'
1
+ import {
2
+ Checkmark,
3
+ Information,
4
+ WarningFilled,
5
+ type CarbonIconType,
6
+ } from '@cerberus/icons'
2
7
  import type { ElementType } from 'react'
3
8
 
4
9
  export interface DefinedIcons {
10
+ confirmModal?: CarbonIconType | ElementType
11
+ promptModal?: CarbonIconType | ElementType
5
12
  invalid: CarbonIconType | ElementType
6
- toggleChecked: CarbonIconType | ElementType
13
+ toggleChecked?: CarbonIconType | ElementType
7
14
  }
8
15
 
9
16
  export const defaultIcons: DefinedIcons = {
17
+ confirmModal: Information,
18
+ promptModal: Information,
10
19
  invalid: WarningFilled,
11
20
  toggleChecked: Checkmark,
12
21
  }
@@ -8,12 +8,15 @@ function _validateIconsProperties(icons: DefinedIcons) {
8
8
  }
9
9
  }
10
10
 
11
- export function defineIcons(icons: DefinedIcons): DefinedIcons {
11
+ export function defineIcons(icons: DefinedIcons): Required<DefinedIcons> {
12
12
  _validateIconsProperties(icons)
13
- $cerberusIcons = icons
13
+ $cerberusIcons = {
14
+ ...defaultIcons,
15
+ ...icons,
16
+ } as Required<DefinedIcons>
14
17
  return $cerberusIcons
15
18
  }
16
19
 
17
20
  // Default icons
18
21
 
19
- export let $cerberusIcons: DefinedIcons = defaultIcons
22
+ export let $cerberusIcons = defaultIcons as Required<DefinedIcons>
@@ -0,0 +1,185 @@
1
+ 'use client'
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type MouseEvent,
11
+ type PropsWithChildren,
12
+ } from 'react'
13
+ import { Portal } from '../components/Portal'
14
+ import { Button } from '../components/Button'
15
+ import { css } from '@cerberus-design/styled-system/css'
16
+ import { hstack } from '@cerberus-design/styled-system/patterns'
17
+ import { $cerberusIcons } from '../config/defineIcons'
18
+ import { trapFocus } from '../aria-helpers/trap-focus.aria'
19
+ import { ModalIcon } from '../components/ModalIcon'
20
+ import { Show } from '../components/Show'
21
+ import { Modal } from '../components/Modal'
22
+ import { useModal } from '../hooks/useModal'
23
+ import { ModalHeader } from '../components/ModalHeader'
24
+ import { ModalHeading } from '../components/ModalHeading'
25
+ import { ModalDescription } from '../components/ModalDescription'
26
+
27
+ /**
28
+ * This module provides a context and hook for the confirm modal.
29
+ * @module
30
+ */
31
+
32
+ export interface ShowConfirmModalOptions {
33
+ kind?: 'destructive' | 'non-destructive'
34
+ heading: string
35
+ description?: string
36
+ actionText: string
37
+ cancelText: string
38
+ }
39
+ export type ShowResult =
40
+ | ((value: boolean | PromiseLike<boolean>) => void)
41
+ | null
42
+
43
+ export interface ConfirmModalValue {
44
+ show: (options: ShowConfirmModalOptions) => Promise<boolean>
45
+ }
46
+
47
+ const ConfirmModalContext = createContext<ConfirmModalValue | null>(null)
48
+
49
+ export interface ConfirmModalProviderProps {}
50
+
51
+ /**
52
+ * Provides a confirm modal to the app.
53
+ * @example
54
+ * ```tsx
55
+ * // Wrap the Provider around the root of the feature.
56
+ * <ConfirmModal>
57
+ * <SomeFeatureSection />
58
+ * </ConfirmModal>
59
+ *
60
+ * // Use the hook to show the confirm modal.
61
+ * const confirm = useConfirmModal()
62
+ *
63
+ * const handleClick = useCallback(async () => {
64
+ * const userConsent = await confirm.show({
65
+ * heading: 'Add new payment method?',
66
+ * description:
67
+ * 'This will add a new payment method to your account to be billed for future purchases.',
68
+ * actionText: 'Yes, add payment method',
69
+ * cancelText: 'No, cancel',
70
+ * })
71
+ * setConsent(userConsent)
72
+ * }, [confirm])
73
+ * ```
74
+ */
75
+ export function ConfirmModal(
76
+ props: PropsWithChildren<ConfirmModalProviderProps>,
77
+ ) {
78
+ const { modalRef, show, close } = useModal()
79
+ const resolveRef = useRef<ShowResult>(null)
80
+ const [content, setContent] = useState<ShowConfirmModalOptions | null>(null)
81
+ const focusTrap = trapFocus(modalRef)
82
+ const ConfirmIcon = $cerberusIcons.confirmModal
83
+
84
+ const palette = useMemo(
85
+ () => (content?.kind === 'destructive' ? 'danger' : 'action'),
86
+ [content],
87
+ )
88
+
89
+ const handleChoice = useCallback(
90
+ (e: MouseEvent<HTMLButtonElement>) => {
91
+ const target = e.currentTarget as HTMLButtonElement
92
+ if (target.value === 'true') {
93
+ resolveRef.current?.(true)
94
+ }
95
+ resolveRef.current?.(false)
96
+ close()
97
+ },
98
+ [close],
99
+ )
100
+
101
+ const handleShow = useCallback(
102
+ (options: ShowConfirmModalOptions) => {
103
+ return new Promise<boolean>((resolve) => {
104
+ setContent({ ...options, kind: options.kind || 'non-destructive' })
105
+ show()
106
+ resolveRef.current = resolve
107
+ })
108
+ },
109
+ [show],
110
+ )
111
+
112
+ const value = useMemo(
113
+ () => ({
114
+ show: handleShow,
115
+ }),
116
+ [handleShow],
117
+ )
118
+
119
+ return (
120
+ <ConfirmModalContext.Provider value={value}>
121
+ {props.children}
122
+
123
+ <Portal>
124
+ <Modal onKeyDown={focusTrap} ref={modalRef}>
125
+ <ModalHeader>
126
+ <Show
127
+ when={palette === 'danger'}
128
+ fallback={
129
+ <ModalIcon palette="action">
130
+ <ConfirmIcon size={24} />
131
+ </ModalIcon>
132
+ }
133
+ >
134
+ <ModalIcon palette="danger">
135
+ <ConfirmIcon size={24} />
136
+ </ModalIcon>
137
+ </Show>
138
+ <ModalHeading>{content?.heading}</ModalHeading>
139
+ <ModalDescription>{content?.description}</ModalDescription>
140
+ </ModalHeader>
141
+
142
+ <div
143
+ className={hstack({
144
+ gap: '4',
145
+ })}
146
+ >
147
+ <Button
148
+ autoFocus
149
+ className={css({
150
+ w: '1/2',
151
+ })}
152
+ name="confirm"
153
+ onClick={handleChoice}
154
+ palette={palette}
155
+ value="true"
156
+ >
157
+ {content?.actionText}
158
+ </Button>
159
+ <Button
160
+ className={css({
161
+ w: '1/2',
162
+ })}
163
+ name="cancel"
164
+ onClick={handleChoice}
165
+ usage="outlined"
166
+ value="false"
167
+ >
168
+ {content?.cancelText}
169
+ </Button>
170
+ </div>
171
+ </Modal>
172
+ </Portal>
173
+ </ConfirmModalContext.Provider>
174
+ )
175
+ }
176
+
177
+ export function useConfirmModal(): ConfirmModalValue {
178
+ const context = useContext(ConfirmModalContext)
179
+ if (context === null) {
180
+ throw new Error(
181
+ 'useConfirmModal must be used within a ConfirmModal Provider',
182
+ )
183
+ }
184
+ return context
185
+ }
@@ -0,0 +1,60 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext, type PropsWithChildren } from 'react'
4
+
5
+ /**
6
+ * This module provides a context and hook for feature flags.
7
+ * @module
8
+ */
9
+
10
+ export interface FeatureFlagValue {
11
+ [key: string]: boolean
12
+ }
13
+
14
+ const FeatureFlagContext = createContext<FeatureFlagValue | null>(null)
15
+
16
+ export interface FeatureFlagProviderProps {
17
+ flags: FeatureFlagValue
18
+ }
19
+
20
+ /**
21
+ * Provides feature flags to the application.
22
+ * @param flags - The flags data for the provider.
23
+ * @example
24
+ * ```tsx
25
+ * // This should be a JSON file or a server response.
26
+ * const flags = {
27
+ * featureOne: true,
28
+ * featureTwo: false
29
+ * }
30
+ *
31
+ * // Wrap the Provider around the root of your application.
32
+ * <FeatureFlags flags={flags}>
33
+ * <FeatureFlag flag="featureOne">
34
+ * This is visible.
35
+ * </FeatureFlag>
36
+ * <FeatureFlag flag="featureTwo">
37
+ * This is hidden.
38
+ * </FeatureFlag>
39
+ * </FeatureFlags>
40
+ * ```
41
+ */
42
+ export function FeatureFlags(
43
+ props: PropsWithChildren<FeatureFlagProviderProps>,
44
+ ) {
45
+ return (
46
+ <FeatureFlagContext.Provider value={props.flags}>
47
+ {props.children}
48
+ </FeatureFlagContext.Provider>
49
+ )
50
+ }
51
+
52
+ export function useFeatureFlags(key: string): boolean {
53
+ const context = useContext(FeatureFlagContext)
54
+ if (context === null) {
55
+ throw new Error(
56
+ 'useFeatureFlag must be used within a FeatureFlags Provider',
57
+ )
58
+ }
59
+ return context[key] ?? false
60
+ }
@@ -0,0 +1,232 @@
1
+ 'use client'
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ChangeEvent,
11
+ type MouseEvent,
12
+ type PropsWithChildren,
13
+ } from 'react'
14
+ import { Portal } from '../components/Portal'
15
+ import { Button } from '../components/Button'
16
+ import { css } from '@cerberus-design/styled-system/css'
17
+ import { hstack, vstack } from '@cerberus-design/styled-system/patterns'
18
+ import { trapFocus } from '../aria-helpers/trap-focus.aria'
19
+ import { Input } from '../components/Input'
20
+ import { Field } from './field'
21
+ import { Label } from '../components/Label'
22
+ import { $cerberusIcons } from '../config/defineIcons'
23
+ import { ModalIcon } from '../components/ModalIcon'
24
+ import { Show } from '../components/Show'
25
+ import { useModal } from '../hooks/useModal'
26
+ import { Modal } from '../components/Modal'
27
+ import { ModalHeader } from '../components/ModalHeader'
28
+ import { ModalHeading } from '../components/ModalHeading'
29
+ import { ModalDescription } from '../components/ModalDescription'
30
+
31
+ /**
32
+ * This module provides a context and hook for the prompt modal.
33
+ * @module
34
+ */
35
+
36
+ export interface ShowPromptModalOptions {
37
+ kind?: 'destructive' | 'non-destructive'
38
+ heading: string
39
+ description?: string
40
+ key: string
41
+ actionText: string
42
+ cancelText: string
43
+ }
44
+ export type PromptShowResult =
45
+ | ((value: string | PromiseLike<string>) => void)
46
+ | null
47
+
48
+ export interface PromptModalValue {
49
+ show: (options: ShowPromptModalOptions) => Promise<string>
50
+ }
51
+
52
+ const PromptModalContext = createContext<PromptModalValue | null>(null)
53
+
54
+ export interface PromptModalProviderProps {}
55
+
56
+ /**
57
+ * Provides a prompt modal to the app.
58
+ * @example
59
+ * ```tsx
60
+ * // Wrap the Provider around the root of the feature.
61
+ * <PromptModal>
62
+ * <SomeFeatureSection />
63
+ * </PromptModal>
64
+ *
65
+ * // Use the hook to show the prompt modal.
66
+ * const prompt = usePromptModal()
67
+ *
68
+ * const handleClick = useCallback(async () => {
69
+ * const accepted = await prompt.show({
70
+ * kind: 'destructive',
71
+ * heading: 'Delete channel?',
72
+ * description:
73
+ * 'This will permanently delete a channel on your account. There is no going back.',
74
+ * key: CHANNEL_NAME,
75
+ * actionText: 'Yes, delete channel',
76
+ * cancelText: 'No, cancel',
77
+ * })
78
+ * // do something with accepted
79
+ * }, [prompt])
80
+ * ```
81
+ */
82
+ export function PromptModal(
83
+ props: PropsWithChildren<PromptModalProviderProps>,
84
+ ) {
85
+ const { modalRef, show, close } = useModal()
86
+ const resolveRef = useRef<PromptShowResult>(null)
87
+ const [content, setContent] = useState<ShowPromptModalOptions | null>(null)
88
+ const [inputValue, setInputValue] = useState<string>('')
89
+ const focusTrap = trapFocus(modalRef)
90
+ const PromptIcon = $cerberusIcons.promptModal
91
+
92
+ const isValid = useMemo(
93
+ () => inputValue === content?.key,
94
+ [inputValue, content],
95
+ )
96
+
97
+ const palette = useMemo(
98
+ () => (content?.kind === 'destructive' ? 'danger' : 'action'),
99
+ [content],
100
+ )
101
+
102
+ const handleChange = useCallback(
103
+ (e: ChangeEvent<HTMLInputElement>) => {
104
+ setInputValue(e.currentTarget.value)
105
+ },
106
+ [content],
107
+ )
108
+
109
+ const handleChoice = useCallback(
110
+ (e: MouseEvent<HTMLButtonElement>) => {
111
+ const target = e.currentTarget as HTMLButtonElement
112
+ if (target.value === 'true') {
113
+ resolveRef.current?.(inputValue)
114
+ }
115
+ close()
116
+ },
117
+ [inputValue, close],
118
+ )
119
+
120
+ const handleShow = useCallback(
121
+ (options: ShowPromptModalOptions) => {
122
+ return new Promise<string>((resolve) => {
123
+ setContent({ ...options, kind: options.kind || 'non-destructive' })
124
+ show()
125
+ resolveRef.current = resolve
126
+ })
127
+ },
128
+ [show],
129
+ )
130
+
131
+ const value = useMemo(
132
+ () => ({
133
+ show: handleShow,
134
+ }),
135
+ [handleShow],
136
+ )
137
+
138
+ return (
139
+ <PromptModalContext.Provider value={value}>
140
+ {props.children}
141
+
142
+ <Portal>
143
+ <Modal onKeyDown={focusTrap} ref={modalRef}>
144
+ <ModalHeader>
145
+ <Show
146
+ when={palette === 'danger'}
147
+ fallback={
148
+ <ModalIcon palette="action">
149
+ <PromptIcon size={24} />
150
+ </ModalIcon>
151
+ }
152
+ >
153
+ <ModalIcon palette="danger">
154
+ <PromptIcon size={24} />
155
+ </ModalIcon>
156
+ </Show>
157
+ <ModalHeading>{content?.heading}</ModalHeading>
158
+ <ModalDescription>{content?.description}</ModalDescription>
159
+ </ModalHeader>
160
+
161
+ <div
162
+ className={vstack({
163
+ alignItems: 'flex-start',
164
+ mt: '4',
165
+ mb: '8',
166
+ })}
167
+ >
168
+ <Field invalid={!isValid}>
169
+ <Label htmlFor="confirm" size="md">
170
+ Type
171
+ <strong
172
+ className={css({
173
+ textTransform: 'uppercase',
174
+ })}
175
+ >
176
+ {content?.key}
177
+ </strong>
178
+ to confirm
179
+ </Label>
180
+ <Input
181
+ id="confirm"
182
+ name="confirm"
183
+ onChange={handleChange}
184
+ type="text"
185
+ />
186
+ </Field>
187
+ </div>
188
+
189
+ <div
190
+ className={hstack({
191
+ justifyContent: 'stretch',
192
+ gap: '4',
193
+ })}
194
+ >
195
+ <Button
196
+ autoFocus
197
+ className={css({
198
+ w: '1/2',
199
+ })}
200
+ disabled={!isValid}
201
+ name="confirm"
202
+ onClick={handleChoice}
203
+ palette={palette}
204
+ value="true"
205
+ >
206
+ {content?.actionText}
207
+ </Button>
208
+ <Button
209
+ className={css({
210
+ w: '1/2',
211
+ })}
212
+ name="cancel"
213
+ onClick={handleChoice}
214
+ usage="outlined"
215
+ value="false"
216
+ >
217
+ {content?.cancelText}
218
+ </Button>
219
+ </div>
220
+ </Modal>
221
+ </Portal>
222
+ </PromptModalContext.Provider>
223
+ )
224
+ }
225
+
226
+ export function usePromptModal(): PromptModalValue {
227
+ const context = useContext(PromptModalContext)
228
+ if (context === null) {
229
+ throw new Error('usePromptModal must be used within a PromptModal Provider')
230
+ }
231
+ return context
232
+ }
@@ -1,5 +1,10 @@
1
1
  'use client'
2
2
 
3
+ import {
4
+ tabs,
5
+ type TabsVariantProps,
6
+ } from '@cerberus-design/styled-system/recipes'
7
+ import type { Pretty } from '@cerberus-design/styled-system/types'
3
8
  import {
4
9
  createContext,
5
10
  useContext,
@@ -20,6 +25,7 @@ export interface TabsContextValue {
20
25
  tabs: MutableRefObject<HTMLButtonElement[]>
21
26
  id: string
22
27
  active: string
28
+ styles: Pretty<Record<'tabList' | 'tab' | 'tabPanel', string>>
23
29
  onTabUpdate: (active: string) => void
24
30
  }
25
31
 
@@ -33,6 +39,7 @@ export interface TabsProps {
33
39
 
34
40
  /**
35
41
  * The Tabs component provides a context to manage tab state.
42
+ * @param id - the id of the tabs component,
36
43
  * @param active - the default active tab id,
37
44
  * @param cache - whether to cache the active tab state in local storage
38
45
  * @example
@@ -49,22 +56,25 @@ export interface TabsProps {
49
56
  * </Tabs>
50
57
  * ```
51
58
  */
52
- export function Tabs(props: PropsWithChildren<TabsProps>): JSX.Element {
53
- const { cache, active, id } = props
59
+ export function Tabs(
60
+ props: PropsWithChildren<TabsProps & TabsVariantProps>,
61
+ ): JSX.Element {
62
+ const { cache, active, id, palette } = props
54
63
  const [activeTab, setActiveTab] = useState(() => (cache ? '' : active ?? ''))
55
- const tabs = useRef<HTMLButtonElement[]>([])
64
+ const tabsList = useRef<HTMLButtonElement[]>([])
56
65
  const uuid = useMemo(() => {
57
66
  return id ? `cerberus-tabs-${id}` : 'cerberus-tabs'
58
67
  }, [id])
59
68
 
60
69
  const value = useMemo(
61
70
  () => ({
62
- tabs,
71
+ tabs: tabsList,
63
72
  id: uuid,
64
73
  active: activeTab,
74
+ styles: tabs({ palette }),
65
75
  onTabUpdate: setActiveTab,
66
76
  }),
67
- [activeTab, setActiveTab, uuid, tabs],
77
+ [activeTab, setActiveTab, palette, uuid, tabsList],
68
78
  )
69
79
 
70
80
  // Get the active tab from local storage
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useMemo, useRef, type RefObject } from 'react'
4
+
5
+ /**
6
+ * This module provides a hook for using a custom modal.
7
+ * @module
8
+ */
9
+
10
+ interface UseModalReturnValue {
11
+ modalRef: RefObject<HTMLDialogElement>
12
+ show: () => void
13
+ close: () => void
14
+ }
15
+
16
+ export function useModal(): UseModalReturnValue {
17
+ const modalRef = useRef<HTMLDialogElement | null>(null)
18
+
19
+ const show = useCallback(() => {
20
+ modalRef.current?.showModal()
21
+ }, [])
22
+
23
+ const close = useCallback(() => {
24
+ modalRef.current?.close()
25
+ }, [])
26
+
27
+ return useMemo(() => {
28
+ return {
29
+ modalRef,
30
+ show,
31
+ close,
32
+ }
33
+ }, [modalRef, show, close])
34
+ }