panelui-native 0.2.0 → 0.5.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 (106) hide show
  1. package/README.md +258 -0
  2. package/lib/module/components/accordion/index.js +312 -0
  3. package/lib/module/components/accordion/index.js.map +1 -0
  4. package/lib/module/components/alert/index.js +106 -13
  5. package/lib/module/components/alert/index.js.map +1 -1
  6. package/lib/module/components/avatar/index.js +58 -17
  7. package/lib/module/components/avatar/index.js.map +1 -1
  8. package/lib/module/components/badge/index.js +29 -8
  9. package/lib/module/components/badge/index.js.map +1 -1
  10. package/lib/module/components/button/index.js +87 -26
  11. package/lib/module/components/button/index.js.map +1 -1
  12. package/lib/module/components/checkbox/index.js +59 -18
  13. package/lib/module/components/checkbox/index.js.map +1 -1
  14. package/lib/module/components/empty-state/index.js +178 -0
  15. package/lib/module/components/empty-state/index.js.map +1 -0
  16. package/lib/module/components/frame/index.js +64 -13
  17. package/lib/module/components/frame/index.js.map +1 -1
  18. package/lib/module/components/input-group/index.js +132 -0
  19. package/lib/module/components/input-group/index.js.map +1 -0
  20. package/lib/module/components/label/index.js +117 -0
  21. package/lib/module/components/label/index.js.map +1 -0
  22. package/lib/module/components/progress/index.js +138 -0
  23. package/lib/module/components/progress/index.js.map +1 -0
  24. package/lib/module/components/steps/index.js +354 -0
  25. package/lib/module/components/steps/index.js.map +1 -0
  26. package/lib/module/components/tabs/index.js +9 -3
  27. package/lib/module/components/tabs/index.js.map +1 -1
  28. package/lib/module/components/timeline/index.js +633 -0
  29. package/lib/module/components/timeline/index.js.map +1 -0
  30. package/lib/module/components/toast/index.js +569 -0
  31. package/lib/module/components/toast/index.js.map +1 -0
  32. package/lib/module/components/toast/toast-store.js +81 -0
  33. package/lib/module/components/toast/toast-store.js.map +1 -0
  34. package/lib/module/components/typography/index.js +126 -0
  35. package/lib/module/components/typography/index.js.map +1 -0
  36. package/lib/module/icons/index.js +590 -8
  37. package/lib/module/icons/index.js.map +1 -1
  38. package/lib/module/index.js +11 -2
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/providers/panel-ui-provider.js +2 -1
  41. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  42. package/lib/module/theme/use-theme.js +90 -3
  43. package/lib/module/theme/use-theme.js.map +1 -1
  44. package/lib/typescript/src/components/accordion/index.d.ts +56 -0
  45. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -0
  46. package/lib/typescript/src/components/alert/index.d.ts +56 -4
  47. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  48. package/lib/typescript/src/components/avatar/index.d.ts +8 -1
  49. package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/badge/index.d.ts +38 -0
  51. package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/button/index.d.ts +57 -0
  53. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/checkbox/index.d.ts +50 -0
  55. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  56. package/lib/typescript/src/components/empty-state/index.d.ts +73 -0
  57. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -0
  58. package/lib/typescript/src/components/frame/index.d.ts +39 -5
  59. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  60. package/lib/typescript/src/components/input-group/index.d.ts +34 -0
  61. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -0
  62. package/lib/typescript/src/components/label/index.d.ts +28 -0
  63. package/lib/typescript/src/components/label/index.d.ts.map +1 -0
  64. package/lib/typescript/src/components/progress/index.d.ts +132 -0
  65. package/lib/typescript/src/components/progress/index.d.ts.map +1 -0
  66. package/lib/typescript/src/components/steps/index.d.ts +62 -0
  67. package/lib/typescript/src/components/steps/index.d.ts.map +1 -0
  68. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  69. package/lib/typescript/src/components/timeline/index.d.ts +96 -0
  70. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -0
  71. package/lib/typescript/src/components/toast/index.d.ts +176 -0
  72. package/lib/typescript/src/components/toast/index.d.ts.map +1 -0
  73. package/lib/typescript/src/components/toast/toast-store.d.ts +65 -0
  74. package/lib/typescript/src/components/toast/toast-store.d.ts.map +1 -0
  75. package/lib/typescript/src/components/typography/index.d.ts +77 -0
  76. package/lib/typescript/src/components/typography/index.d.ts.map +1 -0
  77. package/lib/typescript/src/icons/index.d.ts +60 -0
  78. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  79. package/lib/typescript/src/index.d.ts +13 -4
  80. package/lib/typescript/src/index.d.ts.map +1 -1
  81. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  82. package/lib/typescript/src/theme/use-theme.d.ts +64 -4
  83. package/lib/typescript/src/theme/use-theme.d.ts.map +1 -1
  84. package/package.json +22 -4
  85. package/src/components/accordion/index.tsx +344 -0
  86. package/src/components/alert/index.tsx +102 -16
  87. package/src/components/avatar/index.tsx +68 -20
  88. package/src/components/badge/index.tsx +38 -9
  89. package/src/components/button/index.tsx +102 -19
  90. package/src/components/checkbox/index.tsx +64 -12
  91. package/src/components/empty-state/index.tsx +136 -0
  92. package/src/components/frame/index.tsx +101 -31
  93. package/src/components/input-group/index.tsx +164 -0
  94. package/src/components/label/index.tsx +128 -0
  95. package/src/components/progress/index.tsx +147 -0
  96. package/src/components/steps/index.tsx +343 -0
  97. package/src/components/tabs/index.tsx +10 -2
  98. package/src/components/timeline/index.tsx +456 -0
  99. package/src/components/toast/index.tsx +568 -0
  100. package/src/components/toast/toast-store.ts +120 -0
  101. package/src/components/typography/index.tsx +121 -0
  102. package/src/icons/index.tsx +423 -7
  103. package/src/index.ts +114 -4
  104. package/src/providers/panel-ui-provider.tsx +3 -0
  105. package/src/theme/use-theme.ts +119 -4
  106. package/theme.css +330 -7
