alouette 20.0.0 → 20.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -3
- package/skills/alouette-actions/SKILL.md +151 -0
- package/skills/alouette-animation/SKILL.md +214 -0
- package/skills/alouette-external-links/SKILL.md +108 -0
- package/skills/alouette-feedback/SKILL.md +133 -0
- package/skills/alouette-forms/SKILL.md +140 -0
- package/skills/alouette-icons/SKILL.md +122 -0
- package/skills/alouette-layout/SKILL.md +189 -0
- package/skills/alouette-layout/references/spacing-radius-shadow.md +53 -0
- package/skills/alouette-responsive/SKILL.md +155 -0
- package/skills/alouette-setup/SKILL.md +299 -0
- package/skills/alouette-theming/SKILL.md +208 -0
- package/skills/alouette-theming/references/tokens.md +74 -0
- package/skills/alouette-typography/SKILL.md +151 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alouette",
|
|
3
|
-
"version": "20.0.
|
|
3
|
+
"version": "20.0.1",
|
|
4
4
|
"description": "A modern, customizable design system built on top of NativeWind v5 with configurable defaults",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tanstack-intent"
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
|
21
21
|
"dist",
|
|
22
|
-
"metro.cjs"
|
|
22
|
+
"metro.cjs",
|
|
23
|
+
"skills"
|
|
23
24
|
],
|
|
24
25
|
"type": "module",
|
|
25
26
|
"sideEffects": false,
|
|
@@ -47,6 +48,10 @@
|
|
|
47
48
|
"import": "./dist/index-browser.es.js"
|
|
48
49
|
}
|
|
49
50
|
},
|
|
51
|
+
"./global.css": {
|
|
52
|
+
"style": "./src/global.css",
|
|
53
|
+
"default": "./src/global.css"
|
|
54
|
+
},
|
|
50
55
|
"./metro.cjs": "./metro.cjs"
|
|
51
56
|
},
|
|
52
57
|
"dependencies": {
|
|
@@ -135,7 +140,8 @@
|
|
|
135
140
|
}
|
|
136
141
|
],
|
|
137
142
|
"extraEntries": [
|
|
138
|
-
"metro.cjs"
|
|
143
|
+
"metro.cjs",
|
|
144
|
+
"global.css"
|
|
139
145
|
],
|
|
140
146
|
"jsx": true,
|
|
141
147
|
"typescript": true
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: alouette-actions
|
|
3
|
+
description: >
|
|
4
|
+
Buttons (Button, IconButton, ExternalLinkButton, InternalLinkButton) and
|
|
5
|
+
pressable surfaces (PressableBox, PressableListItem). variant is
|
|
6
|
+
contained|outlined|ghost, size is sm|md, accent defaults to brand. Button
|
|
7
|
+
label is the required text prop (not children); IconButton
|
|
8
|
+
requires aria-label. Interactive hover/focus/active/disabled states are built
|
|
9
|
+
in. Load when adding buttons or custom pressable elements.
|
|
10
|
+
type: core
|
|
11
|
+
library: alouette
|
|
12
|
+
library_version: "20.0.0"
|
|
13
|
+
requires:
|
|
14
|
+
- alouette-theming
|
|
15
|
+
sources:
|
|
16
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/actions/Button.tsx"
|
|
17
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/actions/IconButton.tsx"
|
|
18
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/data/PressableBox.tsx"
|
|
19
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/data/PressableListItem.tsx"
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
This skill builds on alouette-theming. Read it first for the accent model.
|
|
23
|
+
|
|
24
|
+
# alouette — Actions
|
|
25
|
+
|
|
26
|
+
Buttons and pressables carry interactive token states (hover/focus/active/
|
|
27
|
+
disabled) automatically. `variant` is `"contained" | "outlined" | "ghost"`;
|
|
28
|
+
`size` is `"sm" | "md"`; `accent` defaults to `"brand"`.
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Button } from "alouette";
|
|
34
|
+
import { CheckRegularIcon } from "alouette-icons/phosphor-icons/CheckRegularIcon";
|
|
35
|
+
|
|
36
|
+
<Button text="Save" icon={<CheckRegularIcon />} onPress={save} />;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Core Patterns
|
|
40
|
+
|
|
41
|
+
### Button variants and accents
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
<Button text="Save" /> {/* contained, brand */}
|
|
45
|
+
<Button variant="outlined" text="Cancel" />
|
|
46
|
+
<Button variant="ghost" text="Dismiss" />
|
|
47
|
+
<Button accent="danger" text="Delete" />
|
|
48
|
+
<Button size="sm" text="Small" />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Icon-only button
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { IconButton } from "alouette";
|
|
55
|
+
import { XRegularIcon } from "alouette-icons/phosphor-icons/XRegularIcon";
|
|
56
|
+
|
|
57
|
+
<IconButton icon={<XRegularIcon />} aria-label="Close" onPress={close} />;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`size` is `"sm" | "md"` or a number (custom diameter in px); `iconSize="fill"`
|
|
61
|
+
makes the icon take 80% of the button.
|
|
62
|
+
|
|
63
|
+
### Pressable surfaces
|
|
64
|
+
|
|
65
|
+
`PressableBox` is a themed, pressable container (`variant`, `accent`,
|
|
66
|
+
`forceStyle`). `PressableListItem` is a row with a trailing caret.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { PressableBox, PressableListItem, Text } from "alouette";
|
|
70
|
+
|
|
71
|
+
<PressableBox onPress={open}>
|
|
72
|
+
<Text>Custom card</Text>
|
|
73
|
+
</PressableBox>
|
|
74
|
+
|
|
75
|
+
<PressableListItem onPress={open}>
|
|
76
|
+
<Text>Row label</Text>
|
|
77
|
+
</PressableListItem>;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Link buttons
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { ExternalLinkButton, InternalLinkButton } from "alouette";
|
|
84
|
+
|
|
85
|
+
<ExternalLinkButton href="https://example.com" text="Open" />
|
|
86
|
+
<InternalLinkButton href="/settings" text="Settings" />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For full external-link control (in-app browser vs new tab), see
|
|
90
|
+
alouette-external-links/SKILL.md.
|
|
91
|
+
|
|
92
|
+
## Common Mistakes
|
|
93
|
+
|
|
94
|
+
### HIGH Passing the button label as children instead of text
|
|
95
|
+
|
|
96
|
+
Wrong:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<Button>Save</Button>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Correct:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<Button text="Save" />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`Button` renders its label from the required `text` prop (plus an optional `icon`
|
|
109
|
+
prop); children are ignored, so `<Button>Save</Button>` shows no label.
|
|
110
|
+
|
|
111
|
+
Source: packages/alouette/src/ui/actions/Button.tsx
|
|
112
|
+
|
|
113
|
+
### HIGH Using non-existent variant names
|
|
114
|
+
|
|
115
|
+
Wrong:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<Button variant="ghost-contained" text="Cancel" />
|
|
119
|
+
<Button variant="primary" text="Save" />
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Correct:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<Button variant="ghost" text="Cancel" />
|
|
126
|
+
<Button accent="brand" text="Save" />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`variant` is only `"contained" | "outlined" | "ghost"`; the accent is chosen via
|
|
130
|
+
the `accent` prop. (`ghost` is a variant value, not a separate boolean prop.)
|
|
131
|
+
|
|
132
|
+
Source: packages/alouette/src/ui/data/PressableBox.tsx, ui/actions/Button.tsx
|
|
133
|
+
|
|
134
|
+
### MEDIUM IconButton without aria-label
|
|
135
|
+
|
|
136
|
+
Wrong:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
<IconButton icon={<XRegularIcon />} onPress={close} />
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Correct:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
<IconButton icon={<XRegularIcon />} aria-label="Close" onPress={close} />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`IconButton` types `aria-label` as required because it has no text label;
|
|
149
|
+
omitting it is an accessibility failure and a type error.
|
|
150
|
+
|
|
151
|
+
Source: packages/alouette/src/ui/actions/IconButton.tsx
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: alouette-animation
|
|
3
|
+
description: >
|
|
4
|
+
Animate alouette UI two ways: NativeWind transitions (transition-*, duration-*,
|
|
5
|
+
ease-* on state changes like active:/hover:/focus:) and CSS keyframe presence
|
|
6
|
+
animations via PresenceOne / PresenceList (animate-slide-in/out,
|
|
7
|
+
animate-collapse-in/out). exitDurationMs must come from animationDurationsMs so
|
|
8
|
+
the unmount timer matches the keyframe. Both run on native via
|
|
9
|
+
react-native-reanimated. Load when adding transitions or enter/exit animations.
|
|
10
|
+
type: core
|
|
11
|
+
library: alouette
|
|
12
|
+
library_version: "20.0.0"
|
|
13
|
+
sources:
|
|
14
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/containers/Presence.tsx"
|
|
15
|
+
- "christophehurpeau/alouette:packages/alouette/src/animationDurationsMs.ts"
|
|
16
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/containers/Presence.stories.tsx"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# alouette — Animation
|
|
20
|
+
|
|
21
|
+
alouette animates with plain CSS — no animation library in your code — and on
|
|
22
|
+
native NativeWind v5 runs it through react-native-reanimated. There are two
|
|
23
|
+
mechanisms:
|
|
24
|
+
|
|
25
|
+
- **Transitions** — `transition-*` + `duration-*` + `ease-*` smooth a property
|
|
26
|
+
change driven by state (`active:`, `hover:`, `focus:`, theme/accent change).
|
|
27
|
+
- **Presence (keyframes)** — `PresenceOne` / `PresenceList` play enter/exit
|
|
28
|
+
keyframe animations on mount/unmount (`animate-slide-in/out`,
|
|
29
|
+
`animate-collapse-in/out`), with durations from `animationDurationsMs`.
|
|
30
|
+
|
|
31
|
+
## Transitions (state changes)
|
|
32
|
+
|
|
33
|
+
State-change transitions (`transition-*` + `duration-*` + `ease-*`) are already
|
|
34
|
+
built into alouette's interactive components. Use `InteractiveBox` (or
|
|
35
|
+
`PressableBox`, `Button`, `IconButton`) — they animate press/hover/focus
|
|
36
|
+
coherently (e.g. press scale plus background/border color) with the system's
|
|
37
|
+
duration and easing. Don't reach for a raw react-native `Pressable`.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { InteractiveBox, Text } from "alouette";
|
|
41
|
+
|
|
42
|
+
<InteractiveBox onPress={open} className="bg-surface rounded-sm p-m">
|
|
43
|
+
<Text>Animates on press / hover / focus</Text>
|
|
44
|
+
</InteractiveBox>;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For a custom transition, add `transition-*` utilities on an alouette component
|
|
48
|
+
that forwards `className` so it composes with the built-in ones:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<InteractiveBox className="transition-[background-color] duration-200 ease-in hover:bg-lowered" />
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Presence: swap a single child
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { PresenceOne, Box, Text, animationDurationsMs } from "alouette";
|
|
58
|
+
|
|
59
|
+
function Swap({ step }: { step: number }) {
|
|
60
|
+
return (
|
|
61
|
+
<Box className="relative h-24 w-64">
|
|
62
|
+
<PresenceOne
|
|
63
|
+
activeKey={step}
|
|
64
|
+
exitDurationMs={animationDurationsMs.slide}
|
|
65
|
+
enterClassName="animate-slide-in"
|
|
66
|
+
exitClassName="animate-slide-out"
|
|
67
|
+
className="absolute inset-0 flex-center bg-surface rounded-md"
|
|
68
|
+
>
|
|
69
|
+
<Box>
|
|
70
|
+
<Text className="font-heading-bold text-2xl">Step {step}</Text>
|
|
71
|
+
</Box>
|
|
72
|
+
</PresenceOne>
|
|
73
|
+
</Box>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Core Patterns
|
|
79
|
+
|
|
80
|
+
### Animated list (add / remove)
|
|
81
|
+
|
|
82
|
+
Each child must have a stable `key`. Adding a key animates that item in; removing
|
|
83
|
+
one animates only that item out before unmounting.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { PresenceList, InfoMessage, animationDurationsMs } from "alouette";
|
|
87
|
+
|
|
88
|
+
<PresenceList
|
|
89
|
+
exitDurationMs={animationDurationsMs.collapse}
|
|
90
|
+
enterClassName="animate-collapse-in"
|
|
91
|
+
exitClassName="animate-collapse-out"
|
|
92
|
+
className="overflow-hidden"
|
|
93
|
+
>
|
|
94
|
+
{items.map((item) => (
|
|
95
|
+
<InfoMessage
|
|
96
|
+
key={item.id}
|
|
97
|
+
onDismiss={() => remove(item.id)}
|
|
98
|
+
dismissIconAriaLabel="Dismiss"
|
|
99
|
+
>
|
|
100
|
+
{item.label}
|
|
101
|
+
</InfoMessage>
|
|
102
|
+
))}
|
|
103
|
+
</PresenceList>;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`PresenceOne` swaps a single child and merges the animation classes onto it via
|
|
107
|
+
`cloneElement` (no wrapper); `PresenceList` wraps each child in its own `View`.
|
|
108
|
+
|
|
109
|
+
## Common Mistakes
|
|
110
|
+
|
|
111
|
+
### HIGH Hand-rolling transitions on a raw Pressable
|
|
112
|
+
|
|
113
|
+
Wrong:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { Pressable } from "react-native";
|
|
117
|
+
<Pressable className="transition-[transform] duration-200 active:scale-[0.975]">
|
|
118
|
+
{children}
|
|
119
|
+
</Pressable>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Correct:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { InteractiveBox } from "alouette";
|
|
126
|
+
<InteractiveBox onPress={open}>{children}</InteractiveBox>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
alouette's interactive components already bundle press/hover/focus transitions
|
|
130
|
+
with consistent duration and easing, plus disabled and focus-visible handling.
|
|
131
|
+
Re-implementing them on a bare `Pressable` drifts from the system and misses
|
|
132
|
+
that behavior.
|
|
133
|
+
|
|
134
|
+
Source: packages/alouette/src/ui/containers/Box.tsx (InteractiveBox); ui/data/PressableBox.tsx
|
|
135
|
+
|
|
136
|
+
### HIGH Hardcoding exitDurationMs instead of animationDurationsMs
|
|
137
|
+
|
|
138
|
+
Wrong:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<PresenceOne activeKey={id} exitDurationMs={600}
|
|
142
|
+
enterClassName="animate-slide-in" exitClassName="animate-slide-out">
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Correct:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { animationDurationsMs } from "alouette";
|
|
149
|
+
<PresenceOne activeKey={id} exitDurationMs={animationDurationsMs.slide}
|
|
150
|
+
enterClassName="animate-slide-in" exitClassName="animate-slide-out">
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The unmount timer must equal the CSS keyframe duration. A hardcoded number drifts
|
|
154
|
+
from the framework value, cutting the exit animation short or leaving ghost nodes
|
|
155
|
+
mounted.
|
|
156
|
+
|
|
157
|
+
Source: packages/alouette/src/animationDurationsMs.ts; ui/containers/Presence.stories.tsx
|
|
158
|
+
|
|
159
|
+
### HIGH PresenceList children without stable keys
|
|
160
|
+
|
|
161
|
+
Wrong:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
{items.map((it, i) => <InfoMessage key={i}>{it}</InfoMessage>)}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Correct:
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
{items.map((it) => <InfoMessage key={it.id}>{it.label}</InfoMessage>)}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`PresenceList` diffs children by key to play add/remove animations. Index keys
|
|
174
|
+
make removed items jump or skip their exit animation.
|
|
175
|
+
|
|
176
|
+
Source: packages/alouette/src/ui/containers/Presence.tsx (usePresenceList)
|
|
177
|
+
|
|
178
|
+
### MEDIUM PresenceOne child that doesn't forward className
|
|
179
|
+
|
|
180
|
+
Wrong:
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
<PresenceOne activeKey={id} exitDurationMs={animationDurationsMs.slide}>
|
|
184
|
+
<>{content}</>
|
|
185
|
+
</PresenceOne>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Correct:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
<PresenceOne activeKey={id} exitDurationMs={animationDurationsMs.slide}
|
|
192
|
+
enterClassName="animate-slide-in" exitClassName="animate-slide-out">
|
|
193
|
+
<Box>{content}</Box>
|
|
194
|
+
</PresenceOne>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`PresenceOne` merges the animation classes onto the child via `cloneElement`. A
|
|
198
|
+
Fragment or a component that drops `className` gets no animation; use an alouette
|
|
199
|
+
component (or one that forwards `className` to its root view).
|
|
200
|
+
|
|
201
|
+
Source: packages/alouette/src/ui/containers/Presence.tsx (PresenceOne)
|
|
202
|
+
|
|
203
|
+
### HIGH Expecting animations to run on native without reanimated
|
|
204
|
+
|
|
205
|
+
Wrong: relying on the CSS animation while react-native-reanimated (and the
|
|
206
|
+
`react-native-worklets/plugin` babel plugin) are not installed.
|
|
207
|
+
|
|
208
|
+
Correct: install `react-native-reanimated` and add the worklets babel plugin, so
|
|
209
|
+
NativeWind can run CSS transitions/animations on device.
|
|
210
|
+
|
|
211
|
+
NativeWind v5 drives native animations through reanimated; without it, animations
|
|
212
|
+
silently no-op on iOS/Android while still working on web.
|
|
213
|
+
|
|
214
|
+
Source: packages/storybook-native-app/babel.config.js; packages/alouette/package.json (peerDependencies)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: alouette-external-links
|
|
3
|
+
description: >
|
|
4
|
+
Open external URLs with ExternalLink (wraps expo-web-browser / Linking) or
|
|
5
|
+
ExternalLinkButton, configuring openLinkBehavior per platform (native:
|
|
6
|
+
linking|webBrowser; web: targetBlank|targetSelf). Load when linking out to
|
|
7
|
+
external URLs from alouette UI.
|
|
8
|
+
type: composition
|
|
9
|
+
library: alouette
|
|
10
|
+
library_version: "20.0.0"
|
|
11
|
+
requires:
|
|
12
|
+
- alouette-actions
|
|
13
|
+
sources:
|
|
14
|
+
- "christophehurpeau/alouette:packages/alouette/src/expo/ExternalLink.tsx"
|
|
15
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/actions/Button.tsx"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
This skill builds on alouette-actions. Read it first for button props.
|
|
19
|
+
|
|
20
|
+
# alouette + expo-web-browser — External links
|
|
21
|
+
|
|
22
|
+
`ExternalLink` centralizes outbound-URL behavior per platform: an in-app browser
|
|
23
|
+
sheet (expo-web-browser) or `Linking` on native, and a new tab or same tab on
|
|
24
|
+
web. `ExternalLinkButton` is the button shortcut for the common case.
|
|
25
|
+
|
|
26
|
+
`ExternalOpenLinkBehavior`:
|
|
27
|
+
`{ native: "linking" | "webBrowser"; web: "targetBlank" | "targetSelf" }`.
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { ExternalLinkButton } from "alouette";
|
|
33
|
+
|
|
34
|
+
<ExternalLinkButton href="https://example.com" text="Open docs" />;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Core Patterns
|
|
38
|
+
|
|
39
|
+
### Wrap any component with explicit per-platform behavior
|
|
40
|
+
|
|
41
|
+
`ExternalLink` takes the target component via `as` and forwards remaining props.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { ExternalLink, Button } from "alouette";
|
|
45
|
+
|
|
46
|
+
<ExternalLink
|
|
47
|
+
as={Button}
|
|
48
|
+
href="https://example.com"
|
|
49
|
+
openLinkBehavior={{ native: "webBrowser", web: "targetBlank" }}
|
|
50
|
+
text="Open"
|
|
51
|
+
/>;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `native: "webBrowser"` opens an in-app browser sheet themed with alouette colors.
|
|
55
|
+
- `native: "linking"` hands off to the OS browser via `Linking.openURL`.
|
|
56
|
+
- `web: "targetBlank"` opens a new tab; `"targetSelf"` navigates in place.
|
|
57
|
+
|
|
58
|
+
## Common Mistakes
|
|
59
|
+
|
|
60
|
+
### MEDIUM Calling Linking.openURL directly
|
|
61
|
+
|
|
62
|
+
Wrong:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { Linking } from "react-native";
|
|
66
|
+
<Pressable onPress={() => Linking.openURL(href)} />
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Correct:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<ExternalLink
|
|
73
|
+
as={Button}
|
|
74
|
+
href={href}
|
|
75
|
+
openLinkBehavior={{ native: "webBrowser", web: "targetBlank" }}
|
|
76
|
+
text="Open"
|
|
77
|
+
/>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`ExternalLink` handles the in-app themed browser sheet on native and the correct
|
|
81
|
+
target behavior on web; calling `Linking` directly loses both.
|
|
82
|
+
|
|
83
|
+
Source: packages/alouette/src/expo/ExternalLink.tsx
|
|
84
|
+
|
|
85
|
+
### MEDIUM Omitting openLinkBehavior
|
|
86
|
+
|
|
87
|
+
Wrong:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<ExternalLink as={Button} href={href} text="Open" />
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Correct:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<ExternalLink
|
|
97
|
+
as={Button}
|
|
98
|
+
href={href}
|
|
99
|
+
openLinkBehavior={{ native: "linking", web: "targetSelf" }}
|
|
100
|
+
text="Open"
|
|
101
|
+
/>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`openLinkBehavior` is required and selects the native and web strategies; without
|
|
105
|
+
it the link can't decide how to open and throws on an unsupported branch.
|
|
106
|
+
(`ExternalLinkButton` defaults this for you — use it for the simple case.)
|
|
107
|
+
|
|
108
|
+
Source: packages/alouette/src/expo/ExternalLink.tsx
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: alouette-feedback
|
|
3
|
+
description: >
|
|
4
|
+
Semantic message banners: Message (requires accent + icon) and the presets
|
|
5
|
+
InfoMessage, ConfirmationMessage, WarningMessage. Optional dismiss requires
|
|
6
|
+
onDismiss and dismissIconAriaLabel together; size is sm/md/lg. Load when
|
|
7
|
+
showing inline status, alerts, or dismissible notices.
|
|
8
|
+
type: core
|
|
9
|
+
library: alouette
|
|
10
|
+
library_version: "20.0.0"
|
|
11
|
+
requires:
|
|
12
|
+
- alouette-theming
|
|
13
|
+
sources:
|
|
14
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/feedback/Message.tsx"
|
|
15
|
+
- "christophehurpeau/alouette:packages/alouette/src/ui/feedback/Message.stories.tsx"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
This skill builds on alouette-theming. Read it first for the accent model.
|
|
19
|
+
|
|
20
|
+
# alouette — Feedback messages
|
|
21
|
+
|
|
22
|
+
`Message` is an accent-themed banner with an icon and optional dismiss button.
|
|
23
|
+
Prefer the presets `InfoMessage` / `ConfirmationMessage` / `WarningMessage`,
|
|
24
|
+
which set the accent and icon for you.
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { InfoMessage } from "alouette";
|
|
30
|
+
|
|
31
|
+
<InfoMessage>Your changes were saved.</InfoMessage>;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core Patterns
|
|
35
|
+
|
|
36
|
+
### Presets
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { InfoMessage, ConfirmationMessage, WarningMessage } from "alouette";
|
|
40
|
+
|
|
41
|
+
<InfoMessage>Heads up.</InfoMessage> {/* accent="info" */}
|
|
42
|
+
<ConfirmationMessage>Saved.</ConfirmationMessage> {/* accent="success" */}
|
|
43
|
+
<WarningMessage>Careful.</WarningMessage> {/* accent="warning" */}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Dismissible message
|
|
47
|
+
|
|
48
|
+
`onDismiss` and `dismissIconAriaLabel` must be provided together:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<WarningMessage onDismiss={hide} dismissIconAriaLabel="Dismiss">
|
|
52
|
+
Low disk space.
|
|
53
|
+
</WarningMessage>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Custom Message (other accents / icons)
|
|
57
|
+
|
|
58
|
+
For accents without a preset (e.g. `danger`), use the base `Message` with an
|
|
59
|
+
explicit `accent` and `icon`. `size` is `"sm" | "md" | "lg"`.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { Message } from "alouette";
|
|
63
|
+
import { XCircleRegularIcon } from "alouette-icons/phosphor-icons/XCircleRegularIcon";
|
|
64
|
+
|
|
65
|
+
<Message accent="danger" icon={<XCircleRegularIcon />} size="lg">
|
|
66
|
+
Payment failed.
|
|
67
|
+
</Message>;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Common Mistakes
|
|
71
|
+
|
|
72
|
+
### MEDIUM onDismiss without dismissIconAriaLabel
|
|
73
|
+
|
|
74
|
+
Wrong:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<InfoMessage onDismiss={hide}>Saved</InfoMessage>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Correct:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<InfoMessage onDismiss={hide} dismissIconAriaLabel="Dismiss">
|
|
84
|
+
Saved
|
|
85
|
+
</InfoMessage>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`Message` types `onDismiss` and `dismissIconAriaLabel` as a required pair;
|
|
89
|
+
providing one without the other is a type error and leaves the dismiss button
|
|
90
|
+
without an accessible label.
|
|
91
|
+
|
|
92
|
+
Source: packages/alouette/src/ui/feedback/Message.tsx
|
|
93
|
+
|
|
94
|
+
### MEDIUM Rendering the base Message without accent/icon
|
|
95
|
+
|
|
96
|
+
Wrong:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<Message>Heads up</Message>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Correct:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<WarningMessage>Heads up</WarningMessage>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The base `Message` requires both `accent` and `icon`. For the common cases use a
|
|
109
|
+
preset, which sets them.
|
|
110
|
+
|
|
111
|
+
Source: packages/alouette/src/ui/feedback/Message.tsx
|
|
112
|
+
|
|
113
|
+
### MEDIUM Hand-building a colored banner instead of Message
|
|
114
|
+
|
|
115
|
+
Wrong:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<Box className="bg-yellow-100"><Text>Warning</Text></Box>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Correct:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<WarningMessage>Warning</WarningMessage>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A manual banner loses accent theming, the leading icon, dark-mode support and
|
|
128
|
+
the dismiss a11y that `Message` provides.
|
|
129
|
+
|
|
130
|
+
Source: packages/alouette/src/ui/feedback/Message.tsx
|
|
131
|
+
|
|
132
|
+
See also: alouette-animation/SKILL.md — render messages in PresenceList for
|
|
133
|
+
animated add/remove.
|