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.
- package/README.md +258 -0
- package/lib/module/components/accordion/index.js +312 -0
- package/lib/module/components/accordion/index.js.map +1 -0
- package/lib/module/components/alert/index.js +106 -13
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/avatar/index.js +58 -17
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/badge/index.js +29 -8
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/button/index.js +87 -26
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +59 -18
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/empty-state/index.js +178 -0
- package/lib/module/components/empty-state/index.js.map +1 -0
- package/lib/module/components/frame/index.js +64 -13
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +132 -0
- package/lib/module/components/input-group/index.js.map +1 -0
- package/lib/module/components/label/index.js +117 -0
- package/lib/module/components/label/index.js.map +1 -0
- package/lib/module/components/progress/index.js +138 -0
- package/lib/module/components/progress/index.js.map +1 -0
- package/lib/module/components/steps/index.js +354 -0
- package/lib/module/components/steps/index.js.map +1 -0
- package/lib/module/components/tabs/index.js +9 -3
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +633 -0
- package/lib/module/components/timeline/index.js.map +1 -0
- package/lib/module/components/toast/index.js +569 -0
- package/lib/module/components/toast/index.js.map +1 -0
- package/lib/module/components/toast/toast-store.js +81 -0
- package/lib/module/components/toast/toast-store.js.map +1 -0
- package/lib/module/components/typography/index.js +126 -0
- package/lib/module/components/typography/index.js.map +1 -0
- package/lib/module/icons/index.js +590 -8
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +11 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/panel-ui-provider.js +2 -1
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/module/theme/use-theme.js +90 -3
- package/lib/module/theme/use-theme.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +56 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -0
- package/lib/typescript/src/components/alert/index.d.ts +56 -4
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
- package/lib/typescript/src/components/avatar/index.d.ts +8 -1
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/badge/index.d.ts +38 -0
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +57 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts +50 -0
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/empty-state/index.d.ts +73 -0
- package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -0
- package/lib/typescript/src/components/frame/index.d.ts +39 -5
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input-group/index.d.ts +34 -0
- package/lib/typescript/src/components/input-group/index.d.ts.map +1 -0
- package/lib/typescript/src/components/label/index.d.ts +28 -0
- package/lib/typescript/src/components/label/index.d.ts.map +1 -0
- package/lib/typescript/src/components/progress/index.d.ts +132 -0
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -0
- package/lib/typescript/src/components/steps/index.d.ts +62 -0
- package/lib/typescript/src/components/steps/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts +96 -0
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/index.d.ts +176 -0
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/toast-store.d.ts +65 -0
- package/lib/typescript/src/components/toast/toast-store.d.ts.map +1 -0
- package/lib/typescript/src/components/typography/index.d.ts +77 -0
- package/lib/typescript/src/components/typography/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +60 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +13 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/lib/typescript/src/theme/use-theme.d.ts +64 -4
- package/lib/typescript/src/theme/use-theme.d.ts.map +1 -1
- package/package.json +22 -4
- package/src/components/accordion/index.tsx +344 -0
- package/src/components/alert/index.tsx +102 -16
- package/src/components/avatar/index.tsx +68 -20
- package/src/components/badge/index.tsx +38 -9
- package/src/components/button/index.tsx +102 -19
- package/src/components/checkbox/index.tsx +64 -12
- package/src/components/empty-state/index.tsx +136 -0
- package/src/components/frame/index.tsx +101 -31
- package/src/components/input-group/index.tsx +164 -0
- package/src/components/label/index.tsx +128 -0
- package/src/components/progress/index.tsx +147 -0
- package/src/components/steps/index.tsx +343 -0
- package/src/components/tabs/index.tsx +10 -2
- package/src/components/timeline/index.tsx +456 -0
- package/src/components/toast/index.tsx +568 -0
- package/src/components/toast/toast-store.ts +120 -0
- package/src/components/typography/index.tsx +121 -0
- package/src/icons/index.tsx +423 -7
- package/src/index.ts +114 -4
- package/src/providers/panel-ui-provider.tsx +3 -0
- package/src/theme/use-theme.ts +119 -4
- 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;
|
|
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
|
-
|
|
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
|
-
/**
|
|
8
|
-
theme:
|
|
9
|
-
/** Switch theme
|
|
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,
|
|
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.
|
|
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/
|
|
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/
|
|
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-
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
62
|
-
<View className=
|
|
63
|
-
<View className=
|
|
64
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
});
|