@@ -1 +1 @@
1
- {"version":3,"file":"panel-ui-provider.d.ts","sourceRoot":"","sources":["../../../../src/providers/panel-ui-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,SAAS,EACT,UAAiB,GAClB,EAAE,oBAAoB,+BAWtB"}
1
+ {"version":3,"file":"panel-ui-provider.d.ts","sourceRoot":"","sources":["../../../../src/providers/panel-ui-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAOvC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,SAAS,EACT,UAAiB,GAClB,EAAE,oBAAoB,+BAatB"}
@@ -1,12 +1,72 @@
1
- export type ThemeName = 'light' | 'dark' | 'system';
1
+ /** Themes shipped in `panelui-native/theme.css`. */
2
+ export type PanelTheme = 'light' | 'dark' | 'moon' | 'moon-dark' | 'grass' | 'grass-dark';
3
+ export type ThemeName = PanelTheme | 'system';
4
+ /** Light/dark mode within a theme family. */
5
+ export type ThemeMode = 'light' | 'dark';
6
+ /** A theme family — one look, in its light and dark form. */
7
+ export interface PanelThemeFamily {
8
+ /** Stable id, e.g. `'moon'`. */
9
+ id: string;
10
+ /** Short display name, for a theme picker. */
11
+ name: string;
12
+ light: PanelTheme;
13
+ dark: PanelTheme;
14
+ /** Representative colour for a swatch, as `[light, dark]`. */
15
+ swatch: [string, string];
16
+ }
17
+ /**
18
+ * The theme families PanelUI ships. Each sets its own palette *and* its own
19
+ * radius scale, so switching family changes the shape of the UI as well as
20
+ * its colour — Panel is the Coss default, Moon is sharp and monochrome,
21
+ * Grass is soft and green.
22
+ *
23
+ * Everything beyond `panel` must be registered in your metro config, or
24
+ * `setTheme` throws "it was not registered":
25
+ *
26
+ * ```js
27
+ * withUniwindConfig(config, {
28
+ * cssEntryFile: './global.css',
29
+ * extraThemes: ['moon', 'moon-dark', 'grass', 'grass-dark'],
30
+ * });
31
+ * ```
32
+ */
33
+ export declare const PANEL_THEMES: readonly PanelThemeFamily[];
34
+ /** Every concrete theme name, in registration order. */
35
+ export declare const PANEL_THEME_NAMES: readonly PanelTheme[];
36
+ /** The names to pass as `extraThemes` in your metro config. */
37
+ export declare const PANEL_EXTRA_THEMES: readonly string[];
2
38
  /**
3
39
  * Reactive access to the current Uniwind theme plus a setter.
4
40
  * Theme changes are applied natively by Uniwind without re-rendering the tree.
5
41
  */
6
42
  export declare function useTheme(): {
7
- /** Currently resolved theme ('light' or 'dark'). */
8
- theme: string;
9
- /** Switch theme: 'light', 'dark', or 'system' (follows device). */
43
+ /** The active theme name, e.g. `'dark'` or `'moon-dark'`. */
44
+ theme: PanelTheme;
45
+ /** Switch theme by name, or `'system'` to follow the device. */
10
46
  setTheme: (name: ThemeName) => void;
11
47
  };
48
+ /**
49
+ * Theme family and light/dark mode as two separate axes — what a picker like
50
+ * "choose a brand, then toggle the sun" actually needs.
51
+ *
52
+ * Named themes are not adaptive in Uniwind, so each brand ships as a
53
+ * light/dark pair. Toggling the mode keeps the current family, so a user in
54
+ * Moon dark lands in Moon light rather than the default theme.
55
+ *
56
+ * ```tsx
57
+ * const { family, mode, setFamily, toggleMode } = useThemeMode();
58
+ * ```
59
+ */
60
+ export declare function useThemeMode(): {
61
+ /** The active theme family. */
62
+ family: PanelThemeFamily;
63
+ /** `'light'` or `'dark'` within that family. */
64
+ mode: ThemeMode;
65
+ /** Switch brand, staying in the current light/dark mode. */
66
+ setFamily: (id: string) => void;
67
+ /** Set the mode, staying in the current family. */
68
+ setMode: (next: ThemeMode) => void;
69
+ /** Flip light ↔ dark within the current family. */
70
+ toggleMode: () => void;
71
+ };
12
72
  //# sourceMappingURL=use-theme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-theme.d.ts","sourceRoot":"","sources":["../../../../src/theme/use-theme.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEpD;;;GAGG;AACH,wBAAgB,QAAQ;IAIpB,oDAAoD;;IAEpD,mEAAmE;qBAClD,SAAS;EAE7B"}
1
+ {"version":3,"file":"use-theme.d.ts","sourceRoot":"","sources":["../../../../src/theme/use-theme.ts"],"names":[],"mappings":"AAEA,oDAAoD;AACpD,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,MAAM,GACN,MAAM,GACN,WAAW,GACX,OAAO,GACP,YAAY,CAAC;AAEjB,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE9C,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEzC,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC/B,gCAAgC;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,UAAU,CAAC;IACjB,8DAA8D;IAC9D,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,gBAAgB,EAsBnD,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,SAAS,UAAU,EAElD,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAE/C,CAAC;AASF;;;GAGG;AACH,wBAAgB,QAAQ;IAIpB,6DAA6D;WAC7C,UAAU;IAC1B,gEAAgE;qBAC/C,SAAS;EAE7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY;IAMxB,+BAA+B;;IAE/B,gDAAgD;;IAEhD,4DAA4D;oBAC5C,MAAM;IAItB,mDAAmD;oBACnC,SAAS;IACzB,mDAAmD;;EAGtD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panelui-native",
3
- "version": "0.2.0",
3
+ "version": "0.5.1",
4
4
  "description": "High-performance React Native UI components for Expo. Coss UI design language, powered by Uniwind (Tailwind v4) and Reanimated.",
5
5
  "main": "./lib/module/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -19,6 +19,7 @@
19
19
  "src",
20
20
  "lib",
21
21
  "theme.css",
22
+ "README.md",
22
23
  "!**/__tests__",
23
24
  "!**/*.tsbuildinfo"
24
25
  ],
@@ -32,19 +33,36 @@
32
33
  "react-native",
33
34
  "expo",
34
35
  "ui",
36
+ "ui-library",
37
+ "ui-components",
35
38
  "components",
39
+ "component-library",
36
40
  "tailwind",
41
+ "tailwindcss",
37
42
  "uniwind",
43
+ "nativewind",
44
+ "reanimated",
38
45
  "design-system",
39
- "shadcn"
46
+ "shadcn",
47
+ "dark-mode",
48
+ "theming",
49
+ "accessibility",
50
+ "typescript",
51
+ "ios",
52
+ "android",
53
+ "mobile"
40
54
  ],
41
55
  "repository": {
42
56
  "type": "git",
43
- "url": "git+https://github.com/Khalidabdi1/PanelUI.git"
57
+ "url": "git+https://github.com/panel-ui/PanelUI.git",
58
+ "directory": "packages/panelui"
59
+ },
60
+ "bugs": {
61
+ "url": "https://github.com/panel-ui/PanelUI/issues"
44
62
  },
45
63
  "author": "Khalid Abdi",
46
64
  "license": "MIT",
47
- "homepage": "https://github.com/Khalidabdi1/PanelUI#readme",
65
+ "homepage": "https://github.com/panel-ui/PanelUI#readme",
48
66
  "peerDependencies": {
49
67
  "react": "*",
50
68
  "react-native": "*",
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Accordion — collapsible sections.
3
+ *
4
+ * Adapted from: heroui-inc/heroui-native src/components/accordion/
5
+ * (the Item / Trigger / Indicator / Content anatomy, single vs multiple
6
+ * selection, the rotating chevron indicator, and layout-transition expand).
7
+ *
8
+ * Upstream ships `default` and `surface`; the extra variants here follow the
9
+ * same slot structure.
10
+ */
11
+ import {
12
+ Children,
13
+ createContext,
14
+ forwardRef,
15
+ isValidElement,
16
+ useCallback,
17
+ useContext,
18
+ useMemo,
19
+ useState,
20
+ type ReactNode,
21
+ } from 'react';
22
+ import { Pressable, View, type Text as RNText, type ViewProps } from 'react-native';
23
+ import Animated, {
24
+ LinearTransition,
25
+ useAnimatedStyle,
26
+ useDerivedValue,
27
+ withTiming,
28
+ } from 'react-native-reanimated';
29
+ import { tv } from 'tailwind-variants';
30
+ import { ChevronDownIcon } from '../../icons';
31
+ import { Text, type TextProps } from '../../primitives/text';
32
+
33
+ export type AccordionVariant =
34
+ | 'default'
35
+ | 'surface'
36
+ | 'separated'
37
+ | 'bordered'
38
+ | 'ghost';
39
+
40
+ export type AccordionSelectionMode = 'single' | 'multiple';
41
+
42
+ const accordionVariants = tv({
43
+ slots: {
44
+ root: 'w-full flex-col overflow-hidden',
45
+ separator: 'h-px bg-border',
46
+ item: 'flex-col overflow-hidden',
47
+ trigger: 'flex-row items-center justify-between gap-4 py-4',
48
+ title: 'flex-1 text-base font-medium text-foreground',
49
+ indicator: 'items-center justify-center',
50
+ content: 'pb-4',
51
+ contentText: 'text-sm text-muted-foreground',
52
+ },
53
+ variants: {
54
+ variant: {
55
+ default: { trigger: 'px-1', content: 'px-1' },
56
+ surface: {
57
+ root: 'rounded-2xl border border-border bg-surface',
58
+ separator: 'mx-4',
59
+ trigger: 'px-4',
60
+ content: 'px-4',
61
+ },
62
+ separated: {
63
+ root: 'gap-2.5 overflow-visible',
64
+ separator: 'hidden',
65
+ item: 'rounded-xl border border-border bg-card',
66
+ trigger: 'px-4',
67
+ content: 'px-4',
68
+ },
69
+ bordered: {
70
+ separator: 'hidden',
71
+ item: 'mb-2 rounded-xl border border-border',
72
+ trigger: 'px-4',
73
+ content: 'px-4',
74
+ },
75
+ ghost: {
76
+ separator: 'hidden',
77
+ trigger: 'px-0',
78
+ content: 'px-0',
79
+ },
80
+ },
81
+ },
82
+ defaultVariants: {
83
+ variant: 'default',
84
+ },
85
+ });
86
+
87
+ interface AccordionContextValue {
88
+ expanded: string[];
89
+ toggle: (value: string) => void;
90
+ variant: AccordionVariant;
91
+ }
92
+
93
+ interface AccordionItemContextValue {
94
+ value: string;
95
+ isExpanded: boolean;
96
+ isDisabled: boolean;
97
+ }
98
+
99
+ const AccordionContext = createContext<AccordionContextValue | null>(null);
100
+ const AccordionItemContext = createContext<AccordionItemContextValue | null>(null);
101
+
102
+ function useAccordion(component: string): AccordionContextValue {
103
+ const context = useContext(AccordionContext);
104
+ if (!context) throw new Error(`${component} must be used within an <Accordion>`);
105
+ return context;
106
+ }
107
+
108
+ function useAccordionItem(component: string): AccordionItemContextValue {
109
+ const context = useContext(AccordionItemContext);
110
+ if (!context) throw new Error(`${component} must be used within an <Accordion.Item>`);
111
+ return context;
112
+ }
113
+
114
+ export interface AccordionProps extends ViewProps {
115
+ className?: string;
116
+ variant?: AccordionVariant;
117
+ /** `single` collapses the open item when another opens. */
118
+ selectionMode?: AccordionSelectionMode;
119
+ /** Expanded item value(s), controlled. */
120
+ value?: string | string[];
121
+ defaultValue?: string | string[];
122
+ onValueChange?: (value: string | string[]) => void;
123
+ /** Hide the hairlines drawn between items. */
124
+ hideSeparator?: boolean;
125
+ children?: ReactNode;
126
+ }
127
+
128
+ const toArray = (value: string | string[] | undefined): string[] =>
129
+ value === undefined ? [] : Array.isArray(value) ? value : [value];
130
+
131
+ const AccordionRoot = forwardRef<View, AccordionProps>(
132
+ (
133
+ {
134
+ className,
135
+ variant = 'default',
136
+ selectionMode = 'single',
137
+ value,
138
+ defaultValue,
139
+ onValueChange,
140
+ hideSeparator = false,
141
+ children,
142
+ ...props
143
+ },
144
+ ref
145
+ ) => {
146
+ const [internal, setInternal] = useState<string[]>(() => toArray(defaultValue));
147
+ const isControlled = value !== undefined;
148
+ const expanded = isControlled ? toArray(value) : internal;
149
+
150
+ const toggle = useCallback(
151
+ (itemValue: string) => {
152
+ const isOpen = expanded.includes(itemValue);
153
+ const next =
154
+ selectionMode === 'single'
155
+ ? isOpen
156
+ ? []
157
+ : [itemValue]
158
+ : isOpen
159
+ ? expanded.filter((entry) => entry !== itemValue)
160
+ : [...expanded, itemValue];
161
+
162
+ if (!isControlled) setInternal(next);
163
+ // Hand back the shape the caller gave us.
164
+ onValueChange?.(selectionMode === 'single' ? (next[0] ?? '') : next);
165
+ },
166
+ [expanded, selectionMode, isControlled, onValueChange]
167
+ );
168
+
169
+ const context = useMemo(
170
+ () => ({ expanded, toggle, variant }),
171
+ [expanded, toggle, variant]
172
+ );
173
+
174
+ const { root, separator } = accordionVariants({ variant });
175
+ const items = Children.toArray(children).filter(isValidElement);
176
+
177
+ return (
178
+ <AccordionContext.Provider value={context}>
179
+ <Animated.View
180
+ ref={ref}
181
+ layout={LinearTransition.duration(200)}
182
+ className={root({ className })}
183
+ {...props}
184
+ >
185
+ {items.map((child, index) => (
186
+ <View key={child.key ?? index}>
187
+ {child}
188
+ {!hideSeparator && index < items.length - 1 ? (
189
+ <View className={separator()} />
190
+ ) : null}
191
+ </View>
192
+ ))}
193
+ </Animated.View>
194
+ </AccordionContext.Provider>
195
+ );
196
+ }
197
+ );
198
+ AccordionRoot.displayName = 'Accordion';
199
+
200
+ export interface AccordionItemProps extends ViewProps {
201
+ className?: string;
202
+ /** Identifies this item in the accordion's value. */
203
+ value: string;
204
+ isDisabled?: boolean;
205
+ children?: ReactNode;
206
+ }
207
+
208
+ const AccordionItem = forwardRef<View, AccordionItemProps>(
209
+ ({ className, value, isDisabled = false, children, ...props }, ref) => {
210
+ const { expanded, variant } = useAccordion('Accordion.Item');
211
+ const isExpanded = expanded.includes(value);
212
+ const { item } = accordionVariants({ variant });
213
+
214
+ const context = useMemo(
215
+ () => ({ value, isExpanded, isDisabled }),
216
+ [value, isExpanded, isDisabled]
217
+ );
218
+
219
+ return (
220
+ <AccordionItemContext.Provider value={context}>
221
+ <Animated.View
222
+ ref={ref}
223
+ layout={LinearTransition.duration(200)}
224
+ className={item({ className })}
225
+ {...props}
226
+ >
227
+ {children}
228
+ </Animated.View>
229
+ </AccordionItemContext.Provider>
230
+ );
231
+ }
232
+ );
233
+ AccordionItem.displayName = 'Accordion.Item';
234
+
235
+ export interface AccordionTriggerProps extends ViewProps {
236
+ className?: string;
237
+ children?: ReactNode;
238
+ }
239
+
240
+ /** The pressable header row. Bare strings are wrapped in the title style. */
241
+ const AccordionTrigger = forwardRef<View, AccordionTriggerProps>(
242
+ ({ className, children, ...props }, ref) => {
243
+ const { toggle, variant } = useAccordion('Accordion.Trigger');
244
+ const { value, isExpanded, isDisabled } = useAccordionItem('Accordion.Trigger');
245
+ const { trigger, title } = accordionVariants({ variant });
246
+
247
+ return (
248
+ <Pressable
249
+ ref={ref}
250
+ accessibilityRole="button"
251
+ accessibilityState={{ expanded: isExpanded, disabled: isDisabled }}
252
+ disabled={isDisabled}
253
+ onPress={() => toggle(value)}
254
+ className={trigger({ className })}
255
+ {...props}
256
+ >
257
+ {typeof children === 'string' ? (
258
+ <Text className={title()}>{children}</Text>
259
+ ) : (
260
+ children
261
+ )}
262
+ </Pressable>
263
+ );
264
+ }
265
+ );
266
+ AccordionTrigger.displayName = 'Accordion.Trigger';
267
+
268
+ export interface AccordionIndicatorProps extends ViewProps {
269
+ className?: string;
270
+ /** Replaces the default chevron. */
271
+ children?: ReactNode;
272
+ }
273
+
274
+ /** Chevron that rotates 180° while the item is open. */
275
+ const AccordionIndicator = forwardRef<View, AccordionIndicatorProps>(
276
+ ({ className, children, ...props }, ref) => {
277
+ const { variant } = useAccordion('Accordion.Indicator');
278
+ const { isExpanded } = useAccordionItem('Accordion.Indicator');
279
+ const { indicator } = accordionVariants({ variant });
280
+
281
+ const rotation = useDerivedValue(
282
+ () => withTiming(isExpanded ? 180 : 0, { duration: 200 }),
283
+ [isExpanded]
284
+ );
285
+ const style = useAnimatedStyle(() => ({
286
+ transform: [{ rotate: `${rotation.value}deg` }],
287
+ }));
288
+
289
+ return (
290
+ <Animated.View style={style}>
291
+ <View ref={ref} className={indicator({ className })} {...props}>
292
+ {children ?? <ChevronDownIcon size={18} />}
293
+ </View>
294
+ </Animated.View>
295
+ );
296
+ }
297
+ );
298
+ AccordionIndicator.displayName = 'Accordion.Indicator';
299
+
300
+ export interface AccordionContentProps extends ViewProps {
301
+ className?: string;
302
+ children?: ReactNode;
303
+ }
304
+
305
+ /**
306
+ * The collapsible body. Unmounts when closed, per the repo's convention for
307
+ * conditionally shown content — the layout transition on the item animates
308
+ * the height change.
309
+ */
310
+ const AccordionContent = forwardRef<View, AccordionContentProps>(
311
+ ({ className, children, ...props }, ref) => {
312
+ const { variant } = useAccordion('Accordion.Content');
313
+ const { isExpanded } = useAccordionItem('Accordion.Content');
314
+ const { content, contentText } = accordionVariants({ variant });
315
+
316
+ if (!isExpanded) return null;
317
+
318
+ return (
319
+ <View ref={ref} className={content({ className })} {...props}>
320
+ {typeof children === 'string' ? (
321
+ <Text className={contentText()}>{children}</Text>
322
+ ) : (
323
+ children
324
+ )}
325
+ </View>
326
+ );
327
+ }
328
+ );
329
+ AccordionContent.displayName = 'Accordion.Content';
330
+
331
+ const AccordionTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
332
+ const { variant } = useAccordion('Accordion.Title');
333
+ const { title } = accordionVariants({ variant });
334
+ return <Text ref={ref} className={title({ className })} {...props} />;
335
+ });
336
+ AccordionTitle.displayName = 'Accordion.Title';
337
+
338
+ export const Accordion = Object.assign(AccordionRoot, {
339
+ Item: AccordionItem,
340
+ Trigger: AccordionTrigger,
341
+ Title: AccordionTitle,
342
+ Indicator: AccordionIndicator,
343
+ Content: AccordionContent,
344
+ });
@@ -1,31 +1,50 @@
1
+ /**
2
+ * Alert — a status message with an optional leading status icon.
3
+ *
4
+ * Adapted from: heroui-inc/heroui-native src/components/alert/alert.tsx
5
+ * (Indicator / Content / Title / Description anatomy, status-driven icon and
6
+ * title colour), restyled with Coss UI tokens.
7
+ *
8
+ * The `variant` prop name is kept from PanelUI v0.2 rather than renamed to
9
+ * HeroUI's `status`, so existing code keeps working.
10
+ */
1
11
  import { createContext, forwardRef, useContext, type ReactNode } from 'react';
