orn-ui 0.1.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 +107 -0
- package/bin/orn-ui.mjs +228 -0
- package/lib/commonjs/atoms/Avatar.js +42 -0
- package/lib/commonjs/atoms/Avatar.js.map +1 -0
- package/lib/commonjs/atoms/Badge.js +82 -0
- package/lib/commonjs/atoms/Badge.js.map +1 -0
- package/lib/commonjs/atoms/Button.js +222 -0
- package/lib/commonjs/atoms/Button.js.map +1 -0
- package/lib/commonjs/atoms/Card.js +33 -0
- package/lib/commonjs/atoms/Card.js.map +1 -0
- package/lib/commonjs/atoms/Checkbox.js +107 -0
- package/lib/commonjs/atoms/Checkbox.js.map +1 -0
- package/lib/commonjs/atoms/Divider.js +27 -0
- package/lib/commonjs/atoms/Divider.js.map +1 -0
- package/lib/commonjs/atoms/EmptyState.js +69 -0
- package/lib/commonjs/atoms/EmptyState.js.map +1 -0
- package/lib/commonjs/atoms/Fab.js +62 -0
- package/lib/commonjs/atoms/Fab.js.map +1 -0
- package/lib/commonjs/atoms/IconButton.js +60 -0
- package/lib/commonjs/atoms/IconButton.js.map +1 -0
- package/lib/commonjs/atoms/Image.js +113 -0
- package/lib/commonjs/atoms/Image.js.map +1 -0
- package/lib/commonjs/atoms/Input.js +173 -0
- package/lib/commonjs/atoms/Input.js.map +1 -0
- package/lib/commonjs/atoms/KeyValueRow.js +60 -0
- package/lib/commonjs/atoms/KeyValueRow.js.map +1 -0
- package/lib/commonjs/atoms/PressableScale.js +60 -0
- package/lib/commonjs/atoms/PressableScale.js.map +1 -0
- package/lib/commonjs/atoms/Spinner.js +156 -0
- package/lib/commonjs/atoms/Spinner.js.map +1 -0
- package/lib/commonjs/atoms/Text.js +63 -0
- package/lib/commonjs/atoms/Text.js.map +1 -0
- package/lib/commonjs/atoms/index.js +135 -0
- package/lib/commonjs/atoms/index.js.map +1 -0
- package/lib/commonjs/icons/Icon.js +40 -0
- package/lib/commonjs/icons/Icon.js.map +1 -0
- package/lib/commonjs/icons/defaultIcons.js +386 -0
- package/lib/commonjs/icons/defaultIcons.js.map +1 -0
- package/lib/commonjs/icons/index.js +20 -0
- package/lib/commonjs/icons/index.js.map +1 -0
- package/lib/commonjs/icons/types.js +6 -0
- package/lib/commonjs/icons/types.js.map +1 -0
- package/lib/commonjs/index.js +61 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/molecules/AvatarHeader.js +87 -0
- package/lib/commonjs/molecules/AvatarHeader.js.map +1 -0
- package/lib/commonjs/molecules/FormActions.js +47 -0
- package/lib/commonjs/molecules/FormActions.js.map +1 -0
- package/lib/commonjs/molecules/InfoRow.js +83 -0
- package/lib/commonjs/molecules/InfoRow.js.map +1 -0
- package/lib/commonjs/molecules/OptionCard.js +98 -0
- package/lib/commonjs/molecules/OptionCard.js.map +1 -0
- package/lib/commonjs/molecules/Stepper.js +104 -0
- package/lib/commonjs/molecules/Stepper.js.map +1 -0
- package/lib/commonjs/molecules/Steps.js +220 -0
- package/lib/commonjs/molecules/Steps.js.map +1 -0
- package/lib/commonjs/molecules/ThemeToggle.js +91 -0
- package/lib/commonjs/molecules/ThemeToggle.js.map +1 -0
- package/lib/commonjs/molecules/index.js +55 -0
- package/lib/commonjs/molecules/index.js.map +1 -0
- package/lib/commonjs/organisms/Alert.js +223 -0
- package/lib/commonjs/organisms/Alert.js.map +1 -0
- package/lib/commonjs/organisms/AlertProvider.js +109 -0
- package/lib/commonjs/organisms/AlertProvider.js.map +1 -0
- package/lib/commonjs/organisms/BottomSheet.js +154 -0
- package/lib/commonjs/organisms/BottomSheet.js.map +1 -0
- package/lib/commonjs/organisms/DateField.js +147 -0
- package/lib/commonjs/organisms/DateField.js.map +1 -0
- package/lib/commonjs/organisms/DatePicker.js +253 -0
- package/lib/commonjs/organisms/DatePicker.js.map +1 -0
- package/lib/commonjs/organisms/List.js +143 -0
- package/lib/commonjs/organisms/List.js.map +1 -0
- package/lib/commonjs/organisms/Modal.js +286 -0
- package/lib/commonjs/organisms/Modal.js.map +1 -0
- package/lib/commonjs/organisms/Screen.js +75 -0
- package/lib/commonjs/organisms/Screen.js.map +1 -0
- package/lib/commonjs/organisms/SearchList.js +180 -0
- package/lib/commonjs/organisms/SearchList.js.map +1 -0
- package/lib/commonjs/organisms/Select.js +178 -0
- package/lib/commonjs/organisms/Select.js.map +1 -0
- package/lib/commonjs/organisms/Toast.js +157 -0
- package/lib/commonjs/organisms/Toast.js.map +1 -0
- package/lib/commonjs/organisms/ToastProvider.js +131 -0
- package/lib/commonjs/organisms/ToastProvider.js.map +1 -0
- package/lib/commonjs/organisms/Wizard.js +116 -0
- package/lib/commonjs/organisms/Wizard.js.map +1 -0
- package/lib/commonjs/organisms/index.js +127 -0
- package/lib/commonjs/organisms/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/theme/UIProvider.js +110 -0
- package/lib/commonjs/theme/UIProvider.js.map +1 -0
- package/lib/commonjs/theme/createStyles.js +42 -0
- package/lib/commonjs/theme/createStyles.js.map +1 -0
- package/lib/commonjs/theme/createTheme.js +50 -0
- package/lib/commonjs/theme/createTheme.js.map +1 -0
- package/lib/commonjs/theme/index.js +151 -0
- package/lib/commonjs/theme/index.js.map +1 -0
- package/lib/commonjs/theme/insets.js +15 -0
- package/lib/commonjs/theme/insets.js.map +1 -0
- package/lib/commonjs/theme/labels.js +18 -0
- package/lib/commonjs/theme/labels.js.map +1 -0
- package/lib/commonjs/theme/palettes.js +42 -0
- package/lib/commonjs/theme/palettes.js.map +1 -0
- package/lib/commonjs/theme/tokens.js +120 -0
- package/lib/commonjs/theme/tokens.js.map +1 -0
- package/lib/commonjs/theme/types.js +6 -0
- package/lib/commonjs/theme/types.js.map +1 -0
- package/lib/module/atoms/Avatar.js +37 -0
- package/lib/module/atoms/Avatar.js.map +1 -0
- package/lib/module/atoms/Badge.js +77 -0
- package/lib/module/atoms/Badge.js.map +1 -0
- package/lib/module/atoms/Button.js +217 -0
- package/lib/module/atoms/Button.js.map +1 -0
- package/lib/module/atoms/Card.js +28 -0
- package/lib/module/atoms/Card.js.map +1 -0
- package/lib/module/atoms/Checkbox.js +102 -0
- package/lib/module/atoms/Checkbox.js.map +1 -0
- package/lib/module/atoms/Divider.js +22 -0
- package/lib/module/atoms/Divider.js.map +1 -0
- package/lib/module/atoms/EmptyState.js +64 -0
- package/lib/module/atoms/EmptyState.js.map +1 -0
- package/lib/module/atoms/Fab.js +57 -0
- package/lib/module/atoms/Fab.js.map +1 -0
- package/lib/module/atoms/IconButton.js +55 -0
- package/lib/module/atoms/IconButton.js.map +1 -0
- package/lib/module/atoms/Image.js +108 -0
- package/lib/module/atoms/Image.js.map +1 -0
- package/lib/module/atoms/Input.js +168 -0
- package/lib/module/atoms/Input.js.map +1 -0
- package/lib/module/atoms/KeyValueRow.js +55 -0
- package/lib/module/atoms/KeyValueRow.js.map +1 -0
- package/lib/module/atoms/PressableScale.js +55 -0
- package/lib/module/atoms/PressableScale.js.map +1 -0
- package/lib/module/atoms/Spinner.js +151 -0
- package/lib/module/atoms/Spinner.js.map +1 -0
- package/lib/module/atoms/Text.js +58 -0
- package/lib/module/atoms/Text.js.map +1 -0
- package/lib/module/atoms/index.js +18 -0
- package/lib/module/atoms/index.js.map +1 -0
- package/lib/module/icons/Icon.js +35 -0
- package/lib/module/icons/Icon.js.map +1 -0
- package/lib/module/icons/defaultIcons.js +381 -0
- package/lib/module/icons/defaultIcons.js.map +1 -0
- package/lib/module/icons/index.js +5 -0
- package/lib/module/icons/index.js.map +1 -0
- package/lib/module/icons/types.js +4 -0
- package/lib/module/icons/types.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/molecules/AvatarHeader.js +82 -0
- package/lib/module/molecules/AvatarHeader.js.map +1 -0
- package/lib/module/molecules/FormActions.js +42 -0
- package/lib/module/molecules/FormActions.js.map +1 -0
- package/lib/module/molecules/InfoRow.js +78 -0
- package/lib/module/molecules/InfoRow.js.map +1 -0
- package/lib/module/molecules/OptionCard.js +93 -0
- package/lib/module/molecules/OptionCard.js.map +1 -0
- package/lib/module/molecules/Stepper.js +99 -0
- package/lib/module/molecules/Stepper.js.map +1 -0
- package/lib/module/molecules/Steps.js +215 -0
- package/lib/module/molecules/Steps.js.map +1 -0
- package/lib/module/molecules/ThemeToggle.js +86 -0
- package/lib/module/molecules/ThemeToggle.js.map +1 -0
- package/lib/module/molecules/index.js +10 -0
- package/lib/module/molecules/index.js.map +1 -0
- package/lib/module/organisms/Alert.js +218 -0
- package/lib/module/organisms/Alert.js.map +1 -0
- package/lib/module/organisms/AlertProvider.js +103 -0
- package/lib/module/organisms/AlertProvider.js.map +1 -0
- package/lib/module/organisms/BottomSheet.js +149 -0
- package/lib/module/organisms/BottomSheet.js.map +1 -0
- package/lib/module/organisms/DateField.js +142 -0
- package/lib/module/organisms/DateField.js.map +1 -0
- package/lib/module/organisms/DatePicker.js +247 -0
- package/lib/module/organisms/DatePicker.js.map +1 -0
- package/lib/module/organisms/List.js +138 -0
- package/lib/module/organisms/List.js.map +1 -0
- package/lib/module/organisms/Modal.js +281 -0
- package/lib/module/organisms/Modal.js.map +1 -0
- package/lib/module/organisms/Screen.js +70 -0
- package/lib/module/organisms/Screen.js.map +1 -0
- package/lib/module/organisms/SearchList.js +175 -0
- package/lib/module/organisms/SearchList.js.map +1 -0
- package/lib/module/organisms/Select.js +173 -0
- package/lib/module/organisms/Select.js.map +1 -0
- package/lib/module/organisms/Toast.js +152 -0
- package/lib/module/organisms/Toast.js.map +1 -0
- package/lib/module/organisms/ToastProvider.js +125 -0
- package/lib/module/organisms/ToastProvider.js.map +1 -0
- package/lib/module/organisms/Wizard.js +111 -0
- package/lib/module/organisms/Wizard.js.map +1 -0
- package/lib/module/organisms/index.js +16 -0
- package/lib/module/organisms/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/theme/UIProvider.js +97 -0
- package/lib/module/theme/UIProvider.js.map +1 -0
- package/lib/module/theme/createStyles.js +38 -0
- package/lib/module/theme/createStyles.js.map +1 -0
- package/lib/module/theme/createTheme.js +45 -0
- package/lib/module/theme/createTheme.js.map +1 -0
- package/lib/module/theme/index.js +10 -0
- package/lib/module/theme/index.js.map +1 -0
- package/lib/module/theme/insets.js +11 -0
- package/lib/module/theme/insets.js.map +1 -0
- package/lib/module/theme/labels.js +14 -0
- package/lib/module/theme/labels.js.map +1 -0
- package/lib/module/theme/palettes.js +38 -0
- package/lib/module/theme/palettes.js.map +1 -0
- package/lib/module/theme/tokens.js +117 -0
- package/lib/module/theme/tokens.js.map +1 -0
- package/lib/module/theme/types.js +4 -0
- package/lib/module/theme/types.js.map +1 -0
- package/lib/typescript/atoms/Avatar.d.ts +15 -0
- package/lib/typescript/atoms/Avatar.d.ts.map +1 -0
- package/lib/typescript/atoms/Badge.d.ts +19 -0
- package/lib/typescript/atoms/Badge.d.ts.map +1 -0
- package/lib/typescript/atoms/Button.d.ts +30 -0
- package/lib/typescript/atoms/Button.d.ts.map +1 -0
- package/lib/typescript/atoms/Card.d.ts +8 -0
- package/lib/typescript/atoms/Card.d.ts.map +1 -0
- package/lib/typescript/atoms/Checkbox.d.ts +11 -0
- package/lib/typescript/atoms/Checkbox.d.ts.map +1 -0
- package/lib/typescript/atoms/Divider.d.ts +8 -0
- package/lib/typescript/atoms/Divider.d.ts.map +1 -0
- package/lib/typescript/atoms/EmptyState.d.ts +12 -0
- package/lib/typescript/atoms/EmptyState.d.ts.map +1 -0
- package/lib/typescript/atoms/Fab.d.ts +22 -0
- package/lib/typescript/atoms/Fab.d.ts.map +1 -0
- package/lib/typescript/atoms/IconButton.d.ts +23 -0
- package/lib/typescript/atoms/IconButton.d.ts.map +1 -0
- package/lib/typescript/atoms/Image.d.ts +45 -0
- package/lib/typescript/atoms/Image.d.ts.map +1 -0
- package/lib/typescript/atoms/Input.d.ts +24 -0
- package/lib/typescript/atoms/Input.d.ts.map +1 -0
- package/lib/typescript/atoms/KeyValueRow.d.ts +17 -0
- package/lib/typescript/atoms/KeyValueRow.d.ts.map +1 -0
- package/lib/typescript/atoms/PressableScale.d.ts +15 -0
- package/lib/typescript/atoms/PressableScale.d.ts.map +1 -0
- package/lib/typescript/atoms/Spinner.d.ts +20 -0
- package/lib/typescript/atoms/Spinner.d.ts.map +1 -0
- package/lib/typescript/atoms/Text.d.ts +13 -0
- package/lib/typescript/atoms/Text.d.ts.map +1 -0
- package/lib/typescript/atoms/index.d.ts +31 -0
- package/lib/typescript/atoms/index.d.ts.map +1 -0
- package/lib/typescript/icons/Icon.d.ts +18 -0
- package/lib/typescript/icons/Icon.d.ts.map +1 -0
- package/lib/typescript/icons/defaultIcons.d.ts +5 -0
- package/lib/typescript/icons/defaultIcons.d.ts.map +1 -0
- package/lib/typescript/icons/index.d.ts +5 -0
- package/lib/typescript/icons/index.d.ts.map +1 -0
- package/lib/typescript/icons/types.d.ts +10 -0
- package/lib/typescript/icons/types.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/molecules/AvatarHeader.d.ts +20 -0
- package/lib/typescript/molecules/AvatarHeader.d.ts.map +1 -0
- package/lib/typescript/molecules/FormActions.d.ts +21 -0
- package/lib/typescript/molecules/FormActions.d.ts.map +1 -0
- package/lib/typescript/molecules/InfoRow.d.ts +17 -0
- package/lib/typescript/molecules/InfoRow.d.ts.map +1 -0
- package/lib/typescript/molecules/OptionCard.d.ts +20 -0
- package/lib/typescript/molecules/OptionCard.d.ts.map +1 -0
- package/lib/typescript/molecules/Stepper.d.ts +15 -0
- package/lib/typescript/molecules/Stepper.d.ts.map +1 -0
- package/lib/typescript/molecules/Steps.d.ts +29 -0
- package/lib/typescript/molecules/Steps.d.ts.map +1 -0
- package/lib/typescript/molecules/ThemeToggle.d.ts +15 -0
- package/lib/typescript/molecules/ThemeToggle.d.ts.map +1 -0
- package/lib/typescript/molecules/index.d.ts +15 -0
- package/lib/typescript/molecules/index.d.ts.map +1 -0
- package/lib/typescript/organisms/Alert.d.ts +29 -0
- package/lib/typescript/organisms/Alert.d.ts.map +1 -0
- package/lib/typescript/organisms/AlertProvider.d.ts +32 -0
- package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -0
- package/lib/typescript/organisms/BottomSheet.d.ts +15 -0
- package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -0
- package/lib/typescript/organisms/DateField.d.ts +26 -0
- package/lib/typescript/organisms/DateField.d.ts.map +1 -0
- package/lib/typescript/organisms/DatePicker.d.ts +49 -0
- package/lib/typescript/organisms/DatePicker.d.ts.map +1 -0
- package/lib/typescript/organisms/List.d.ts +35 -0
- package/lib/typescript/organisms/List.d.ts.map +1 -0
- package/lib/typescript/organisms/Modal.d.ts +25 -0
- package/lib/typescript/organisms/Modal.d.ts.map +1 -0
- package/lib/typescript/organisms/Screen.d.ts +26 -0
- package/lib/typescript/organisms/Screen.d.ts.map +1 -0
- package/lib/typescript/organisms/SearchList.d.ts +40 -0
- package/lib/typescript/organisms/SearchList.d.ts.map +1 -0
- package/lib/typescript/organisms/Select.d.ts +25 -0
- package/lib/typescript/organisms/Select.d.ts.map +1 -0
- package/lib/typescript/organisms/Toast.d.ts +28 -0
- package/lib/typescript/organisms/Toast.d.ts.map +1 -0
- package/lib/typescript/organisms/ToastProvider.d.ts +36 -0
- package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -0
- package/lib/typescript/organisms/Wizard.d.ts +40 -0
- package/lib/typescript/organisms/Wizard.d.ts.map +1 -0
- package/lib/typescript/organisms/index.d.ts +27 -0
- package/lib/typescript/organisms/index.d.ts.map +1 -0
- package/lib/typescript/theme/UIProvider.d.ts +47 -0
- package/lib/typescript/theme/UIProvider.d.ts.map +1 -0
- package/lib/typescript/theme/createStyles.d.ts +21 -0
- package/lib/typescript/theme/createStyles.d.ts.map +1 -0
- package/lib/typescript/theme/createTheme.d.ts +9 -0
- package/lib/typescript/theme/createTheme.d.ts.map +1 -0
- package/lib/typescript/theme/index.d.ts +14 -0
- package/lib/typescript/theme/index.d.ts.map +1 -0
- package/lib/typescript/theme/insets.d.ts +9 -0
- package/lib/typescript/theme/insets.d.ts.map +1 -0
- package/lib/typescript/theme/labels.d.ts +13 -0
- package/lib/typescript/theme/labels.d.ts.map +1 -0
- package/lib/typescript/theme/palettes.d.ts +24 -0
- package/lib/typescript/theme/palettes.d.ts.map +1 -0
- package/lib/typescript/theme/tokens.d.ts +41 -0
- package/lib/typescript/theme/tokens.d.ts.map +1 -0
- package/lib/typescript/theme/types.d.ts +24 -0
- package/lib/typescript/theme/types.d.ts.map +1 -0
- package/package.json +311 -0
- package/registry/alert-provider.json +21 -0
- package/registry/alert.json +19 -0
- package/registry/avatar-header.json +18 -0
- package/registry/avatar.json +18 -0
- package/registry/badge.json +18 -0
- package/registry/bottom-sheet.json +18 -0
- package/registry/button.json +18 -0
- package/registry/card.json +18 -0
- package/registry/checkbox.json +18 -0
- package/registry/core.json +64 -0
- package/registry/date-field.json +24 -0
- package/registry/date-picker.json +23 -0
- package/registry/divider.json +18 -0
- package/registry/empty-state.json +18 -0
- package/registry/fab.json +19 -0
- package/registry/form-actions.json +19 -0
- package/registry/icon-button.json +19 -0
- package/registry/image.json +20 -0
- package/registry/index.json +465 -0
- package/registry/info-row.json +19 -0
- package/registry/input.json +18 -0
- package/registry/key-value-row.json +18 -0
- package/registry/list.json +20 -0
- package/registry/manifest.json +263 -0
- package/registry/modal.json +21 -0
- package/registry/option-card.json +18 -0
- package/registry/pressable-scale.json +18 -0
- package/registry/screen.json +18 -0
- package/registry/search-list.json +23 -0
- package/registry/select.json +18 -0
- package/registry/spinner.json +18 -0
- package/registry/stepper.json +18 -0
- package/registry/steps.json +18 -0
- package/registry/theme-toggle.json +18 -0
- package/registry/title.json +21 -0
- package/registry/toast-provider.json +20 -0
- package/registry/toast.json +18 -0
- package/registry/wizard.json +20 -0
- package/src/atoms/Avatar.tsx +40 -0
- package/src/atoms/Badge.tsx +55 -0
- package/src/atoms/Button.tsx +186 -0
- package/src/atoms/Card.tsx +27 -0
- package/src/atoms/Checkbox.tsx +75 -0
- package/src/atoms/Divider.tsx +14 -0
- package/src/atoms/EmptyState.tsx +60 -0
- package/src/atoms/Fab.tsx +63 -0
- package/src/atoms/IconButton.tsx +53 -0
- package/src/atoms/Image.tsx +137 -0
- package/src/atoms/Input.tsx +166 -0
- package/src/atoms/KeyValueRow.tsx +48 -0
- package/src/atoms/PressableScale.tsx +52 -0
- package/src/atoms/Spinner.tsx +127 -0
- package/src/atoms/Text.tsx +58 -0
- package/src/atoms/index.ts +30 -0
- package/src/icons/Icon.tsx +27 -0
- package/src/icons/defaultIcons.tsx +292 -0
- package/src/icons/index.ts +4 -0
- package/src/icons/types.ts +27 -0
- package/src/index.ts +5 -0
- package/src/molecules/AvatarHeader.tsx +69 -0
- package/src/molecules/FormActions.tsx +52 -0
- package/src/molecules/InfoRow.tsx +52 -0
- package/src/molecules/OptionCard.tsx +82 -0
- package/src/molecules/Stepper.tsx +76 -0
- package/src/molecules/Steps.tsx +198 -0
- package/src/molecules/ThemeToggle.tsx +67 -0
- package/src/molecules/index.ts +14 -0
- package/src/organisms/Alert.tsx +162 -0
- package/src/organisms/AlertProvider.tsx +125 -0
- package/src/organisms/BottomSheet.tsx +95 -0
- package/src/organisms/DateField.tsx +134 -0
- package/src/organisms/DatePicker.tsx +255 -0
- package/src/organisms/List.tsx +154 -0
- package/src/organisms/Modal.tsx +251 -0
- package/src/organisms/Screen.tsx +84 -0
- package/src/organisms/SearchList.tsx +186 -0
- package/src/organisms/Select.tsx +156 -0
- package/src/organisms/Toast.tsx +135 -0
- package/src/organisms/ToastProvider.tsx +166 -0
- package/src/organisms/Wizard.tsx +137 -0
- package/src/organisms/index.ts +26 -0
- package/src/theme/UIProvider.tsx +129 -0
- package/src/theme/createStyles.ts +40 -0
- package/src/theme/createTheme.ts +49 -0
- package/src/theme/index.ts +22 -0
- package/src/theme/insets.ts +9 -0
- package/src/theme/labels.ts +23 -0
- package/src/theme/palettes.ts +53 -0
- package/src/theme/tokens.ts +113 -0
- package/src/theme/types.ts +27 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Button",
|
|
3
|
+
"slug": "button",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"Button"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "atoms/Button.tsx",
|
|
12
|
+
"content": "import React, { memo, useRef } from 'react';\nimport {\n ActivityIndicator,\n Animated,\n Pressable,\n Text,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors, useTheme } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nconst AnimatedPressable = Animated.createAnimatedComponent(Pressable);\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive';\nexport type ButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps {\n title: string;\n onPress: (e: GestureResponderEvent) => void;\n variant?: ButtonVariant;\n size?: ButtonSize;\n loading?: boolean;\n disabled?: boolean;\n style?: StyleProp<ViewStyle>;\n textStyle?: StyleProp<TextStyle>;\n leftIcon?: React.ReactNode;\n rightIcon?: React.ReactNode;\n leftIconName?: IconName;\n rightIconName?: IconName;\n iconColor?: string;\n testID?: string;\n accessibilityLabel?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n base: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 0,\n },\n sm: { paddingVertical: theme.tokens.spacing.sm, paddingHorizontal: theme.tokens.spacing.md },\n md: { paddingVertical: theme.tokens.spacing.lg - 1, paddingHorizontal: theme.tokens.spacing.xl },\n lg: { paddingVertical: theme.tokens.spacing.lg + 2, paddingHorizontal: theme.tokens.spacing.xxl },\n link: { paddingVertical: theme.tokens.spacing.xs, paddingHorizontal: 0, justifyContent: 'flex-start' },\n outline: { borderWidth: 1 },\n filledShadow: { ...theme.tokens.shadow.md, shadowColor: '#000' },\n text: { fontWeight: theme.tokens.fontWeight.semibold, textAlign: 'center' },\n textSm: { fontSize: theme.tokens.fontSize.sm },\n textMd: { fontSize: theme.tokens.fontSize.lg },\n textLg: { fontSize: theme.tokens.fontSize.xl },\n underline: { textDecorationLine: 'underline' },\n iconLeft: { marginRight: theme.tokens.spacing.sm },\n iconRight: { marginLeft: theme.tokens.spacing.sm },\n}));\n\nfunction resolveColors(variant: ButtonVariant, disabled: boolean, colors: ReturnType<typeof useColors>) {\n if (disabled) {\n return { background: colors.inputBackground, text: colors.textLight, border: 'transparent' };\n }\n switch (variant) {\n case 'outline':\n return { background: 'transparent', text: colors.primary, border: colors.primary };\n case 'ghost':\n return { background: 'transparent', text: colors.primary, border: 'transparent' };\n case 'link':\n return { background: 'transparent', text: colors.primary, border: 'transparent' };\n case 'secondary':\n return { background: colors.secondary, text: colors.white, border: 'transparent' };\n case 'destructive':\n return { background: colors.error, text: colors.white, border: 'transparent' };\n case 'primary':\n default:\n return { background: colors.primary, text: colors.white, border: 'transparent' };\n }\n}\n\n/**\n * Button\n * Único componente de botón de la librería. `variant` decide el estilo\n * semántico, `size` la densidad. El feedback de \"presionado\" anima solo\n * `transform` (Animated.spring, useNativeDriver) — nunca opacity ni layout.\n */\nexport const Button = memo(\n ({\n title,\n onPress,\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n style,\n textStyle,\n leftIcon,\n rightIcon,\n leftIconName,\n rightIconName,\n iconColor,\n testID,\n accessibilityLabel,\n }: ButtonProps) => {\n const styles = useStyles();\n const colors = useColors();\n const theme = useTheme();\n const allowFontScaling = useAllowFontScaling();\n const scale = useRef(new Animated.Value(1)).current;\n const isDisabled = disabled || loading;\n const isFilled = variant === 'primary' || variant === 'secondary' || variant === 'destructive';\n\n const resolved = resolveColors(variant, disabled, colors);\n const effectiveIconColor = iconColor ?? resolved.text;\n\n const animateTo = (value: number) => {\n Animated.spring(scale, { toValue: value, useNativeDriver: true, speed: 50, bounciness: 6 }).start();\n };\n\n const containerStyle: StyleProp<ViewStyle> = [\n styles.base,\n styles[size],\n variant === 'link' && styles.link,\n variant === 'outline' && [styles.outline, { borderColor: resolved.border }],\n { backgroundColor: resolved.background },\n isFilled && !disabled && styles.filledShadow,\n { opacity: isDisabled ? 0.7 : 1 },\n style,\n ];\n\n const labelStyle: StyleProp<TextStyle> = [\n styles.text,\n size === 'sm' ? styles.textSm : size === 'lg' ? styles.textLg : styles.textMd,\n { color: resolved.text },\n variant === 'link' && styles.underline,\n textStyle,\n ];\n\n const iconSize = size === 'sm' ? 16 : size === 'lg' ? 22 : 20;\n\n return (\n // El Pressable ES el contenedor animado, no un wrapper alrededor de uno.\n // Con un Animated.View interno, un `style` de layout (flex, width,\n // margin) caía en el hijo mientras el Pressable — que es quien ocupa\n // lugar en el layout del padre — quedaba sin él: el ancho colapsaba y\n // el label desaparecía (visible en los botones en fila del Alert).\n <AnimatedPressable\n onPress={onPress}\n onPressIn={() => {\n if (!isDisabled) animateTo(0.95);\n }}\n onPressOut={() => {\n if (!isDisabled) animateTo(1);\n }}\n disabled={isDisabled}\n testID={testID}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel ?? title}\n accessibilityState={{ disabled: isDisabled, busy: loading }}\n style={[containerStyle, { transform: [{ scale }] }]}\n >\n {loading ? (\n <ActivityIndicator size=\"small\" color={resolved.text} />\n ) : (\n <>\n {leftIcon}\n {!leftIcon && leftIconName && (\n <Icon name={leftIconName} size={iconSize} color={effectiveIconColor} style={styles.iconLeft} />\n )}\n <Text allowFontScaling={allowFontScaling} style={labelStyle}>\n {title}\n </Text>\n {rightIcon}\n {!rightIcon && rightIconName && (\n <Icon name={rightIconName} size={iconSize} color={effectiveIconColor} style={styles.iconRight} />\n )}\n </>\n )}\n </AnimatedPressable>\n );\n }\n);\nButton.displayName = 'Button';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Card",
|
|
3
|
+
"slug": "card",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"Card"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "atoms/Card.tsx",
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { View, type ViewProps, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\n\nexport interface CardProps extends ViewProps {\n children: React.ReactNode;\n style?: ViewStyle;\n}\n\nconst useStyles = createStyles((theme) => ({\n card: {\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.xl,\n padding: theme.tokens.spacing.lg,\n ...theme.tokens.shadow.sm,\n },\n}));\n\nexport const Card = memo(({ children, style, ...props }: CardProps) => {\n const styles = useStyles();\n return (\n <View style={[styles.card, style]} {...props}>\n {children}\n </View>\n );\n});\nCard.displayName = 'Card';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Checkbox",
|
|
3
|
+
"slug": "checkbox",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"Checkbox"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "atoms/Checkbox.tsx",
|
|
12
|
+
"content": "import React, { memo, useEffect, useRef } from 'react';\nimport { Animated, Text, TouchableOpacity } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\n\nexport interface CheckboxProps {\n value: boolean;\n onValueChange: (value: boolean) => void;\n label?: string;\n color?: string;\n disabled?: boolean;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flexDirection: 'row', alignItems: 'center', marginBottom: theme.tokens.spacing.sm },\n box: {\n width: 24,\n height: 24,\n borderRadius: theme.tokens.radius.sm,\n borderWidth: 2,\n justifyContent: 'center',\n alignItems: 'center',\n marginRight: theme.tokens.spacing.sm + 2,\n },\n label: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },\n}));\n\nexport const Checkbox = memo(({ value, onValueChange, label, color, disabled, testID }: CheckboxProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const activeColor = color || colors.primary;\n const progress = useRef(new Animated.Value(value ? 1 : 0)).current;\n const checkScale = useRef(new Animated.Value(value ? 1 : 0)).current;\n\n useEffect(() => {\n Animated.timing(progress, { toValue: value ? 1 : 0, duration: 150, useNativeDriver: false }).start();\n Animated.spring(checkScale, {\n toValue: value ? 1 : 0,\n useNativeDriver: true,\n speed: 40,\n bounciness: 10,\n }).start();\n }, [value]);\n\n const backgroundColor = progress.interpolate({ inputRange: [0, 1], outputRange: [colors.surface, activeColor] });\n const borderColor = progress.interpolate({ inputRange: [0, 1], outputRange: [colors.border, activeColor] });\n\n return (\n <TouchableOpacity\n style={styles.container}\n activeOpacity={0.7}\n disabled={disabled}\n onPress={() => onValueChange(!value)}\n accessibilityRole=\"checkbox\"\n accessibilityState={{ checked: value, disabled: !!disabled }}\n accessibilityLabel={label}\n testID={testID}\n >\n <Animated.View style={[styles.box, { backgroundColor, borderColor, opacity: disabled ? 0.5 : 1 }]}>\n <Animated.View style={{ transform: [{ scale: checkScale }] }}>\n <Icon name=\"check\" size={16} color={colors.white} />\n </Animated.View>\n </Animated.View>\n {label && (\n <Text allowFontScaling={allowFontScaling} style={styles.label}>\n {label}\n </Text>\n )}\n </TouchableOpacity>\n );\n});\nCheckbox.displayName = 'Checkbox';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "core",
|
|
3
|
+
"type": "core",
|
|
4
|
+
"description": "theme (UIProvider, tokens, palettes) + icons — requerido por todos los componentes.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "icons/Icon.tsx",
|
|
8
|
+
"content": "import React, { memo } from 'react';\nimport { View, type StyleProp, type ViewStyle } from 'react-native';\nimport { useColors, useIcons } from '../theme/UIProvider';\nimport type { IconName } from './types';\n\nexport interface IconProps {\n name: IconName;\n size?: number;\n color?: string;\n style?: StyleProp<ViewStyle>;\n}\n\n/**\n * Icon\n * Resuelve el glifo vía el renderer activo (UIProvider.icons, default zero-dep\n * si no se configuró ninguno). Uso interno de la librería — los componentes\n * públicos exponen `icon?: ReactNode` además de esto, para que el consumidor\n * pueda inyectar cualquier ícono sin pasar por el sistema de nombres.\n */\nexport const Icon = memo(({ name, size = 20, color, style }: IconProps) => {\n const colors = useColors();\n const render = useIcons();\n const glyph = render(name, { size, color: color ?? colors.text });\n if (!style) return <>{glyph}</>;\n return <View style={style}>{glyph}</View>;\n});\nIcon.displayName = 'Icon';\n"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"path": "icons/defaultIcons.tsx",
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { Text, View } from 'react-native';\nimport type { IconName, IconRenderProps } from './types';\n\n/**\n * Glifos default dibujados con primitivas de RN (View + border + transform).\n * Cero dependencias, cero fuentes, cero carga asíncrona. Cubren únicamente\n * lo que la librería necesita internamente (checkbox, chevrons de Select,\n * toggle de password en Input, etc). Para íconos de producto, el consumidor\n * inyecta su propio set vía <UIProvider icons={...}> o la prop `icon`.\n */\n\nconst Chevron = memo(({ size, color, direction }: IconRenderProps & { direction: 'up' | 'down' | 'left' | 'right' }) => {\n const strokeWidth = Math.max(1.5, size / 12);\n const arm = size * 0.32;\n const rotation: Record<typeof direction, number> = {\n down: 45,\n up: 225,\n left: 135,\n right: -45,\n };\n return (\n <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>\n <View\n style={{\n width: arm,\n height: arm,\n borderRightWidth: strokeWidth,\n borderBottomWidth: strokeWidth,\n borderColor: color,\n transform: [{ rotate: `${rotation[direction]}deg` }, { translateY: -arm * 0.15 }],\n }}\n />\n </View>\n );\n});\nChevron.displayName = 'Chevron';\n\nconst Check = memo(({ size, color }: IconRenderProps) => {\n const strokeWidth = Math.max(1.5, size / 10);\n return (\n <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>\n <View\n style={{\n width: size * 0.5,\n height: size * 0.28,\n borderLeftWidth: strokeWidth,\n borderBottomWidth: strokeWidth,\n borderColor: color,\n transform: [{ rotate: '-45deg' }, { translateY: -size * 0.06 }],\n }}\n />\n </View>\n );\n});\nCheck.displayName = 'Check';\n\nconst Close = memo(({ size, color }: IconRenderProps) => {\n const strokeWidth = Math.max(1.5, size / 10);\n const line = size * 0.62;\n return (\n <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>\n <View\n style={{\n position: 'absolute',\n width: line,\n height: strokeWidth,\n backgroundColor: color,\n borderRadius: strokeWidth / 2,\n transform: [{ rotate: '45deg' }],\n }}\n />\n <View\n style={{\n position: 'absolute',\n width: line,\n height: strokeWidth,\n backgroundColor: color,\n borderRadius: strokeWidth / 2,\n transform: [{ rotate: '-45deg' }],\n }}\n />\n </View>\n );\n});\nClose.displayName = 'Close';\n\nconst PlusMinus = memo(({ size, color, minusOnly }: IconRenderProps & { minusOnly?: boolean }) => {\n const strokeWidth = Math.max(1.5, size / 10);\n const line = size * 0.56;\n return (\n <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>\n <View\n style={{\n position: 'absolute',\n width: line,\n height: strokeWidth,\n backgroundColor: color,\n borderRadius: strokeWidth / 2,\n }}\n />\n {!minusOnly && (\n <View\n style={{\n position: 'absolute',\n width: strokeWidth,\n height: line,\n backgroundColor: color,\n borderRadius: strokeWidth / 2,\n }}\n />\n )}\n </View>\n );\n});\nPlusMinus.displayName = 'PlusMinus';\n\n/**\n * Círculo con un glifo tipográfico adentro (!, i, ?).\n *\n * Usa <Text> con la fuente del sistema en vez de componer el glifo con\n * Views: sigue siendo cero-dependencias (no carga ninguna fuente), y \"i\" vs\n * \"!\" vs \"?\" quedan inequívocos. Dibujarlos con barras y puntos daba un\n * \"info\" idéntico a un \"!\" invertido, que es exactamente lo que un ícono de\n * información no debe comunicar.\n */\nconst Circle = memo(\n ({ size, color, filled, label }: IconRenderProps & { filled?: boolean; label?: string }) => {\n const strokeWidth = Math.max(1.5, size / 12);\n return (\n <View\n style={{\n width: size,\n height: size,\n borderRadius: size / 2,\n borderWidth: filled ? 0 : strokeWidth,\n borderColor: color,\n backgroundColor: filled ? color : 'transparent',\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n {!!label && (\n <Text\n allowFontScaling={false}\n style={{\n color: filled ? '#fff' : color,\n fontSize: size * 0.62,\n lineHeight: size * 0.74,\n fontWeight: '700',\n textAlign: 'center',\n includeFontPadding: false,\n }}\n >\n {label}\n </Text>\n )}\n </View>\n );\n }\n);\nCircle.displayName = 'Circle';\n\nconst Eye = memo(({ size, color, off }: IconRenderProps & { off?: boolean }) => {\n const strokeWidth = Math.max(1.5, size / 12);\n return (\n <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>\n <View\n style={{\n width: size * 0.8,\n height: size * 0.8,\n borderRadius: size * 0.4,\n borderWidth: strokeWidth,\n borderColor: color,\n overflow: 'hidden',\n alignItems: 'center',\n justifyContent: 'center',\n transform: [{ scaleY: 0.55 }],\n }}\n >\n <View\n style={{\n width: size * 0.26,\n height: size * 0.26,\n borderRadius: size * 0.13,\n backgroundColor: color,\n transform: [{ scaleY: 1 / 0.55 }],\n }}\n />\n </View>\n {off && (\n <View\n style={{\n position: 'absolute',\n width: size * 1.05,\n height: strokeWidth,\n backgroundColor: color,\n borderRadius: strokeWidth / 2,\n transform: [{ rotate: '-45deg' }],\n }}\n />\n )}\n </View>\n );\n});\nEye.displayName = 'Eye';\n\nconst Search = memo(({ size, color }: IconRenderProps) => {\n const strokeWidth = Math.max(1.5, size / 9);\n // Lente grande (72% del alto) arriba-izquierda y mango corto saliendo de su\n // borde inferior-derecho sobre la diagonal de 45°. Las proporciones importan\n // a tamaños chicos (16-20px): con un lente chico y mango largo el glifo se\n // lee como una coma, no como una lupa.\n const lensD = size * 0.72;\n const lensR = lensD / 2;\n const handleLen = size * 0.34;\n const diag = Math.SQRT1_2; // cos(45°) === sin(45°)\n // El mango se centra sobre la diagonal, arrancando justo en el borde del\n // lente: distancia = radio + mitad del mango, menos un solapamiento de\n // medio trazo para que no quede un hueco entre borde y mango.\n const dist = lensR + handleLen / 2 - strokeWidth / 2;\n const cx = lensR + dist * diag;\n const cy = lensR + dist * diag;\n\n return (\n <View style={{ width: size, height: size }}>\n <View\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n width: lensD,\n height: lensD,\n borderRadius: lensR,\n borderWidth: strokeWidth,\n borderColor: color,\n }}\n />\n <View\n style={{\n position: 'absolute',\n left: cx - strokeWidth / 2,\n top: cy - handleLen / 2,\n width: strokeWidth,\n height: handleLen,\n backgroundColor: color,\n borderRadius: strokeWidth / 2,\n transform: [{ rotate: '-45deg' }],\n }}\n />\n </View>\n );\n});\nSearch.displayName = 'Search';\n\n/** Renderer default. Se usa cuando <UIProvider icons={...}> no pasa uno propio. */\nexport function renderDefaultIcon(name: IconName, props: IconRenderProps): React.ReactNode {\n switch (name) {\n case 'check':\n return <Check {...props} />;\n case 'close':\n return <Close {...props} />;\n case 'chevron-down':\n return <Chevron {...props} direction=\"down\" />;\n case 'chevron-up':\n return <Chevron {...props} direction=\"up\" />;\n case 'chevron-left':\n return <Chevron {...props} direction=\"left\" />;\n case 'chevron-right':\n return <Chevron {...props} direction=\"right\" />;\n case 'eye':\n return <Eye {...props} />;\n case 'eye-off':\n return <Eye {...props} off />;\n case 'search':\n return <Search {...props} />;\n case 'plus':\n return <PlusMinus {...props} />;\n case 'minus':\n return <PlusMinus {...props} minusOnly />;\n case 'alert':\n return <Circle {...props} filled label=\"!\" />;\n case 'info':\n return <Circle {...props} label=\"i\" />;\n case 'warning':\n return <Circle {...props} filled label=\"!\" />;\n case 'question':\n return <Circle {...props} label=\"?\" />;\n default:\n return null;\n }\n}\n"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "icons/index.ts",
|
|
16
|
+
"content": "export { Icon } from './Icon';\nexport type { IconProps } from './Icon';\nexport { renderDefaultIcon } from './defaultIcons';\nexport type { IconName, IconRenderer, IconRenderProps } from './types';\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "icons/types.ts",
|
|
20
|
+
"content": "import type { ReactNode } from 'react';\n\n/** Glifos que los componentes de la librería necesitan internamente. */\nexport type IconName =\n | 'check'\n | 'close'\n | 'chevron-down'\n | 'chevron-up'\n | 'chevron-left'\n | 'chevron-right'\n | 'eye'\n | 'eye-off'\n | 'search'\n | 'plus'\n | 'minus'\n | 'alert'\n | 'info'\n | 'warning'\n | 'question';\n\nexport interface IconRenderProps {\n size: number;\n color: string;\n}\n\n/** Renderer inyectable: reemplaza los glifos default por los de cualquier set de iconos. */\nexport type IconRenderer = (name: IconName, props: IconRenderProps) => ReactNode;\n"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "theme/UIProvider.tsx",
|
|
24
|
+
"content": "import React, { createContext, useContext, useMemo, useState, useCallback } from 'react';\nimport { Appearance, useColorScheme } from 'react-native';\nimport { defaultTheme } from './createTheme';\nimport { defaultLabels, type Labels } from './labels';\nimport { zeroInsets, type EdgeInsets } from './insets';\nimport type { Theme, ThemePair } from './types';\nimport { renderDefaultIcon } from '../icons/defaultIcons';\nimport type { IconRenderer } from '../icons/types';\n\nexport type ThemeMode = 'system' | 'light' | 'dark';\n\ninterface UIContextValue {\n theme: Theme;\n mode: ThemeMode;\n setMode: (mode: ThemeMode) => void;\n icons: IconRenderer;\n insets: EdgeInsets;\n labels: Labels;\n allowFontScaling: boolean;\n}\n\nconst UIContext = createContext<UIContextValue | undefined>(undefined);\n\nexport interface UIProviderProps {\n children: React.ReactNode;\n /** Par light/dark. Por defecto el theme de marca de la librería (créalo con createTheme()). */\n theme?: ThemePair;\n /** Modo inicial (no controlado) si no se pasa `mode`. @default 'system' */\n defaultMode?: ThemeMode;\n /** Modo controlado. Si se pasa junto a onModeChange, el provider no gestiona estado propio. */\n mode?: ThemeMode;\n onModeChange?: (mode: ThemeMode) => void;\n /** Renderer de íconos. Por defecto usa los glifos zero-dep incluidos. */\n icons?: IconRenderer;\n /** Insets de safe area (de useSafeAreaInsets()). Por defecto {0,0,0,0}. */\n insets?: EdgeInsets;\n labels?: Partial<Labels>;\n /** @default false, para paridad con el resto de la librería (accesibilidad primero). */\n allowFontScaling?: boolean;\n}\n\n/**\n * UIProvider\n * Único punto de entrada de la librería: resuelve el theme activo (sistema u\n * override manual), e inyecta íconos, insets y textos por defecto a todo el\n * árbol. No persiste el modo — si el consumidor quiere recordar la preferencia\n * entre sesiones, controla `mode`/`onModeChange` y persiste donde prefiera.\n */\nexport function UIProvider({\n children,\n theme = defaultTheme,\n defaultMode = 'system',\n mode: controlledMode,\n onModeChange,\n icons = renderDefaultIcon,\n insets = zeroInsets,\n labels: labelOverrides,\n allowFontScaling = false,\n}: UIProviderProps) {\n const systemScheme = useColorScheme();\n const [uncontrolledMode, setUncontrolledMode] = useState<ThemeMode>(defaultMode);\n const isControlled = controlledMode !== undefined;\n const mode = isControlled ? controlledMode : uncontrolledMode;\n\n const setMode = useCallback(\n (next: ThemeMode) => {\n if (!isControlled) setUncontrolledMode(next);\n onModeChange?.(next);\n },\n [isControlled, onModeChange]\n );\n\n const scheme: 'light' | 'dark' =\n mode === 'system' ? (systemScheme === 'dark' ? 'dark' : 'light') : mode;\n\n const resolvedTheme = theme[scheme];\n\n const labels = useMemo(\n () => (labelOverrides ? { ...defaultLabels, ...labelOverrides } : defaultLabels),\n [labelOverrides]\n );\n\n const value = useMemo<UIContextValue>(\n () => ({ theme: resolvedTheme, mode, setMode, icons, insets, labels, allowFontScaling }),\n [resolvedTheme, mode, setMode, icons, insets, labels, allowFontScaling]\n );\n\n return <UIContext.Provider value={value}>{children}</UIContext.Provider>;\n}\n\nfunction useUIContext(): UIContextValue {\n const ctx = useContext(UIContext);\n if (!ctx) {\n throw new Error('orn-ui: this hook must be used within a <UIProvider>.');\n }\n return ctx;\n}\n\nexport function useTheme(): Theme {\n return useUIContext().theme;\n}\n\nexport function useColors(): Theme['colors'] {\n return useUIContext().theme.colors;\n}\n\nexport function useThemeMode() {\n const { mode, setMode } = useUIContext();\n return { mode, setMode };\n}\n\nexport function useIcons(): IconRenderer {\n return useUIContext().icons;\n}\n\nexport function useInsets(): EdgeInsets {\n return useUIContext().insets;\n}\n\nexport function useLabels(): Labels {\n return useUIContext().labels;\n}\n\nexport function useAllowFontScaling(): boolean {\n return useUIContext().allowFontScaling;\n}\n\n/** Re-exportado para que tests/consumidores puedan simular cambios de sistema sin RN real. */\nexport const __testUtils = { Appearance };\n"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "theme/createStyles.ts",
|
|
28
|
+
"content": "import { StyleSheet } from 'react-native';\nimport { useTheme } from './UIProvider';\nimport type { Theme } from './types';\n\n/**\n * createStyles(factory)\n * Devuelve un hook useStyles() que memoiza el StyleSheet resultante por\n * theme (WeakMap<Theme, Styles>), no por instancia de componente. Todas las\n * filas de una lista comparten el mismo objeto de estilos: sin esto, N filas\n * ⇒ N StyleSheet.create() por render, que es lo que hacía cada componente\n * de la app original con `useMemo(() => createStyles(colors), [colors])`.\n *\n * La cache vive en el closure del factory (una WeakMap por llamada a\n * createStyles, es decir, una por componente), así que temas de distintos\n * <UIProvider> en el mismo árbol no colisionan entre sí.\n *\n * El tipo de `factory` sigue el patrón oficial de RN para StyleSheet\n * tipados (`T extends StyleSheet.NamedStyles<T> | StyleSheet.NamedStyles<any>`):\n * es lo que le da contexto a TS para inferir literales ('row', 'center', ...)\n * en vez de ensancharlos a `string` dentro del objeto devuelto.\n */\nexport function createStyles<T extends StyleSheet.NamedStyles<T> | StyleSheet.NamedStyles<any>>(\n factory: (theme: Theme) => T | StyleSheet.NamedStyles<T>\n): () => T {\n const cache = new WeakMap<Theme, T>();\n\n return function useStyles(): T {\n const theme = useTheme();\n let styles = cache.get(theme);\n if (!styles) {\n // El cast es seguro: StyleSheet.create<T>(...) con el mismo T del\n // factory siempre devuelve T; TS no puede probarlo por sí solo dentro\n // de un cuerpo genérico (T podría \"instanciarse\" con un subtipo no\n // relacionado desde su punto de vista, aunque en runtime es el mismo).\n styles = StyleSheet.create(factory(theme)) as T;\n cache.set(theme, styles);\n }\n return styles;\n };\n}\n"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "theme/createTheme.ts",
|
|
32
|
+
"content": "import { lightPalette, darkPalette } from './palettes';\nimport { tokens as defaultTokens } from './tokens';\nimport type { Theme, ThemeOverrides, ThemePair } from './types';\n\nfunction mergeDeep<T extends Record<string, any>>(base: T, patch?: Partial<T>): T {\n if (!patch) return base;\n const out: Record<string, any> = { ...base };\n for (const key of Object.keys(patch)) {\n const patchVal = (patch as Record<string, any>)[key];\n const baseVal = (base as Record<string, any>)[key];\n if (\n patchVal &&\n baseVal &&\n typeof patchVal === 'object' &&\n typeof baseVal === 'object' &&\n !Array.isArray(patchVal)\n ) {\n out[key] = mergeDeep(baseVal, patchVal);\n } else if (patchVal !== undefined) {\n out[key] = patchVal;\n }\n }\n return out as T;\n}\n\n/**\n * Construye el par de themes light/dark que consume <UIProvider theme={...}>.\n * Nunca muta los defaults: cada llamada produce objetos nuevos, seguros de\n * compartir entre providers distintos (tests, multi-brand en el mismo árbol).\n */\nexport function createTheme(overrides?: ThemeOverrides): ThemePair {\n const tokens = mergeDeep(defaultTokens, overrides?.tokens as any);\n\n const light: Theme = {\n colors: mergeDeep(lightPalette, overrides?.colors?.light),\n tokens,\n scheme: 'light',\n };\n\n const dark: Theme = {\n colors: mergeDeep(darkPalette, overrides?.colors?.dark),\n tokens,\n scheme: 'dark',\n };\n\n return { light, dark };\n}\n\nexport const defaultTheme: ThemePair = createTheme();\n"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "theme/index.ts",
|
|
36
|
+
"content": "export { spacing, radius, fontSize, fontWeight, lineHeight, duration, shadow, tokens } from './tokens';\nexport type { Tokens } from './tokens';\nexport { lightPalette, darkPalette } from './palettes';\nexport type { ThemeColors } from './palettes';\nexport { createTheme, defaultTheme } from './createTheme';\nexport type { Theme, ThemePair, ThemeOverrides, DeepPartial } from './types';\nexport { defaultLabels } from './labels';\nexport type { Labels } from './labels';\nexport { zeroInsets } from './insets';\nexport type { EdgeInsets } from './insets';\nexport { createStyles } from './createStyles';\nexport {\n UIProvider,\n useTheme,\n useColors,\n useThemeMode,\n useIcons,\n useInsets,\n useLabels,\n useAllowFontScaling,\n} from './UIProvider';\nexport type { UIProviderProps, ThemeMode } from './UIProvider';\n"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "theme/insets.ts",
|
|
40
|
+
"content": "/** Estructuralmente igual a EdgeInsets de react-native-safe-area-context, sin depender del paquete. */\nexport interface EdgeInsets {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nexport const zeroInsets: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };\n"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"path": "theme/labels.ts",
|
|
44
|
+
"content": "export interface Labels {\n close: string;\n cancel: string;\n confirm: string;\n loading: string;\n loadingMore: string;\n noResultsTitle: string;\n noResultsDescription: string;\n search: string;\n selectPlaceholder: string;\n}\n\nexport const defaultLabels: Labels = {\n close: 'Close',\n cancel: 'Cancel',\n confirm: 'OK',\n loading: 'Loading...',\n loadingMore: 'Loading more...',\n noResultsTitle: 'No results found',\n noResultsDescription: 'Try a different search term',\n search: 'Search...',\n selectPlaceholder: 'Select...',\n};\n"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"path": "theme/palettes.ts",
|
|
48
|
+
"content": "/**\n * Cada color es `string`, no un literal (`as const`): light y dark tienen\n * valores distintos para la misma clave, y createTheme() debe poder\n * aceptar cualquier hex/rgba en los overrides — literal typing lo impediría.\n */\nexport interface ThemeColors {\n primary: string;\n secondary: string;\n white: string;\n black: string;\n error: string;\n success: string;\n warning: string;\n background: string;\n surface: string;\n text: string;\n textLight: string;\n border: string;\n inputBackground: string;\n overlay: string;\n}\n\nconst shared = {\n primary: '#004cef',\n secondary: '#00cae1',\n white: '#ffffff',\n black: '#000000',\n error: '#ff3b30',\n success: '#34c759',\n warning: '#f59e0b',\n};\n\nexport const lightPalette: ThemeColors = {\n ...shared,\n background: '#f8f9fa',\n surface: '#ffffff',\n text: '#333333',\n textLight: '#666666',\n border: '#e5e7eb',\n inputBackground: '#f5f7fa',\n overlay: 'rgba(0,0,0,0.5)',\n};\n\nexport const darkPalette: ThemeColors = {\n ...shared,\n background: '#121212',\n surface: '#1e1e1e',\n text: '#ecedee',\n textLight: '#9ba1a6',\n border: '#2a2a2e',\n inputBackground: '#232326',\n overlay: 'rgba(0,0,0,0.6)',\n};\n"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"path": "theme/tokens.ts",
|
|
52
|
+
"content": "import { Platform, type ViewStyle } from 'react-native';\n\n// Las escalas numéricas se tipan explícitamente como `number` (no `as const`):\n// createTheme() debe poder sobreescribir cualquier valor con otro número\n// arbitrario: con literal typing, un override de `radius.md` a `999` fallaría\n// porque el tipo inferido sería el literal `8`, no `number`.\n\n/**\n * Escala de espaciado. Usar siempre estos valores en vez de números mágicos\n * para que el ritmo vertical/horizontal sea consistente entre componentes.\n */\nexport const spacing: Record<'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl', number> = {\n xxs: 2,\n xs: 4,\n sm: 8,\n md: 12,\n lg: 16,\n xl: 20,\n xxl: 24,\n xxxl: 32,\n};\n\nexport const radius: Record<'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'full', number> = {\n none: 0,\n xs: 4,\n sm: 6,\n md: 8,\n lg: 12,\n xl: 16,\n xxl: 20,\n full: 9999,\n};\n\nexport const fontSize: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', number> = {\n xs: 11,\n sm: 12,\n md: 14,\n lg: 16,\n xl: 18,\n xxl: 24,\n};\n\n// fontWeight sí necesita literales: TextStyle.fontWeight de RN exige un union\n// de strings específicos ('400' | '500' | ...), no `string` arbitrario.\nexport const fontWeight = {\n regular: '400',\n medium: '500',\n semibold: '600',\n bold: '700',\n} as const;\n\nexport const lineHeight: Record<'sm' | 'md' | 'lg', number> = {\n sm: 16,\n md: 20,\n lg: 24,\n};\n\n/** Duraciones en ms para Animated.timing/spring. */\nexport const duration: Record<'fast' | 'base' | 'slow', number> = {\n fast: 150,\n base: 200,\n slow: 250,\n};\n\ntype ShadowStyle = Pick<\n ViewStyle,\n 'shadowColor' | 'shadowOffset' | 'shadowOpacity' | 'shadowRadius' | 'elevation'\n>;\n\n/**\n * Sombras cross-platform: shadow* para iOS, elevation para Android.\n * `shadowColor` se deja en negro fijo — el color de sombra por tema es un\n * detalle demasiado fino para justificar la complejidad de resolverlo aquí.\n */\nexport const shadow: Record<'none' | 'sm' | 'md' | 'lg', ShadowStyle> = {\n none: Platform.select({\n android: { elevation: 0 },\n default: { shadowOpacity: 0 },\n }) as ShadowStyle,\n sm: {\n shadowColor: '#000',\n shadowOffset: { width: 0, height: 1 },\n shadowOpacity: 0.08,\n shadowRadius: 2,\n elevation: 2,\n },\n md: {\n shadowColor: '#000',\n shadowOffset: { width: 0, height: 2 },\n shadowOpacity: 0.1,\n shadowRadius: 8,\n elevation: 4,\n },\n lg: {\n shadowColor: '#000',\n shadowOffset: { width: 0, height: 4 },\n shadowOpacity: 0.15,\n shadowRadius: 16,\n elevation: 8,\n },\n};\n\nexport const tokens = {\n spacing,\n radius,\n fontSize,\n fontWeight,\n lineHeight,\n duration,\n shadow,\n};\n\nexport type Tokens = typeof tokens;\n"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"path": "theme/types.ts",
|
|
56
|
+
"content": "import type { Tokens } from './tokens';\nimport type { ThemeColors } from './palettes';\n\n/** Theme resuelto: paleta de un scheme concreto + tokens compartidos. */\nexport interface Theme {\n colors: ThemeColors;\n tokens: Tokens;\n scheme: 'light' | 'dark';\n}\n\n/** Par de themes light/dark que produce createTheme() y consume UIProvider. */\nexport interface ThemePair {\n light: Theme;\n dark: Theme;\n}\n\nexport type DeepPartial<T> = {\n [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];\n};\n\nexport interface ThemeOverrides {\n colors?: {\n light?: Partial<ThemeColors>;\n dark?: Partial<ThemeColors>;\n };\n tokens?: DeepPartial<Tokens>;\n}\n"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"registryDependencies": [],
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"react": ">=18.0.0",
|
|
62
|
+
"react-native": ">=0.73.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "DateField",
|
|
3
|
+
"slug": "date-field",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "organisms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"DateField"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "organisms/DateField.tsx",
|
|
12
|
+
"content": "import React, { memo, useState } from 'react';\nimport { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Modal } from './Modal';\nimport { DatePicker, DEFAULT_MONTH_NAMES, type DatePickerProps } from './DatePicker';\nimport { Button } from '../atoms/Button';\n\nexport interface DateFieldProps\n extends Pick<DatePickerProps, 'minDate' | 'maxDate' | 'monthNames' | 'weekdayNames' | 'firstDayOfWeek'> {\n label?: string;\n required?: boolean;\n value?: Date;\n onChange: (date: Date) => void;\n placeholder?: string;\n error?: string;\n disabled?: boolean;\n /** Formatea la fecha en el campo. @default \"D de Month de YYYY\" en inglés */\n format?: (date: Date) => string;\n /** Título del modal del calendario. @default 'Select date' */\n modalTitle?: string;\n clearLabel?: string;\n containerStyle?: StyleProp<ViewStyle>;\n}\n\nfunction defaultFormat(date: Date, monthNames: string[]): string {\n return `${monthNames[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { marginBottom: theme.tokens.spacing.lg, width: '100%', gap: theme.tokens.spacing.sm },\n label: { fontSize: theme.tokens.fontSize.md, fontWeight: theme.tokens.fontWeight.medium, color: theme.colors.text },\n required: { color: theme.colors.error },\n field: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-between',\n backgroundColor: theme.colors.inputBackground,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 2,\n borderColor: theme.colors.border,\n minHeight: 50,\n paddingHorizontal: theme.tokens.spacing.lg,\n },\n fieldError: { borderColor: theme.colors.error },\n fieldText: { fontSize: theme.tokens.fontSize.lg, color: theme.colors.text },\n placeholder: { color: theme.colors.textLight },\n errorText: { fontSize: theme.tokens.fontSize.sm, color: theme.colors.error },\n}));\n\n/**\n * DateField\n * Campo de fecha: se ve como un Input, abre un `DatePicker` en un Modal al\n * tocarlo. Sin picker nativo del sistema, así que se ve y se comporta igual\n * en iOS y Android.\n */\nexport const DateField = memo(\n ({\n label,\n required = false,\n value,\n onChange,\n placeholder = 'Select a date',\n error,\n disabled = false,\n format,\n modalTitle = 'Select date',\n clearLabel = 'Clear',\n containerStyle,\n monthNames = DEFAULT_MONTH_NAMES,\n ...pickerProps\n }: DateFieldProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const [open, setOpen] = useState(false);\n\n const display = value ? (format ? format(value) : defaultFormat(value, monthNames)) : placeholder;\n\n return (\n <View style={[styles.container, containerStyle]}>\n {!!label && (\n <Text allowFontScaling={allowFontScaling} style={styles.label}>\n {label}\n {required && <Text style={styles.required}> *</Text>}\n </Text>\n )}\n\n <TouchableOpacity\n style={[styles.field, !!error && styles.fieldError, disabled && { opacity: 0.5 }]}\n onPress={() => setOpen(true)}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityLabel={label ?? placeholder}\n accessibilityState={{ disabled, expanded: open }}\n >\n <Text allowFontScaling={allowFontScaling} style={[styles.fieldText, !value && styles.placeholder]}>\n {display}\n </Text>\n <Text allowFontScaling={allowFontScaling} style={{ color: colors.textLight }}>\n {'▾'}\n </Text>\n </TouchableOpacity>\n\n {!!error && (\n <Text allowFontScaling={allowFontScaling} style={styles.errorText}>\n {error}\n </Text>\n )}\n\n <Modal\n visible={open}\n onClose={() => setOpen(false)}\n title={modalTitle}\n variant=\"overlay\"\n scrollable={false}\n footer={<Button title={clearLabel} variant=\"ghost\" onPress={() => setOpen(false)} />}\n >\n <DatePicker\n value={value}\n monthNames={monthNames}\n {...pickerProps}\n onChange={(date) => {\n onChange(date);\n setOpen(false);\n }}\n />\n </Modal>\n </View>\n );\n }\n);\nDateField.displayName = 'DateField';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core",
|
|
17
|
+
"modal",
|
|
18
|
+
"title",
|
|
19
|
+
"icon-button",
|
|
20
|
+
"pressable-scale",
|
|
21
|
+
"date-picker",
|
|
22
|
+
"button"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "DatePicker",
|
|
3
|
+
"slug": "date-picker",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "organisms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"DatePicker",
|
|
8
|
+
"buildMonthGrid",
|
|
9
|
+
"DEFAULT_MONTH_NAMES",
|
|
10
|
+
"DEFAULT_WEEKDAY_NAMES"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "organisms/DatePicker.tsx",
|
|
15
|
+
"content": "import React, { memo, useMemo, useState } from 'react';\nimport { Text, TouchableOpacity, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { IconButton } from '../atoms/IconButton';\n\nexport const DEFAULT_MONTH_NAMES = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\n\n/** Índice 0 = domingo, para alinear con Date.getDay(). */\nexport const DEFAULT_WEEKDAY_NAMES = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];\n\nexport interface DateRange {\n start: Date;\n /** Falta mientras el rango está a medio elegir (sólo se tocó el inicio). */\n end?: Date;\n}\n\nexport interface DatePickerProps {\n /**\n * 'single' elige una fecha; 'range' elige un intervalo con dos toques.\n * @default 'single'\n */\n mode?: 'single' | 'range';\n /** Fecha elegida en mode=\"single\". */\n value?: Date;\n /** Se llama en mode=\"single\". */\n onChange?: (date: Date) => void;\n /** Rango elegido en mode=\"range\". */\n range?: DateRange;\n /** Se llama en mode=\"range\", tanto al fijar el inicio como al cerrar el rango. */\n onRangeChange?: (range: DateRange) => void;\n minDate?: Date;\n maxDate?: Date;\n /** Mes mostrado al abrir si no hay `value`. @default hoy */\n defaultMonth?: Date;\n monthNames?: string[];\n weekdayNames?: string[];\n /** 0 = domingo, 1 = lunes. @default 0 */\n firstDayOfWeek?: 0 | 1;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\n/** Compara sólo año/mes/día, ignorando hora — dos Date del mismo día son \"iguales\". */\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\nfunction startOfDay(d: Date): Date {\n return new Date(d.getFullYear(), d.getMonth(), d.getDate());\n}\n\n/** Estrictamente entre start y end, sin incluir los extremos. */\nfunction isBetween(day: Date, start: Date, end: Date): boolean {\n const t = startOfDay(day).getTime();\n return t > startOfDay(start).getTime() && t < startOfDay(end).getTime();\n}\n\nfunction isOutOfRange(day: Date, minDate?: Date, maxDate?: Date): boolean {\n const t = startOfDay(day).getTime();\n if (minDate && t < startOfDay(minDate).getTime()) return true;\n if (maxDate && t > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Devuelve la grilla del mes: 6 filas de 7 celdas. Las celdas fuera del mes\n * son null, así el layout no salta de alto entre meses (un mes puede\n * necesitar 4, 5 o 6 filas según en qué día caiga el 1).\n */\nexport function buildMonthGrid(year: number, month: number, firstDayOfWeek: 0 | 1): (Date | null)[][] {\n const first = new Date(year, month, 1);\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n const leading = (first.getDay() - firstDayOfWeek + 7) % 7;\n\n const cells: (Date | null)[] = [];\n for (let i = 0; i < leading; i++) cells.push(null);\n for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, month, d));\n while (cells.length < 42) cells.push(null);\n\n const rows: (Date | null)[][] = [];\n for (let r = 0; r < 6; r++) rows.push(cells.slice(r * 7, r * 7 + 7));\n return rows;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: {\n backgroundColor: theme.colors.surface,\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 1,\n borderColor: theme.colors.border,\n padding: theme.tokens.spacing.md,\n },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: theme.tokens.spacing.sm },\n monthLabel: { fontSize: theme.tokens.fontSize.lg, fontWeight: theme.tokens.fontWeight.semibold, color: theme.colors.text },\n weekRow: { flexDirection: 'row' },\n weekday: {\n flex: 1,\n textAlign: 'center',\n fontSize: theme.tokens.fontSize.xs,\n fontWeight: theme.tokens.fontWeight.semibold,\n color: theme.colors.textLight,\n paddingVertical: theme.tokens.spacing.sm,\n },\n row: { flexDirection: 'row' },\n cell: { flex: 1, aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },\n dayButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },\n dayText: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },\n daySelected: { backgroundColor: theme.colors.primary },\n dayTextSelected: { color: theme.colors.white, fontWeight: theme.tokens.fontWeight.bold },\n // El resaltado del tramo intermedio va en la celda (cuadrada, sin gaps) y\n // no en el botón redondo, así la barra del rango se ve continua.\n cellInRange: { backgroundColor: theme.colors.primary + '1f' },\n cellRangeStart: { borderTopLeftRadius: 18, borderBottomLeftRadius: 18 },\n cellRangeEnd: { borderTopRightRadius: 18, borderBottomRightRadius: 18 },\n dayTextInRange: { color: theme.colors.primary, fontWeight: theme.tokens.fontWeight.semibold },\n dayToday: { borderWidth: 1, borderColor: theme.colors.primary },\n dayTextDisabled: { color: theme.colors.textLight, opacity: 0.4 },\n}));\n\n/**\n * DatePicker\n * Calendario mensual dibujado con View/Text puros — sin\n * @react-native-community/datetimepicker ni ningún picker nativo, para\n * mantener la librería en cero dependencias y verse igual en iOS y Android.\n */\nexport const DatePicker = memo(\n ({\n mode = 'single',\n value,\n onChange,\n range,\n onRangeChange,\n minDate,\n maxDate,\n defaultMonth,\n monthNames = DEFAULT_MONTH_NAMES,\n weekdayNames = DEFAULT_WEEKDAY_NAMES,\n firstDayOfWeek = 0,\n style,\n testID,\n }: DatePickerProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const isRange = mode === 'range';\n\n const handlePress = (day: Date) => {\n if (!isRange) {\n onChange?.(day);\n return;\n }\n // Primer toque (o rango ya cerrado, o toque antes del inicio): arranca\n // un rango nuevo. Segundo toque en adelante: lo cierra.\n if (!range?.start || range.end || startOfDay(day) < startOfDay(range.start)) {\n onRangeChange?.({ start: day });\n } else {\n onRangeChange?.({ start: range.start, end: day });\n }\n };\n\n const initial = value ?? range?.start ?? defaultMonth ?? new Date();\n const [visibleMonth, setVisibleMonth] = useState(() => new Date(initial.getFullYear(), initial.getMonth(), 1));\n\n const today = useMemo(() => new Date(), []);\n const grid = useMemo(\n () => buildMonthGrid(visibleMonth.getFullYear(), visibleMonth.getMonth(), firstDayOfWeek),\n [visibleMonth, firstDayOfWeek]\n );\n\n const orderedWeekdays = useMemo(\n () => (firstDayOfWeek === 0 ? weekdayNames : [...weekdayNames.slice(1), weekdayNames[0]!]),\n [weekdayNames, firstDayOfWeek]\n );\n\n const shiftMonth = (delta: number) =>\n setVisibleMonth((m) => new Date(m.getFullYear(), m.getMonth() + delta, 1));\n\n const monthLabel = `${monthNames[visibleMonth.getMonth()]} ${visibleMonth.getFullYear()}`;\n\n return (\n <View style={[styles.container, style]} testID={testID}>\n <View style={styles.header}>\n <IconButton iconName=\"chevron-left\" accessibilityLabel=\"Previous month\" onPress={() => shiftMonth(-1)} />\n <Text allowFontScaling={allowFontScaling} style={styles.monthLabel} accessibilityRole=\"header\">\n {monthLabel}\n </Text>\n <IconButton iconName=\"chevron-right\" accessibilityLabel=\"Next month\" onPress={() => shiftMonth(1)} />\n </View>\n\n <View style={styles.weekRow}>\n {orderedWeekdays.map((w, i) => (\n <Text allowFontScaling={allowFontScaling} key={`${w}-${i}`} style={styles.weekday}>\n {w}\n </Text>\n ))}\n </View>\n\n {grid.map((row, rowIndex) => (\n <View style={styles.row} key={rowIndex}>\n {row.map((day, cellIndex) => {\n if (!day) return <View style={styles.cell} key={cellIndex} />;\n\n const isStart = isRange && !!range?.start && isSameDay(day, range.start);\n const isEnd = isRange && !!range?.end && isSameDay(day, range.end);\n const inRange =\n isRange && !!range?.start && !!range.end && isBetween(day, range.start, range.end);\n const selected = isRange ? isStart || isEnd : !!value && isSameDay(day, value);\n\n const disabled = isOutOfRange(day, minDate, maxDate);\n const isToday = isSameDay(day, today);\n return (\n <View\n style={[\n styles.cell,\n (inRange || (isStart && !!range?.end) || isEnd) && styles.cellInRange,\n isStart && !!range?.end && styles.cellRangeStart,\n isEnd && styles.cellRangeEnd,\n ]}\n key={cellIndex}\n >\n <TouchableOpacity\n style={[styles.dayButton, selected && styles.daySelected, !selected && isToday && styles.dayToday]}\n onPress={() => handlePress(day)}\n disabled={disabled}\n activeOpacity={0.7}\n accessibilityRole=\"button\"\n accessibilityLabel={`${monthNames[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`}\n accessibilityState={{ selected, disabled }}\n >\n <Text\n allowFontScaling={allowFontScaling}\n style={[\n styles.dayText,\n inRange && styles.dayTextInRange,\n selected && styles.dayTextSelected,\n disabled && styles.dayTextDisabled,\n ]}\n >\n {day.getDate()}\n </Text>\n </TouchableOpacity>\n </View>\n );\n })}\n </View>\n ))}\n </View>\n );\n }\n);\nDatePicker.displayName = 'DatePicker';\n"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"registryDependencies": [
|
|
19
|
+
"core",
|
|
20
|
+
"icon-button",
|
|
21
|
+
"pressable-scale"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Divider",
|
|
3
|
+
"slug": "divider",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"Divider"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "atoms/Divider.tsx",
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { View, type StyleProp, type ViewStyle } from 'react-native';\nimport { useColors } from '../theme/UIProvider';\n\nexport interface DividerProps {\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nexport const Divider = memo(({ style, testID }: DividerProps) => {\n const colors = useColors();\n return <View testID={testID} style={[{ height: 1, backgroundColor: colors.border, width: '100%' }, style]} />;\n});\nDivider.displayName = 'Divider';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "EmptyState",
|
|
3
|
+
"slug": "empty-state",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"EmptyState"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "atoms/EmptyState.tsx",
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { Text, View, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors, useLabels } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface EmptyStateProps {\n title?: string;\n description?: string;\n iconName?: IconName;\n icon?: React.ReactNode;\n style?: ViewStyle;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: {\n flex: 1,\n justifyContent: 'center',\n alignItems: 'center',\n padding: theme.tokens.spacing.xxl - 4,\n minHeight: 200,\n },\n title: {\n fontSize: theme.tokens.fontSize.lg,\n fontWeight: theme.tokens.fontWeight.semibold,\n color: theme.colors.textLight,\n marginTop: theme.tokens.spacing.md - 2,\n textAlign: 'center',\n },\n description: {\n fontSize: theme.tokens.fontSize.md,\n color: theme.colors.textLight,\n marginTop: theme.tokens.spacing.xs + 1,\n textAlign: 'center',\n },\n}));\n\nexport const EmptyState = memo(\n ({ title, description, iconName = 'search', icon, style }: EmptyStateProps) => {\n const colors = useColors();\n const styles = useStyles();\n const labels = useLabels();\n const allowFontScaling = useAllowFontScaling();\n const resolvedTitle = title ?? labels.noResultsTitle;\n const resolvedDescription = description ?? labels.noResultsDescription;\n return (\n <View style={[styles.container, style]} accessible accessibilityRole=\"text\">\n {icon ?? <Icon name={iconName} size={48} color={colors.textLight} />}\n <Text allowFontScaling={allowFontScaling} style={styles.title}>\n {resolvedTitle}\n </Text>\n <Text allowFontScaling={allowFontScaling} style={styles.description}>\n {resolvedDescription}\n </Text>\n </View>\n );\n }\n);\nEmptyState.displayName = 'EmptyState';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Fab",
|
|
3
|
+
"slug": "fab",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"Fab"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "atoms/Fab.tsx",
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { type GestureResponderEvent, type StyleProp, type ViewStyle } from 'react-native';\nimport { PressableScale } from './PressableScale';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors } from '../theme/UIProvider';\n\nexport interface FabProps {\n onPress: (e: GestureResponderEvent) => void;\n iconName?: IconName;\n icon?: React.ReactNode;\n size?: number;\n bottom?: number;\n right?: number;\n color?: string;\n style?: StyleProp<ViewStyle>;\n accessibilityLabel: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n fab: {\n position: 'absolute',\n justifyContent: 'center',\n alignItems: 'center',\n ...theme.tokens.shadow.lg,\n },\n}));\n\n/**\n * Fab\n * Botón flotante circular. `bottom`/`right` en vez del `tabBarHeight`\n * obligatorio de la versión original — el consumidor decide su propio\n * offset (con o sin tab bar).\n */\nexport const Fab = memo(\n ({ onPress, iconName = 'plus', icon, size = 60, bottom = 20, right = 20, color, style, accessibilityLabel }: FabProps) => {\n const styles = useStyles();\n const colors = useColors();\n return (\n <PressableScale\n onPress={onPress}\n style={[\n styles.fab,\n {\n width: size,\n height: size,\n borderRadius: size / 2,\n bottom,\n right,\n backgroundColor: color ?? colors.primary,\n },\n style,\n ]}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel}\n >\n {icon ?? <Icon name={iconName} size={size * 0.5} color={colors.white} />}\n </PressableScale>\n );\n }\n);\nFab.displayName = 'Fab';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core",
|
|
17
|
+
"pressable-scale"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "FormActions",
|
|
3
|
+
"slug": "form-actions",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "molecules",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"FormActions"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "molecules/FormActions.tsx",
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { View, type StyleProp, type ViewStyle } from 'react-native';\nimport { Button } from '../atoms/Button';\nimport type { IconName } from '../icons/types';\nimport { useLabels } from '../theme/UIProvider';\n\nexport interface FormActionsProps {\n /** Si se omite, solo se muestra el botón secundario. */\n primaryLabel?: string;\n onPrimaryPress?: () => void;\n primaryDisabled?: boolean;\n primaryLoading?: boolean;\n primaryLeftIconName?: IconName;\n secondaryLabel?: string;\n onSecondaryPress: () => void;\n style?: StyleProp<ViewStyle>;\n}\n\n/**\n * FormActions\n * Par de botones: uno primario (guardar/editar) y uno ghost (cerrar/cancelar).\n * Uso típico: al final de un form, o en el footer de un Modal.\n */\nexport const FormActions = memo(\n ({\n primaryLabel,\n onPrimaryPress,\n primaryDisabled = false,\n primaryLoading = false,\n primaryLeftIconName,\n secondaryLabel,\n onSecondaryPress,\n style,\n }: FormActionsProps) => {\n const labels = useLabels();\n return (\n <View style={[{ gap: 12 }, style]}>\n {primaryLabel && onPrimaryPress && (\n <Button\n title={primaryLabel}\n onPress={onPrimaryPress}\n disabled={primaryDisabled}\n loading={primaryLoading}\n leftIconName={primaryLeftIconName}\n />\n )}\n <Button title={secondaryLabel ?? labels.close} onPress={onSecondaryPress} variant=\"ghost\" />\n </View>\n );\n }\n);\nFormActions.displayName = 'FormActions';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core",
|
|
17
|
+
"button"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "IconButton",
|
|
3
|
+
"slug": "icon-button",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"IconButton"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "atoms/IconButton.tsx",
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { type GestureResponderEvent, type StyleProp, type ViewStyle } from 'react-native';\nimport { PressableScale } from './PressableScale';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\nimport { createStyles } from '../theme/createStyles';\n\nexport interface IconButtonProps {\n iconName?: IconName;\n icon?: React.ReactNode;\n onPress?: (e: GestureResponderEvent) => void;\n size?: number;\n color?: string;\n disabled?: boolean;\n style?: StyleProp<ViewStyle>;\n accessibilityLabel: string;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n hitArea: {\n padding: theme.tokens.spacing.sm,\n alignItems: 'center',\n justifyContent: 'center',\n },\n}));\n\n/**\n * IconButton\n * Botón de solo ícono con área táctil mínima de 44x44 garantizada por el\n * padding (icon 24 + padding 8*2 = 40; sube a 44 con size>=24 default RN).\n * `accessibilityLabel` es obligatorio: sin texto visible, es la única forma\n * de que un lector de pantalla anuncie qué hace el botón.\n */\nexport const IconButton = memo(\n ({ iconName, icon, onPress, size = 24, color, disabled, style, accessibilityLabel, testID }: IconButtonProps) => {\n const styles = useStyles();\n return (\n <PressableScale\n onPress={onPress}\n disabled={disabled}\n style={[styles.hitArea, { opacity: disabled ? 0.5 : 1 }, style]}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel}\n accessibilityState={{ disabled: !!disabled }}\n testID={testID}\n >\n {icon ?? (iconName && <Icon name={iconName} size={size} color={color} />)}\n </PressableScale>\n );\n }\n);\nIconButton.displayName = 'IconButton';\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"core",
|
|
17
|
+
"pressable-scale"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Image",
|
|
3
|
+
"slug": "image",
|
|
4
|
+
"type": "component",
|
|
5
|
+
"category": "atoms",
|
|
6
|
+
"exportedNames": [
|
|
7
|
+
"Image",
|
|
8
|
+
"prefetchImage"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "atoms/Image.tsx",
|
|
13
|
+
"content": "import React, { memo, useCallback, useEffect, useState } from 'react';\nimport {\n Image as RNImage,\n Platform,\n StyleSheet,\n View,\n type ImageErrorEventData,\n type ImageResizeMode,\n type ImageSourcePropType,\n type NativeSyntheticEvent,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { Spinner } from './Spinner';\n\nexport interface ImageProps {\n source: ImageSourcePropType;\n width?: number;\n height?: number;\n /** @default 'cover' */\n resizeMode?: ImageResizeMode;\n /** Radio de las esquinas, mismo patrón que Avatar/Card. @default 0 */\n radius?: number;\n /** Qué mostrar mientras carga. `false` para no mostrar nada. @default true (spinner chico) */\n loading?: React.ReactNode | boolean;\n /** Qué mostrar si la carga falla. @default un placeholder gris, sin contenido */\n fallback?: React.ReactNode;\n /**\n * 'high' dispara Image.prefetch() al montar, antes de que <Image> arranque\n * su propio ciclo de carga — pensado para la imagen above-the-fold de una\n * pantalla (la primera que el usuario ve). Sin costo si ya está en caché.\n * @default 'normal'\n */\n priority?: 'low' | 'normal' | 'high';\n style?: StyleProp<ViewStyle>;\n onLoadEnd?: () => void;\n onError?: (error: NativeSyntheticEvent<ImageErrorEventData>) => void;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { overflow: 'hidden', backgroundColor: theme.colors.border + '30' },\n overlay: { alignItems: 'center', justifyContent: 'center' },\n fallback: { flex: 1, backgroundColor: theme.colors.border + '30' },\n}));\n\nfunction sourceUri(source: ImageSourcePropType): string | undefined {\n return typeof source === 'object' && source !== null && 'uri' in source ? source.uri : undefined;\n}\n\n/**\n * Image\n * Wrapper cero-dependencias sobre <Image> de RN: agrega estado de carga y\n * error (con los mismos placeholders que el resto de la librería), y expone\n * `priority=\"high\"` para adelantar la descarga vía Image.prefetch(). El\n * cache real (disco/memoria) lo maneja el motor de imágenes nativo de RN —\n * esto no lo reimplementa, solo no lo deja apagado por accidente: en iOS\n * fuerza `cache=\"force-cache\"` (usa el disco aunque el header HTTP no lo\n * pida); en Android el cache de disco de la imagen nativa ya está siempre\n * activo, no hay knob equivalente que tocar.\n */\nexport const Image = memo(\n ({\n source,\n width,\n height,\n resizeMode = 'cover',\n radius = 0,\n loading = true,\n fallback,\n priority = 'normal',\n style,\n onLoadEnd,\n onError,\n testID,\n }: ImageProps) => {\n const styles = useStyles();\n const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading');\n const uri = sourceUri(source);\n\n useEffect(() => {\n // Resetea el estado si cambia la imagen (misma instancia de Image,\n // source distinto: p.ej. avatar de un usuario a otro en la misma fila).\n setStatus('loading');\n }, [uri ?? source]);\n\n useEffect(() => {\n if (priority === 'high' && uri) {\n RNImage.prefetch(uri).catch(() => {});\n }\n }, [priority, uri]);\n\n const handleError = useCallback(\n (e: NativeSyntheticEvent<ImageErrorEventData>) => {\n setStatus('error');\n onError?.(e);\n },\n [onError]\n );\n\n const handleLoadEnd = useCallback(() => {\n setStatus('loaded');\n onLoadEnd?.();\n }, [onLoadEnd]);\n\n return (\n <View style={[styles.container, { width, height, borderRadius: radius }, style]} testID={testID}>\n {status !== 'error' && (\n <RNImage\n source={source}\n resizeMode={resizeMode}\n style={StyleSheet.absoluteFill}\n fadeDuration={Platform.OS === 'android' ? 150 : undefined}\n {...(Platform.OS === 'ios' ? { cache: 'force-cache' as const } : null)}\n onLoadEnd={handleLoadEnd}\n onError={handleError}\n />\n )}\n {status === 'loading' && loading !== false && (\n <View style={[StyleSheet.absoluteFill, styles.overlay]}>\n {loading === true ? <Spinner size=\"small\" fullscreen={false} /> : loading}\n </View>\n )}\n {status === 'error' && (fallback ?? <View style={styles.fallback} />)}\n </View>\n );\n }\n);\nImage.displayName = 'Image';\n\n/**\n * Precarga una URL antes de montar el componente (p.ej. al recibir la\n * respuesta de un listado, antes de navegar al detalle). Re-exporta\n * Image.prefetch de RN — no hay nada zero-dep que agregarle encima.\n */\nexport const prefetchImage = RNImage.prefetch;\n"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"registryDependencies": [
|
|
17
|
+
"core",
|
|
18
|
+
"spinner"
|
|
19
|
+
]
|
|
20
|
+
}
|