@xetwa/design-system 1.0.2
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/.github/workflows/publish.yml +30 -0
- package/.storybook/main.ts +17 -0
- package/.storybook/preview.tsx +65 -0
- package/eslint.config.js +22 -0
- package/index.html +13 -0
- package/package.json +55 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/src/App.css +184 -0
- package/src/App.tsx +122 -0
- package/src/BordasSombras.mdx +343 -0
- package/src/Colors.mdx +393 -0
- package/src/GrelhaLayout.mdx +375 -0
- package/src/Icons.mdx +42 -0
- package/src/MovimentoBreakpoints.mdx +379 -0
- package/src/Spacing.mdx +231 -0
- package/src/Typography.mdx +321 -0
- package/src/assets/Mascots/vava/estaticos/vava-acenar.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-acertou.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-celebrar.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-com-autocolantes.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-dormir.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-errou.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-feliz.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-neutra.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-original.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-pensar.svg +3 -0
- package/src/assets/Mascots/vava/estaticos/vava-surpresa.svg +3 -0
- package/src/assets/Mascots/yaya/avatar/yaya-avatar-falar.svg +7 -0
- package/src/assets/Mascots/yaya/avatar/yaya-avatar-feliz.svg +7 -0
- package/src/assets/Mascots/yaya/avatar/yaya-avatar-transparente.svg +6 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-acertou.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-celebrar.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-dormir.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-errou.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-falar.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-feliz.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-neutra.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-pensar.svg +13 -0
- package/src/assets/Mascots/yaya/estaticos/yaya-surpresa.svg +13 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/Buttons/BackButton/BackButton.stories.tsx +86 -0
- package/src/components/Buttons/BackButton/BackButton.tsx +107 -0
- package/src/components/Buttons/FabButton/FabButton.stories.tsx +74 -0
- package/src/components/Buttons/FabButton/FabButton.tsx +161 -0
- package/src/components/Buttons/IconButton/IconButton.stories.tsx +99 -0
- package/src/components/Buttons/IconButton/IconButton.tsx +189 -0
- package/src/components/Buttons/JourneyButton/JourneyButton.stories.tsx +80 -0
- package/src/components/Buttons/JourneyButton/JourneyButton.tsx +194 -0
- package/src/components/Buttons/MainButton/MainButton.stories.tsx +144 -0
- package/src/components/Buttons/MainButton/MainButton.tsx +258 -0
- package/src/components/Buttons/WordChip/WordChip.stories.tsx +84 -0
- package/src/components/Buttons/WordChip/WordChip.tsx +157 -0
- package/src/components/Buttons/WordChip/index.ts +1 -0
- package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.stories.tsx +181 -0
- package/src/components/Cards/AnswerOptionCard/AnswerOptionCard.tsx +201 -0
- package/src/components/Cards/BaseCard/BaseCard.stories.tsx +148 -0
- package/src/components/Cards/BaseCard/BaseCard.tsx +181 -0
- package/src/components/Cards/BoardingPassCard/BoardingPassCard.stories.tsx +205 -0
- package/src/components/Cards/BoardingPassCard/BoardingPassCard.tsx +384 -0
- package/src/components/Cards/BoardingPassCard/FlightIcon.tsx +21 -0
- package/src/components/Cards/BoardingPassCard/FlightIcon.web.tsx +18 -0
- package/src/components/Cards/CountryCard/CountryCard.stories.tsx +97 -0
- package/src/components/Cards/CountryCard/CountryCard.tsx +185 -0
- package/src/components/Cards/LevelProgressCard/LevelProgressCard.stories.tsx +61 -0
- package/src/components/Cards/LevelProgressCard/LevelProgressCard.tsx +143 -0
- package/src/components/Cards/ModuleCard/ModuleCard.stories.tsx +108 -0
- package/src/components/Cards/ModuleCard/ModuleCard.tsx +304 -0
- package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.stories.tsx +53 -0
- package/src/components/Cards/ModuleCompletionCard/ModuleCompletionCard.tsx +167 -0
- package/src/components/Cards/Passport/PassportCover.tsx +114 -0
- package/src/components/Cards/Passport/PassportIDPage.tsx +217 -0
- package/src/components/Cards/Passport/PassportStampPage.tsx +253 -0
- package/src/components/Cards/Passport/PassportViewer.stories.tsx +95 -0
- package/src/components/Cards/Passport/PassportViewer.tsx +328 -0
- package/src/components/Cards/PlanCard/PlanCard.stories.tsx +159 -0
- package/src/components/Cards/PlanCard/PlanCard.tsx +317 -0
- package/src/components/Cards/PricingCard/PricingCard.stories.tsx +83 -0
- package/src/components/Cards/PricingCard/PricingCard.tsx +144 -0
- package/src/components/Cards/SelectionCard/SelectionCard.stories.tsx +77 -0
- package/src/components/Cards/SelectionCard/SelectionCard.tsx +111 -0
- package/src/components/Cards/TeacherCard/TeacherCard.stories.tsx +126 -0
- package/src/components/Cards/TeacherCard/TeacherCard.tsx +236 -0
- package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.stories.tsx +89 -0
- package/src/components/Charts/WeeklyProgressChart/WeeklyProgressChart.tsx +179 -0
- package/src/components/Charts/WeeklyProgressChart/index.ts +1 -0
- package/src/components/DataDisplay/Avatar/Avatar.stories.tsx +85 -0
- package/src/components/DataDisplay/Avatar/Avatar.tsx +118 -0
- package/src/components/DataDisplay/Avatar/index.ts +1 -0
- package/src/components/DataDisplay/Badge/Badge.stories.tsx +62 -0
- package/src/components/DataDisplay/Badge/Badge.tsx +146 -0
- package/src/components/DataDisplay/Flag/Flag.stories.tsx +98 -0
- package/src/components/DataDisplay/Flag/Flag.tsx +347 -0
- package/src/components/DataDisplay/Flag/Flag.web.tsx +334 -0
- package/src/components/DataDisplay/InstructionBubble/InstructionBubble.stories.tsx +80 -0
- package/src/components/DataDisplay/InstructionBubble/InstructionBubble.tsx +86 -0
- package/src/components/DataDisplay/LevelBadge/LevelBadge.stories.tsx +99 -0
- package/src/components/DataDisplay/LevelBadge/LevelBadge.tsx +118 -0
- package/src/components/DataDisplay/ProgressBar/ProgressBar.stories.tsx +95 -0
- package/src/components/DataDisplay/ProgressBar/ProgressBar.tsx +117 -0
- package/src/components/DataDisplay/StatBadge/StatBadge.stories.tsx +82 -0
- package/src/components/DataDisplay/StatBadge/StatBadge.tsx +135 -0
- package/src/components/DataDisplay/StatBadge/index.ts +1 -0
- package/src/components/DataDisplay/Tag/Tag.stories.tsx +63 -0
- package/src/components/DataDisplay/Tag/Tag.tsx +148 -0
- package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.stories.tsx +74 -0
- package/src/components/Feedback/FeedbackBottomSheet/FeedbackBottomSheet.tsx +200 -0
- package/src/components/Forms/Checkbox/Checkbox.stories.tsx +60 -0
- package/src/components/Forms/Checkbox/Checkbox.tsx +127 -0
- package/src/components/Forms/Checkbox/index.ts +1 -0
- package/src/components/Forms/DaySelector/DaySelector.stories.tsx +62 -0
- package/src/components/Forms/DaySelector/DaySelector.tsx +133 -0
- package/src/components/Forms/Radio/Radio.stories.tsx +61 -0
- package/src/components/Forms/Radio/Radio.tsx +141 -0
- package/src/components/Forms/Radio/index.ts +1 -0
- package/src/components/Forms/SegmentedToggle/SegmentedToggle.stories.tsx +67 -0
- package/src/components/Forms/SegmentedToggle/SegmentedToggle.tsx +150 -0
- package/src/components/Forms/SegmentedToggle/index.ts +1 -0
- package/src/components/Forms/Select/Select.stories.tsx +59 -0
- package/src/components/Forms/Select/Select.tsx +255 -0
- package/src/components/Forms/Select/index.ts +1 -0
- package/src/components/Forms/Switch/Switch.stories.tsx +57 -0
- package/src/components/Forms/Switch/Switch.tsx +120 -0
- package/src/components/Forms/Switch/index.ts +1 -0
- package/src/components/Forms/TimeSlot/TimeSlot.stories.tsx +62 -0
- package/src/components/Forms/TimeSlot/TimeSlot.tsx +116 -0
- package/src/components/Headers/CountryHeader/CountryHeader.stories.tsx +114 -0
- package/src/components/Headers/CountryHeader/CountryHeader.tsx +147 -0
- package/src/components/Headers/ProfileHeader/ProfileHeader.stories.tsx +141 -0
- package/src/components/Headers/ProfileHeader/ProfileHeader.tsx +233 -0
- package/src/components/Inputs/FillInTheBlank/FillInTheBlank.stories.tsx +333 -0
- package/src/components/Inputs/FillInTheBlank/FillInTheBlank.tsx +287 -0
- package/src/components/Inputs/FillInTheBlank/index.ts +1 -0
- package/src/components/Inputs/TextInput/TextInput.stories.tsx +103 -0
- package/src/components/Inputs/TextInput/TextInput.tsx +187 -0
- package/src/components/Mascots/Vava/Vava.stories.tsx +114 -0
- package/src/components/Mascots/Vava/Vava.tsx +276 -0
- package/src/components/Mascots/Yaya/Yaya.stories.tsx +98 -0
- package/src/components/Mascots/Yaya/Yaya.tsx +233 -0
- package/src/components/Navigation/BottomTabBar/BottomTabBar.stories.tsx +68 -0
- package/src/components/Navigation/BottomTabBar/BottomTabBar.tsx +85 -0
- package/src/components/Navigation/BottomTabBar/BottomTabBarItem.tsx +68 -0
- package/src/components/Navigation/BottomTabBar/index.ts +2 -0
- package/src/components/Navigation/PaginationDots/PaginationDots.stories.tsx +62 -0
- package/src/components/Navigation/PaginationDots/PaginationDots.tsx +81 -0
- package/src/components/Navigation/PaginationDots/index.ts +1 -0
- package/src/components/Navigation/ProgressMap/ProgressMap.stories.tsx +116 -0
- package/src/components/Navigation/ProgressMap/ProgressMap.tsx +827 -0
- package/src/components/Navigation/WorldProgressMap/WorldProgressMap.stories.tsx +169 -0
- package/src/components/Navigation/WorldProgressMap/WorldProgressMap.tsx +790 -0
- package/src/components/Typography/Typography.stories.tsx +127 -0
- package/src/components/Typography/Typography.tsx +53 -0
- package/src/hooks/useResponsive.ts +41 -0
- package/src/hooks/useTheme.ts +32 -0
- package/src/index.css +355 -0
- package/src/index.ts +28 -0
- package/src/main.tsx +10 -0
- package/src/mocks/codegenNativeComponent.ts +5 -0
- package/src/styles/countryThemes.ts +79 -0
- package/src/styles/theme.ts +297 -0
- package/src/utils/iconMap.ts +23 -0
- package/tsconfig.app.json +25 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +51 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Yaya } from './Yaya';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Mascots/Yaya',
|
|
6
|
+
component: Yaya,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
},
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
argTypes: {
|
|
12
|
+
variant: {
|
|
13
|
+
control: 'select',
|
|
14
|
+
options: [
|
|
15
|
+
'neutra',
|
|
16
|
+
'feliz',
|
|
17
|
+
'errou',
|
|
18
|
+
'acertou',
|
|
19
|
+
'celebrar',
|
|
20
|
+
'dormir',
|
|
21
|
+
'falar',
|
|
22
|
+
'pensar',
|
|
23
|
+
'surpresa',
|
|
24
|
+
],
|
|
25
|
+
description: 'A expressão ou estado da mascote Yaya.',
|
|
26
|
+
},
|
|
27
|
+
size: {
|
|
28
|
+
control: { type: 'range', min: 50, max: 500, step: 10 },
|
|
29
|
+
description: 'O tamanho do componente (largura e altura em pixels).',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} satisfies Meta<any>;
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<any>;
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
variant: 'feliz',
|
|
40
|
+
size: 200,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Neutra: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
variant: 'neutra',
|
|
47
|
+
size: 200,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const Errou: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
variant: 'errou',
|
|
54
|
+
size: 200,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Acertou: Story = {
|
|
59
|
+
args: {
|
|
60
|
+
variant: 'acertou',
|
|
61
|
+
size: 200,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const Celebrar: Story = {
|
|
66
|
+
args: {
|
|
67
|
+
variant: 'celebrar',
|
|
68
|
+
size: 200,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Dormir: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
variant: 'dormir',
|
|
75
|
+
size: 200,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const Falar: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
variant: 'falar',
|
|
82
|
+
size: 200,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const Pensar: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
variant: 'pensar',
|
|
89
|
+
size: 200,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const Surpresa: Story = {
|
|
94
|
+
args: {
|
|
95
|
+
variant: 'surpresa',
|
|
96
|
+
size: 200,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Svg, { Path, Rect, Circle, Ellipse, G, Text as SvgText } from 'react-native-svg';
|
|
3
|
+
|
|
4
|
+
export type YayaVariant =
|
|
5
|
+
| 'neutra'
|
|
6
|
+
| 'feliz'
|
|
7
|
+
| 'errou'
|
|
8
|
+
| 'acertou'
|
|
9
|
+
| 'celebrar'
|
|
10
|
+
| 'dormir'
|
|
11
|
+
| 'falar'
|
|
12
|
+
| 'pensar'
|
|
13
|
+
| 'surpresa';
|
|
14
|
+
|
|
15
|
+
export interface YayaProps {
|
|
16
|
+
/**
|
|
17
|
+
* A expressão ou estado da mascote Yaya.
|
|
18
|
+
* @default 'feliz'
|
|
19
|
+
*/
|
|
20
|
+
variant?: YayaVariant;
|
|
21
|
+
/**
|
|
22
|
+
* O tamanho do componente (largura e altura em pixels).
|
|
23
|
+
* @default 200
|
|
24
|
+
*/
|
|
25
|
+
size?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Se verdadeiro, renderiza apenas o rosto/cabeça da mascote em formato Avatar.
|
|
28
|
+
* Útil para exibir em caixas de feedback.
|
|
29
|
+
* @default false
|
|
30
|
+
*/
|
|
31
|
+
avatarOnly?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Componente Yaya - A mascote assistente do Xetlingo.
|
|
36
|
+
*
|
|
37
|
+
* Este componente renderiza de forma estática, vetorial e performática as diferentes expressões da Yaya.
|
|
38
|
+
* É compatível tanto com React Native (iOS/Android) quanto com React Native Web.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* // Yaya feliz com tamanho padrão (200px)
|
|
43
|
+
* <Yaya variant="feliz" />
|
|
44
|
+
*
|
|
45
|
+
* // Yaya celebrando com 150px de tamanho
|
|
46
|
+
* <Yaya variant="celebrar" size={150} />
|
|
47
|
+
*
|
|
48
|
+
* // Apenas o avatar para usar num bottom sheet
|
|
49
|
+
* <Yaya variant="acertou" avatarOnly size={64} />
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const Yaya: React.FC<YayaProps> = ({ variant = 'feliz', size = 200, avatarOnly = false }) => {
|
|
53
|
+
const renderFace = () => {
|
|
54
|
+
switch (variant) {
|
|
55
|
+
case 'neutra':
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<Path d="M78 81 l-5 -3 M122 81 l5 -3" stroke="#15100d" strokeWidth={2} strokeLinecap="round" />
|
|
59
|
+
<Ellipse cx={86} cy={88} rx={8} ry={10} fill="#fff" />
|
|
60
|
+
<Ellipse cx={114} cy={88} rx={8} ry={10} fill="#fff" />
|
|
61
|
+
<Circle cx={87} cy={90} r={4.3} fill="#15100d" />
|
|
62
|
+
<Circle cx={115} cy={90} r={4.3} fill="#15100d" />
|
|
63
|
+
<Circle cx={85} cy={86} r={1.6} fill="#fff" />
|
|
64
|
+
<Circle cx={113} cy={86} r={1.6} fill="#fff" />
|
|
65
|
+
<Path d="M93 104 Q100 109 107 104" fill="none" stroke="#cf7a30" strokeWidth={3} strokeLinecap="round" />
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
case 'feliz':
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<Path d="M78 81 l-5 -3 M122 81 l5 -3" stroke="#15100d" strokeWidth={2} strokeLinecap="round" />
|
|
73
|
+
<Ellipse cx={86} cy={88} rx={8} ry={10} fill="#fff" />
|
|
74
|
+
<Ellipse cx={114} cy={88} rx={8} ry={10} fill="#fff" />
|
|
75
|
+
<Circle cx={87} cy={90} r={4.3} fill="#15100d" />
|
|
76
|
+
<Circle cx={115} cy={90} r={4.3} fill="#15100d" />
|
|
77
|
+
<Circle cx={85} cy={86} r={1.6} fill="#fff" />
|
|
78
|
+
<Circle cx={113} cy={86} r={1.6} fill="#fff" />
|
|
79
|
+
<Path d="M91 103 Q100 113 109 103 Z" fill="#f2974b" />
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
case 'errou':
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<Path d="M76 82 l11 4 M124 82 l-11 4" stroke="#15100d" strokeWidth={2.2} strokeLinecap="round" />
|
|
87
|
+
<Ellipse cx={86} cy={91} rx={7.5} ry={9} fill="#fff" />
|
|
88
|
+
<Ellipse cx={114} cy={91} rx={7.5} ry={9} fill="#fff" />
|
|
89
|
+
<Circle cx={87} cy={93} r={4} fill="#15100d" />
|
|
90
|
+
<Circle cx={115} cy={93} r={4} fill="#15100d" />
|
|
91
|
+
<Path d="M94 107 Q100 102 106 107" stroke="#cf7a30" strokeWidth={3} fill="none" strokeLinecap="round" />
|
|
92
|
+
<Path d="M133 86 q3.6 5 0 9 q-3.6 -4 0 -9 Z" fill="#7ab8e0" />
|
|
93
|
+
{!avatarOnly && (
|
|
94
|
+
<G transform="translate(150, 58)">
|
|
95
|
+
<Circle r={16} fill="#cf5b48" />
|
|
96
|
+
<Path d="M-6 -6 l12 12 M6 -6 l-12 12" stroke="#fff" strokeWidth={3.5} strokeLinecap="round" />
|
|
97
|
+
</G>
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
case 'acertou':
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
<Path d="M79 90 Q86 82 93 90" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
|
|
106
|
+
<Path d="M107 90 Q114 82 121 90" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
|
|
107
|
+
<Path d="M91 103 Q100 113 109 103 Z" fill="#f2974b" />
|
|
108
|
+
{avatarOnly ? (
|
|
109
|
+
<G>
|
|
110
|
+
<Path d="M55 75 Q60 75 60 70 Q60 75 65 75 Q60 75 60 80 Q60 75 55 75 Z" fill="#f2974b" />
|
|
111
|
+
<Path d="M68 60 Q70 60 70 58 Q70 60 72 60 Q70 60 70 62 Q70 60 68 60 Z" fill="#f2974b" />
|
|
112
|
+
</G>
|
|
113
|
+
) : (
|
|
114
|
+
<G transform="translate(150, 58)">
|
|
115
|
+
<Circle r={16} fill="#16915a" />
|
|
116
|
+
<Path d="M-7 0 l4 5 9 -10" stroke="#fff" strokeWidth={3.5} fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
|
117
|
+
</G>
|
|
118
|
+
)}
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
case 'celebrar':
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<Path d="M79 90 Q86 82 93 90" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
|
|
126
|
+
<Path d="M107 90 Q114 82 121 90" stroke="#15100d" strokeWidth={3.2} fill="none" strokeLinecap="round" />
|
|
127
|
+
<Path d="M90 102 Q100 117 110 102 Q100 108 90 102 Z" fill="#fff" />
|
|
128
|
+
<Path d="M92 103 Q100 114 108 103" fill="#f2974b" />
|
|
129
|
+
</>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
case 'dormir':
|
|
133
|
+
return (
|
|
134
|
+
<>
|
|
135
|
+
<Path d="M78 89 Q86 95 94 89" stroke="#15100d" strokeWidth={3} fill="none" strokeLinecap="round" />
|
|
136
|
+
<Path d="M106 89 Q114 95 122 89" stroke="#15100d" strokeWidth={3} fill="none" strokeLinecap="round" />
|
|
137
|
+
<Circle cx={100} cy={105} r={3} fill="none" stroke="#cf7a30" strokeWidth={2.5} />
|
|
138
|
+
<SvgText x="50" y="50" fontFamily="sans-serif" fontWeight="700" fontSize={15} fill="#9a8478">z</SvgText>
|
|
139
|
+
<SvgText x="50" y="50" fontFamily="sans-serif" fontWeight="700" fontSize={20} fill="#9a8478">Z</SvgText>
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
case 'falar':
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
<Ellipse cx={86} cy={88} rx={8} ry={10} fill="#fff" />
|
|
147
|
+
<Ellipse cx={114} cy={88} rx={8} ry={10} fill="#fff" />
|
|
148
|
+
<Circle cx={87} cy={90} r={4.3} fill="#15100d" />
|
|
149
|
+
<Circle cx={115} cy={90} r={4.3} fill="#15100d" />
|
|
150
|
+
<Circle cx={85} cy={86} r={1.6} fill="#fff" />
|
|
151
|
+
<Circle cx={113} cy={86} r={1.6} fill="#fff" />
|
|
152
|
+
<Ellipse cx={100} cy={106} rx={6.5} ry={6} fill="#f2974b" />
|
|
153
|
+
<Ellipse cx={100} cy={104} rx={3.5} ry={1.6} fill="#fff" opacity={0.5} />
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
case 'pensar':
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
<Ellipse cx={86} cy={88} rx={8} ry={10} fill="#fff" />
|
|
161
|
+
<Ellipse cx={114} cy={88} rx={8} ry={10} fill="#fff" />
|
|
162
|
+
<Circle cx={88} cy={84} r={4} fill="#15100d" />
|
|
163
|
+
<Circle cx={116} cy={84} r={4} fill="#15100d" />
|
|
164
|
+
<Path d="M93 105 L105 103" stroke="#cf7a30" strokeWidth={3} fill="none" strokeLinecap="round" />
|
|
165
|
+
<SvgText x="50" y="50" fontFamily="sans-serif" fontWeight="700" fontSize={28} fill="#f2974b">?</SvgText>
|
|
166
|
+
</>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
case 'surpresa':
|
|
170
|
+
return (
|
|
171
|
+
<>
|
|
172
|
+
<Circle cx={86} cy={88} r={10} fill="#fff" />
|
|
173
|
+
<Circle cx={114} cy={88} r={10} fill="#fff" />
|
|
174
|
+
<Circle cx={86} cy={89} r={5} fill="#15100d" />
|
|
175
|
+
<Circle cx={114} cy={89} r={5} fill="#15100d" />
|
|
176
|
+
<Circle cx={84} cy={86} r={2} fill="#fff" />
|
|
177
|
+
<Circle cx={112} cy={86} r={2} fill="#fff" />
|
|
178
|
+
<Ellipse cx={100} cy={105} rx={6} ry={7} fill="#f2974b" />
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
default:
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<Svg
|
|
189
|
+
width={size}
|
|
190
|
+
height={size}
|
|
191
|
+
viewBox={avatarOnly ? "50 32 100 100" : "0 0 200 200"}
|
|
192
|
+
role="img"
|
|
193
|
+
aria-label={`Mascote Yaya: ${variant}`}
|
|
194
|
+
>
|
|
195
|
+
{!avatarOnly && (
|
|
196
|
+
<>
|
|
197
|
+
{/* Corpo base */}
|
|
198
|
+
<Path d="M72 170 Q70 124 100 124 Q130 124 128 170 Z" fill="#f2974b" />
|
|
199
|
+
<Path d="M82 128 L120 150" stroke="#3f2620" strokeWidth={3} fill="none" />
|
|
200
|
+
|
|
201
|
+
{/* Dispositivo / Tablet lateral */}
|
|
202
|
+
<Rect x={112} y={148} width={22} height={18} rx={5} fill="#cf7a30" />
|
|
203
|
+
<Rect x={118} y={144} width={10} height={6} rx={3} fill="none" stroke="#3f2620" strokeWidth={2} />
|
|
204
|
+
|
|
205
|
+
{/* Colar / Cachecol */}
|
|
206
|
+
<Path d="M76 122 Q100 138 124 122 L124 130 Q100 146 76 130 Z" fill="#e0662e" />
|
|
207
|
+
<Circle cx={88} cy={129} r={1.5} fill="#fbe6cf" />
|
|
208
|
+
<Circle cx={100} cy={133} r={1.5} fill="#fbe6cf" />
|
|
209
|
+
<Circle cx={112} cy={129} r={1.5} fill="#fbe6cf" />
|
|
210
|
+
|
|
211
|
+
{/* Orelhas / Cabelo lateral */}
|
|
212
|
+
<Path d="M62 96 q-6 18 4 30 q8 -10 6 -26 Z" fill="#3f2620" />
|
|
213
|
+
<Path d="M138 96 q6 18 -4 30 q-8 -10 -6 -26 Z" fill="#3f2620" />
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{/* Cabeça (Fundo) */}
|
|
218
|
+
<Circle cx={100} cy={88} r={40} fill="#51332d" />
|
|
219
|
+
|
|
220
|
+
{/* Bochechas */}
|
|
221
|
+
<Circle cx={74} cy={101} r={6} fill="#f2974b" opacity={0.4} />
|
|
222
|
+
<Circle cx={126} cy={101} r={6} fill="#f2974b" opacity={0.4} />
|
|
223
|
+
|
|
224
|
+
{/* Expressão Facial */}
|
|
225
|
+
{renderFace()}
|
|
226
|
+
|
|
227
|
+
{/* Antena / Chapéu superior */}
|
|
228
|
+
<Ellipse cx={100} cy={54} rx={33} ry={13} fill="#f2974b" />
|
|
229
|
+
<Ellipse cx={100} cy={50} rx={30} ry={10} fill="#ff9f57" />
|
|
230
|
+
<Circle cx={100} cy={42} r={3.6} fill="#f2974b" />
|
|
231
|
+
</Svg>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// eslint-disable-next-line storybook/no-renderer-packages
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { View } from 'react-native';
|
|
5
|
+
import { BottomTabBar } from './BottomTabBar';
|
|
6
|
+
import { theme } from '../../../styles/theme';
|
|
7
|
+
import { Home, BookOpen, Trophy, User, PlusCircle } from 'lucide-react-native';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Components/Navigation/BottomTabBar',
|
|
11
|
+
component: BottomTabBar,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'padded',
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component: 'A **BottomTabBar** é a barra de navegação principal da aplicação, fixa no fundo do ecrã.\n\nContém as secções vitais (Aprender, Revisão, Social, Perfil) e integra animações de escala quando um botão é pressionado.\n\n### Exemplo de Uso\n```tsx\nconst [activeTab, setActiveTab] = useState("home");\n\nreturn (\n <BottomTabBar \n currentTab={activeTab}\n onTabPress={setActiveTab}\n safeAreaBottom={20}\n />\n)\n```',
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
tags: ['autodocs'],
|
|
21
|
+
decorators: [
|
|
22
|
+
(Story) => (
|
|
23
|
+
<View style={{ flex: 1, backgroundColor: theme.colors.cream, justifyContent: 'flex-end', minHeight: 200 }}>
|
|
24
|
+
<Story />
|
|
25
|
+
</View>
|
|
26
|
+
),
|
|
27
|
+
],
|
|
28
|
+
} satisfies Meta<any>;
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<any>;
|
|
32
|
+
|
|
33
|
+
const defaultTabs = [
|
|
34
|
+
{ id: 'inicio', label: 'Início', icon: Home },
|
|
35
|
+
{ id: 'passaporte', label: 'Passaporte', icon: BookOpen },
|
|
36
|
+
{ id: 'aulas', label: 'Aulas', icon: Trophy },
|
|
37
|
+
{ id: 'perfil', label: 'Perfil', icon: User },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const fiveTabs = [
|
|
41
|
+
...defaultTabs,
|
|
42
|
+
{ id: 'extra', label: 'Extra Tab', icon: PlusCircle },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const InteractiveTabBar = ({ tabs, safeAreaBottom }: { tabs: any[], safeAreaBottom?: number }) => {
|
|
46
|
+
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<BottomTabBar
|
|
50
|
+
tabs={tabs}
|
|
51
|
+
activeTabId={activeTab}
|
|
52
|
+
onTabChange={setActiveTab}
|
|
53
|
+
safeAreaBottom={safeAreaBottom}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const DefaultMobile: Story = {
|
|
59
|
+
render: () => <InteractiveTabBar tabs={defaultTabs} />,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const WithSafeAreaInset: Story = {
|
|
63
|
+
render: () => <InteractiveTabBar tabs={defaultTabs} safeAreaBottom={34} />,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const ScalabilityFiveTabs: Story = {
|
|
67
|
+
render: () => <InteractiveTabBar tabs={fiveTabs} />,
|
|
68
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { BottomTabBarItem } from './BottomTabBarItem';
|
|
4
|
+
import { theme } from '../../../styles/theme';
|
|
5
|
+
import { useResponsive } from '../../../hooks/useResponsive';
|
|
6
|
+
|
|
7
|
+
export interface TabConfig {
|
|
8
|
+
/** Identificador único do separador */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Texto a mostrar abaixo do ícone */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Componente do ícone (ex: da biblioteca lucide-react-native) */
|
|
13
|
+
icon: React.ElementType;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Barra de Navegação Inferior (Bottom Tab Bar)
|
|
18
|
+
* Renderiza ícones com labels para navegação primária da app.
|
|
19
|
+
*/
|
|
20
|
+
export interface BottomTabBarProps {
|
|
21
|
+
/** Array de configurações dos separadores a renderizar */
|
|
22
|
+
tabs: TabConfig[];
|
|
23
|
+
/** O id do separador atualmente selecionado */
|
|
24
|
+
activeTabId: string;
|
|
25
|
+
/** Callback executado quando o utilizador clica num separador */
|
|
26
|
+
onTabChange: (id: string) => void;
|
|
27
|
+
/** Valor numérico correspondente ao safe-area-inset-bottom (útil para iOS home indicator) */
|
|
28
|
+
safeAreaBottom?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* O componente principal para a barra de navegação inferior (mobile).
|
|
33
|
+
*
|
|
34
|
+
* É totalmente flexível e ajusta-se automaticamente ao número de separadores inseridos.
|
|
35
|
+
* Para funcionar corretamente em dispositivos com ecrãs curvos ou barra inicial (ex: iPhone),
|
|
36
|
+
* deve receber o tamanho da `safeAreaBottom`.
|
|
37
|
+
*/
|
|
38
|
+
export const BottomTabBar = ({
|
|
39
|
+
tabs,
|
|
40
|
+
activeTabId,
|
|
41
|
+
onTabChange,
|
|
42
|
+
safeAreaBottom = 0,
|
|
43
|
+
}: BottomTabBarProps) => {
|
|
44
|
+
const { scale } = useResponsive();
|
|
45
|
+
|
|
46
|
+
const baseHeight = scale(56);
|
|
47
|
+
const totalHeight = baseHeight + safeAreaBottom;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View
|
|
51
|
+
style={[
|
|
52
|
+
styles.container,
|
|
53
|
+
{
|
|
54
|
+
height: totalHeight,
|
|
55
|
+
paddingBottom: safeAreaBottom,
|
|
56
|
+
paddingHorizontal: scale(6),
|
|
57
|
+
}
|
|
58
|
+
]}
|
|
59
|
+
accessibilityRole="tablist"
|
|
60
|
+
>
|
|
61
|
+
{tabs.map((tab) => (
|
|
62
|
+
<BottomTabBarItem
|
|
63
|
+
key={tab.id}
|
|
64
|
+
id={tab.id}
|
|
65
|
+
label={tab.label}
|
|
66
|
+
icon={tab.icon}
|
|
67
|
+
isActive={activeTabId === tab.id}
|
|
68
|
+
onPress={onTabChange}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const styles = StyleSheet.create({
|
|
76
|
+
container: {
|
|
77
|
+
flexDirection: 'row',
|
|
78
|
+
justifyContent: 'space-around',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
backgroundColor: theme.colors.white,
|
|
81
|
+
borderTopWidth: 1.5,
|
|
82
|
+
borderTopColor: theme.colors.brown[100],
|
|
83
|
+
width: '100%',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View, StyleSheet } from 'react-native';
|
|
3
|
+
import { theme } from '../../../styles/theme';
|
|
4
|
+
import { useResponsive } from '../../../hooks/useResponsive';
|
|
5
|
+
import { Typography } from '../../Typography/Typography';
|
|
6
|
+
|
|
7
|
+
export interface BottomTabBarItemProps {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon: React.ElementType;
|
|
11
|
+
isActive: boolean;
|
|
12
|
+
onPress: (id: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const BottomTabBarItem = ({
|
|
16
|
+
id,
|
|
17
|
+
label,
|
|
18
|
+
icon: IconComponent,
|
|
19
|
+
isActive,
|
|
20
|
+
onPress,
|
|
21
|
+
}: BottomTabBarItemProps) => {
|
|
22
|
+
const { scale, scaleText } = useResponsive();
|
|
23
|
+
|
|
24
|
+
const color = isActive ? theme.colors.primary : theme.colors.brown[300];
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Pressable
|
|
28
|
+
onPress={() => onPress(id)}
|
|
29
|
+
style={styles.container}
|
|
30
|
+
accessibilityRole="tab"
|
|
31
|
+
accessibilityState={{ selected: isActive }}
|
|
32
|
+
>
|
|
33
|
+
<View style={styles.iconContainer}>
|
|
34
|
+
<IconComponent stroke={color} size={scale(24)} strokeWidth={2} />
|
|
35
|
+
</View>
|
|
36
|
+
<Typography
|
|
37
|
+
style={[
|
|
38
|
+
styles.label,
|
|
39
|
+
{
|
|
40
|
+
color,
|
|
41
|
+
fontSize: scaleText(10),
|
|
42
|
+
marginTop: scale(4),
|
|
43
|
+
}
|
|
44
|
+
]}
|
|
45
|
+
numberOfLines={1}
|
|
46
|
+
>
|
|
47
|
+
{label}
|
|
48
|
+
</Typography>
|
|
49
|
+
</Pressable>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const styles = StyleSheet.create({
|
|
54
|
+
container: {
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
flex: 1, // To distribute space evenly (space-around effectively works well if all flex:1)
|
|
58
|
+
height: '100%',
|
|
59
|
+
},
|
|
60
|
+
iconContainer: {
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'center',
|
|
63
|
+
},
|
|
64
|
+
label: {
|
|
65
|
+
|
|
66
|
+
textAlign: 'center',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// eslint-disable-next-line storybook/no-renderer-packages
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { View, Button } from 'react-native';
|
|
5
|
+
import { PaginationDots } from './PaginationDots';
|
|
6
|
+
import { theme } from '../../../styles/theme';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/Navigation/PaginationDots',
|
|
10
|
+
component: PaginationDots,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'padded',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component: 'O **PaginationDots** é o indicador de posição para listas horizontais (como tutoriais de Onboarding ou carrosséis de imagens).\n\nEle suporta uma animação fluida (usando `react-native-reanimated`) onde o ponto ativo se expande e contrai em forma de "pílula" ao transitar entre páginas.\n\n### Exemplo de Uso\n```tsx\n<PaginationDots \n totalPages={4}\n currentPage={1}\n/>\n```',
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
tags: ['autodocs'],
|
|
20
|
+
decorators: [
|
|
21
|
+
(Story) => (
|
|
22
|
+
<View style={{ padding: 40, backgroundColor: theme.colors.cream }}>
|
|
23
|
+
<Story />
|
|
24
|
+
</View>
|
|
25
|
+
),
|
|
26
|
+
],
|
|
27
|
+
} satisfies Meta<any>;
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<any>;
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
args: { totalPages: 4, currentPage: 1 } as any,
|
|
34
|
+
render: (args: any) => <PaginationDots {...args} />,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const InteractiveDots = ({ totalPages }: { totalPages: number }) => {
|
|
38
|
+
const [currentPage, setCurrentStep] = useState(0);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View style={{ alignItems: 'center' }}>
|
|
42
|
+
<PaginationDots totalPages={totalPages} currentPage={currentPage} />
|
|
43
|
+
|
|
44
|
+
<View style={{ flexDirection: 'row', gap: 20, marginTop: 20 }}>
|
|
45
|
+
<Button
|
|
46
|
+
title="Prev"
|
|
47
|
+
onPress={() => setCurrentStep(prev => Math.max(0, prev - 1))}
|
|
48
|
+
disabled={currentPage === 0}
|
|
49
|
+
/>
|
|
50
|
+
<Button
|
|
51
|
+
title="Next"
|
|
52
|
+
onPress={() => setCurrentStep(prev => Math.min(totalPages - 1, prev + 1))}
|
|
53
|
+
disabled={currentPage === totalPages - 1}
|
|
54
|
+
/>
|
|
55
|
+
</View>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Interactive: Story = {
|
|
61
|
+
render: () => <InteractiveDots totalPages={4} />,
|
|
62
|
+
};
|