@grundtone/react-native 2.0.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/dist/index.mjs ADDED
@@ -0,0 +1,912 @@
1
+ // src/ThemeContext.tsx
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useState
8
+ } from "react";
9
+ import { Appearance } from "react-native";
10
+ import { resolveThemeMode } from "@grundtone/utils";
11
+ import { jsx } from "react/jsx-runtime";
12
+ var GrundtoneThemeContext = createContext(void 0);
13
+ function getSystemIsDark() {
14
+ return Appearance.getColorScheme() === "dark";
15
+ }
16
+ function GrundtoneThemeProvider({
17
+ light,
18
+ dark,
19
+ defaultMode = "auto",
20
+ mode: controlledMode,
21
+ children
22
+ }) {
23
+ const [internalMode, setInternalMode] = useState(defaultMode);
24
+ const [systemIsDark, setSystemIsDark] = useState(getSystemIsDark);
25
+ useEffect(() => {
26
+ const sub = Appearance.addChangeListener(() => {
27
+ setSystemIsDark(getSystemIsDark());
28
+ });
29
+ return () => sub.remove();
30
+ }, []);
31
+ const activeMode = controlledMode ?? internalMode;
32
+ const resolved = resolveThemeMode(activeMode, systemIsDark);
33
+ const setMode = useCallback(
34
+ (newMode) => {
35
+ if (controlledMode === void 0) {
36
+ setInternalMode(newMode);
37
+ }
38
+ },
39
+ [controlledMode]
40
+ );
41
+ const theme = resolved === "dark" ? dark : light;
42
+ const isDark = resolved === "dark";
43
+ const value = useMemo(
44
+ () => ({
45
+ theme,
46
+ mode: activeMode,
47
+ isDark,
48
+ setMode
49
+ }),
50
+ [theme, activeMode, isDark, setMode]
51
+ );
52
+ return /* @__PURE__ */ jsx(GrundtoneThemeContext.Provider, { value, children });
53
+ }
54
+
55
+ // src/useGrundtoneTheme.ts
56
+ import { useContext } from "react";
57
+ function useGrundtoneTheme() {
58
+ const context = useContext(GrundtoneThemeContext);
59
+ if (context === void 0) {
60
+ throw new Error(
61
+ "useGrundtoneTheme must be used within a GrundtoneThemeProvider. Wrap your app with <GrundtoneThemeProvider light={...} dark={...}>."
62
+ );
63
+ }
64
+ return context;
65
+ }
66
+
67
+ // src/index.ts
68
+ import { createTheme } from "@grundtone/core";
69
+
70
+ // src/shadows.ts
71
+ function shadowToRN(layers) {
72
+ const layer = layers[0];
73
+ if (!layer) {
74
+ return {
75
+ shadowColor: "transparent",
76
+ shadowOffset: { width: 0, height: 0 },
77
+ shadowOpacity: 0,
78
+ shadowRadius: 0,
79
+ elevation: 0
80
+ };
81
+ }
82
+ return {
83
+ shadowColor: layer.color,
84
+ shadowOffset: { width: layer.x, height: layer.y },
85
+ shadowOpacity: layer.opacity,
86
+ shadowRadius: layer.blur / 2,
87
+ elevation: Math.ceil(layer.blur / 2)
88
+ };
89
+ }
90
+
91
+ // src/components/Icon/Icon.tsx
92
+ import { SvgXml } from "react-native-svg";
93
+ import { getIconColor } from "@grundtone/core";
94
+
95
+ // src/IconRegistryContext.tsx
96
+ import { createContext as createContext2, useContext as useContext2 } from "react";
97
+ import { jsx as jsx2 } from "react/jsx-runtime";
98
+ var IconRegistryContext = createContext2(null);
99
+ function IconRegistryProvider({
100
+ registry,
101
+ children
102
+ }) {
103
+ return /* @__PURE__ */ jsx2(IconRegistryContext.Provider, { value: registry, children });
104
+ }
105
+ function useIconRegistry() {
106
+ return useContext2(IconRegistryContext);
107
+ }
108
+
109
+ // src/components/Icon/Icon.tsx
110
+ import { jsx as jsx3 } from "react/jsx-runtime";
111
+ var ICON_SIZES = {
112
+ xs: 12,
113
+ sm: 16,
114
+ md: 20,
115
+ lg: 24,
116
+ xl: 32,
117
+ "2xl": 40
118
+ };
119
+ function GTIcon({ icon, name, size = "lg", label, color }) {
120
+ const registry = useIconRegistry();
121
+ let resolved = null;
122
+ if (icon) {
123
+ resolved = icon;
124
+ } else if (name && registry) {
125
+ resolved = registry[name] ?? null;
126
+ }
127
+ if (!resolved) {
128
+ if (process.env.NODE_ENV !== "production") {
129
+ if (name && !registry) {
130
+ console.warn(
131
+ `[GTIcon] No icon registry provided. Wrap your app in <IconRegistryProvider> or pass the "icon" prop directly.`
132
+ );
133
+ } else if (name) {
134
+ console.warn(`[GTIcon] Icon "${name}" not found in registry.`);
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ const px = ICON_SIZES[size];
140
+ const resolvedColor = color ?? getIconColor();
141
+ const xml = `<svg viewBox="${resolved.viewBox}" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="${resolvedColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${resolved.body}</svg>`;
142
+ return /* @__PURE__ */ jsx3(
143
+ SvgXml,
144
+ {
145
+ xml,
146
+ width: px,
147
+ height: px,
148
+ accessibilityLabel: label,
149
+ accessibilityRole: label ? "image" : void 0,
150
+ accessible: !!label
151
+ }
152
+ );
153
+ }
154
+
155
+ // src/components/Button/Button.tsx
156
+ import {
157
+ Pressable,
158
+ Text,
159
+ ActivityIndicator,
160
+ StyleSheet
161
+ } from "react-native";
162
+ import { jsx as jsx4 } from "react/jsx-runtime";
163
+ function rem(value) {
164
+ return parseFloat(value) * 16;
165
+ }
166
+ function GTButton({
167
+ variant = "primary",
168
+ size = "md",
169
+ rounded,
170
+ disabled = false,
171
+ loading = false,
172
+ block = false,
173
+ onPress,
174
+ accessibilityLabel,
175
+ children
176
+ }) {
177
+ const { theme } = useGrundtoneTheme();
178
+ const isDisabled = disabled || loading;
179
+ const variantStyles = getVariantStyles(variant, theme.colors);
180
+ const sizeStyles = getSizeStyles(size, theme);
181
+ const radiusStyle = getRadiusStyle(rounded, theme.radius);
182
+ const containerStyle = {
183
+ ...styles.base,
184
+ ...variantStyles.container,
185
+ ...sizeStyles.container,
186
+ ...radiusStyle,
187
+ ...block ? styles.block : void 0,
188
+ ...isDisabled ? styles.disabled : void 0
189
+ };
190
+ const textStyle = {
191
+ ...sizeStyles.text,
192
+ ...variantStyles.text,
193
+ fontFamily: theme.typography.fontFamily.base,
194
+ fontWeight: `${theme.typography.fontWeight.medium}`
195
+ };
196
+ const spinnerColor = variantStyles.text.color;
197
+ function handlePress() {
198
+ if (isDisabled) return;
199
+ onPress?.();
200
+ }
201
+ return /* @__PURE__ */ jsx4(
202
+ Pressable,
203
+ {
204
+ onPress: handlePress,
205
+ disabled: isDisabled,
206
+ style: ({ pressed }) => [
207
+ containerStyle,
208
+ pressed && !isDisabled ? variantStyles.pressed : void 0
209
+ ],
210
+ accessibilityRole: "button",
211
+ accessibilityLabel,
212
+ accessibilityState: { disabled: isDisabled, busy: loading },
213
+ children: loading ? /* @__PURE__ */ jsx4(ActivityIndicator, { size: "small", color: spinnerColor }) : typeof children === "string" ? /* @__PURE__ */ jsx4(Text, { style: textStyle, children }) : children
214
+ }
215
+ );
216
+ }
217
+ function getVariantStyles(variant, colors) {
218
+ switch (variant) {
219
+ case "primary":
220
+ return {
221
+ container: {
222
+ backgroundColor: colors.primary,
223
+ borderColor: colors.primary
224
+ },
225
+ text: { color: colors.onPrimary },
226
+ pressed: { backgroundColor: colors.primaryDark }
227
+ };
228
+ case "secondary":
229
+ return {
230
+ container: {
231
+ backgroundColor: colors.secondary,
232
+ borderColor: colors.secondary
233
+ },
234
+ text: { color: colors.text },
235
+ pressed: { backgroundColor: colors.secondaryDark }
236
+ };
237
+ case "outlined":
238
+ return {
239
+ container: {
240
+ backgroundColor: "transparent",
241
+ borderColor: colors.borderMedium
242
+ },
243
+ text: { color: colors.primary },
244
+ pressed: {
245
+ backgroundColor: colors.primary,
246
+ borderColor: colors.primary
247
+ }
248
+ };
249
+ case "negative":
250
+ return {
251
+ container: {
252
+ backgroundColor: colors.error,
253
+ borderColor: colors.error
254
+ },
255
+ text: { color: colors.onPrimary },
256
+ pressed: { backgroundColor: colors.errorDark }
257
+ };
258
+ case "unstyled":
259
+ return {
260
+ container: {
261
+ backgroundColor: "transparent",
262
+ borderColor: "transparent",
263
+ borderWidth: 0,
264
+ paddingHorizontal: 0,
265
+ paddingVertical: 0
266
+ },
267
+ text: { color: colors.text },
268
+ pressed: {}
269
+ };
270
+ }
271
+ }
272
+ function getSizeStyles(size, theme) {
273
+ switch (size) {
274
+ case "sm":
275
+ return {
276
+ container: {
277
+ paddingVertical: rem(theme.spacing.xs),
278
+ paddingHorizontal: rem(theme.spacing.sm)
279
+ },
280
+ text: { fontSize: rem(theme.typography.fontSize.sm) }
281
+ };
282
+ case "md":
283
+ return {
284
+ container: {
285
+ paddingVertical: rem(theme.spacing.sm),
286
+ paddingHorizontal: rem(theme.spacing.md)
287
+ },
288
+ text: { fontSize: rem(theme.typography.fontSize.base) }
289
+ };
290
+ case "lg":
291
+ return {
292
+ container: {
293
+ paddingVertical: rem(theme.spacing.md),
294
+ paddingHorizontal: rem(theme.spacing.xl)
295
+ },
296
+ text: { fontSize: rem(theme.typography.fontSize.lg) }
297
+ };
298
+ }
299
+ }
300
+ function getRadiusStyle(rounded, radius) {
301
+ if (!rounded) return { borderRadius: rem(radius.md) };
302
+ if (rounded === "full") return { borderRadius: 9999 };
303
+ if (rounded === "none") return { borderRadius: 0 };
304
+ return { borderRadius: rem(radius[rounded]) };
305
+ }
306
+ var styles = StyleSheet.create({
307
+ base: {
308
+ flexDirection: "row",
309
+ alignItems: "center",
310
+ justifyContent: "center",
311
+ borderWidth: 1,
312
+ gap: 4
313
+ },
314
+ block: {
315
+ width: "100%"
316
+ },
317
+ disabled: {
318
+ opacity: 0.5
319
+ }
320
+ });
321
+
322
+ // src/components/Input/Input.tsx
323
+ import { useState as useState2 } from "react";
324
+ import {
325
+ View,
326
+ Text as Text2,
327
+ TextInput
328
+ } from "react-native";
329
+ import { jsx as jsx5, jsxs } from "react/jsx-runtime";
330
+ function rem2(value) {
331
+ return parseFloat(value) * 16;
332
+ }
333
+ function getKeyboardType(type) {
334
+ switch (type) {
335
+ case "email":
336
+ return "email-address";
337
+ case "number":
338
+ return "numeric";
339
+ case "tel":
340
+ return "phone-pad";
341
+ case "url":
342
+ return "url";
343
+ default:
344
+ return "default";
345
+ }
346
+ }
347
+ function GTInput({
348
+ value,
349
+ onChangeText,
350
+ onFocus,
351
+ onBlur,
352
+ type = "text",
353
+ size = "md",
354
+ rounded,
355
+ placeholder,
356
+ label,
357
+ helpText,
358
+ errorText,
359
+ disabled = false,
360
+ readonly = false,
361
+ required: required2 = false,
362
+ optionalLabel,
363
+ block = false,
364
+ maxLength: maxLength2,
365
+ prefix,
366
+ suffix,
367
+ accessibilityLabel
368
+ }) {
369
+ const { theme } = useGrundtoneTheme();
370
+ const [isFocused, setIsFocused] = useState2(false);
371
+ const sp = (key) => rem2(theme.spacing[key]);
372
+ const fs = (key) => rem2(theme.typography.fontSize[key]);
373
+ const fw = (key) => `${theme.typography.fontWeight[key]}`;
374
+ const sizeStyles = getSizeStyles2(size, theme);
375
+ const radiusStyle = getRadiusStyle2(rounded, theme.radius);
376
+ const borderColor = errorText ? theme.colors.error : isFocused ? theme.colors.primary : theme.colors.borderMedium;
377
+ const inputStyle = {
378
+ borderWidth: 1,
379
+ ...sizeStyles,
380
+ ...radiusStyle,
381
+ borderColor,
382
+ backgroundColor: disabled || readonly ? theme.colors.surfaceAlt : theme.colors.background,
383
+ color: theme.colors.text,
384
+ fontFamily: theme.typography.fontFamily.base,
385
+ opacity: disabled ? 0.5 : 1,
386
+ ...block ? { width: "100%" } : void 0
387
+ };
388
+ const hasAffix = !!prefix || !!suffix;
389
+ const affixInputStyle = hasAffix ? {
390
+ ...inputStyle,
391
+ ...prefix ? {
392
+ borderTopLeftRadius: 0,
393
+ borderBottomLeftRadius: 0,
394
+ borderLeftWidth: 0
395
+ } : {},
396
+ ...suffix ? {
397
+ borderTopRightRadius: 0,
398
+ borderBottomRightRadius: 0,
399
+ borderRightWidth: 0
400
+ } : {},
401
+ flex: 1
402
+ } : inputStyle;
403
+ const affixStyle = {
404
+ justifyContent: "center",
405
+ paddingHorizontal: sp("md"),
406
+ backgroundColor: theme.colors.surface,
407
+ borderWidth: 1,
408
+ borderColor
409
+ };
410
+ function handleFocus() {
411
+ setIsFocused(true);
412
+ onFocus?.();
413
+ }
414
+ function handleBlur() {
415
+ setIsFocused(false);
416
+ onBlur?.();
417
+ }
418
+ const textInput = /* @__PURE__ */ jsx5(
419
+ TextInput,
420
+ {
421
+ style: hasAffix ? affixInputStyle : inputStyle,
422
+ value,
423
+ onChangeText,
424
+ onFocus: handleFocus,
425
+ onBlur: handleBlur,
426
+ placeholder,
427
+ placeholderTextColor: theme.colors.textSecondary,
428
+ editable: !disabled && !readonly,
429
+ secureTextEntry: type === "password",
430
+ keyboardType: getKeyboardType(type),
431
+ maxLength: maxLength2,
432
+ accessibilityLabel: accessibilityLabel ?? label,
433
+ accessibilityState: { disabled }
434
+ }
435
+ );
436
+ return /* @__PURE__ */ jsxs(View, { style: block ? { width: "100%" } : void 0, children: [
437
+ label ? /* @__PURE__ */ jsxs(
438
+ Text2,
439
+ {
440
+ style: {
441
+ marginBottom: sp("xs"),
442
+ color: theme.colors.text,
443
+ fontFamily: theme.typography.fontFamily.base,
444
+ fontSize: fs("sm"),
445
+ fontWeight: fw("medium")
446
+ },
447
+ children: [
448
+ label,
449
+ optionalLabel && !required2 ? /* @__PURE__ */ jsxs(
450
+ Text2,
451
+ {
452
+ style: {
453
+ color: theme.colors.textSecondary,
454
+ fontWeight: fw("normal")
455
+ },
456
+ children: [
457
+ " ",
458
+ optionalLabel
459
+ ]
460
+ }
461
+ ) : null
462
+ ]
463
+ }
464
+ ) : null,
465
+ helpText && !errorText ? /* @__PURE__ */ jsx5(
466
+ Text2,
467
+ {
468
+ style: {
469
+ marginBottom: sp("xs"),
470
+ color: theme.colors.textSecondary,
471
+ fontFamily: theme.typography.fontFamily.base,
472
+ fontSize: fs("sm")
473
+ },
474
+ children: helpText
475
+ }
476
+ ) : null,
477
+ errorText ? /* @__PURE__ */ jsx5(
478
+ Text2,
479
+ {
480
+ style: {
481
+ marginBottom: sp("xs"),
482
+ color: theme.colors.error,
483
+ fontFamily: theme.typography.fontFamily.base,
484
+ fontSize: fs("sm"),
485
+ fontWeight: fw("semibold")
486
+ },
487
+ accessibilityRole: "alert",
488
+ children: errorText
489
+ }
490
+ ) : null,
491
+ hasAffix ? /* @__PURE__ */ jsxs(View, { style: { flexDirection: "row", alignItems: "stretch" }, children: [
492
+ prefix ? /* @__PURE__ */ jsx5(
493
+ View,
494
+ {
495
+ style: [
496
+ affixStyle,
497
+ radiusStyle,
498
+ { borderTopRightRadius: 0, borderBottomRightRadius: 0 }
499
+ ],
500
+ accessibilityElementsHidden: true,
501
+ importantForAccessibility: "no-hide-descendants",
502
+ children: /* @__PURE__ */ jsx5(
503
+ Text2,
504
+ {
505
+ style: {
506
+ color: theme.colors.textSecondary,
507
+ fontFamily: theme.typography.fontFamily.base,
508
+ fontSize: fs("base")
509
+ },
510
+ children: prefix
511
+ }
512
+ )
513
+ }
514
+ ) : null,
515
+ textInput,
516
+ suffix ? /* @__PURE__ */ jsx5(
517
+ View,
518
+ {
519
+ style: [
520
+ affixStyle,
521
+ radiusStyle,
522
+ { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }
523
+ ],
524
+ accessibilityElementsHidden: true,
525
+ importantForAccessibility: "no-hide-descendants",
526
+ children: /* @__PURE__ */ jsx5(
527
+ Text2,
528
+ {
529
+ style: {
530
+ color: theme.colors.textSecondary,
531
+ fontFamily: theme.typography.fontFamily.base,
532
+ fontSize: fs("base")
533
+ },
534
+ children: suffix
535
+ }
536
+ )
537
+ }
538
+ ) : null
539
+ ] }) : textInput
540
+ ] });
541
+ }
542
+ function getSizeStyles2(size, theme) {
543
+ switch (size) {
544
+ case "sm":
545
+ return {
546
+ fontSize: rem2(theme.typography.fontSize.sm),
547
+ paddingVertical: rem2(theme.spacing.xs),
548
+ paddingHorizontal: rem2(theme.spacing.sm)
549
+ };
550
+ case "md":
551
+ return {
552
+ fontSize: rem2(theme.typography.fontSize.base),
553
+ paddingVertical: rem2(theme.spacing.sm),
554
+ paddingHorizontal: rem2(theme.spacing.md)
555
+ };
556
+ case "lg":
557
+ return {
558
+ fontSize: rem2(theme.typography.fontSize.lg),
559
+ paddingVertical: rem2(theme.spacing.md),
560
+ paddingHorizontal: rem2(theme.spacing.xl)
561
+ };
562
+ }
563
+ }
564
+ function getRadiusStyle2(rounded, radius) {
565
+ if (!rounded) return { borderRadius: rem2(radius.md) };
566
+ if (rounded === "full") return { borderRadius: 9999 };
567
+ if (rounded === "none") return { borderRadius: 0 };
568
+ return { borderRadius: rem2(radius[rounded]) };
569
+ }
570
+
571
+ // src/components/Toggle/Toggle.tsx
572
+ import { useEffect as useEffect2, useRef } from "react";
573
+ import {
574
+ View as View2,
575
+ Text as Text3,
576
+ Pressable as Pressable2,
577
+ Animated
578
+ } from "react-native";
579
+ import { TOGGLE_SIZES } from "@grundtone/core";
580
+ import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
581
+ function rem3(value) {
582
+ return parseFloat(value) * 16;
583
+ }
584
+ function GTToggle({
585
+ value = false,
586
+ onValueChange,
587
+ label,
588
+ size = "md",
589
+ disabled = false,
590
+ accessibilityLabel
591
+ }) {
592
+ const { theme } = useGrundtoneTheme();
593
+ const dims = TOGGLE_SIZES[size];
594
+ const thumbOffset = 2;
595
+ const thumbTravel = dims.width - dims.thumb - thumbOffset * 2;
596
+ const anim = useRef(new Animated.Value(value ? 1 : 0)).current;
597
+ useEffect2(() => {
598
+ Animated.timing(anim, {
599
+ toValue: value ? 1 : 0,
600
+ duration: 150,
601
+ useNativeDriver: false
602
+ }).start();
603
+ }, [value, anim]);
604
+ function handlePress() {
605
+ if (disabled) return;
606
+ onValueChange?.(!value);
607
+ }
608
+ const fieldStyle = {
609
+ flexDirection: "row",
610
+ alignItems: "center",
611
+ gap: rem3(theme.spacing.sm),
612
+ opacity: disabled ? 0.5 : 1
613
+ };
614
+ const labelStyle = {
615
+ color: theme.colors.text,
616
+ fontFamily: theme.typography.fontFamily.base,
617
+ fontSize: rem3(theme.typography.fontSize.sm),
618
+ fontWeight: theme.typography.fontWeight.medium
619
+ };
620
+ const trackBg = anim.interpolate({
621
+ inputRange: [0, 1],
622
+ outputRange: [theme.colors.borderMedium, theme.colors.primary]
623
+ });
624
+ const thumbTranslate = anim.interpolate({
625
+ inputRange: [0, 1],
626
+ outputRange: [0, thumbTravel]
627
+ });
628
+ return /* @__PURE__ */ jsxs2(View2, { style: fieldStyle, children: [
629
+ label ? /* @__PURE__ */ jsx6(Text3, { style: labelStyle, children: label }) : null,
630
+ /* @__PURE__ */ jsx6(
631
+ Pressable2,
632
+ {
633
+ onPress: handlePress,
634
+ disabled,
635
+ accessibilityRole: "switch",
636
+ accessibilityLabel: accessibilityLabel ?? label,
637
+ accessibilityState: { checked: value, disabled },
638
+ children: /* @__PURE__ */ jsx6(
639
+ Animated.View,
640
+ {
641
+ style: {
642
+ width: dims.width,
643
+ height: dims.height,
644
+ borderRadius: 9999,
645
+ backgroundColor: trackBg,
646
+ justifyContent: "center"
647
+ },
648
+ children: /* @__PURE__ */ jsx6(
649
+ Animated.View,
650
+ {
651
+ style: {
652
+ position: "absolute",
653
+ width: dims.thumb,
654
+ height: dims.thumb,
655
+ borderRadius: 9999,
656
+ backgroundColor: theme.colors.background,
657
+ left: thumbOffset,
658
+ transform: [{ translateX: thumbTranslate }]
659
+ }
660
+ }
661
+ )
662
+ }
663
+ )
664
+ }
665
+ )
666
+ ] });
667
+ }
668
+
669
+ // src/components/Alert/Alert.tsx
670
+ import {
671
+ View as View3,
672
+ Text as Text4,
673
+ Pressable as Pressable3
674
+ } from "react-native";
675
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
676
+ function rem4(value) {
677
+ return parseFloat(value) * 16;
678
+ }
679
+ function hexAlpha(hex, alpha) {
680
+ const r = parseInt(hex.slice(1, 3), 16);
681
+ const g = parseInt(hex.slice(3, 5), 16);
682
+ const b = parseInt(hex.slice(5, 7), 16);
683
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
684
+ }
685
+ function GTAlert({
686
+ variant,
687
+ heading,
688
+ icon,
689
+ dismissible = false,
690
+ onDismiss,
691
+ accessibilityLabel,
692
+ children,
693
+ footer
694
+ }) {
695
+ const { theme } = useGrundtoneTheme();
696
+ const borderColor = {
697
+ info: theme.colors.info,
698
+ success: theme.colors.success,
699
+ warning: theme.colors.warning,
700
+ error: theme.colors.error
701
+ }[variant];
702
+ const containerStyle = {
703
+ flexDirection: "row",
704
+ alignItems: "flex-start",
705
+ gap: rem4(theme.spacing.md),
706
+ padding: rem4(theme.spacing.md),
707
+ borderRadius: rem4(theme.radius.lg),
708
+ borderWidth: 1,
709
+ borderColor,
710
+ backgroundColor: hexAlpha(borderColor, 0.12)
711
+ };
712
+ const iconStyle = {
713
+ alignSelf: "center"
714
+ };
715
+ const contentStyle = {
716
+ flex: 1
717
+ };
718
+ const headingStyle = {
719
+ fontWeight: theme.typography.fontWeight.semibold,
720
+ fontFamily: theme.typography.fontFamily.base,
721
+ fontSize: rem4(theme.typography.fontSize.sm),
722
+ color: theme.colors.text,
723
+ marginBottom: rem4(theme.spacing.xs)
724
+ };
725
+ const footerStyle = {
726
+ marginTop: rem4(theme.spacing.lg),
727
+ paddingTop: rem4(theme.spacing.lg),
728
+ borderTopWidth: 1,
729
+ borderTopColor: theme.colors.text,
730
+ opacity: 0.3
731
+ };
732
+ const closeStyle = {
733
+ opacity: 0.7
734
+ };
735
+ return /* @__PURE__ */ jsxs3(
736
+ View3,
737
+ {
738
+ style: containerStyle,
739
+ accessibilityRole: "alert",
740
+ accessibilityLabel,
741
+ children: [
742
+ icon ? /* @__PURE__ */ jsx7(View3, { style: iconStyle, children: /* @__PURE__ */ jsx7(GTIcon, { name: icon, size: "lg", color: theme.colors.text }) }) : null,
743
+ /* @__PURE__ */ jsxs3(View3, { style: contentStyle, children: [
744
+ heading ? /* @__PURE__ */ jsx7(Text4, { style: headingStyle, children: heading }) : null,
745
+ /* @__PURE__ */ jsx7(View3, { children }),
746
+ footer ? /* @__PURE__ */ jsx7(View3, { style: footerStyle, children: footer }) : null
747
+ ] }),
748
+ dismissible ? /* @__PURE__ */ jsx7(
749
+ Pressable3,
750
+ {
751
+ style: closeStyle,
752
+ onPress: onDismiss,
753
+ accessibilityLabel: "Close",
754
+ accessibilityRole: "button",
755
+ children: /* @__PURE__ */ jsx7(GTIcon, { name: "close", size: "xs", color: theme.colors.text })
756
+ }
757
+ ) : null
758
+ ]
759
+ }
760
+ );
761
+ }
762
+
763
+ // src/useField.ts
764
+ import { useState as useState3, useCallback as useCallback2, useRef as useRef2 } from "react";
765
+ function useField(options = {}) {
766
+ const { validators = [], validateOn = "blur", initialValue = "" } = options;
767
+ const [value, setValueState] = useState3(initialValue);
768
+ const [touched, setTouched] = useState3(false);
769
+ const [validationMessage, setValidationMessage] = useState3(void 0);
770
+ const valueRef = useRef2(value);
771
+ const runValidation = useCallback2(
772
+ (val) => {
773
+ for (const validator of validators) {
774
+ const result = validator(val);
775
+ if (!result.isValid) {
776
+ setValidationMessage(result.message);
777
+ return result;
778
+ }
779
+ }
780
+ setValidationMessage(void 0);
781
+ return { isValid: true };
782
+ },
783
+ [validators]
784
+ );
785
+ const validate = useCallback2(() => {
786
+ return runValidation(valueRef.current);
787
+ }, [runValidation]);
788
+ const setValue = useCallback2(
789
+ (v) => {
790
+ setValueState(v);
791
+ valueRef.current = v;
792
+ if (validateOn === "input") {
793
+ setTouched(true);
794
+ runValidation(v);
795
+ }
796
+ },
797
+ [validateOn, runValidation]
798
+ );
799
+ const onBlur = useCallback2(() => {
800
+ if (validateOn === "blur") {
801
+ setTouched(true);
802
+ runValidation(valueRef.current);
803
+ }
804
+ }, [validateOn, runValidation]);
805
+ const reset = useCallback2(() => {
806
+ setValueState(initialValue);
807
+ valueRef.current = initialValue;
808
+ setTouched(false);
809
+ setValidationMessage(void 0);
810
+ }, [initialValue]);
811
+ return {
812
+ value,
813
+ setValue,
814
+ errorText: touched ? validationMessage : void 0,
815
+ touched,
816
+ isValid: validationMessage === void 0,
817
+ validate,
818
+ reset,
819
+ fieldProps: {
820
+ value,
821
+ onChangeText: setValue,
822
+ onBlur
823
+ }
824
+ };
825
+ }
826
+
827
+ // src/useFormValidation.ts
828
+ import { useMemo as useMemo2, useCallback as useCallback3 } from "react";
829
+ function useFormValidation(fields) {
830
+ const fieldValues = Object.values(fields);
831
+ const isValid = useMemo2(
832
+ () => fieldValues.every((f) => f.isValid),
833
+ [fieldValues]
834
+ );
835
+ const validateAll = useCallback3(() => {
836
+ let allValid = true;
837
+ for (const field of Object.values(fields)) {
838
+ const result = field.validate();
839
+ if (!result.isValid) allValid = false;
840
+ }
841
+ return allValid;
842
+ }, [fields]);
843
+ const resetAll = useCallback3(() => {
844
+ for (const field of Object.values(fields)) {
845
+ field.reset();
846
+ }
847
+ }, [fields]);
848
+ return { isValid, validateAll, resetAll };
849
+ }
850
+
851
+ // src/index.ts
852
+ import {
853
+ required,
854
+ email,
855
+ phone,
856
+ cpr,
857
+ cvr,
858
+ minLength,
859
+ maxLength,
860
+ pattern,
861
+ url,
862
+ composeValidators
863
+ } from "@grundtone/utils";
864
+
865
+ // src/branding.ts
866
+ import { defaultBranding } from "@grundtone/core";
867
+ import {
868
+ defaultBranding as defaultBranding2,
869
+ createBranding,
870
+ LOGO_VARIANT_SIZES
871
+ } from "@grundtone/core";
872
+ var defaultLogoSource = {
873
+ uri: defaultBranding.logos.primary
874
+ };
875
+ function getLogoSource(branding) {
876
+ if (!branding || branding.logos.primary === defaultBranding.logos.primary) {
877
+ return defaultLogoSource;
878
+ }
879
+ return { uri: branding.logos.primary };
880
+ }
881
+ export {
882
+ GTAlert,
883
+ GTButton,
884
+ GTIcon,
885
+ GTInput,
886
+ GTToggle,
887
+ GrundtoneThemeContext,
888
+ GrundtoneThemeProvider,
889
+ IconRegistryProvider,
890
+ LOGO_VARIANT_SIZES,
891
+ composeValidators,
892
+ cpr,
893
+ createBranding,
894
+ createTheme,
895
+ cvr,
896
+ defaultBranding2 as defaultBranding,
897
+ defaultLogoSource,
898
+ email,
899
+ getLogoSource,
900
+ maxLength,
901
+ minLength,
902
+ pattern,
903
+ phone,
904
+ required,
905
+ shadowToRN,
906
+ url,
907
+ useField,
908
+ useFormValidation,
909
+ useGrundtoneTheme,
910
+ useIconRegistry
911
+ };
912
+ //# sourceMappingURL=index.mjs.map