2
- import { View, type ViewProps } from 'react-native';
12
+ import { View, type Text as RNText, type ViewProps } from 'react-native';
3
13
  import { tv, type VariantProps } from 'tailwind-variants';
14
+ import { useCSSVariable } from 'uniwind';
4
15
  import { cn } from '../../utils/cn';
5
16
  import { Text, type TextProps } from '../../primitives/text';
17
+ import { AlertTriangleIcon, CheckCircleIcon, InfoIcon } from '../../icons';
6
18
 
7
19
  const alertVariants = tv({
8
20
  slots: {
9
- root: 'w-full gap-0.5 rounded-xl border px-3.5 py-3',
10
- title: 'text-sm font-medium text-card-foreground',
21
+ root: 'w-full flex-row gap-3 rounded-xl border px-3.5 py-3',
22
+ indicator: 'pt-px',
23
+ content: 'flex-1 gap-0.5',
24
+ title: 'text-sm font-medium',
11
25
  description: 'text-sm text-muted-foreground',
12
26
  },
13
27
  variants: {
14
28
  variant: {
15
29
  default: {
16
30
  root: 'border-border bg-surface',
31
+ title: 'text-card-foreground',
17
32
  },
18
33
  info: {
19
- root: 'border-info/32 bg-info/10 dark:bg-info/[0.06]',
34
+ root: 'border-info/32 bg-info-soft',
35
+ title: 'text-info-foreground',
20
36
  },
21
37
  success: {
22
- root: 'border-success/32 bg-success/10 dark:bg-success/[0.06]',
38
+ root: 'border-success/32 bg-success-soft',
39
+ title: 'text-success-foreground',
23
40
  },
24
41
  warning: {
25
- root: 'border-warning/32 bg-warning/10 dark:bg-warning/[0.06]',
42
+ root: 'border-warning/32 bg-warning-soft',
43
+ title: 'text-warning-foreground',
26
44
  },
27
45
  destructive: {
28
- root: 'border-destructive/32 bg-destructive/10 dark:bg-destructive/[0.06]',
46
+ root: 'border-destructive/32 bg-destructive-soft',
47
+ title: 'text-destructive-foreground',
29
48
  },
30
49
  },
31
50
  },
@@ -34,20 +53,42 @@ const alertVariants = tv({
34
53
  },
35
54
  });
36
55
 
37
- type AlertVariant = VariantProps<typeof alertVariants>['variant'];
56
+ type AlertVariant = NonNullable<VariantProps<typeof alertVariants>['variant']>;
38
57
 
39
58
  const AlertContext = createContext<AlertVariant>('default');
40
59
 
60
+ /** CSS variable each variant's indicator icon takes its colour from. */
61
+ const INDICATOR_COLOR_VAR: Record<AlertVariant, string> = {
62
+ default: '--color-muted-foreground',
63
+ info: '--color-info-foreground',
64
+ success: '--color-success-foreground',
65
+ warning: '--color-warning-foreground',
66
+ destructive: '--color-destructive-foreground',
67
+ };
68
+
69
+ const INDICATOR_ICON: Record<AlertVariant, typeof InfoIcon> = {
70
+ default: InfoIcon,
71
+ info: InfoIcon,
72
+ success: CheckCircleIcon,
73
+ warning: AlertTriangleIcon,
74
+ destructive: AlertTriangleIcon,
75
+ };
76
+
41
77
  export interface AlertProps extends ViewProps, VariantProps<typeof alertVariants> {
42
78
  className?: string;
43
- /** Optional leading icon rendered before the content. */
79
+ /**
80
+ * Leading element rendered before the content.
81
+ *
82
+ * @deprecated Prefer `<Alert.Indicator>`, which picks a status icon for you.
83
+ * Still honoured so v0.2 call sites keep rendering.
84
+ */
44
85
  icon?: ReactNode;
45
86
  children?: ReactNode;
46
87
  }
47
88
 
48
89
  const AlertRoot = forwardRef<View, AlertProps>(
49
90
  ({ className, variant = 'default', icon, children, ...props }, ref) => {
50
- const { root } = alertVariants({ variant });
91
+ const { root, indicator, content } = alertVariants({ variant });
51
92
 
52
93
  return (
53
94
  <AlertContext.Provider value={variant}>
@@ -58,10 +99,10 @@ const AlertRoot = forwardRef<View, AlertProps>(
58
99
  {...props}
59
100
  >
60
101
  {icon ? (
61
- <View className="flex-row items-start gap-3">
62
- <View className="mt-0.5">{icon}</View>
63
- <View className="flex-1 gap-1">{children}</View>
64
- </View>
102
+ <>
103
+ <View className={indicator()}>{icon}</View>
104
+ <View className={content()}>{children}</View>
105
+ </>
65
106
  ) : (
66
107
  children
67
108
  )}
@@ -72,7 +113,48 @@ const AlertRoot = forwardRef<View, AlertProps>(
72
113
  );
73
114
  AlertRoot.displayName = 'Alert';
74
115
 
75
- const AlertTitle = forwardRef<React.ElementRef<typeof Text>, TextProps>(
116
+ export interface AlertIndicatorProps extends ViewProps {
117
+ className?: string;
118
+ /** Overrides the size/colour of the default status icon. */
119
+ iconProps?: { size?: number; color?: string };
120
+ /** Replaces the default status icon entirely. */
121
+ children?: ReactNode;
122
+ }
123
+
124
+ /**
125
+ * Leading status icon. Resolves both the icon and its colour from the parent
126
+ * Alert's variant, so it follows the active theme.
127
+ */
128
+ const AlertIndicator = forwardRef<View, AlertIndicatorProps>(
129
+ ({ className, iconProps, children, ...props }, ref) => {
130
+ const variant = useContext(AlertContext);
131
+ const { indicator } = alertVariants({ variant });
132
+ const themeColor = useCSSVariable(INDICATOR_COLOR_VAR[variant]);
133
+ const Icon = INDICATOR_ICON[variant];
134
+
135
+ const color =
136
+ iconProps?.color ?? (typeof themeColor === 'string' ? themeColor : undefined);
137
+
138
+ return (
139
+ <View ref={ref} className={indicator({ className })} {...props}>
140
+ {children ?? <Icon size={iconProps?.size ?? 18} color={color} />}
141
+ </View>
142
+ );
143
+ }
144
+ );
145
+ AlertIndicator.displayName = 'Alert.Indicator';
146
+
147
+ /** Flex-1 wrapper for Alert.Title and Alert.Description. */
148
+ const AlertContent = forwardRef<View, ViewProps & { className?: string }>(
149
+ ({ className, ...props }, ref) => {
150
+ const variant = useContext(AlertContext);
151
+ const { content } = alertVariants({ variant });
152
+ return <View ref={ref} className={content({ className })} {...props} />;
153
+ }
154
+ );
155
+ AlertContent.displayName = 'Alert.Content';
156
+
157
+ const AlertTitle = forwardRef<RNText, TextProps>(
76
158
  ({ className, ...props }, ref) => {
77
159
  const variant = useContext(AlertContext);
78
160
  const { title } = alertVariants({ variant });
@@ -81,7 +163,7 @@ const AlertTitle = forwardRef<React.ElementRef<typeof Text>, TextProps>(
81
163
  );
82
164
  AlertTitle.displayName = 'Alert.Title';
83
165
 
84
- const AlertDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
166
+ const AlertDescription = forwardRef<RNText, TextProps>(
85
167
  ({ className, ...props }, ref) => {
86
168
  const variant = useContext(AlertContext);
87
169
  const { description } = alertVariants({ variant });
@@ -91,6 +173,10 @@ const AlertDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
91
173
  AlertDescription.displayName = 'Alert.Description';
92
174
 
93
175
  export const Alert = Object.assign(AlertRoot, {
176
+ /** @optional Status icon rendered as the leading visual element. */
177
+ Indicator: AlertIndicator,
178
+ /** @optional Wrapper for title and description. */
179
+ Content: AlertContent,
94
180
  Title: AlertTitle,
95
181
  Description: AlertDescription,
96
182
  });