@medway-ui/native 1.3.3 → 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.
@@ -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
- if (isResolvedColor(value)) return value;
118
- if (isResolvedColor(resolvedValue)) return resolvedValue;
165
+ const color = isResolvedColor(value)
166
+ ? value
167
+ : isResolvedColor(resolvedValue)
168
+ ? resolvedValue
169
+ : getFallbackColor(token, aliasMap);
119
170
 
120
- return getFallbackColor(token, aliasMap);
171
+ return alpha === undefined ? color : withAlpha(color, alpha);
121
172
  }
122
173
 
123
- export { useThemeColor, toCssVariable, getFallbackColor, DEFAULT_COLOR };
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