@innovui/react-native 0.0.0 → 0.2.0
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/LICENSE +21 -0
- package/dist/components/_shared/anchored-overlay.js +37 -12
- package/dist/components/_shared/anchored-overlay.js.map +1 -1
- package/dist/components/_shared/sliding-indicator.js +98 -0
- package/dist/components/_shared/sliding-indicator.js.map +1 -0
- package/dist/components/_shared/use-transition.js +53 -0
- package/dist/components/_shared/use-transition.js.map +1 -0
- package/dist/components/drawer/drawer.d.ts +11 -1
- package/dist/components/drawer/drawer.js +37 -22
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/drawer/use-swipe-to-close.js +143 -0
- package/dist/components/drawer/use-swipe-to-close.js.map +1 -0
- package/dist/components/input/input.d.ts +8 -2
- package/dist/components/input/input.js +10 -2
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/modal/modal.js +59 -39
- package/dist/components/modal/modal.js.map +1 -1
- package/dist/components/segmented-control/segmented-control.d.ts +2 -2
- package/dist/components/segmented-control/segmented-control.js +20 -7
- package/dist/components/segmented-control/segmented-control.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +2 -2
- package/dist/components/tabs/tabs.js +32 -9
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/components/toast/toast.js +66 -5
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/tooltip/tooltip.d.ts +7 -3
- package/dist/components/tooltip/tooltip.js +10 -4
- package/dist/components/tooltip/tooltip.js.map +1 -1
- package/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.js","names":[],"sources":["../../../src/components/tabs/tabs.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext, useMemo, useState } from 'react'\nimport {\n Pressable,\n type PressableProps,\n ScrollView,\n type ScrollViewProps,\n Text,\n View,\n type ViewProps,\n} from 'react-native'\nimport { cn } from '../../utils/cn'\n\ntype TabsContextValue = {
|
|
1
|
+
{"version":3,"file":"tabs.js","names":[],"sources":["../../../src/components/tabs/tabs.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { createContext, type ReactNode, useContext, useMemo, useState } from 'react'\nimport {\n Pressable,\n type PressableProps,\n ScrollView,\n type ScrollViewProps,\n Text,\n View,\n type ViewProps,\n} from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { type Rect, SlidingIndicator, useItemLayouts } from '../_shared/sliding-indicator'\n\ntype TabsContextValue = {\n value: string | undefined\n setValue: (value: string) => void\n /** Posição de cada aba no contêiner da lista, para o indicador deslizante. */\n layouts: Record<string, Rect>\n setLayout: (value: string, rect: Rect) => void\n}\n\nconst TabsContext = createContext<TabsContextValue>({\n value: undefined,\n setValue: () => {},\n layouts: {},\n setLayout: () => {},\n})\n\nexport type TabsProps = ViewProps & {\n className?: string\n /** Aba ativa (controlado). */\n value?: string\n /** Aba ativa inicial (não controlado). */\n defaultValue?: string\n onValueChange?: (value: string) => void\n}\n\n/** Raiz das abas: controla qual `Tab` está ativa (`value`/`defaultValue`/`onValueChange`). */\nexport function Tabs({ className, value, defaultValue, onValueChange, ...props }: TabsProps) {\n const [internal, setInternal] = useState(defaultValue)\n const current = value !== undefined ? value : internal\n\n const { layouts, setLayout } = useItemLayouts()\n const context = useMemo(\n () => ({\n value: current,\n setValue: (next: string) => {\n if (value === undefined) setInternal(next)\n onValueChange?.(next)\n },\n layouts,\n setLayout,\n }),\n [current, value, onValueChange, layouts, setLayout],\n )\n\n return (\n <TabsContext.Provider value={context}>\n <View className={className} {...props} />\n </TabsContext.Provider>\n )\n}\n\nexport type TabListProps = Omit<ScrollViewProps, 'horizontal' | 'style'> & {\n className?: string\n}\n\n/** Lista de gatilhos. Rola na horizontal quando as abas não cabem na tela. */\nexport function TabList({ className, children, ...props }: TabListProps) {\n const { value, layouts } = useContext(TabsContext)\n const active = value === undefined ? undefined : layouts[value]\n return (\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n accessibilityRole=\"tablist\"\n className={cn('grow-0', className)}\n contentContainerClassName=\"items-start\"\n {...props}\n >\n {/* a linha de 2px é do indicador: fica na base da aba ativa e desliza para a próxima */}\n <SlidingIndicator\n target={active && { ...active, y: active.y + active.height - 2, height: 2 }}\n className=\"bg-interactive-primary\"\n />\n {children}\n </ScrollView>\n )\n}\n\nexport type TabProps = Omit<PressableProps, 'children' | 'style'> & {\n className?: string\n /** Valor que liga a aba ao `TabPanel` de mesmo `value`. */\n value: string\n /** Ícone à esquerda do rótulo (ex.: um ícone SVG de 24px). */\n icon?: ReactNode\n children?: ReactNode\n}\n\nexport function Tab({\n className,\n value,\n icon,\n children,\n disabled,\n onPress,\n onLayout,\n accessibilityState,\n ...props\n}: TabProps) {\n const { value: active, setValue, layouts, setLayout } = useContext(TabsContext)\n const selected = active === value\n // antes de o indicador medir a aba ativa, ela ainda se desenha sozinha\n const slid = active !== undefined && layouts[active] !== undefined\n\n return (\n <Pressable\n accessibilityRole=\"tab\"\n accessibilityState={{ ...accessibilityState, selected, disabled: Boolean(disabled) }}\n disabled={disabled}\n onPress={(event) => {\n setValue(value)\n onPress?.(event)\n }}\n onLayout={(event) => {\n setLayout(value, event.nativeEvent.layout)\n onLayout?.(event)\n }}\n className={cn('items-start gap-[6px] px-3 pt-2', className)}\n {...props}\n >\n <View className=\"flex-row items-center gap-1\">\n {icon}\n {typeof children === 'string' ? (\n <Text\n className={cn(\n 'text-sm text-text-secondary',\n selected && 'font-medium text-text-brand',\n disabled && 'text-text-disabled',\n )}\n >\n {children}\n </Text>\n ) : (\n children\n )}\n </View>\n <View\n className={cn(\n 'h-0.5 self-stretch bg-transparent',\n selected && !slid && 'bg-interactive-primary',\n )}\n />\n </Pressable>\n )\n}\n\nexport type TabPanelProps = ViewProps & {\n className?: string\n value: string\n}\n\n/** Painel da aba: só é montado enquanto a aba de mesmo `value` está ativa (igual ao web). */\nexport function TabPanel({ className, value, ...props }: TabPanelProps) {\n const { value: active } = useContext(TabsContext)\n if (active !== value) return null\n return <View className={className} {...props} />\n}\n"],"mappings":";;;;;;AAuBA,MAAM,cAAc,cAAgC;CAClD,OAAO,KAAA;CACP,gBAAgB,CAAC;CACjB,SAAS,CAAC;CACV,iBAAiB,CAAC;AACpB,CAAC;;AAYD,SAAgB,KAAK,EAAE,WAAW,OAAO,cAAc,eAAe,GAAG,SAAoB;CAC3F,MAAM,CAAC,UAAU,eAAe,SAAS,YAAY;CACrD,MAAM,UAAU,UAAU,KAAA,IAAY,QAAQ;CAE9C,MAAM,EAAE,SAAS,cAAc,eAAe;CAC9C,MAAM,UAAU,eACP;EACL,OAAO;EACP,WAAW,SAAiB;GAC1B,IAAI,UAAU,KAAA,GAAW,YAAY,IAAI;GACzC,gBAAgB,IAAI;EACtB;EACA;EACA;CACF,IACA;EAAC;EAAS;EAAO;EAAe;EAAS;CAAS,CACpD;CAEA,OACE,oBAAC,YAAY,UAAb;EAAsB,OAAO;EAC3B,UAAA,oBAAC,MAAD;GAAiB;GAAW,GAAI;EAAQ,CAAA;CACpB,CAAA;AAE1B;;AAOA,SAAgB,QAAQ,EAAE,WAAW,UAAU,GAAG,SAAuB;CACvE,MAAM,EAAE,OAAO,YAAY,WAAW,WAAW;CACjD,MAAM,SAAS,UAAU,KAAA,IAAY,KAAA,IAAY,QAAQ;CACzD,OACE,qBAAC,YAAD;EACE,YAAA;EACA,gCAAgC;EAChC,mBAAkB;EAClB,WAAW,GAAG,UAAU,SAAS;EACjC,2BAA0B;EAC1B,GAAI;EANN,UAAA,CASE,oBAAC,kBAAD;GACE,QAAQ,UAAU;IAAE,GAAG;IAAQ,GAAG,OAAO,IAAI,OAAO,SAAS;IAAG,QAAQ;GAAE;GAC1E,WAAU;EACX,CAAA,GACA,QACS;;AAEhB;AAWA,SAAgB,IAAI,EAClB,WACA,OACA,MACA,UACA,UACA,SACA,UACA,oBACA,GAAG,SACQ;CACX,MAAM,EAAE,OAAO,QAAQ,UAAU,SAAS,cAAc,WAAW,WAAW;CAC9E,MAAM,WAAW,WAAW;CAE5B,MAAM,OAAO,WAAW,KAAA,KAAa,QAAQ,YAAY,KAAA;CAEzD,OACE,qBAAC,WAAD;EACE,mBAAkB;EAClB,oBAAoB;GAAE,GAAG;GAAoB;GAAU,UAAU,QAAQ,QAAQ;EAAE;EACzE;EACV,UAAU,UAAU;GAClB,SAAS,KAAK;GACd,UAAU,KAAK;EACjB;EACA,WAAW,UAAU;GACnB,UAAU,OAAO,MAAM,YAAY,MAAM;GACzC,WAAW,KAAK;EAClB;EACA,WAAW,GAAG,mCAAmC,SAAS;EAC1D,GAAI;EAbN,UAAA,CAeE,qBAAC,MAAD;GAAM,WAAU;GAAhB,UAAA,CACG,MACA,OAAO,aAAa,WACnB,oBAAC,MAAD;IACE,WAAW,GACT,+BACA,YAAY,+BACZ,YAAY,oBACd;IAEC;GACG,CAAA,IAEN,QAEE;EACN,CAAA,GAAA,oBAAC,MAAD,EACE,WAAW,GACT,qCACA,YAAY,CAAC,QAAQ,wBACvB,EACD,CAAA,CACQ;;AAEf;;AAQA,SAAgB,SAAS,EAAE,WAAW,OAAO,GAAG,SAAwB;CACtE,MAAM,EAAE,OAAO,WAAW,WAAW,WAAW;CAChD,IAAI,WAAW,OAAO,OAAO;CAC7B,OAAO,oBAAC,MAAD;EAAiB;EAAW,GAAI;CAAQ,CAAA;AACjD"}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { CloseIcon, StatusIcon, statusRole, statusText, statusTextToken } from "../_shared/status.js";
|
|
2
2
|
import { useToken } from "../_shared/use-token.js";
|
|
3
3
|
import { Button } from "../button/button.js";
|
|
4
|
+
import { useReducedMotion } from "../_shared/sliding-indicator.js";
|
|
4
5
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
-
import { Animated, Platform, Pressable, StatusBar, Text, View } from "react-native";
|
|
6
|
+
import { Animated, LayoutAnimation, PanResponder, Platform, Pressable, StatusBar, Text, UIManager, View } from "react-native";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/components/toast/toast.tsx
|
|
8
9
|
const ToastContext = createContext(null);
|
|
10
|
+
if (Platform.OS === "android") UIManager.setLayoutAnimationEnabledExperimental?.(true);
|
|
11
|
+
/** Arrastar o toast para o lado além disto (px), ou mais rápido que isto (px/ms), o dispensa. */
|
|
12
|
+
const SWIPE_DISTANCE = 50;
|
|
13
|
+
const SWIPE_VELOCITY = .5;
|
|
9
14
|
const topInset = Platform.OS === "ios" ? 56 : (StatusBar.currentHeight ?? 24) + 8;
|
|
10
15
|
/**
|
|
11
16
|
* Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica
|
|
@@ -20,9 +25,11 @@ function ToastProvider({ children, label = "Notificações", dismissLabel = "Fec
|
|
|
20
25
|
closing: true
|
|
21
26
|
} : item));
|
|
22
27
|
}, []);
|
|
28
|
+
const reduced = useReducedMotion();
|
|
23
29
|
const remove = useCallback((id) => {
|
|
30
|
+
if (!reduced) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
24
31
|
setItems((current) => current.filter((item) => item.id !== id));
|
|
25
|
-
}, []);
|
|
32
|
+
}, [reduced]);
|
|
26
33
|
const toast = useCallback((options) => {
|
|
27
34
|
const id = nextId.current++;
|
|
28
35
|
setItems((current) => [...current, {
|
|
@@ -67,6 +74,58 @@ function ToastProvider({ children, label = "Notificações", dismissLabel = "Fec
|
|
|
67
74
|
function ToastView({ item, dismissLabel, onDismiss, onRemove }) {
|
|
68
75
|
const { id, status = "info", title, description, action, duration = 5e3, showIcon = true, dismissible = true, closing } = item;
|
|
69
76
|
const progress = useRef(new Animated.Value(0)).current;
|
|
77
|
+
const dragX = useRef(new Animated.Value(0)).current;
|
|
78
|
+
const [dragging, setDragging] = useState(false);
|
|
79
|
+
const latest = useRef({
|
|
80
|
+
id,
|
|
81
|
+
onDismiss
|
|
82
|
+
});
|
|
83
|
+
latest.current = {
|
|
84
|
+
id,
|
|
85
|
+
onDismiss
|
|
86
|
+
};
|
|
87
|
+
const panResponder = useRef(PanResponder.create({
|
|
88
|
+
onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 && Math.abs(g.dx) > Math.abs(g.dy),
|
|
89
|
+
onPanResponderTerminationRequest: () => false,
|
|
90
|
+
onPanResponderGrant: () => setDragging(true),
|
|
91
|
+
onPanResponderMove: (_, g) => dragX.setValue(g.dx),
|
|
92
|
+
onPanResponderRelease: (_, g) => {
|
|
93
|
+
setDragging(false);
|
|
94
|
+
if (Math.abs(g.dx) > SWIPE_DISTANCE || Math.abs(g.vx) > SWIPE_VELOCITY) {
|
|
95
|
+
Animated.timing(dragX, {
|
|
96
|
+
toValue: (g.dx || g.vx) > 0 ? 400 : -400,
|
|
97
|
+
duration: 150,
|
|
98
|
+
useNativeDriver: true
|
|
99
|
+
}).start();
|
|
100
|
+
latest.current.onDismiss(latest.current.id);
|
|
101
|
+
} else Animated.timing(dragX, {
|
|
102
|
+
toValue: 0,
|
|
103
|
+
duration: 200,
|
|
104
|
+
useNativeDriver: true
|
|
105
|
+
}).start();
|
|
106
|
+
},
|
|
107
|
+
onPanResponderTerminate: () => {
|
|
108
|
+
setDragging(false);
|
|
109
|
+
Animated.timing(dragX, {
|
|
110
|
+
toValue: 0,
|
|
111
|
+
duration: 200,
|
|
112
|
+
useNativeDriver: true
|
|
113
|
+
}).start();
|
|
114
|
+
}
|
|
115
|
+
})).current;
|
|
116
|
+
const swipeFade = dragX.interpolate({
|
|
117
|
+
inputRange: [
|
|
118
|
+
-150,
|
|
119
|
+
0,
|
|
120
|
+
150
|
|
121
|
+
],
|
|
122
|
+
outputRange: [
|
|
123
|
+
.2,
|
|
124
|
+
1,
|
|
125
|
+
.2
|
|
126
|
+
],
|
|
127
|
+
extrapolate: "clamp"
|
|
128
|
+
});
|
|
70
129
|
const iconColor = useToken(statusTextToken[status]);
|
|
71
130
|
const closeColor = useToken("--color-text-primary");
|
|
72
131
|
useEffect(() => {
|
|
@@ -95,11 +154,12 @@ function ToastView({ item, dismissLabel, onDismiss, onRemove }) {
|
|
|
95
154
|
progress
|
|
96
155
|
]);
|
|
97
156
|
useEffect(() => {
|
|
98
|
-
if (closing || duration <= 0) return;
|
|
157
|
+
if (closing || dragging || duration <= 0) return;
|
|
99
158
|
const timer = setTimeout(() => onDismiss(id), duration);
|
|
100
159
|
return () => clearTimeout(timer);
|
|
101
160
|
}, [
|
|
102
161
|
closing,
|
|
162
|
+
dragging,
|
|
103
163
|
duration,
|
|
104
164
|
id,
|
|
105
165
|
onDismiss
|
|
@@ -109,9 +169,10 @@ function ToastView({ item, dismissLabel, onDismiss, onRemove }) {
|
|
|
109
169
|
outputRange: [-16, 0]
|
|
110
170
|
});
|
|
111
171
|
return /* @__PURE__ */ jsx(Animated.View, {
|
|
172
|
+
...panResponder.panHandlers,
|
|
112
173
|
style: {
|
|
113
|
-
opacity: progress,
|
|
114
|
-
transform: [{ translateY }]
|
|
174
|
+
opacity: Animated.multiply(progress, swipeFade),
|
|
175
|
+
transform: [{ translateY }, { translateX: dragX }]
|
|
115
176
|
},
|
|
116
177
|
children: /* @__PURE__ */ jsxs(View, {
|
|
117
178
|
accessibilityRole: statusRole(status),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toast.js","names":[],"sources":["../../../src/components/toast/toast.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { Animated, Platform, Pressable, StatusBar, Text, View } from 'react-native'\nimport {\n CloseIcon,\n type Status,\n StatusIcon,\n statusRole,\n statusText,\n statusTextToken,\n} from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { Button } from '../button/button'\n\nexport type ToastOptions = {\n status?: Status\n /** Resumo (opcional). */\n title?: string\n description: string\n /** Ação contextual (ex.: \"Desfazer\"). Tocar nela também fecha o toast. */\n action?: { label: string; onClick: () => void }\n /** Tempo até fechar sozinho, em ms. `0` não fecha sozinho. Padrão: 5000. */\n duration?: number\n /** Mostra o ícone do status. Padrão: `true`. */\n showIcon?: boolean\n /** Mostra o botão de fechar (×). Sem ele, o toast ainda fecha pelo `duration`. Padrão: `true`. */\n dismissible?: boolean\n}\n\ntype ToastItem = ToastOptions & { id: number; closing: boolean }\n\ntype ToastContextValue = {\n /** Exibe um toast e devolve o id, para fechá-lo com `dismiss`. */\n toast: (options: ToastOptions) => number\n dismiss: (id: number) => void\n}\n\nconst ToastContext = createContext<ToastContextValue | null>(null)\n\nexport type ToastProviderProps = {\n children: ReactNode\n /** Nome acessível da região de notificações. */\n label?: string\n /** Nome acessível do botão de fechar de cada toast. */\n dismissLabel?: string\n testID?: string\n}\n\n// Sem react-native-safe-area-context: margem fixa para o notch no iOS e a barra de status no Android.\nconst topInset = Platform.OS === 'ios' ? 56 : (StatusBar.currentHeight ?? 24) + 8\n\n/**\n * Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica\n * no topo da tela, acima do conteúdo do app, mas abaixo de Modal/Drawer (que são janelas nativas).\n */\nexport function ToastProvider({\n children,\n label = 'Notificações',\n dismissLabel = 'Fechar',\n testID,\n}: ToastProviderProps) {\n const [items, setItems] = useState<ToastItem[]>([])\n const nextId = useRef(0)\n\n // Marca como \"closing\"; o próprio toast anima a saída e então chama `remove`.\n const dismiss = useCallback((id: number) => {\n setItems((current) =>\n current.map((item) => (item.id === id ? { ...item, closing: true } : item)),\n )\n }, [])\n const remove = useCallback((id: number) => {\n setItems((current) => current.filter((item) => item.id !== id))\n }, [])\n const toast = useCallback((options: ToastOptions) => {\n const id = nextId.current++\n setItems((current) => [...current, { ...options, id, closing: false }])\n return id\n }, [])\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss])\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n <View\n pointerEvents=\"box-none\"\n accessibilityLabel={label}\n testID={testID}\n style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9999, elevation: 9999 }}\n >\n <View\n pointerEvents=\"box-none\"\n className=\"w-full max-w-[360px] gap-2 self-center px-4\"\n style={{ paddingTop: topInset }}\n >\n {items.map((item) => (\n <ToastView\n key={item.id}\n item={item}\n dismissLabel={dismissLabel}\n onDismiss={dismiss}\n onRemove={remove}\n />\n ))}\n </View>\n </View>\n </ToastContext.Provider>\n )\n}\n\nfunction ToastView({\n item,\n dismissLabel,\n onDismiss,\n onRemove,\n}: {\n item: ToastItem\n dismissLabel: string\n onDismiss: (id: number) => void\n onRemove: (id: number) => void\n}) {\n const {\n id,\n status = 'info',\n title,\n description,\n action,\n duration = 5000,\n showIcon = true,\n dismissible = true,\n closing,\n } = item\n const progress = useRef(new Animated.Value(0)).current\n const iconColor = useToken(statusTextToken[status])\n const closeColor = useToken('--color-text-primary')\n\n useEffect(() => {\n if (closing) return\n const animation = Animated.timing(progress, {\n toValue: 1,\n duration: 200,\n useNativeDriver: true,\n })\n animation.start()\n return () => animation.stop()\n }, [closing, progress])\n\n useEffect(() => {\n if (!closing) return\n const animation = Animated.timing(progress, {\n toValue: 0,\n duration: 150,\n useNativeDriver: true,\n })\n animation.start(({ finished }) => finished && onRemove(id))\n return () => animation.stop()\n }, [closing, id, onRemove, progress])\n\n useEffect(() => {\n if (closing || duration <= 0) return\n const timer = setTimeout(() => onDismiss(id), duration)\n return () => clearTimeout(timer)\n }, [closing, duration, id, onDismiss])\n\n const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] })\n\n return (\n <Animated.View style={{ opacity: progress, transform: [{ translateY }] }}>\n <View\n accessibilityRole={statusRole(status)}\n // Erro e aviso interrompem o leitor de tela; o resto é anunciado com calma.\n accessibilityLiveRegion={statusRole(status) ? 'assertive' : 'polite'}\n className=\"flex-row items-start gap-3 rounded-md border border-border-default bg-surface-elevated py-3 pr-3 pl-4 shadow-md\"\n style={{ elevation: 6 }}\n >\n {showIcon && <StatusIcon status={status} color={iconColor} />}\n <View className=\"min-w-0 flex-1 items-start gap-1\">\n {title ? (\n <Text className={`text-sm font-medium leading-5 ${statusText[status]}`}>{title}</Text>\n ) : null}\n <Text className=\"text-sm leading-5 text-text-primary\">{description}</Text>\n {action && (\n <Button\n variant=\"ghost\"\n onPress={() => {\n action.onClick()\n onDismiss(id)\n }}\n >\n {action.label}\n </Button>\n )}\n </View>\n {dismissible && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={dismissLabel}\n hitSlop={8}\n onPress={() => onDismiss(id)}\n className=\"size-8 items-center justify-center rounded-full active:bg-button-secondary-bg-hover\"\n >\n <CloseIcon color={closeColor} />\n </Pressable>\n )}\n </View>\n </Animated.View>\n )\n}\n\nexport function useToast(): ToastContextValue {\n const context = useContext(ToastContext)\n if (!context) throw new Error('useToast precisa estar dentro de <ToastProvider>.')\n return context\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"toast.js","names":[],"sources":["../../../src/components/toast/toast.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport {\n Animated,\n LayoutAnimation,\n PanResponder,\n Platform,\n Pressable,\n StatusBar,\n Text,\n UIManager,\n View,\n} from 'react-native'\nimport { useReducedMotion } from '../_shared/sliding-indicator'\nimport {\n CloseIcon,\n type Status,\n StatusIcon,\n statusRole,\n statusText,\n statusTextToken,\n} from '../_shared/status'\nimport { useToken } from '../_shared/use-token'\nimport { Button } from '../button/button'\n\nexport type ToastOptions = {\n status?: Status\n /** Resumo (opcional). */\n title?: string\n description: string\n /** Ação contextual (ex.: \"Desfazer\"). Tocar nela também fecha o toast. */\n action?: { label: string; onClick: () => void }\n /** Tempo até fechar sozinho, em ms. `0` não fecha sozinho. Padrão: 5000. */\n duration?: number\n /** Mostra o ícone do status. Padrão: `true`. */\n showIcon?: boolean\n /** Mostra o botão de fechar (×). Sem ele, o toast ainda fecha pelo `duration`. Padrão: `true`. */\n dismissible?: boolean\n}\n\ntype ToastItem = ToastOptions & { id: number; closing: boolean }\n\ntype ToastContextValue = {\n /** Exibe um toast e devolve o id, para fechá-lo com `dismiss`. */\n toast: (options: ToastOptions) => number\n dismiss: (id: number) => void\n}\n\nconst ToastContext = createContext<ToastContextValue | null>(null)\n\nexport type ToastProviderProps = {\n children: ReactNode\n /** Nome acessível da região de notificações. */\n label?: string\n /** Nome acessível do botão de fechar de cada toast. */\n dismissLabel?: string\n testID?: string\n}\n\n// No Android antigo o LayoutAnimation precisa ser habilitado. Na New Architecture a chamada é no-op.\nif (Platform.OS === 'android') {\n UIManager.setLayoutAnimationEnabledExperimental?.(true)\n}\n\n/** Arrastar o toast para o lado além disto (px), ou mais rápido que isto (px/ms), o dispensa. */\nconst SWIPE_DISTANCE = 50\nconst SWIPE_VELOCITY = 0.5\n\n// Sem react-native-safe-area-context: margem fixa para o notch no iOS e a barra de status no Android.\nconst topInset = Platform.OS === 'ios' ? 56 : (StatusBar.currentHeight ?? 24) + 8\n\n/**\n * Envolva o app uma vez; qualquer componente abaixo dispara toasts com `useToast()`. A pilha fica\n * no topo da tela, acima do conteúdo do app, mas abaixo de Modal/Drawer (que são janelas nativas).\n */\nexport function ToastProvider({\n children,\n label = 'Notificações',\n dismissLabel = 'Fechar',\n testID,\n}: ToastProviderProps) {\n const [items, setItems] = useState<ToastItem[]>([])\n const nextId = useRef(0)\n\n // Marca como \"closing\"; o próprio toast anima a saída e então chama `remove`.\n const dismiss = useCallback((id: number) => {\n setItems((current) =>\n current.map((item) => (item.id === id ? { ...item, closing: true } : item)),\n )\n }, [])\n const reduced = useReducedMotion()\n const remove = useCallback(\n (id: number) => {\n // os de baixo deslizam para o lugar em vez de pular\n if (!reduced) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)\n setItems((current) => current.filter((item) => item.id !== id))\n },\n [reduced],\n )\n const toast = useCallback((options: ToastOptions) => {\n const id = nextId.current++\n setItems((current) => [...current, { ...options, id, closing: false }])\n return id\n }, [])\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss])\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n <View\n pointerEvents=\"box-none\"\n accessibilityLabel={label}\n testID={testID}\n style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9999, elevation: 9999 }}\n >\n <View\n pointerEvents=\"box-none\"\n className=\"w-full max-w-[360px] gap-2 self-center px-4\"\n style={{ paddingTop: topInset }}\n >\n {items.map((item) => (\n <ToastView\n key={item.id}\n item={item}\n dismissLabel={dismissLabel}\n onDismiss={dismiss}\n onRemove={remove}\n />\n ))}\n </View>\n </View>\n </ToastContext.Provider>\n )\n}\n\nfunction ToastView({\n item,\n dismissLabel,\n onDismiss,\n onRemove,\n}: {\n item: ToastItem\n dismissLabel: string\n onDismiss: (id: number) => void\n onRemove: (id: number) => void\n}) {\n const {\n id,\n status = 'info',\n title,\n description,\n action,\n duration = 5000,\n showIcon = true,\n dismissible = true,\n closing,\n } = item\n const progress = useRef(new Animated.Value(0)).current\n // arrastar para o lado dispensa (como o swipe do Radix no web); o auto-fechar pausa enquanto isso\n const dragX = useRef(new Animated.Value(0)).current\n const [dragging, setDragging] = useState(false)\n const latest = useRef({ id, onDismiss })\n latest.current = { id, onDismiss }\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 6 && Math.abs(g.dx) > Math.abs(g.dy),\n onPanResponderTerminationRequest: () => false,\n onPanResponderGrant: () => setDragging(true),\n onPanResponderMove: (_, g) => dragX.setValue(g.dx),\n onPanResponderRelease: (_, g) => {\n setDragging(false)\n if (Math.abs(g.dx) > SWIPE_DISTANCE || Math.abs(g.vx) > SWIPE_VELOCITY) {\n // continua para o lado de onde o dedo ia, enquanto o toast some\n Animated.timing(dragX, {\n toValue: (g.dx || g.vx) > 0 ? 400 : -400,\n duration: 150,\n useNativeDriver: true,\n }).start()\n latest.current.onDismiss(latest.current.id)\n } else {\n Animated.timing(dragX, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n }\n },\n onPanResponderTerminate: () => {\n setDragging(false)\n Animated.timing(dragX, { toValue: 0, duration: 200, useNativeDriver: true }).start()\n },\n }),\n ).current\n const swipeFade = dragX.interpolate({\n inputRange: [-150, 0, 150],\n outputRange: [0.2, 1, 0.2],\n extrapolate: 'clamp',\n })\n const iconColor = useToken(statusTextToken[status])\n const closeColor = useToken('--color-text-primary')\n\n useEffect(() => {\n if (closing) return\n const animation = Animated.timing(progress, {\n toValue: 1,\n duration: 200,\n useNativeDriver: true,\n })\n animation.start()\n return () => animation.stop()\n }, [closing, progress])\n\n useEffect(() => {\n if (!closing) return\n const animation = Animated.timing(progress, {\n toValue: 0,\n duration: 150,\n useNativeDriver: true,\n })\n animation.start(({ finished }) => finished && onRemove(id))\n return () => animation.stop()\n }, [closing, id, onRemove, progress])\n\n useEffect(() => {\n if (closing || dragging || duration <= 0) return\n const timer = setTimeout(() => onDismiss(id), duration)\n return () => clearTimeout(timer)\n }, [closing, dragging, duration, id, onDismiss])\n\n const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [-16, 0] })\n\n return (\n <Animated.View\n {...panResponder.panHandlers}\n style={{\n opacity: Animated.multiply(progress, swipeFade),\n transform: [{ translateY }, { translateX: dragX }],\n }}\n >\n <View\n accessibilityRole={statusRole(status)}\n // Erro e aviso interrompem o leitor de tela; o resto é anunciado com calma.\n accessibilityLiveRegion={statusRole(status) ? 'assertive' : 'polite'}\n className=\"flex-row items-start gap-3 rounded-md border border-border-default bg-surface-elevated py-3 pr-3 pl-4 shadow-md\"\n style={{ elevation: 6 }}\n >\n {showIcon && <StatusIcon status={status} color={iconColor} />}\n <View className=\"min-w-0 flex-1 items-start gap-1\">\n {title ? (\n <Text className={`text-sm font-medium leading-5 ${statusText[status]}`}>{title}</Text>\n ) : null}\n <Text className=\"text-sm leading-5 text-text-primary\">{description}</Text>\n {action && (\n <Button\n variant=\"ghost\"\n onPress={() => {\n action.onClick()\n onDismiss(id)\n }}\n >\n {action.label}\n </Button>\n )}\n </View>\n {dismissible && (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={dismissLabel}\n hitSlop={8}\n onPress={() => onDismiss(id)}\n className=\"size-8 items-center justify-center rounded-full active:bg-button-secondary-bg-hover\"\n >\n <CloseIcon color={closeColor} />\n </Pressable>\n )}\n </View>\n </Animated.View>\n )\n}\n\nexport function useToast(): ToastContextValue {\n const context = useContext(ToastContext)\n if (!context) throw new Error('useToast precisa estar dentro de <ToastProvider>.')\n return context\n}\n"],"mappings":"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"}
|
|
@@ -14,15 +14,19 @@ export type TooltipProps = {
|
|
|
14
14
|
side?: 'top' | 'bottom';
|
|
15
15
|
/** Fecha sozinho depois deste tempo, em ms. `0` mantém aberto até tocar fora. Padrão: 3000. */
|
|
16
16
|
duration?: number;
|
|
17
|
+
/** Seta apontando para o gatilho. Padrão: `true`. */
|
|
18
|
+
showArrow?: boolean;
|
|
19
|
+
/** Quanto tempo segurar o gatilho antes de abrir, em ms (o `delayLongPress` do RN, 500 por padrão). */
|
|
20
|
+
delayDuration?: number;
|
|
17
21
|
className?: string;
|
|
18
22
|
testID?: string;
|
|
19
23
|
};
|
|
20
24
|
/**
|
|
21
25
|
* Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),
|
|
22
26
|
* e fecha após `duration` ou ao tocar fora. Como o long press não é óbvio para leitores de tela,
|
|
23
|
-
* o `content` (se for texto) vira o `accessibilityHint` do gatilho.
|
|
24
|
-
*
|
|
27
|
+
* o `content` (se for texto) vira o `accessibilityHint` do gatilho. Fica centrado no gatilho, com
|
|
28
|
+
* seta. Diferença do web: sem os lados `left`/`right`.
|
|
25
29
|
*/
|
|
26
|
-
export declare function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side, duration, className, testID }: TooltipProps): import("react").JSX.Element;
|
|
30
|
+
export declare function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side, duration, showArrow, delayDuration, className, testID }: TooltipProps): import("react").JSX.Element;
|
|
27
31
|
//#endregion
|
|
28
32
|
//# sourceMappingURL=tooltip.d.ts.map
|
|
@@ -9,10 +9,10 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
/**
|
|
10
10
|
* Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),
|
|
11
11
|
* e fecha após `duration` ou ao tocar fora. Como o long press não é óbvio para leitores de tela,
|
|
12
|
-
* o `content` (se for texto) vira o `accessibilityHint` do gatilho.
|
|
13
|
-
*
|
|
12
|
+
* o `content` (se for texto) vira o `accessibilityHint` do gatilho. Fica centrado no gatilho, com
|
|
13
|
+
* seta. Diferença do web: sem os lados `left`/`right`.
|
|
14
14
|
*/
|
|
15
|
-
function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side = "top", duration = 3e3, className, testID }) {
|
|
15
|
+
function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, side = "top", duration = 3e3, showArrow = true, delayDuration, className, testID }) {
|
|
16
16
|
const [isOpen, setOpen] = useOpenState({
|
|
17
17
|
open,
|
|
18
18
|
defaultOpen,
|
|
@@ -34,7 +34,10 @@ function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, si
|
|
|
34
34
|
anchorRef: ref,
|
|
35
35
|
event: "onLongPress",
|
|
36
36
|
onTrigger: () => setOpen(true),
|
|
37
|
-
extra:
|
|
37
|
+
extra: {
|
|
38
|
+
...hint ? { accessibilityHint: hint } : null,
|
|
39
|
+
...delayDuration === void 0 ? null : { delayLongPress: delayDuration }
|
|
40
|
+
},
|
|
38
41
|
children
|
|
39
42
|
}), /* @__PURE__ */ jsx(AnchoredOverlay, {
|
|
40
43
|
visible: isOpen,
|
|
@@ -42,6 +45,9 @@ function Tooltip({ open, defaultOpen, onOpenChange, children, content, title, si
|
|
|
42
45
|
onRequestClose: close,
|
|
43
46
|
placement: side,
|
|
44
47
|
estimatedHeight: 80,
|
|
48
|
+
align: "center",
|
|
49
|
+
offset: showArrow ? 8 : 4,
|
|
50
|
+
arrowClassName: showArrow ? "bg-bg-inverse" : void 0,
|
|
45
51
|
testID,
|
|
46
52
|
className: cn("max-w-[240px] gap-1 rounded-sm bg-bg-inverse px-3 py-2", className),
|
|
47
53
|
children: /* @__PURE__ */ jsxs(View, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip.js","names":[],"sources":["../../../src/components/tooltip/tooltip.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { type ReactElement, type ReactNode, useEffect } from 'react'\nimport { Text, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay } from '../_shared/anchored-overlay'\nimport { AnchorTrigger, useControlledAnchor } from '../_shared/overlay-anchor'\nimport { useOpenState } from '../_shared/overlay-state'\n\nexport type TooltipProps = {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** O elemento que dispara o tooltip (ex.: um `<Button>`); recebe `onLongPress`. */\n children: ReactElement\n /** O texto principal. */\n content: ReactNode\n /** Título em destaque acima do texto (opcional). */\n title?: ReactNode\n /** Lado do gatilho em que o tooltip aparece; inverte se não couber. Padrão: `top`. */\n side?: 'top' | 'bottom'\n /** Fecha sozinho depois deste tempo, em ms. `0` mantém aberto até tocar fora. Padrão: 3000. */\n duration?: number\n className?: string\n testID?: string\n}\n\n/**\n * Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),\n * e fecha após `duration` ou ao tocar fora. Como o long press não é óbvio para leitores de tela,\n * o `content` (se for texto) vira o `accessibilityHint` do gatilho.
|
|
1
|
+
{"version":3,"file":"tooltip.js","names":[],"sources":["../../../src/components/tooltip/tooltip.tsx"],"sourcesContent":["/// <reference types=\"uniwind/types\" />\n\nimport { type ReactElement, type ReactNode, useEffect } from 'react'\nimport { Text, View } from 'react-native'\nimport { cn } from '../../utils/cn'\nimport { AnchoredOverlay } from '../_shared/anchored-overlay'\nimport { AnchorTrigger, useControlledAnchor } from '../_shared/overlay-anchor'\nimport { useOpenState } from '../_shared/overlay-state'\n\nexport type TooltipProps = {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /** O elemento que dispara o tooltip (ex.: um `<Button>`); recebe `onLongPress`. */\n children: ReactElement\n /** O texto principal. */\n content: ReactNode\n /** Título em destaque acima do texto (opcional). */\n title?: ReactNode\n /** Lado do gatilho em que o tooltip aparece; inverte se não couber. Padrão: `top`. */\n side?: 'top' | 'bottom'\n /** Fecha sozinho depois deste tempo, em ms. `0` mantém aberto até tocar fora. Padrão: 3000. */\n duration?: number\n /** Seta apontando para o gatilho. Padrão: `true`. */\n showArrow?: boolean\n /** Quanto tempo segurar o gatilho antes de abrir, em ms (o `delayLongPress` do RN, 500 por padrão). */\n delayDuration?: number\n className?: string\n testID?: string\n}\n\n/**\n * Dica contextual. No toque não há hover: abre ao pressionar e segurar o gatilho (`onLongPress`),\n * e fecha após `duration` ou ao tocar fora. Como o long press não é óbvio para leitores de tela,\n * o `content` (se for texto) vira o `accessibilityHint` do gatilho. Fica centrado no gatilho, com\n * seta. Diferença do web: sem os lados `left`/`right`.\n */\nexport function Tooltip({\n open,\n defaultOpen,\n onOpenChange,\n children,\n content,\n title,\n side = 'top',\n duration = 3000,\n showArrow = true,\n delayDuration,\n className,\n testID,\n}: TooltipProps) {\n const [isOpen, setOpen] = useOpenState({ open, defaultOpen, onOpenChange })\n const { ref, rect } = useControlledAnchor(isOpen)\n const close = () => setOpen(false)\n\n useEffect(() => {\n if (!isOpen || duration <= 0) return\n const timer = setTimeout(() => setOpen(false), duration)\n return () => clearTimeout(timer)\n }, [isOpen, duration, setOpen])\n\n const hint = typeof content === 'string' ? content : undefined\n\n return (\n <>\n <AnchorTrigger\n anchorRef={ref}\n event=\"onLongPress\"\n onTrigger={() => setOpen(true)}\n extra={{\n ...(hint ? { accessibilityHint: hint } : null),\n ...(delayDuration === undefined ? null : { delayLongPress: delayDuration }),\n }}\n >\n {children}\n </AnchorTrigger>\n <AnchoredOverlay\n visible={isOpen}\n rect={rect}\n onRequestClose={close}\n placement={side}\n estimatedHeight={80}\n align=\"center\"\n offset={showArrow ? 8 : 4}\n arrowClassName={showArrow ? 'bg-bg-inverse' : undefined}\n testID={testID}\n className={cn('max-w-[240px] gap-1 rounded-sm bg-bg-inverse px-3 py-2', className)}\n >\n <View accessibilityRole=\"text\">\n {title && (\n <Text className=\"text-[13px] font-medium leading-4 text-text-inverse\">{title}</Text>\n )}\n <Text className=\"text-xs leading-4 text-text-inverse-secondary\">{content}</Text>\n </View>\n </AnchoredOverlay>\n </>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAqCA,SAAgB,QAAQ,EACtB,MACA,aACA,cACA,UACA,SACA,OACA,OAAO,OACP,WAAW,KACX,YAAY,MACZ,eACA,WACA,UACe;CACf,MAAM,CAAC,QAAQ,WAAW,aAAa;EAAE;EAAM;EAAa;CAAa,CAAC;CAC1E,MAAM,EAAE,KAAK,SAAS,oBAAoB,MAAM;CAChD,MAAM,cAAc,QAAQ,KAAK;CAEjC,gBAAgB;EACd,IAAI,CAAC,UAAU,YAAY,GAAG;EAC9B,MAAM,QAAQ,iBAAiB,QAAQ,KAAK,GAAG,QAAQ;EACvD,aAAa,aAAa,KAAK;CACjC,GAAG;EAAC;EAAQ;EAAU;CAAO,CAAC;CAE9B,MAAM,OAAO,OAAO,YAAY,WAAW,UAAU,KAAA;CAErD,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,eAAD;EACE,WAAW;EACX,OAAM;EACN,iBAAiB,QAAQ,IAAI;EAC7B,OAAO;GACL,GAAI,OAAO,EAAE,mBAAmB,KAAK,IAAI;GACzC,GAAI,kBAAkB,KAAA,IAAY,OAAO,EAAE,gBAAgB,cAAc;EAC3E;EAEC;CACY,CAAA,GACf,oBAAC,iBAAD;EACE,SAAS;EACH;EACN,gBAAgB;EAChB,WAAW;EACX,iBAAiB;EACjB,OAAM;EACN,QAAQ,YAAY,IAAI;EACxB,gBAAgB,YAAY,kBAAkB,KAAA;EACtC;EACR,WAAW,GAAG,0DAA0D,SAAS;EAEjF,UAAA,qBAAC,MAAD;GAAM,mBAAkB;GAAxB,UAAA,CACG,SACC,oBAAC,MAAD;IAAM,WAAU;IAAuD,UAAA;GAAY,CAAA,GAErF,oBAAC,MAAD;IAAM,WAAU;IAAiD,UAAA;GAAc,CAAA,CAC3E;;CACS,CAAA,CACjB,EAAA,CAAA;AAEN"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@innovui/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Componentes React Native do design system InnovUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,13 +32,6 @@
|
|
|
32
32
|
"tailwindcss": ">=4",
|
|
33
33
|
"uniwind": ">=1.13.0"
|
|
34
34
|
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build": "tsdown",
|
|
37
|
-
"generate:tokens": "node scripts/generate-tokens.ts",
|
|
38
|
-
"test": "node scripts/generate-tokens.ts --check && jest",
|
|
39
|
-
"test:coverage": "node scripts/generate-tokens.ts --check && jest --coverage",
|
|
40
|
-
"typecheck": "tsc --noEmit"
|
|
41
|
-
},
|
|
42
35
|
"devDependencies": {
|
|
43
36
|
"@react-native/babel-preset": "0.86.3",
|
|
44
37
|
"@react-native/jest-preset": "0.86.3",
|
|
@@ -60,5 +53,12 @@
|
|
|
60
53
|
"class-variance-authority": "^0.7.1",
|
|
61
54
|
"clsx": "^2.1.1",
|
|
62
55
|
"tailwind-merge": "^3.7.0"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"generate:tokens": "node scripts/generate-tokens.ts",
|
|
60
|
+
"test": "node scripts/generate-tokens.ts --check && jest",
|
|
61
|
+
"test:coverage": "node scripts/generate-tokens.ts --check && jest --coverage",
|
|
62
|
+
"typecheck": "tsc --noEmit"
|
|
63
63
|
}
|
|
64
|
-
}
|
|
64
|
+
}
|