trunative 1.0.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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/cli.js +105 -0
- package/dist/commands/build.js +35 -0
- package/dist/commands/detect.js +91 -0
- package/dist/commands/doctor.js +110 -0
- package/dist/commands/graph.js +106 -0
- package/dist/commands/install.js +76 -0
- package/dist/commands/lint.js +87 -0
- package/dist/commands/rubric.js +104 -0
- package/dist/commands/spec.js +300 -0
- package/dist/compile.js +235 -0
- package/dist/detect/rules.js +208 -0
- package/dist/detect/types.js +36 -0
- package/dist/emit.js +77 -0
- package/dist/graph.js +288 -0
- package/dist/heuristics.js +185 -0
- package/dist/lock.js +18 -0
- package/dist/mdx.js +84 -0
- package/dist/paths.js +62 -0
- package/dist/skill.js +69 -0
- package/package.json +51 -0
- package/src/skills/SKILL.md +125 -0
- package/src/skills/flow/build.md +57 -0
- package/src/skills/flow/firebase.md +102 -0
- package/src/skills/flow/init.md +116 -0
- package/src/skills/flow/review.md +186 -0
- package/src/skills/flow/spec.md +149 -0
- package/src/skills/heuristics/accessibility.md +124 -0
- package/src/skills/heuristics/ads.md +140 -0
- package/src/skills/heuristics/auth.md +130 -0
- package/src/skills/heuristics/background-work.md +129 -0
- package/src/skills/heuristics/buttons.md +99 -0
- package/src/skills/heuristics/camera.md +127 -0
- package/src/skills/heuristics/chat.md +125 -0
- package/src/skills/heuristics/colors.md +129 -0
- package/src/skills/heuristics/copy.md +157 -0
- package/src/skills/heuristics/data-display.md +124 -0
- package/src/skills/heuristics/feedback.md +122 -0
- package/src/skills/heuristics/forms.md +124 -0
- package/src/skills/heuristics/icons-and-imagery.md +135 -0
- package/src/skills/heuristics/layout.md +125 -0
- package/src/skills/heuristics/lists.md +129 -0
- package/src/skills/heuristics/localization.md +128 -0
- package/src/skills/heuristics/maps.md +129 -0
- package/src/skills/heuristics/media.md +130 -0
- package/src/skills/heuristics/motion.md +113 -0
- package/src/skills/heuristics/navigation.md +116 -0
- package/src/skills/heuristics/network.md +118 -0
- package/src/skills/heuristics/notifications.md +121 -0
- package/src/skills/heuristics/offline.md +124 -0
- package/src/skills/heuristics/onboarding.md +103 -0
- package/src/skills/heuristics/payments.md +138 -0
- package/src/skills/heuristics/performance.md +111 -0
- package/src/skills/heuristics/permissions.md +125 -0
- package/src/skills/heuristics/privacy-ui.md +112 -0
- package/src/skills/heuristics/scrolling.md +114 -0
- package/src/skills/heuristics/search.md +127 -0
- package/src/skills/heuristics/sense.md +128 -0
- package/src/skills/heuristics/settings.md +129 -0
- package/src/skills/heuristics/sharing.md +102 -0
- package/src/skills/heuristics/sound.md +95 -0
- package/src/skills/heuristics/splashscreen.md +111 -0
- package/src/skills/heuristics/states.md +120 -0
- package/src/skills/heuristics/touch.md +95 -0
- package/src/skills/heuristics/typography.md +99 -0
- package/src/skills/heuristics/updates.md +129 -0
- package/src/skills/heuristics/webviews.md +114 -0
- package/src/skills/heuristics/widgets.md +128 -0
- package/src/skills/references/capability-checks.md +59 -0
- package/src/skills/references/fonts.json +339 -0
- package/src/skills/references/icon-and-image-assets.md +103 -0
- package/src/skills/references/input-fields.md +82 -0
- package/src/skills/references/launch-surface.md +94 -0
- package/src/skills/references/motion-tokens.md +89 -0
- package/src/skills/references/navigation-containers.md +51 -0
- package/src/skills/references/search-controls.md +49 -0
- package/src/skills/references/type-scales.md +60 -0
- package/src/skills/references/wireframe-frame.md +209 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Typefaces reachable per platform and per stack, and what it costs to reach them. Lookup only: the rules are in heuristics/typography.md and the per-role sizes are in type-scales.md.",
|
|
3
|
+
"updated": "2026-09-06",
|
|
4
|
+
"sources": [
|
|
5
|
+
"https://developer.apple.com/fonts/",
|
|
6
|
+
"https://fonts.google.com",
|
|
7
|
+
"https://github.com/material-components/material-components-android"
|
|
8
|
+
],
|
|
9
|
+
"rules_of_thumb": [
|
|
10
|
+
"A face that ships with the OS costs no download, no cold start and no licence review. Start there and leave it only for a reason you can state.",
|
|
11
|
+
"A face that arrives without a reason is the tell. Inter unchosen is not a choice.",
|
|
12
|
+
"Two families is the ceiling, and the second one needs a job the first cannot do.",
|
|
13
|
+
"Whichever face wins, it reaches the screen through the platform text style, so the user's size setting still applies.",
|
|
14
|
+
"Before committing to a face for a localised app, check that it covers the scripts the product ships in. Coverage is per family, not per foundry."
|
|
15
|
+
],
|
|
16
|
+
"platforms": {
|
|
17
|
+
"ios": {
|
|
18
|
+
"ui_default": "SF Pro",
|
|
19
|
+
"licence": "SF Pro, SF Compact, SF Mono and New York are licensed by Apple for use on Apple platforms. They do not travel to an Android build or a website.",
|
|
20
|
+
"system_faces": [
|
|
21
|
+
{
|
|
22
|
+
"family": "SF Pro",
|
|
23
|
+
"kind": "sans",
|
|
24
|
+
"note": "the UI face. Optical sizes switch on their own: Text below 20pt, Display at 20pt and above"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"family": "SF Pro Rounded",
|
|
28
|
+
"kind": "sans",
|
|
29
|
+
"note": "softer register, same metrics"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"family": "SF Compact",
|
|
33
|
+
"kind": "sans",
|
|
34
|
+
"note": "narrower, drawn for cramped UI"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"family": "SF Mono",
|
|
38
|
+
"kind": "mono",
|
|
39
|
+
"note": "code and fixed-width data"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"family": "New York",
|
|
43
|
+
"kind": "serif",
|
|
44
|
+
"note": "the system serif, cut to pair with SF at the same optical sizes"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"preinstalled": {
|
|
48
|
+
"note": "Reachable by family name with no bundling. These are not the UI face and Apple does not guarantee them across OS versions, so confirm before depending on one.",
|
|
49
|
+
"sans": [
|
|
50
|
+
"Arial",
|
|
51
|
+
"Avenir",
|
|
52
|
+
"Avenir Next",
|
|
53
|
+
"Futura",
|
|
54
|
+
"Gill Sans",
|
|
55
|
+
"Helvetica",
|
|
56
|
+
"Helvetica Neue",
|
|
57
|
+
"Optima",
|
|
58
|
+
"Trebuchet MS",
|
|
59
|
+
"Verdana"
|
|
60
|
+
],
|
|
61
|
+
"serif": [
|
|
62
|
+
"American Typewriter",
|
|
63
|
+
"Baskerville",
|
|
64
|
+
"Bodoni 72",
|
|
65
|
+
"Charter",
|
|
66
|
+
"Cochin",
|
|
67
|
+
"Didot",
|
|
68
|
+
"Georgia",
|
|
69
|
+
"Hoefler Text",
|
|
70
|
+
"Palatino",
|
|
71
|
+
"Times New Roman"
|
|
72
|
+
],
|
|
73
|
+
"mono": [
|
|
74
|
+
"Courier New",
|
|
75
|
+
"Menlo"
|
|
76
|
+
],
|
|
77
|
+
"display_script": [
|
|
78
|
+
"Chalkboard SE",
|
|
79
|
+
"Copperplate",
|
|
80
|
+
"Marker Felt",
|
|
81
|
+
"Papyrus",
|
|
82
|
+
"Snell Roundhand",
|
|
83
|
+
"Zapfino"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"android": {
|
|
88
|
+
"ui_default": "Roboto on AOSP",
|
|
89
|
+
"licence": "Roboto and Noto are Apache-2.0 and can be bundled anywhere, including into an iOS build or a website.",
|
|
90
|
+
"warning": "The UI face is whatever the OEM shipped. Samsung One UI, OnePlus and MIUI each substitute their own, and the user can change it in settings. Roboto is the AOSP default, not a promise, so never let a layout depend on Roboto metrics.",
|
|
91
|
+
"system_aliases": {
|
|
92
|
+
"note": "How the bundled faces are actually reached, in XML or in Compose FontFamily.",
|
|
93
|
+
"sans-serif": "Roboto",
|
|
94
|
+
"sans-serif-condensed": "Roboto Condensed",
|
|
95
|
+
"sans-serif-medium": "Roboto at 500",
|
|
96
|
+
"sans-serif-light": "Roboto at 300, below the weight floor for body text",
|
|
97
|
+
"sans-serif-thin": "Roboto at 100, display only, and rarely even then",
|
|
98
|
+
"serif": "Noto Serif",
|
|
99
|
+
"monospace": "Droid Sans Mono",
|
|
100
|
+
"serif-monospace": "Cutive Mono",
|
|
101
|
+
"casual": "Coming Soon",
|
|
102
|
+
"cursive": "Dancing Script"
|
|
103
|
+
},
|
|
104
|
+
"other_bundled": [
|
|
105
|
+
{
|
|
106
|
+
"family": "Roboto Flex",
|
|
107
|
+
"note": "variable, full axis set. Bundled only on recent releases: check the API level before relying on it instead of shipping it"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"family": "Noto",
|
|
111
|
+
"note": "the script coverage behind the system. This is what renders text the UI face has no glyphs for"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"family": "Noto Color Emoji",
|
|
115
|
+
"note": "emoji come from here, and they are not a substitute for an icon set"
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"downloadable_fonts": {
|
|
119
|
+
"note": "The whole Google Fonts catalogue without bundling a file, served and cached by Google Play Services. This is the Android answer to a brand face on a size budget.",
|
|
120
|
+
"provider": "com.google.android.gms.fonts",
|
|
121
|
+
"xml": "<font-family app:fontProviderAuthority=\"com.google.android.gms.fonts\" app:fontProviderQuery=\"Manrope\" .../>",
|
|
122
|
+
"compose": "androidx.compose.ui.text.googlefonts.GoogleFont with GoogleFont.Provider",
|
|
123
|
+
"caution": "The first request can miss, and the device may have no Play Services at all. Always declare a bundled or system fallback, and never let the first screen wait on it."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"stacks": {
|
|
128
|
+
"flutter": {
|
|
129
|
+
"default": "Material widgets fall back to Roboto, which the framework carries; Cupertino widgets take the system face on Apple platforms.",
|
|
130
|
+
"custom": "Declare the family in pubspec.yaml, or use the google_fonts package.",
|
|
131
|
+
"caution": "google_fonts downloads at runtime by default, which means a fallback frame and a network dependency. For a face used on the first screen, bundle the file instead."
|
|
132
|
+
},
|
|
133
|
+
"react_native_expo": {
|
|
134
|
+
"default": "fontFamily: 'System' resolves to SF on iOS and to the OEM UI face on Android.",
|
|
135
|
+
"custom": "expo-font, or the @expo-google-fonts/<family> packages, which bundle the file.",
|
|
136
|
+
"caution": "Fonts load asynchronously. Hold the splash screen until they are ready, or the first paint ships in the fallback face and reflows in front of the user."
|
|
137
|
+
},
|
|
138
|
+
"swiftui_uikit": {
|
|
139
|
+
"default": ".font(.body) and the rest of the text styles.",
|
|
140
|
+
"custom": "Font.custom(_:size:relativeTo:) in SwiftUI, UIFontMetrics in UIKit. A custom face without one of those does not scale and does not ship."
|
|
141
|
+
},
|
|
142
|
+
"jetpack_compose": {
|
|
143
|
+
"default": "MaterialTheme.typography, which resolves to the platform face.",
|
|
144
|
+
"custom": "FontFamily from a bundled resource or from the Google Fonts provider, wired into a Typography object rather than applied per Text."
|
|
145
|
+
},
|
|
146
|
+
"mobile_web": {
|
|
147
|
+
"default": "system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
148
|
+
"custom": "Self-host the subset files and declare font-display: swap. Preload only the faces the first screen needs.",
|
|
149
|
+
"caution": "Loading from the Google Fonts CDN sends the visitor's IP to a third party, which has been ruled a problem under GDPR in at least one jurisdiction. Self-hosting removes the question and is usually faster anyway."
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"google_fonts": {
|
|
153
|
+
"note": "Open licence (OFL or Apache-2.0), free to bundle and subset. The catalogue is the practical answer for every stack except a pure Apple build, where the system face is already better integrated.",
|
|
154
|
+
"how_to_choose": [
|
|
155
|
+
"Pick the workhorse first: it carries body, labels and controls, and it needs every weight and the right scripts.",
|
|
156
|
+
"A display face is a second decision, and it only earns its place if the product has display-sized moments.",
|
|
157
|
+
"Prefer a variable family where the stack supports it: the weight axis interpolates, so a hierarchy step never lands on a cut that was never drawn.",
|
|
158
|
+
"Numeric columns need tabular figures. Most families here support the tnum feature, but confirm it before designing a table around it."
|
|
159
|
+
],
|
|
160
|
+
"interface": [
|
|
161
|
+
{
|
|
162
|
+
"family": "Inter",
|
|
163
|
+
"variable": true,
|
|
164
|
+
"scripts": "latin, cyrillic, greek",
|
|
165
|
+
"good_for": "neutral UI at small sizes",
|
|
166
|
+
"caution": "the default a model reaches for. Fine when chosen on purpose, a tell when it just appears"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"family": "Roboto",
|
|
170
|
+
"variable": true,
|
|
171
|
+
"scripts": "latin, cyrillic, greek",
|
|
172
|
+
"good_for": "matching the Android platform look deliberately"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"family": "Roboto Flex",
|
|
176
|
+
"variable": true,
|
|
177
|
+
"axes": "weight, width, optical size, grade and more",
|
|
178
|
+
"scripts": "latin, cyrillic, greek",
|
|
179
|
+
"good_for": "one file covering the whole ramp, including the M3 expressive axes"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"family": "Noto Sans",
|
|
183
|
+
"variable": true,
|
|
184
|
+
"scripts": "the widest coverage in the catalogue",
|
|
185
|
+
"good_for": "a product shipping in scripts other families drop"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"family": "Open Sans",
|
|
189
|
+
"variable": true,
|
|
190
|
+
"scripts": "latin, cyrillic, greek",
|
|
191
|
+
"good_for": "plain, legible, very broad language support"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"family": "Source Sans 3",
|
|
195
|
+
"variable": true,
|
|
196
|
+
"scripts": "latin, cyrillic, greek",
|
|
197
|
+
"good_for": "a neutral workhorse with a matching serif and mono"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"family": "IBM Plex Sans",
|
|
201
|
+
"variable": true,
|
|
202
|
+
"scripts": "latin, cyrillic, greek",
|
|
203
|
+
"good_for": "an engineered voice, with Serif, Mono and Condensed siblings"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"family": "Public Sans",
|
|
207
|
+
"variable": true,
|
|
208
|
+
"scripts": "latin",
|
|
209
|
+
"good_for": "civic and utility products that want no accent at all"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"family": "DM Sans",
|
|
213
|
+
"variable": true,
|
|
214
|
+
"scripts": "latin",
|
|
215
|
+
"good_for": "geometric product UI with slightly rounded forms"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"family": "Plus Jakarta Sans",
|
|
219
|
+
"variable": true,
|
|
220
|
+
"scripts": "latin",
|
|
221
|
+
"good_for": "product UI with a little more character than Inter"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"family": "Manrope",
|
|
225
|
+
"variable": true,
|
|
226
|
+
"scripts": "latin, cyrillic, greek",
|
|
227
|
+
"good_for": "semi-geometric UI, reads well at label sizes"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"family": "Figtree",
|
|
231
|
+
"variable": true,
|
|
232
|
+
"scripts": "latin",
|
|
233
|
+
"good_for": "friendly consumer apps"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"family": "Work Sans",
|
|
237
|
+
"variable": true,
|
|
238
|
+
"scripts": "latin",
|
|
239
|
+
"good_for": "a grotesque with warmth, good from label to headline"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"family": "Archivo",
|
|
243
|
+
"variable": true,
|
|
244
|
+
"axes": "weight and width",
|
|
245
|
+
"scripts": "latin, cyrillic, greek",
|
|
246
|
+
"good_for": "dense UI where a width axis solves a long-label problem"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"family": "Instrument Sans",
|
|
250
|
+
"variable": true,
|
|
251
|
+
"scripts": "latin",
|
|
252
|
+
"good_for": "neutral with an edge, holds up in both UI and headline roles"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"family": "Space Grotesk",
|
|
256
|
+
"variable": true,
|
|
257
|
+
"scripts": "latin",
|
|
258
|
+
"good_for": "a technical voice",
|
|
259
|
+
"caution": "distinctive enough that it tires at body length"
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
"reading_and_display": [
|
|
263
|
+
{
|
|
264
|
+
"family": "Literata",
|
|
265
|
+
"variable": true,
|
|
266
|
+
"kind": "serif",
|
|
267
|
+
"good_for": "long reading on a screen, which is what it was drawn for"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"family": "Source Serif 4",
|
|
271
|
+
"variable": true,
|
|
272
|
+
"kind": "serif",
|
|
273
|
+
"good_for": "body serif that pairs with Source Sans"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"family": "Newsreader",
|
|
277
|
+
"variable": true,
|
|
278
|
+
"kind": "serif",
|
|
279
|
+
"good_for": "editorial body and headline in one family"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"family": "Lora",
|
|
283
|
+
"variable": true,
|
|
284
|
+
"kind": "serif",
|
|
285
|
+
"good_for": "warm body serif for reading-heavy screens"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"family": "Fraunces",
|
|
289
|
+
"variable": true,
|
|
290
|
+
"kind": "serif display",
|
|
291
|
+
"axes": "weight, optical size, softness, wonk",
|
|
292
|
+
"good_for": "display with a real point of view",
|
|
293
|
+
"caution": "display sizes only"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"family": "Playfair Display",
|
|
297
|
+
"variable": true,
|
|
298
|
+
"kind": "serif display",
|
|
299
|
+
"good_for": "high contrast headlines",
|
|
300
|
+
"caution": "the thin strokes disappear at small sizes and in sunlight"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"family": "Instrument Serif",
|
|
304
|
+
"variable": false,
|
|
305
|
+
"kind": "serif display",
|
|
306
|
+
"good_for": "a single large moment per screen"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"family": "Bricolage Grotesque",
|
|
310
|
+
"variable": true,
|
|
311
|
+
"kind": "display",
|
|
312
|
+
"good_for": "an opinionated headline face that still has a usable weight range"
|
|
313
|
+
}
|
|
314
|
+
],
|
|
315
|
+
"mono": [
|
|
316
|
+
{
|
|
317
|
+
"family": "JetBrains Mono",
|
|
318
|
+
"variable": true,
|
|
319
|
+
"good_for": "code, with a tall x-height that survives small sizes"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"family": "IBM Plex Mono",
|
|
323
|
+
"variable": false,
|
|
324
|
+
"good_for": "code and data next to Plex Sans"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"family": "Roboto Mono",
|
|
328
|
+
"variable": true,
|
|
329
|
+
"good_for": "code and tabular data in an Android-native product"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"family": "Space Mono",
|
|
333
|
+
"variable": false,
|
|
334
|
+
"good_for": "short technical labels",
|
|
335
|
+
"caution": "not a reading face"
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Icon and image assets, per stack
|
|
2
|
+
|
|
3
|
+
Lookup only. The rules live in `heuristics/icons-and-imagery.md`. Open this file for one size, one axis or one API name, not as background reading.
|
|
4
|
+
|
|
5
|
+
## Symbol sets
|
|
6
|
+
|
|
7
|
+
| Set | Axes and variants | What it gives for free |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| SF Symbols (iOS) | 9 weights, ultralight to black, each mapped to a San Francisco font weight; 3 scales (small, medium, large) defined against the cap height; outline, fill, slash and enclosed variants | Baseline information on every symbol, Dynamic Type scaling when configured with a text style, and per-script variants that follow the device language |
|
|
10
|
+
| Material Symbols (Android, web) | One variable font in Outlined, Rounded and Sharp. `opsz` 20 to 48, default 24. `wght` 100 to 700, default 400. `GRAD` -50 to 200, default 0. `FILL` 0 to 1, default 0 | One file for every weight and fill; `FILL` is animatable for selection |
|
|
11
|
+
|
|
12
|
+
Axis meanings: `wght` is the stroke weight and moves the overall size a little. `GRAD` changes thickness more finely with almost no size change; -50 is the value for light artwork on a dark ground. `opsz` retunes the stroke so the glyph looks the same at a different size. `FILL` is for state.
|
|
13
|
+
|
|
14
|
+
Only the 20 px and 24 px Material Symbols are drawn on a perfect pixel grid.
|
|
15
|
+
|
|
16
|
+
Flutter's bundled `Icons` is the older Material Icons set, while its `Icon` widget already takes `fill`, `weight`, `grade` and `opticalSize`. A project that wants current Material Symbols imports the font or the SVGs itself.
|
|
17
|
+
|
|
18
|
+
`IconThemeData.fallback()` in Flutter: size 24.0, fill 0.0, weight 400.0, grade 0.0, opticalSize 48.0. Note the mismatch: the default optical size is 48 while the default size is 24. `applyTextScaling` resolves to false unless set on the widget or the `IconTheme`.
|
|
19
|
+
|
|
20
|
+
Compose `Icon` is 24.dp when the painter has no intrinsic size, and is tinted with `LocalContentColor.current`. The `material-icons` and `material-icons-extended` artifacts are no longer recommended by Google.
|
|
21
|
+
|
|
22
|
+
## Raster variants, by stack
|
|
23
|
+
|
|
24
|
+
| Stack | How a variant is named | Notes |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| iOS asset catalog | `@2x` and `@3x` filename suffixes | iOS ships at 2x and 3x. `scale` and `nativeScale` can differ |
|
|
27
|
+
| Android resources | `res/drawable-<bucket>/` | A vector drawable goes in the default `res/drawable/` with no per-density copy |
|
|
28
|
+
| Flutter | `2.0x/name.png` beside `name.png` | Nominal densities 1.5x, 2.0x, 3.0x, 4.0x. List only the main asset or its folder in `pubspec.yaml` |
|
|
29
|
+
| React Native | `name@2x.png` sibling files, one `require` | The closest density is picked when the exact one is missing. A `uri` source carries no dimensions |
|
|
30
|
+
|
|
31
|
+
## Android density buckets
|
|
32
|
+
|
|
33
|
+
| Bucket | Approx dpi | Scale | A 48 px mdpi bitmap becomes |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| ldpi | 120 | 0.75x | 36 px |
|
|
36
|
+
| mdpi | 160 (baseline) | 1x | 48 px |
|
|
37
|
+
| hdpi | 240 | 1.5x | 72 px |
|
|
38
|
+
| xhdpi | 320 | 2x | 96 px |
|
|
39
|
+
| xxhdpi | 480 | 3x | 144 px |
|
|
40
|
+
| xxxhdpi | 640 | 4x | 192 px |
|
|
41
|
+
|
|
42
|
+
Ratio 3:4:6:8:12:16. `px = dp * (dpi / 160)`, converted with `TypedValue.applyDimension()` rather than hardcoded. `nodpi` is never scaled. `sp` matches `dp` until the user changes the text size, and is never used for layout.
|
|
43
|
+
|
|
44
|
+
## Vector drawables
|
|
45
|
+
|
|
46
|
+
Android has no native SVG. Convert with Vector Asset Studio (`res` > New > Vector Asset). Keep one at 200 by 200 dp or under, past which it takes too long to draw. Author a tintable icon in solid black (`android:fillColor="#FF000000"`). `VectorDrawable` and `AnimatedVectorDrawable` land in API 21, with `VectorDrawableCompat` and `AnimatedVectorDrawableCompat` below it.
|
|
47
|
+
|
|
48
|
+
## Formats
|
|
49
|
+
|
|
50
|
+
Flat artwork that scales: PDF or SVG. Bitmap work: de-interlaced PNG, or an 8-bit palette where 24-bit colour is not needed. Photographs: JPEG or HEIC. Design at the lowest resolution and scale up, keeping control points on whole values so the shape stays on the raster grid at 2x and 3x.
|
|
51
|
+
|
|
52
|
+
## Fill and fit
|
|
53
|
+
|
|
54
|
+
| Stack | Fill the frame and crop | Fit inside the frame |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| SwiftUI | `.scaledToFill()` with `.clipped()` | `.scaledToFit()` |
|
|
57
|
+
| UIKit | `.scaleAspectFill` | `.scaleAspectFit` |
|
|
58
|
+
| Compose | `ContentScale.Crop` | `ContentScale.Fit` |
|
|
59
|
+
| Flutter | `BoxFit.cover` | `BoxFit.contain` |
|
|
60
|
+
| React Native | `resizeMode="cover"` | `resizeMode="contain"` |
|
|
61
|
+
| Mobile web | `object-fit: cover` | `object-fit: contain` |
|
|
62
|
+
|
|
63
|
+
## Dark variants
|
|
64
|
+
|
|
65
|
+
iOS: a second appearance inside the asset catalog entry, resolved by the system. Android: `res/drawable-night/` beside `res/drawable/`. Flutter and React Native resolve the file themselves from the platform brightness.
|
|
66
|
+
|
|
67
|
+
## App icon
|
|
68
|
+
|
|
69
|
+
| Platform | Canvas | Structure |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| iOS | 1024 by 1024 px, square, no transparency, no rounded corners | Layered: one background plus one or more foreground layers, assembled in Icon Composer. Prefer SVG or PDF layers; PNG only for mesh gradients and raster art. Ship layers unmasked |
|
|
72
|
+
| Android adaptive | 108 by 108 dp layers | `<adaptive-icon>` in `res/mipmap-anydpi-v26/ic_launcher.xml` with `<background>`, `<foreground>` and `<monochrome>`. Referenced from the manifest as `android:icon`, with `android:roundIcon` alongside it for the launchers that ask for a round variant |
|
|
73
|
+
| Play listing | 512 by 512 px, 32-bit PNG with alpha, 1024 KB maximum | Feature graphic 1024 by 500 px, JPEG or 24-bit PNG, no alpha |
|
|
74
|
+
|
|
75
|
+
Adaptive icon geometry: a 72 dp masked viewport out of the 108 dp canvas, with the outer 18 dp on each side reserved for masking and for parallax or pulse effects. Keep the mark inside the 66 dp safe box and at least 48 dp across. Layers carry no mask and no outline shadow, and vectors are preferred over bitmaps.
|
|
76
|
+
|
|
77
|
+
Themed icons need the `<monochrome>` layer. User theming arrives in Android 13 (API 33). From Android 16 QPR 2 the system themes icons for apps that supply no monochrome layer.
|
|
78
|
+
|
|
79
|
+
iOS appearances: default, dark, clear light, clear dark, tinted light, tinted dark. The system generates any variant not supplied, and each alternate app icon needs its own set. Colour spaces: sRGB, Gray Gamma 2.2, Display P3.
|
|
80
|
+
|
|
81
|
+
Let the system draw the specular highlight, the shadow between layers, the bevel, the blur and the glow. Avoid soft or feathered edges on foreground shapes, extremely thin strokes and sharp corners.
|
|
82
|
+
|
|
83
|
+
Licensing: system symbols may not be used in an app icon, a logo or any other trademarked use, and platform hardware may not be reproduced.
|
|
84
|
+
|
|
85
|
+
Expo keys: `icon`; `ios.icon` taking either a path to a `.icon` directory (SDK 54 and later) or an object of `light`, `dark` and `tinted` PNGs; `android.icon`; `android.adaptiveIcon.foregroundImage`, `.backgroundColor`, `.backgroundImage` and `.monochromeImage`.
|
|
86
|
+
|
|
87
|
+
## Generating the icon set
|
|
88
|
+
|
|
89
|
+
Every stack has a generator that takes one master image and writes every density, catalog entry and adaptive layer. Five things about them cost an afternoon each, and none of them surfaces as an error.
|
|
90
|
+
|
|
91
|
+
**The dedicated config file wins over the manifest block.** Where a generator reads configuration from two places, a file of its own at the project root and a block inside the dependency manifest, the file is tried first and the manifest block is only the fallback. `flutter_launcher_icons` loads `flutter_launcher_icons.yaml` and drops to the `pubspec.yaml` block only when that returns nothing, with no message either way. A project forked from another app carries that file with the *other app's* artwork paths, so the generator runs, reports success, and produces the wrong app's icon. Look for the dedicated file before writing any configuration, and when finished leave one of the two, not both.
|
|
92
|
+
|
|
93
|
+
**The generator applies its own inset, so the safe zone gets applied twice.** It wraps the foreground in an `<inset>` inside the adaptive icon XML, expressed as a percentage: `flutter_launcher_icons` writes `android:inset="16%"` unless `adaptive_icon_foreground_inset` says otherwise, which already accounts for most of the 108/72/66 dp geometry. Artwork pre-shrunk to the 66 dp safe box before handing it over comes out visibly small inside the mask. Either hand over full-bleed artwork and let the inset do the work, or raise the artwork's radius so that radius times the remaining fraction lands inside the safe box. The inset is configurable; editing the generated XML by hand is not, because the next run overwrites it.
|
|
94
|
+
|
|
95
|
+
**Measure the output, do not infer it.** Open the generated foreground, measure the bounding box of the non-transparent pixels, and compare its radius against the safe box. This is the only check that catches the double inset, and it takes one command.
|
|
96
|
+
|
|
97
|
+
**Rasterisers drop what the design tool shows.** Blend modes, filters and effects are commonly ignored when an SVG is converted to PNG, so the raster differs from the artboard. Open the generated file and look at it. When judging a transparent foreground, composite it over the real background colour first: white artwork on the viewer's white backdrop reads as an empty file, and the natural conclusion, that the conversion lost the shape, is wrong.
|
|
98
|
+
|
|
99
|
+
**Generators overwrite what they still produce and orphan the rest.** Switching an adaptive background from image to colour leaves the old background bitmap in every density bucket; dropping a platform leaves its whole directory. Nothing reports it, the files ship inside the binary, and the stale artwork resurfaces later. After changing artwork or configuration, delete the generated directories and regenerate, rather than generating over the top, then scan the output for the old palette to prove nothing survived.
|
|
100
|
+
|
|
101
|
+
## Loading APIs
|
|
102
|
+
|
|
103
|
+
Android decodes at the size drawn: `BitmapFactory.Options.inJustDecodeBounds` reads `outWidth` and `outHeight` without allocating, then `inSampleSize` decodes down. In practice a library does this: Glide, Coil (`AsyncImage`), Picasso or Fresco. Compose loads bundled assets with `painterResource`, which handles PNG, JPEG, WEBP, vector drawables and animated vector drawables. SwiftUI has `AsyncImage` for network images and `Image(decorative:)` for an unlabelled one.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Input fields
|
|
2
|
+
|
|
3
|
+
Lookup only. The rules live in `heuristics/forms.md`. Open this file for one field's keyboard or autofill name, not as background reading.
|
|
4
|
+
|
|
5
|
+
Two settings per field, and they are separate: the keyboard decides what the user can type, the content type decides what the platform can fill in for them. Setting one does not set the other.
|
|
6
|
+
|
|
7
|
+
## Keyboard type
|
|
8
|
+
|
|
9
|
+
| Field | SwiftUI `.keyboardType` | Compose `KeyboardType` | Flutter `TextInputType` | React Native `keyboardType` | Web `inputmode` |
|
|
10
|
+
|---|---|---|---|---|---|
|
|
11
|
+
| email | `.emailAddress` | `Email` | `.emailAddress` | `email-address` | `email` |
|
|
12
|
+
| telephone | `.phonePad` | `Phone` | `.phone` | `phone-pad` | `tel` |
|
|
13
|
+
| whole number | `.numberPad` | `Number` | `.number` | `number-pad` | `numeric` |
|
|
14
|
+
| money or measure | `.decimalPad` | `Decimal` | `.numberWithOptions(decimal: true)` | `decimal-pad` | `decimal` |
|
|
15
|
+
| URL | `.URL` | `Uri` | `.url` | `url` | `url` |
|
|
16
|
+
| search | `.webSearch` | `Text` | `.text` | `web-search` | `search` |
|
|
17
|
+
| password | `.default` | `Password` | `.visiblePassword` where shown | `default` | `text` |
|
|
18
|
+
| multi-line note | `.default` | `Text` | `.multiline` | `default` | `text` |
|
|
19
|
+
|
|
20
|
+
Card numbers and one time codes are numeric keyboards over a text field, never a number field. A number field brings steppers, drops leading zeros, and on the web turns a mistyped digit into a scroll event.
|
|
21
|
+
|
|
22
|
+
## Return key
|
|
23
|
+
|
|
24
|
+
| Meaning | SwiftUI | Compose | Flutter | React Native | Web |
|
|
25
|
+
|---|---|---|---|---|---|
|
|
26
|
+
| next field | `.submitLabel(.next)` | `ImeAction.Next` | `TextInputAction.next` | `returnKeyType="next"` | `enterkeyhint="next"` |
|
|
27
|
+
| last field | `.submitLabel(.done)` | `ImeAction.Done` | `TextInputAction.done` | `"done"` | `"done"` |
|
|
28
|
+
| submit now | `.submitLabel(.go)` | `ImeAction.Go` | `TextInputAction.go` | `"go"` | `"go"` |
|
|
29
|
+
| search | `.submitLabel(.search)` | `ImeAction.Search` | `TextInputAction.search` | `"search"` | `"search"` |
|
|
30
|
+
|
|
31
|
+
## Autofill content type
|
|
32
|
+
|
|
33
|
+
| Value | SwiftUI `.textContentType` | Compose `ContentType` | Flutter `AutofillHints` | React Native `autoComplete` | Web `autocomplete` |
|
|
34
|
+
|---|---|---|---|---|---|
|
|
35
|
+
| email | `.emailAddress` | `EmailAddress` | `.email` | `email` | `email` |
|
|
36
|
+
| username | `.username` | `Username` | `.username` | `username` | `username` |
|
|
37
|
+
| existing password | `.password` | `Password` | `.password` | `current-password` | `current-password` |
|
|
38
|
+
| new password | `.newPassword` | `NewPassword` | `.newPassword` | `new-password` | `new-password` |
|
|
39
|
+
| one time code | `.oneTimeCode` | `SmsOtpCode` | `.oneTimeCode` | `sms-otp` | `one-time-code` |
|
|
40
|
+
| full name | `.name` | `PersonFullName` | `.name` | `name` | `name` |
|
|
41
|
+
| given name | `.givenName` | `PersonFirstName` | `.givenName` | `given-name` | `given-name` |
|
|
42
|
+
| family name | `.familyName` | `PersonLastName` | `.familyName` | `family-name` | `family-name` |
|
|
43
|
+
| telephone | `.telephoneNumber` | `PhoneNumber` | `.telephoneNumber` | `tel` | `tel` |
|
|
44
|
+
| street | `.streetAddressLine1` | `AddressStreet` | `.streetAddressLine1` | `street-address` | `street-address` |
|
|
45
|
+
| city | `.addressCity` | `AddressLocality` | `.addressCity` | `postal-address-locality` | `address-level2` |
|
|
46
|
+
| postal code | `.postalCode` | `PostalCode` | `.postalCode` | `postal-code` | `postal-code` |
|
|
47
|
+
| country | `.countryName` | `AddressCountry` | `.countryName` | `country` | `country-name` |
|
|
48
|
+
| card number | `.creditCardNumber` | `CreditCardNumber` | `.creditCardNumber` | `cc-number` | `cc-number` |
|
|
49
|
+
| card expiry | `.creditCardExpiration` | `CreditCardExpirationDate` | `.creditCardExpirationDate` | `cc-exp` | `cc-exp` |
|
|
50
|
+
| security code | `.creditCardSecurityCode` | `CreditCardSecurityCode` | `.creditCardSecurityCode` | `cc-csc` | `cc-csc` |
|
|
51
|
+
|
|
52
|
+
On iOS, the same attribute is `textContentType` on `UITextField` and a prop of the same name in React Native, which is the one that drives fill on that platform.
|
|
53
|
+
|
|
54
|
+
On Android, the Compose semantics property landed in Compose 1.8; view layouts use `android:autofillHints` with the `AUTOFILL_HINT_*` string of the same meaning.
|
|
55
|
+
|
|
56
|
+
## Grouping and saving
|
|
57
|
+
|
|
58
|
+
A credential is filled and saved as a set, so the fields have to be declared as one.
|
|
59
|
+
|
|
60
|
+
- SwiftUI: fields in the same form are grouped by the system; submit ends the session.
|
|
61
|
+
- Compose: read `LocalAutofillManager` and call `commit()` when the form is submitted, or nothing is offered for saving.
|
|
62
|
+
- Flutter: wrap the fields in `AutofillGroup`, then call `TextInput.finishAutofillContext()` on submit.
|
|
63
|
+
- React Native: `importantForAutofill` on the container, plus the props above per field.
|
|
64
|
+
- Web: one `<form>` element around the fields, and a real submit.
|
|
65
|
+
|
|
66
|
+
## One time codes
|
|
67
|
+
|
|
68
|
+
One field, numeric keyboard, the one time code content type. The platform reads the message and offers the digits above the keyboard.
|
|
69
|
+
|
|
70
|
+
- iOS fills from Messages with no extra work once `.oneTimeCode` is set.
|
|
71
|
+
- Android reads the SMS through the SMS Retriever API, or the User Consent API where the message is not formatted for retrieval.
|
|
72
|
+
- Mobile web can additionally use the WebOTP API through `navigator.credentials.get()` with an `otp` request.
|
|
73
|
+
|
|
74
|
+
## Capitalisation and correction
|
|
75
|
+
|
|
76
|
+
| Field | Capitalisation | Autocorrect |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| email, username, password, code, URL | none | off |
|
|
79
|
+
| person or street name | words | off |
|
|
80
|
+
| free text, note, message | sentences | on |
|
|
81
|
+
|
|
82
|
+
The names of these settings: `.textInputAutocapitalization()` and `.autocorrectionDisabled()` in SwiftUI, `KeyboardOptions(capitalization =, autoCorrectEnabled =)` in Compose, `textCapitalization` and `autocorrect` in Flutter, `autoCapitalize` and `autoCorrect` in React Native, `autocapitalize` and `autocorrect` in HTML.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Launch surface configuration, per stack
|
|
2
|
+
|
|
3
|
+
Lookup only. The rules live in `heuristics/splashscreen.md`. Open this file for one key, one attribute or one dismissal API, not as background reading.
|
|
4
|
+
|
|
5
|
+
Nothing here is a design decision. The decision is in the heuristic; this is where the name of the knob lives.
|
|
6
|
+
|
|
7
|
+
## iOS: the two supported routes
|
|
8
|
+
|
|
9
|
+
Pick one. Both satisfy the same requirement.
|
|
10
|
+
|
|
11
|
+
| Route | Key | What it holds |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Property list | `UILaunchScreen` dictionary in `Info.plist` | `UIColorName` (background color), `UIImageName`, `UIImageRespectsSafeAreaInsets`, plus `UINavigationBar`, `UITabBar`, `UIToolbar` to draw empty bars |
|
|
14
|
+
| Interface file | `UILaunchStoryboardName` pointing at `LaunchScreen.storyboard` | UIKit views only |
|
|
15
|
+
| Per URL scheme | `UILaunchScreens` | one launch screen per scheme |
|
|
16
|
+
| Legacy | `UILaunchImages` | deprecated, do not add it |
|
|
17
|
+
|
|
18
|
+
The storyboard route is deliberately inert: one root `UIView` or `UIViewController`, UIKit classes only, no outlets, no actions, no custom classes, no user defined runtime attributes. Nothing in it executes.
|
|
19
|
+
|
|
20
|
+
The launch screen is required on iOS and iPadOS. No size, resolution or file weight limit is published for it, because the property list route takes a color and the storyboard route is constraint based.
|
|
21
|
+
|
|
22
|
+
A launch that never draws its first frame is killed by the system watchdog. The crash carries termination reason `SPRINGBOARD`, code `0x8badf00d`, and a `scene-create` watchdog event.
|
|
23
|
+
|
|
24
|
+
## Android: theme attributes
|
|
25
|
+
|
|
26
|
+
Two sets with the same job. The compat set has no `android:` prefix and is the one to use, because it produces the same surface back to older releases.
|
|
27
|
+
|
|
28
|
+
| Purpose | Platform (Android 12+) | Compat (`androidx.core:core-splashscreen`) |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| Window background, one opaque color | `android:windowSplashScreenBackground` | `windowSplashScreenBackground` |
|
|
31
|
+
| Centre icon | `android:windowSplashScreenAnimatedIcon` | `windowSplashScreenAnimatedIcon` |
|
|
32
|
+
| Icon animation duration | `android:windowSplashScreenAnimationDuration` | `windowSplashScreenAnimationDuration` |
|
|
33
|
+
| Circle behind the icon | `android:windowSplashScreenIconBackgroundColor` | `windowSplashScreenIconBackgroundColor` |
|
|
34
|
+
| Theme applied once the surface goes | (the activity theme) | `postSplashScreenTheme`, required |
|
|
35
|
+
| Icon size | (fixed) | `splashScreenIconSize` |
|
|
36
|
+
| Branding image at the bottom | `android:windowSplashScreenBrandingImage` | not present |
|
|
37
|
+
| Always show the icon | `android:windowSplashScreenBehavior`, value `icon_preferred` | not present |
|
|
38
|
+
|
|
39
|
+
Compat themes: `Theme.SplashScreen` as the parent, `Theme.SplashScreen.IconBackground` when the icon sits on a circle. Some Android documentation writes the icon background attribute without the `Color` suffix; the library only declares `windowSplashScreenIconBackgroundColor`.
|
|
40
|
+
|
|
41
|
+
Setting `android:windowBackground` in a launch theme is the pre Android 12 pattern. From Android 12 the system discards that custom splash and shows its own default one instead, so the configured background never appears. A dedicated splash Activity is the separate case, and it produces two surfaces rather than one wrong one. Do not pin a `core-splashscreen` version from prose; read the current one from the dependency catalogue.
|
|
42
|
+
|
|
43
|
+
## Android: holding and dismissing
|
|
44
|
+
|
|
45
|
+
| Need | API |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Install the surface | `installSplashScreen(activity)`, called before `super.onCreate()` |
|
|
48
|
+
| Hold it | `setKeepOnScreenCondition { }`, returning `true` to hold. Compat only |
|
|
49
|
+
| Hold it without the library | `ViewTreeObserver.OnPreDrawListener` on `android.R.id.content`, returning `false` to suspend |
|
|
50
|
+
| Own the exit | `setOnExitAnimationListener { }`, then `SplashScreenViewProvider.remove()` |
|
|
51
|
+
| Remaining icon time | `iconAnimationStartMillis`, `iconAnimationDurationMillis` |
|
|
52
|
+
|
|
53
|
+
The framework interface `android.window.SplashScreen` carries only `setOnExitAnimationListener`, `clearOnExitAnimationListener` and `setSplashScreenTheme`. There is no keep on screen condition outside the compat library.
|
|
54
|
+
|
|
55
|
+
## Asset geometry, Android
|
|
56
|
+
|
|
57
|
+
| Asset | Size | Visible area |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Icon with an icon background | 240x240 dp | fits a 160 dp circle |
|
|
60
|
+
| Icon without an icon background | 288x288 dp | fits a 192 dp circle |
|
|
61
|
+
| Animated vector icon | 432 dp icon area | 288 dp inner area |
|
|
62
|
+
| Branding image | 200x80 dp | leave it empty |
|
|
63
|
+
|
|
64
|
+
One third of the icon foreground is masked. Anything drawn in the outer third does not survive.
|
|
65
|
+
|
|
66
|
+
## Flutter
|
|
67
|
+
|
|
68
|
+
| Platform | Where |
|
|
69
|
+
|---|---|
|
|
70
|
+
| iOS | `ios/Runner/Base.lproj/LaunchScreen.storyboard`, assets in the `LaunchImage` set inside `Runner/Assets.xcassets` |
|
|
71
|
+
| Android | `LaunchTheme` in `android/app/src/main/res/values/styles.xml`, with a `values-night` copy for dark |
|
|
72
|
+
| Android handoff | manifest `meta-data` on the Flutter activity, `io.flutter.embedding.android.NormalTheme` pointing at `@style/NormalTheme` |
|
|
73
|
+
|
|
74
|
+
The Flutter template still teaches `android:windowBackground` on `LaunchTheme`. On Android 12 and up, put the `windowSplashScreen*` attributes there instead, or add the compat library. Keep `NormalTheme` on the same background color as the first Flutter frame.
|
|
75
|
+
|
|
76
|
+
## Expo
|
|
77
|
+
|
|
78
|
+
| Need | Where |
|
|
79
|
+
|---|---|
|
|
80
|
+
| Configure | the `expo-splash-screen` config plugin, under `expo.plugins` in the app config |
|
|
81
|
+
| Properties | `backgroundColor`, `image`, `imageWidth`, `resizeMode` (`contain`, `cover`, `native`), `dark` with its own `backgroundColor` and `image`, plus per platform `android` and `ios` blocks |
|
|
82
|
+
| Hold it | `SplashScreen.preventAutoHideAsync()`, called in module scope rather than inside a component |
|
|
83
|
+
| Dismiss it | `SplashScreen.hide()` or `SplashScreen.hideAsync()` |
|
|
84
|
+
| Exit options | `SplashScreen.setOptions({ duration, fade })`, `fade` iOS only |
|
|
85
|
+
|
|
86
|
+
The icon must be a PNG. Any other format fails the production build. A 1024x1024 source with a transparent background is the recommended input.
|
|
87
|
+
|
|
88
|
+
## Bare React Native
|
|
89
|
+
|
|
90
|
+
No core API exists. The surface is the platform's own, `UILaunchScreen` or the storyboard on iOS and `Theme.SplashScreen` on Android, reached either directly or through a package recorded in `STACK.md`.
|
|
91
|
+
|
|
92
|
+
## Mobile web
|
|
93
|
+
|
|
94
|
+
There is no OS drawn launch surface for a page. An installed web app gets one from the manifest: `background_color`, `theme_color`, `name` and the icon set. A first paint that arrives quickly is the only equivalent a browser tab has.
|