@medway-ui/native 1.3.2 → 1.4.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/README.md +3 -1
- package/lib/commonjs/components/IndexLevel.js +442 -0
- package/lib/commonjs/components/IndexLevel.js.map +1 -0
- package/lib/commonjs/hooks/useThemeColor.js +45 -4
- package/lib/commonjs/hooks/useThemeColor.js.map +1 -1
- package/lib/commonjs/index.js +43 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/components/IndexLevel.js +435 -0
- package/lib/module/components/IndexLevel.js.map +1 -0
- package/lib/module/hooks/useThemeColor.js +45 -5
- package/lib/module/hooks/useThemeColor.js.map +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/components/IndexLevel.d.ts +66 -0
- package/lib/typescript/components/IndexLevel.d.ts.map +1 -0
- package/lib/typescript/components/Tag.d.ts +2 -2
- package/lib/typescript/hooks/useThemeColor.d.ts +9 -2
- package/lib/typescript/hooks/useThemeColor.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +17 -3
- package/src/components/IndexLevel.tsx +570 -0
- package/src/hooks/useThemeColor.ts +62 -5
- package/src/index.ts +18 -1
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IndexBarHigh,
|
|
3
|
+
IndexBarLow,
|
|
4
|
+
IndexBarMedium,
|
|
5
|
+
IndexBarPriority,
|
|
6
|
+
} from "@medway-ui/icons/native";
|
|
7
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
8
|
+
import { useUnstableNativeVariable } from "nativewind";
|
|
9
|
+
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
|
10
|
+
import { View, type LayoutChangeEvent, type ViewProps } from "react-native";
|
|
11
|
+
import Animated, {
|
|
12
|
+
Easing,
|
|
13
|
+
cancelAnimation,
|
|
14
|
+
useAnimatedStyle,
|
|
15
|
+
useSharedValue,
|
|
16
|
+
withRepeat,
|
|
17
|
+
withTiming,
|
|
18
|
+
} from "react-native-reanimated";
|
|
19
|
+
import Svg, { Rect, type SvgProps } from "react-native-svg";
|
|
20
|
+
|
|
21
|
+
import { Text } from "@/components/Text";
|
|
22
|
+
import {
|
|
23
|
+
useThemeColor,
|
|
24
|
+
withAlpha,
|
|
25
|
+
type ThemeColorToken,
|
|
26
|
+
} from "@/hooks/useThemeColor";
|
|
27
|
+
import { cn } from "@/lib/utils";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Matches the `PriorityLevelType` enum used by the products:
|
|
31
|
+
* priority = "p" | high_priority = "h" | medium_priority = "m" | low_priority = "l"
|
|
32
|
+
*/
|
|
33
|
+
type IndexLevelValue = "p" | "h" | "m" | "l";
|
|
34
|
+
|
|
35
|
+
type IndexLevelIconComponent = ComponentType<
|
|
36
|
+
{ color: string; strokeWidth?: number } & SvgProps
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
const LEVEL_LABELS: Record<
|
|
40
|
+
IndexLevelValue,
|
|
41
|
+
{ full: string; short: string; icon: IndexLevelIconComponent }
|
|
42
|
+
> = {
|
|
43
|
+
p: {
|
|
44
|
+
full: "Prioritário",
|
|
45
|
+
short: "Prioritário",
|
|
46
|
+
icon: IndexBarPriority,
|
|
47
|
+
},
|
|
48
|
+
h: {
|
|
49
|
+
full: "Alta prevalência",
|
|
50
|
+
short: "Alta",
|
|
51
|
+
icon: IndexBarHigh,
|
|
52
|
+
},
|
|
53
|
+
m: {
|
|
54
|
+
full: "Média prevalência",
|
|
55
|
+
short: "Média",
|
|
56
|
+
icon: IndexBarMedium,
|
|
57
|
+
},
|
|
58
|
+
l: {
|
|
59
|
+
full: "Baixa prevalência",
|
|
60
|
+
short: "Baixa",
|
|
61
|
+
icon: IndexBarLow,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type IndexLevelSpecialty = "default" | "cm" | "go" | "cir" | "ped" | "prev";
|
|
66
|
+
|
|
67
|
+
/** Accepts the specialty in either casing, e.g. `"cm"` or `"CM"`. */
|
|
68
|
+
type IndexLevelSpecialtyProp =
|
|
69
|
+
| IndexLevelSpecialty
|
|
70
|
+
| Uppercase<IndexLevelSpecialty>;
|
|
71
|
+
|
|
72
|
+
const RING_COLOR_TOKEN: Record<IndexLevelSpecialty, ThemeColorToken> = {
|
|
73
|
+
default: "primary-background-fill",
|
|
74
|
+
cm: "cm-background-fill",
|
|
75
|
+
go: "go-background-fill",
|
|
76
|
+
cir: "cir-background-fill",
|
|
77
|
+
ped: "ped-background-fill",
|
|
78
|
+
prev: "prev-background-fill",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Levels that get the animated ring when `animated` is true. */
|
|
82
|
+
const RING_LEVELS: IndexLevelValue[] = ["p", "h"];
|
|
83
|
+
|
|
84
|
+
const ICON_SIZES = {
|
|
85
|
+
sm: 16,
|
|
86
|
+
md: 20,
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
type IndexLevelVariant = "regular" | "soft" | "basic";
|
|
90
|
+
|
|
91
|
+
/** Shared tone for icon color token + base-layer opacity (regular vs soft/basic). */
|
|
92
|
+
const VARIANT_TONE: Record<
|
|
93
|
+
IndexLevelVariant,
|
|
94
|
+
{ colorToken: ThemeColorToken; baseOpacity: "40" | "20" }
|
|
95
|
+
> = {
|
|
96
|
+
regular: {
|
|
97
|
+
colorToken: "primary-foreground-onfill",
|
|
98
|
+
baseOpacity: "40",
|
|
99
|
+
},
|
|
100
|
+
soft: {
|
|
101
|
+
colorToken: "primary-foreground",
|
|
102
|
+
baseOpacity: "20",
|
|
103
|
+
},
|
|
104
|
+
basic: {
|
|
105
|
+
colorToken: "primary-foreground",
|
|
106
|
+
baseOpacity: "20",
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function resolveVariantTone(
|
|
111
|
+
variant: IndexLevelVariant | null | undefined
|
|
112
|
+
): (typeof VARIANT_TONE)[IndexLevelVariant] {
|
|
113
|
+
return VARIANT_TONE[variant ?? "regular"] ?? VARIANT_TONE.regular;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const indexLevelVariants = cva(
|
|
117
|
+
"flex-row shrink-0 items-center justify-center",
|
|
118
|
+
{
|
|
119
|
+
variants: {
|
|
120
|
+
variant: {
|
|
121
|
+
regular: "rounded-md bg-primary-background-fill",
|
|
122
|
+
soft: "rounded-md bg-primary-background-soft-muted",
|
|
123
|
+
basic: "bg-transparent",
|
|
124
|
+
},
|
|
125
|
+
size: {
|
|
126
|
+
sm: "gap-1.5",
|
|
127
|
+
md: "gap-2",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
compoundVariants: [
|
|
131
|
+
{ variant: ["regular", "soft"], size: "sm", class: "px-2 py-1" },
|
|
132
|
+
{ variant: ["regular", "soft"], size: "md", class: "px-2.5 py-1.5" },
|
|
133
|
+
],
|
|
134
|
+
defaultVariants: {
|
|
135
|
+
variant: "regular",
|
|
136
|
+
size: "md",
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const indexLevelTextVariants = cva("text-center font-semibold", {
|
|
142
|
+
variants: {
|
|
143
|
+
variant: {
|
|
144
|
+
regular: "text-primary-foreground-onfill",
|
|
145
|
+
soft: "text-primary-foreground",
|
|
146
|
+
basic: "text-primary-foreground",
|
|
147
|
+
},
|
|
148
|
+
size: {
|
|
149
|
+
sm: "text-xs",
|
|
150
|
+
md: "text-sm",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
defaultVariants: {
|
|
154
|
+
variant: "regular",
|
|
155
|
+
size: "md",
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const indexLevelIconVariants = cva(
|
|
160
|
+
"relative shrink-0 items-center justify-center",
|
|
161
|
+
{
|
|
162
|
+
variants: {
|
|
163
|
+
size: {
|
|
164
|
+
sm: "size-4",
|
|
165
|
+
md: "size-5",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
defaultVariants: {
|
|
169
|
+
size: "md",
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const RING_STROKE = 2;
|
|
175
|
+
/** Fallback when `--radius-md` is not resolvable at runtime (matches tokens.native.css). */
|
|
176
|
+
const RADIUS_MD_FALLBACK = 8;
|
|
177
|
+
const SPIN_DURATION_MS = 1200;
|
|
178
|
+
const RING_BASE_OPACITY = 0.16;
|
|
179
|
+
|
|
180
|
+
/** Resolves `--radius-md` so the ring tracks the same corner radius as `rounded-md`. */
|
|
181
|
+
function useRadiusMd(): number {
|
|
182
|
+
const value = useUnstableNativeVariable("--radius-md");
|
|
183
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
184
|
+
if (typeof value === "string") {
|
|
185
|
+
const parsed = Number.parseFloat(value);
|
|
186
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
187
|
+
}
|
|
188
|
+
return RADIUS_MD_FALLBACK;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type RingDeps = {
|
|
192
|
+
MaskedView: ComponentType<{
|
|
193
|
+
androidRenderingMode?: "software" | "hardware";
|
|
194
|
+
style?: object;
|
|
195
|
+
maskElement: ReactNode;
|
|
196
|
+
children?: ReactNode;
|
|
197
|
+
}>;
|
|
198
|
+
LinearGradient: ComponentType<{
|
|
199
|
+
colors: string[];
|
|
200
|
+
locations?: number[];
|
|
201
|
+
start?: { x: number; y: number };
|
|
202
|
+
end?: { x: number; y: number };
|
|
203
|
+
style?: object;
|
|
204
|
+
}>;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** Lazy-load optional peers so the static chip works without them installed. */
|
|
208
|
+
function loadRingDeps(): RingDeps | null {
|
|
209
|
+
try {
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
211
|
+
const masked = require("@react-native-masked-view/masked-view");
|
|
212
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
213
|
+
const gradient = require("expo-linear-gradient");
|
|
214
|
+
return {
|
|
215
|
+
MaskedView: masked.default ?? masked,
|
|
216
|
+
LinearGradient: gradient.LinearGradient,
|
|
217
|
+
};
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const RING_PATH = {
|
|
224
|
+
fill: "none" as const,
|
|
225
|
+
strokeWidth: RING_STROKE,
|
|
226
|
+
strokeLinecap: "round" as const,
|
|
227
|
+
strokeLinejoin: "round" as const,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
interface IndexLevelRingProps {
|
|
231
|
+
width: number;
|
|
232
|
+
height: number;
|
|
233
|
+
colorToken: ThemeColorToken;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function IndexLevelRing(props: IndexLevelRingProps) {
|
|
237
|
+
const deps = loadRingDeps();
|
|
238
|
+
if (!deps) return null;
|
|
239
|
+
return <IndexLevelRingInner {...props} deps={deps} />;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Continuous 2px squircle: one mask clips both the faint rest ring and the
|
|
244
|
+
* rotating linear gradient, so they share the same stroke.
|
|
245
|
+
*/
|
|
246
|
+
function IndexLevelRingInner({
|
|
247
|
+
width,
|
|
248
|
+
height,
|
|
249
|
+
colorToken,
|
|
250
|
+
deps,
|
|
251
|
+
}: IndexLevelRingProps & { deps: RingDeps }) {
|
|
252
|
+
const { MaskedView, LinearGradient } = deps;
|
|
253
|
+
const color = useThemeColor(colorToken);
|
|
254
|
+
const colorMid = withAlpha(color, 0.8);
|
|
255
|
+
const colorFaded = withAlpha(color, 0);
|
|
256
|
+
const colorBase = withAlpha(color, RING_BASE_OPACITY);
|
|
257
|
+
const ringRadius = useRadiusMd();
|
|
258
|
+
|
|
259
|
+
const inset = RING_STROKE / 2;
|
|
260
|
+
const rectW = Math.max(width - RING_STROKE, 0);
|
|
261
|
+
const rectH = Math.max(height - RING_STROKE, 0);
|
|
262
|
+
const rx = Math.min(ringRadius, rectW / 2, rectH / 2);
|
|
263
|
+
const sweep = Math.max(width, height) * 2;
|
|
264
|
+
|
|
265
|
+
const rotation = useSharedValue(0);
|
|
266
|
+
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
cancelAnimation(rotation);
|
|
269
|
+
rotation.value = 0;
|
|
270
|
+
rotation.value = withRepeat(
|
|
271
|
+
withTiming(360, {
|
|
272
|
+
duration: SPIN_DURATION_MS,
|
|
273
|
+
easing: Easing.linear,
|
|
274
|
+
}),
|
|
275
|
+
-1,
|
|
276
|
+
false
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return () => {
|
|
280
|
+
cancelAnimation(rotation);
|
|
281
|
+
};
|
|
282
|
+
}, [rotation]);
|
|
283
|
+
|
|
284
|
+
const spinStyle = useAnimatedStyle(() => ({
|
|
285
|
+
transform: [{ rotate: `${rotation.value}deg` }],
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<View
|
|
290
|
+
pointerEvents="none"
|
|
291
|
+
accessible={false}
|
|
292
|
+
importantForAccessibility="no-hide-descendants"
|
|
293
|
+
collapsable={false}
|
|
294
|
+
style={{ position: "absolute", top: 0, left: 0, width, height }}
|
|
295
|
+
>
|
|
296
|
+
<MaskedView
|
|
297
|
+
// Hardware masking can fail to clip LinearGradient on Android; software is the reliable path.
|
|
298
|
+
androidRenderingMode="software"
|
|
299
|
+
style={{ width, height }}
|
|
300
|
+
maskElement={
|
|
301
|
+
<View
|
|
302
|
+
collapsable={false}
|
|
303
|
+
style={{ width, height, backgroundColor: "transparent" }}
|
|
304
|
+
>
|
|
305
|
+
<Svg width={width} height={height}>
|
|
306
|
+
<Rect
|
|
307
|
+
x={inset}
|
|
308
|
+
y={inset}
|
|
309
|
+
width={rectW}
|
|
310
|
+
height={rectH}
|
|
311
|
+
rx={rx}
|
|
312
|
+
ry={rx}
|
|
313
|
+
stroke="#FFFFFF"
|
|
314
|
+
{...RING_PATH}
|
|
315
|
+
/>
|
|
316
|
+
</Svg>
|
|
317
|
+
</View>
|
|
318
|
+
}
|
|
319
|
+
>
|
|
320
|
+
<View
|
|
321
|
+
style={{
|
|
322
|
+
position: "absolute",
|
|
323
|
+
top: 0,
|
|
324
|
+
left: 0,
|
|
325
|
+
width,
|
|
326
|
+
height,
|
|
327
|
+
backgroundColor: colorBase,
|
|
328
|
+
}}
|
|
329
|
+
/>
|
|
330
|
+
<Animated.View
|
|
331
|
+
collapsable={false}
|
|
332
|
+
style={[
|
|
333
|
+
{
|
|
334
|
+
position: "absolute",
|
|
335
|
+
width: sweep,
|
|
336
|
+
height: sweep,
|
|
337
|
+
left: (width - sweep) / 2,
|
|
338
|
+
top: (height - sweep) / 2,
|
|
339
|
+
},
|
|
340
|
+
spinStyle,
|
|
341
|
+
]}
|
|
342
|
+
>
|
|
343
|
+
<LinearGradient
|
|
344
|
+
colors={[color, colorMid, colorFaded]}
|
|
345
|
+
locations={[0, 0.255, 0.574]}
|
|
346
|
+
start={{ x: 0.5, y: 0 }}
|
|
347
|
+
end={{ x: 0.5, y: 1 }}
|
|
348
|
+
style={{ width: sweep, height: sweep }}
|
|
349
|
+
/>
|
|
350
|
+
</Animated.View>
|
|
351
|
+
</MaskedView>
|
|
352
|
+
</View>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
interface IndexLevelIconProps
|
|
357
|
+
extends Omit<ViewProps, "children">,
|
|
358
|
+
VariantProps<typeof indexLevelIconVariants> {
|
|
359
|
+
level: IndexLevelValue;
|
|
360
|
+
baseOpacity?: "40" | "20";
|
|
361
|
+
/** Token de cor do ícone (SVG não herda `currentColor` no native). */
|
|
362
|
+
colorToken?: ThemeColorToken;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function IndexLevelIcon({
|
|
366
|
+
className,
|
|
367
|
+
level,
|
|
368
|
+
size,
|
|
369
|
+
baseOpacity = "40",
|
|
370
|
+
colorToken = "primary-foreground",
|
|
371
|
+
...props
|
|
372
|
+
}: IndexLevelIconProps) {
|
|
373
|
+
const color = useThemeColor(colorToken);
|
|
374
|
+
const currentLevel = LEVEL_LABELS[level];
|
|
375
|
+
if (!currentLevel) return null;
|
|
376
|
+
|
|
377
|
+
const LevelIcon = currentLevel.icon;
|
|
378
|
+
const hasBase = level !== "p";
|
|
379
|
+
const iconSize = ICON_SIZES[size ?? "md"];
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<View
|
|
383
|
+
className={cn(indexLevelIconVariants({ size }), className)}
|
|
384
|
+
{...props}
|
|
385
|
+
>
|
|
386
|
+
{hasBase && (
|
|
387
|
+
<View
|
|
388
|
+
className={cn(
|
|
389
|
+
"absolute inset-0 items-center justify-center",
|
|
390
|
+
baseOpacity === "40" ? "opacity-40" : "opacity-20"
|
|
391
|
+
)}
|
|
392
|
+
accessibilityElementsHidden
|
|
393
|
+
importantForAccessibility="no-hide-descendants"
|
|
394
|
+
>
|
|
395
|
+
<IndexBarPriority width={iconSize} height={iconSize} color={color} />
|
|
396
|
+
</View>
|
|
397
|
+
)}
|
|
398
|
+
<View
|
|
399
|
+
className="absolute inset-0 items-center justify-center"
|
|
400
|
+
accessibilityElementsHidden
|
|
401
|
+
importantForAccessibility="no-hide-descendants"
|
|
402
|
+
>
|
|
403
|
+
<LevelIcon width={iconSize} height={iconSize} color={color} />
|
|
404
|
+
</View>
|
|
405
|
+
</View>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
interface IndexLevelProps
|
|
410
|
+
extends Omit<ViewProps, "style">,
|
|
411
|
+
VariantProps<typeof indexLevelVariants> {
|
|
412
|
+
/** Priority level rendered by the component. */
|
|
413
|
+
level: IndexLevelValue;
|
|
414
|
+
/**
|
|
415
|
+
* Draws the traveling gradient stroke on levels `p` / `h`.
|
|
416
|
+
* Chip colors still come from `variant` — `specialty` only tints this ring.
|
|
417
|
+
*/
|
|
418
|
+
animated?: boolean;
|
|
419
|
+
/** Color of the animated ring. Ignored when `animated` is false. */
|
|
420
|
+
specialty?: IndexLevelSpecialtyProp;
|
|
421
|
+
/** Uses the abbreviated label ("Alta") instead of the full one ("Alta prevalência"). */
|
|
422
|
+
shortName?: boolean;
|
|
423
|
+
/** Overrides the label derived from `level`. */
|
|
424
|
+
label?: ReactNode;
|
|
425
|
+
/** Hides the label, keeping only the icon (and any `children`). */
|
|
426
|
+
hideLabel?: boolean;
|
|
427
|
+
/** Extra classes for the icon slot. */
|
|
428
|
+
iconClassName?: string;
|
|
429
|
+
/** Extra classes for the label slot. */
|
|
430
|
+
labelClassName?: string;
|
|
431
|
+
children?: ReactNode;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function IndexLevel({
|
|
435
|
+
className,
|
|
436
|
+
variant,
|
|
437
|
+
size,
|
|
438
|
+
level,
|
|
439
|
+
animated = false,
|
|
440
|
+
specialty = "default",
|
|
441
|
+
shortName = false,
|
|
442
|
+
label,
|
|
443
|
+
hideLabel = false,
|
|
444
|
+
iconClassName,
|
|
445
|
+
labelClassName,
|
|
446
|
+
children,
|
|
447
|
+
accessibilityLabel,
|
|
448
|
+
onLayout,
|
|
449
|
+
...props
|
|
450
|
+
}: IndexLevelProps) {
|
|
451
|
+
const currentLevel = LEVEL_LABELS[level];
|
|
452
|
+
const [frame, setFrame] = useState({ width: 0, height: 0 });
|
|
453
|
+
const specialtyKey = specialty.toLowerCase() as IndexLevelSpecialty;
|
|
454
|
+
const ringToken = RING_COLOR_TOKEN[specialtyKey] ?? RING_COLOR_TOKEN.default;
|
|
455
|
+
const hasRing = Boolean(animated) && RING_LEVELS.includes(level);
|
|
456
|
+
const tone = resolveVariantTone(variant);
|
|
457
|
+
|
|
458
|
+
if (!currentLevel) return null;
|
|
459
|
+
|
|
460
|
+
const resolvedLabel =
|
|
461
|
+
label ?? (shortName ? currentLevel.short : currentLevel.full);
|
|
462
|
+
// When the visual label is hidden, always expose a name for screen readers
|
|
463
|
+
// (icons are a11y-hidden), unless the consumer passed accessibilityLabel.
|
|
464
|
+
const resolvedA11yLabel =
|
|
465
|
+
accessibilityLabel ??
|
|
466
|
+
(hideLabel
|
|
467
|
+
? typeof resolvedLabel === "string"
|
|
468
|
+
? resolvedLabel
|
|
469
|
+
: currentLevel.full
|
|
470
|
+
: undefined);
|
|
471
|
+
|
|
472
|
+
const handleFrameLayout = (event: LayoutChangeEvent) => {
|
|
473
|
+
const { width, height } = event.nativeEvent.layout;
|
|
474
|
+
setFrame((prev) =>
|
|
475
|
+
prev.width === width && prev.height === height ? prev : { width, height }
|
|
476
|
+
);
|
|
477
|
+
onLayout?.(event);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const chipContent = (
|
|
481
|
+
<>
|
|
482
|
+
<IndexLevelIcon
|
|
483
|
+
level={level}
|
|
484
|
+
size={size}
|
|
485
|
+
baseOpacity={tone.baseOpacity}
|
|
486
|
+
colorToken={tone.colorToken}
|
|
487
|
+
className={iconClassName}
|
|
488
|
+
/>
|
|
489
|
+
{!hideLabel && (
|
|
490
|
+
<Text
|
|
491
|
+
className={cn(
|
|
492
|
+
indexLevelTextVariants({ variant, size }),
|
|
493
|
+
labelClassName
|
|
494
|
+
)}
|
|
495
|
+
>
|
|
496
|
+
{resolvedLabel}
|
|
497
|
+
</Text>
|
|
498
|
+
)}
|
|
499
|
+
{typeof children === "string" || typeof children === "number" ? (
|
|
500
|
+
<Text
|
|
501
|
+
className={cn(
|
|
502
|
+
indexLevelTextVariants({ variant, size }),
|
|
503
|
+
"font-normal",
|
|
504
|
+
labelClassName
|
|
505
|
+
)}
|
|
506
|
+
>
|
|
507
|
+
{children}
|
|
508
|
+
</Text>
|
|
509
|
+
) : (
|
|
510
|
+
children
|
|
511
|
+
)}
|
|
512
|
+
</>
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
if (!hasRing) {
|
|
516
|
+
return (
|
|
517
|
+
<View
|
|
518
|
+
accessible={Boolean(resolvedA11yLabel)}
|
|
519
|
+
accessibilityLabel={resolvedA11yLabel}
|
|
520
|
+
className={cn(
|
|
521
|
+
indexLevelVariants({ variant, size }),
|
|
522
|
+
"self-start",
|
|
523
|
+
className
|
|
524
|
+
)}
|
|
525
|
+
onLayout={onLayout}
|
|
526
|
+
{...props}
|
|
527
|
+
>
|
|
528
|
+
{chipContent}
|
|
529
|
+
</View>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<View
|
|
535
|
+
accessible={Boolean(resolvedA11yLabel)}
|
|
536
|
+
accessibilityLabel={resolvedA11yLabel}
|
|
537
|
+
collapsable={false}
|
|
538
|
+
onLayout={handleFrameLayout}
|
|
539
|
+
className="self-start overflow-visible p-1"
|
|
540
|
+
{...props}
|
|
541
|
+
>
|
|
542
|
+
{frame.width > 0 && frame.height > 0 && (
|
|
543
|
+
<IndexLevelRing
|
|
544
|
+
width={frame.width}
|
|
545
|
+
height={frame.height}
|
|
546
|
+
colorToken={ringToken}
|
|
547
|
+
/>
|
|
548
|
+
)}
|
|
549
|
+
<View className={cn(indexLevelVariants({ variant, size }), className)}>
|
|
550
|
+
{chipContent}
|
|
551
|
+
</View>
|
|
552
|
+
</View>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export {
|
|
557
|
+
LEVEL_LABELS as INDEX_LEVEL_LABELS,
|
|
558
|
+
IndexLevel,
|
|
559
|
+
IndexLevelIcon,
|
|
560
|
+
indexLevelIconVariants,
|
|
561
|
+
indexLevelTextVariants,
|
|
562
|
+
indexLevelVariants,
|
|
563
|
+
};
|
|
564
|
+
export type {
|
|
565
|
+
IndexLevelIconProps,
|
|
566
|
+
IndexLevelProps,
|
|
567
|
+
IndexLevelSpecialty,
|
|
568
|
+
IndexLevelSpecialtyProp,
|
|
569
|
+
IndexLevelValue,
|
|
570
|
+
};
|
|
@@ -18,6 +18,11 @@ const LIGHT_COLOR_ALIAS_MAP: Record<string, string> = {
|
|
|
18
18
|
'gray-background-soft-muted': 'gray-30',
|
|
19
19
|
'primary-foreground-onfill': 'white',
|
|
20
20
|
'primary-background-fill': 'primary',
|
|
21
|
+
'cm-background-fill': 'cm-darken-1',
|
|
22
|
+
'go-background-fill': 'go',
|
|
23
|
+
'cir-background-fill': 'cir',
|
|
24
|
+
'ped-background-fill': 'ped',
|
|
25
|
+
'prev-background-fill': 'prev',
|
|
21
26
|
'red-foreground': 'red-darken-2',
|
|
22
27
|
'go-foreground': 'go-darken-1',
|
|
23
28
|
};
|
|
@@ -37,6 +42,11 @@ const DARK_COLOR_ALIAS_MAP: Record<string, string> = {
|
|
|
37
42
|
'gray-background-soft-muted': 'gray-700',
|
|
38
43
|
'primary-foreground-onfill': 'primary',
|
|
39
44
|
'primary-background-fill': 'gray-70',
|
|
45
|
+
'cm-background-fill': 'cm-darken-1',
|
|
46
|
+
'go-background-fill': 'go-darken-1',
|
|
47
|
+
'cir-background-fill': 'cir',
|
|
48
|
+
'ped-background-fill': 'ped',
|
|
49
|
+
'prev-background-fill': 'prev',
|
|
40
50
|
'red-foreground': 'red-lighten-3',
|
|
41
51
|
'go-foreground': 'go-lighten-5',
|
|
42
52
|
};
|
|
@@ -45,6 +55,11 @@ const DARK_COLOR_ALIAS_MAP: Record<string, string> = {
|
|
|
45
55
|
const THEME_COLOR_FALLBACKS: Record<string, string> = {
|
|
46
56
|
white: '#FFFFFF',
|
|
47
57
|
primary: 'rgba(0, 32, 91, 1)',
|
|
58
|
+
'cm-darken-1': 'rgba(56, 112, 178, 1)',
|
|
59
|
+
go: 'rgba(172, 20, 90, 1)',
|
|
60
|
+
cir: 'rgba(0, 239, 200, 1)',
|
|
61
|
+
ped: 'rgba(255, 198, 73, 1)',
|
|
62
|
+
prev: 'rgba(59, 63, 182, 1)',
|
|
48
63
|
'gray-600': 'rgba(49, 57, 73, 1)',
|
|
49
64
|
'gray-450': 'rgba(90, 105, 135, 1)',
|
|
50
65
|
'gray-400': 'rgba(122, 133, 151, 1)',
|
|
@@ -100,11 +115,44 @@ function getFallbackColor(
|
|
|
100
115
|
);
|
|
101
116
|
}
|
|
102
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Aplica alpha na cor já resolvida (`rgba` ou hex). `transparent` no
|
|
120
|
+
* LinearGradient do Expo interpola via preto no Android.
|
|
121
|
+
* `alpha` é clamado em 0–1 para não gerar hex inválido (ex. `#RRGGBB132`).
|
|
122
|
+
*/
|
|
123
|
+
function withAlpha(color: string, alpha: number): string {
|
|
124
|
+
const clamped = Math.min(1, Math.max(0, alpha));
|
|
125
|
+
const rgba = color.match(
|
|
126
|
+
/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*[\d.]+)?\s*\)/i
|
|
127
|
+
);
|
|
128
|
+
if (rgba) {
|
|
129
|
+
return `rgba(${rgba[1]}, ${rgba[2]}, ${rgba[3]}, ${clamped})`;
|
|
130
|
+
}
|
|
131
|
+
if (color.startsWith('#')) {
|
|
132
|
+
let hex = color.slice(1);
|
|
133
|
+
if (hex.length === 3) {
|
|
134
|
+
hex = hex
|
|
135
|
+
.split('')
|
|
136
|
+
.map((char) => `${char}${char}`)
|
|
137
|
+
.join('');
|
|
138
|
+
}
|
|
139
|
+
if (hex.length === 8) {
|
|
140
|
+
hex = hex.slice(0, 6);
|
|
141
|
+
}
|
|
142
|
+
const a = Math.round(clamped * 255)
|
|
143
|
+
.toString(16)
|
|
144
|
+
.padStart(2, '0');
|
|
145
|
+
return `#${hex}${a}`;
|
|
146
|
+
}
|
|
147
|
+
return color;
|
|
148
|
+
}
|
|
149
|
+
|
|
103
150
|
/**
|
|
104
151
|
* Resolve uma cor semântica do design system (tokens.native.css) em runtime.
|
|
105
152
|
* Use quando a prop `color` for obrigatória (SVG, stroke Reanimated, etc.).
|
|
153
|
+
* `alpha` (0–1) aplica a opacidade na cor resolvida.
|
|
106
154
|
*/
|
|
107
|
-
function useThemeColor(token: ThemeColorToken): string {
|
|
155
|
+
function useThemeColor(token: ThemeColorToken, alpha?: number): string {
|
|
108
156
|
const { colorScheme } = useColorScheme();
|
|
109
157
|
const aliasMap =
|
|
110
158
|
colorScheme === 'dark' ? DARK_COLOR_ALIAS_MAP : LIGHT_COLOR_ALIAS_MAP;
|
|
@@ -114,11 +162,20 @@ function useThemeColor(token: ThemeColorToken): string {
|
|
|
114
162
|
toResolvedCssVariable(token, aliasMap)
|
|
115
163
|
);
|
|
116
164
|
|
|
117
|
-
|
|
118
|
-
|
|
165
|
+
const color = isResolvedColor(value)
|
|
166
|
+
? value
|
|
167
|
+
: isResolvedColor(resolvedValue)
|
|
168
|
+
? resolvedValue
|
|
169
|
+
: getFallbackColor(token, aliasMap);
|
|
119
170
|
|
|
120
|
-
return
|
|
171
|
+
return alpha === undefined ? color : withAlpha(color, alpha);
|
|
121
172
|
}
|
|
122
173
|
|
|
123
|
-
export {
|
|
174
|
+
export {
|
|
175
|
+
useThemeColor,
|
|
176
|
+
withAlpha,
|
|
177
|
+
toCssVariable,
|
|
178
|
+
getFallbackColor,
|
|
179
|
+
DEFAULT_COLOR,
|
|
180
|
+
};
|
|
124
181
|
export type { ThemeColorToken };
|
package/src/index.ts
CHANGED
|
@@ -218,6 +218,23 @@ export type {
|
|
|
218
218
|
InputType,
|
|
219
219
|
} from "@/components/Input";
|
|
220
220
|
|
|
221
|
+
// IndexLevel
|
|
222
|
+
export {
|
|
223
|
+
INDEX_LEVEL_LABELS,
|
|
224
|
+
IndexLevel,
|
|
225
|
+
IndexLevelIcon,
|
|
226
|
+
indexLevelIconVariants,
|
|
227
|
+
indexLevelTextVariants,
|
|
228
|
+
indexLevelVariants,
|
|
229
|
+
} from "@/components/IndexLevel";
|
|
230
|
+
export type {
|
|
231
|
+
IndexLevelIconProps,
|
|
232
|
+
IndexLevelProps,
|
|
233
|
+
IndexLevelSpecialty,
|
|
234
|
+
IndexLevelSpecialtyProp,
|
|
235
|
+
IndexLevelValue,
|
|
236
|
+
} from "@/components/IndexLevel";
|
|
237
|
+
|
|
221
238
|
// Label
|
|
222
239
|
export { Label, labelVariants, labelTextVariants } from "@/components/Label";
|
|
223
240
|
export type { LabelProps } from "@/components/Label";
|
|
@@ -386,7 +403,7 @@ export type { TooltipContentProps } from "@/components/Tooltip";
|
|
|
386
403
|
|
|
387
404
|
// Hooks
|
|
388
405
|
export { useToast, toast } from "@/hooks/use-toast";
|
|
389
|
-
export { useThemeColor } from "@/hooks/useThemeColor";
|
|
406
|
+
export { useThemeColor, withAlpha } from "@/hooks/useThemeColor";
|
|
390
407
|
export type { ThemeColorToken } from "@/hooks/useThemeColor";
|
|
391
408
|
|
|
392
409
|
// Theme-aware icon helper
|