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