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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/dist/cli.js +105 -0
  4. package/dist/commands/build.js +35 -0
  5. package/dist/commands/detect.js +91 -0
  6. package/dist/commands/doctor.js +110 -0
  7. package/dist/commands/graph.js +106 -0
  8. package/dist/commands/install.js +76 -0
  9. package/dist/commands/lint.js +87 -0
  10. package/dist/commands/rubric.js +104 -0
  11. package/dist/commands/spec.js +300 -0
  12. package/dist/compile.js +235 -0
  13. package/dist/detect/rules.js +208 -0
  14. package/dist/detect/types.js +36 -0
  15. package/dist/emit.js +77 -0
  16. package/dist/graph.js +288 -0
  17. package/dist/heuristics.js +185 -0
  18. package/dist/lock.js +18 -0
  19. package/dist/mdx.js +84 -0
  20. package/dist/paths.js +62 -0
  21. package/dist/skill.js +69 -0
  22. package/package.json +51 -0
  23. package/src/skills/SKILL.md +125 -0
  24. package/src/skills/flow/build.md +57 -0
  25. package/src/skills/flow/firebase.md +102 -0
  26. package/src/skills/flow/init.md +116 -0
  27. package/src/skills/flow/review.md +186 -0
  28. package/src/skills/flow/spec.md +149 -0
  29. package/src/skills/heuristics/accessibility.md +124 -0
  30. package/src/skills/heuristics/ads.md +140 -0
  31. package/src/skills/heuristics/auth.md +130 -0
  32. package/src/skills/heuristics/background-work.md +129 -0
  33. package/src/skills/heuristics/buttons.md +99 -0
  34. package/src/skills/heuristics/camera.md +127 -0
  35. package/src/skills/heuristics/chat.md +125 -0
  36. package/src/skills/heuristics/colors.md +129 -0
  37. package/src/skills/heuristics/copy.md +157 -0
  38. package/src/skills/heuristics/data-display.md +124 -0
  39. package/src/skills/heuristics/feedback.md +122 -0
  40. package/src/skills/heuristics/forms.md +124 -0
  41. package/src/skills/heuristics/icons-and-imagery.md +135 -0
  42. package/src/skills/heuristics/layout.md +125 -0
  43. package/src/skills/heuristics/lists.md +129 -0
  44. package/src/skills/heuristics/localization.md +128 -0
  45. package/src/skills/heuristics/maps.md +129 -0
  46. package/src/skills/heuristics/media.md +130 -0
  47. package/src/skills/heuristics/motion.md +113 -0
  48. package/src/skills/heuristics/navigation.md +116 -0
  49. package/src/skills/heuristics/network.md +118 -0
  50. package/src/skills/heuristics/notifications.md +121 -0
  51. package/src/skills/heuristics/offline.md +124 -0
  52. package/src/skills/heuristics/onboarding.md +103 -0
  53. package/src/skills/heuristics/payments.md +138 -0
  54. package/src/skills/heuristics/performance.md +111 -0
  55. package/src/skills/heuristics/permissions.md +125 -0
  56. package/src/skills/heuristics/privacy-ui.md +112 -0
  57. package/src/skills/heuristics/scrolling.md +114 -0
  58. package/src/skills/heuristics/search.md +127 -0
  59. package/src/skills/heuristics/sense.md +128 -0
  60. package/src/skills/heuristics/settings.md +129 -0
  61. package/src/skills/heuristics/sharing.md +102 -0
  62. package/src/skills/heuristics/sound.md +95 -0
  63. package/src/skills/heuristics/splashscreen.md +111 -0
  64. package/src/skills/heuristics/states.md +120 -0
  65. package/src/skills/heuristics/touch.md +95 -0
  66. package/src/skills/heuristics/typography.md +99 -0
  67. package/src/skills/heuristics/updates.md +129 -0
  68. package/src/skills/heuristics/webviews.md +114 -0
  69. package/src/skills/heuristics/widgets.md +128 -0
  70. package/src/skills/references/capability-checks.md +59 -0
  71. package/src/skills/references/fonts.json +339 -0
  72. package/src/skills/references/icon-and-image-assets.md +103 -0
  73. package/src/skills/references/input-fields.md +82 -0
  74. package/src/skills/references/launch-surface.md +94 -0
  75. package/src/skills/references/motion-tokens.md +89 -0
  76. package/src/skills/references/navigation-containers.md +51 -0
  77. package/src/skills/references/search-controls.md +49 -0
  78. package/src/skills/references/type-scales.md +60 -0
  79. package/src/skills/references/wireframe-frame.md +209 -0
@@ -0,0 +1,89 @@
1
+ # Motion tokens and APIs
2
+
3
+ Lookup only. The rules live in `heuristics/motion.md`. Open this file for a specific token, value or API name, not as background reading.
4
+
5
+ ## Material durations
6
+
7
+ Sixteen tokens, reached as `?attr/motionDuration<Name>` or through `MotionUtils.resolveThemeDuration`. Available from the Material components library 1.6.0.
8
+
9
+ | Token | Value | Token | Value |
10
+ |---|---|---|---|
11
+ | Short1 | 50ms | Long1 | 450ms |
12
+ | Short2 | 100ms | Long2 | 500ms |
13
+ | Short3 | 150ms | Long3 | 550ms |
14
+ | Short4 | 200ms | Long4 | 600ms |
15
+ | Medium1 | 250ms | ExtraLong1 | 700ms |
16
+ | Medium2 | 300ms | ExtraLong2 | 800ms |
17
+ | Medium3 | 350ms | ExtraLong3 | 900ms |
18
+ | Medium4 | 400ms | ExtraLong4 | 1000ms |
19
+
20
+ The rule attached to the table: duration rises as the area covered or the distance travelled rises. A chip's tint and a full screen cover do not share a number.
21
+
22
+ ## Material easing
23
+
24
+ Seven tokens, reached as `?attr/motionEasing<Name>Interpolator`. Emphasized is the styled set, standard the utility set.
25
+
26
+ | Token | Curve |
27
+ |---|---|
28
+ | Standard | `cubic-bezier(0.2, 0, 0, 1)` |
29
+ | StandardDecelerate | `cubic-bezier(0, 0, 0, 1)` |
30
+ | StandardAccelerate | `cubic-bezier(0.3, 0, 1, 1)` |
31
+ | Emphasized | path `M 0,0 C 0.05,0 0.133333,0.06 0.166666,0.4 C 0.208333,0.82 0.25,1 1,1` |
32
+ | EmphasizedDecelerate | `cubic-bezier(0.05, 0.7, 0.1, 1)` |
33
+ | EmphasizedAccelerate | `cubic-bezier(0.3, 0, 0.8, 0.15)` |
34
+ | Linear | `cubic-bezier(0, 0, 1, 1)` |
35
+
36
+ ## Material springs
37
+
38
+ Three speeds by two kinds. Fast is for a small component such as a switch, slow for a full screen transition, default for everything in between. Spatial springs move a thing (position, size, shape) and are allowed to overshoot; effects springs carry color and opacity, where overshoot is a defect.
39
+
40
+ Standard scheme. These are the six Views theme attributes (`?attr/motionSpring<Speed><Kind>`, library 1.13.0 and up) and the Compose `StandardMotionTokens`, at identical values.
41
+
42
+ | Spec | Damping | Stiffness |
43
+ |---|---|---|
44
+ | fast spatial | 0.9 | 1400 |
45
+ | fast effects | 1.0 | 3800 |
46
+ | default spatial | 0.9 | 700 |
47
+ | default effects | 1.0 | 1600 |
48
+ | slow spatial | 0.9 | 300 |
49
+ | slow effects | 1.0 | 800 |
50
+
51
+ Expressive scheme, Compose only. The Views theme does not publish these, so a rule that says "use the expressive spring" is not implementable from XML attributes alone.
52
+
53
+ | Spec | Damping | Stiffness |
54
+ |---|---|---|
55
+ | fast spatial | 0.6 | 800 |
56
+ | fast effects | 1.0 | 3800 |
57
+ | default spatial | 0.8 | 380 |
58
+ | default effects | 1.0 | 1600 |
59
+ | slow spatial | 0.8 | 200 |
60
+ | slow effects | 1.0 | 800 |
61
+
62
+ In Compose the scheme is a theme value, not a per-animation choice: `MaterialTheme.motionScheme`, holding `MotionScheme.standard()` or `MotionScheme.expressive()`, exposing `defaultSpatialSpec()`, `fastSpatialSpec()`, `slowSpatialSpec()` and the three effects equivalents, and no duration or easing at all. `MaterialExpressiveTheme` defaults the scheme to `expressive()`.
63
+
64
+ Compose spring constants outside Material: `Spring.DampingRatioNoBouncy` 1.0, `LowBouncy` 0.75, `MediumBouncy` 0.5, `HighBouncy` 0.2; `Spring.StiffnessVeryLow` 50, `StiffnessLow` 200, `StiffnessMediumLow` 400, `StiffnessMedium` 1500, which is what a bare `spring()` uses.
65
+
66
+ ## Where motion lives per stack
67
+
68
+ | Stack | Animate | Continuity across screens |
69
+ |---|---|---|
70
+ | SwiftUI | `withAnimation`, `.animation(_:value:)`, `Animation.spring(response:dampingFraction:)` | `.navigationTransition(.zoom(sourceID:in:))` with `.matchedTransitionSource(id:in:)`, iOS 18; `matchedGeometryEffect` before that |
71
+ | UIKit | `UIView.animate(springDuration:bounce:)`, iOS 17; `UIViewPropertyAnimator` | `preferredTransition = .zoom(options:sourceViewProvider:)`, iOS 18 |
72
+ | Compose | `animate*AsState`, `AnimatedVisibility`, `AnimatedContent`, `Crossfade` | `SharedTransitionLayout` with `Modifier.sharedElement` or `sharedBounds` |
73
+ | Views | `SpringAnimation` and `SpringForce` from dynamicanimation | `com.google.android.material.transition`: container transform, shared axis, fade through, fade |
74
+ | Flutter | implicit `Animated*` widgets, `AnimationController`, `TweenAnimationBuilder` | `Hero`, `PageRouteBuilder` |
75
+ | React Native | Reanimated worklets; `Animated` with `useNativeDriver: true` | `react-navigation` presets, `react-native-screens` |
76
+ | Mobile web | CSS transitions and keyframes, Web Animations API | View Transitions where supported |
77
+
78
+ ## Reduced motion flags per stack
79
+
80
+ | Stack | Read |
81
+ |---|---|
82
+ | SwiftUI | `@Environment(\.accessibilityReduceMotion)` |
83
+ | UIKit | `UIAccessibility.isReduceMotionEnabled`, plus `prefersCrossFadeTransitions` before substituting a cross fade; `reduceMotionStatusDidChangeNotification` to react to a change |
84
+ | Android | no single API on phones: `ValueAnimator.areAnimatorsEnabled()` from API 26, or `Settings.Global.ANIMATOR_DURATION_SCALE` and `TRANSITION_ANIMATION_SCALE`. `LocalReduceMotion` exists only on Wear |
85
+ | Flutter | both of `MediaQuery.disableAnimationsOf(context)` and `AccessibilityFeatures.reduceMotion`, because the first carries Android and the second carries iOS |
86
+ | React Native | `AccessibilityInfo.isReduceMotionEnabled()`, and the `reduceMotionChanged` event; on Android it tracks the transition animation scale |
87
+ | Mobile web | `@media (prefers-reduced-motion: reduce)`, equivalent to the bare `@media (prefers-reduced-motion)` |
88
+
89
+ The user-facing names differ. On iOS the setting is Reduce Motion, with Prefer Cross-Fade Transitions beside it. On Android it is Remove animations, under Color and motion in Accessibility.
@@ -0,0 +1,51 @@
1
+ # Navigation containers and restoration, per stack
2
+
3
+ Lookup only. The rules live in `heuristics/navigation.md`. Open this file for one container, one API name or one restoration mechanism, not as background reading.
4
+
5
+ The concept is portable and the name is not. Pick the row for the relationship, then read the column for the stack in `STACK.md`.
6
+
7
+ ## Containers
8
+
9
+ | Container | SwiftUI | Jetpack Compose | Flutter | React Native | Mobile web |
10
+ |---|---|---|---|---|---|
11
+ | Pushed screen | `NavigationStack` with `navigationDestination` | `NavHost` with `navController.navigate` | `Navigator.push`, `MaterialPageRoute`, `context.push` on GoRouter | native stack, `navigation.navigate` | History API `pushState`, or the router's push |
12
+ | Top-level destinations | `TabView` with `Tab` | `NavigationBar` over one nested graph per destination | `NavigationBar` in a `Scaffold`, plus `IndexedStack` or a `Navigator` per branch | bottom tab navigator, one stack inside each tab | one route prefix per section |
13
+ | Bottom sheet | `.sheet` with `.presentationDetents` | `ModalBottomSheet` | `showModalBottomSheet` | a sheet library, or a native stack screen with `presentation: 'formSheet'` | `<dialog>` positioned to the bottom edge |
14
+ | Full screen cover | `.fullScreenCover` | a route on the graph, or `Dialog(usePlatformDefaultWidth = false)` | `MaterialPageRoute(fullscreenDialog: true)` | native stack screen with `presentation: 'fullScreenModal'` | a route of its own |
15
+ | Alert or dialog | `.alert`, `.confirmationDialog` | `AlertDialog` | `showDialog` with `AlertDialog` or `CupertinoAlertDialog` | `Alert.alert` | `<dialog>` with `showModal()` |
16
+
17
+ Bare React Native ships no bottom sheet. Reaching for one means adding a dependency, which is a `STACK.md` decision rather than a detail.
18
+
19
+ ## Back and dismissal
20
+
21
+ | Need | SwiftUI | Jetpack Compose | Flutter | React Native | Mobile web |
22
+ |---|---|---|---|---|---|
23
+ | Dismiss the current surface | `@Environment(\.dismiss)` | `navController.popBackStack()` | `Navigator.pop` | `navigation.goBack()` | `history.back()` |
24
+ | Intercept back | `.interactiveDismissDisabled`, then present the question yourself | `BackHandler` from `androidx.activity.compose` | `PopScope` with `canPop` and `onPopInvokedWithResult` | `beforeRemove` listener, plus `BackHandler` for the Android button | `popstate` with a pushed sentinel entry |
25
+ | Guard unsaved work | `.interactiveDismissDisabled(hasEdits)` plus a confirmation dialog | `BackHandler(enabled = hasEdits)` | `PopScope(canPop: !hasEdits)` | `beforeRemove` with `e.preventDefault()` | `beforeunload` for the tab, the sentinel for in-app |
26
+
27
+ Android's back callback is the modern one, not an override of the old back method, which is what keeps the predictive animation the system draws. `touch-gestures` owns that side.
28
+
29
+ ## Deep links
30
+
31
+ | Stack | Where the route is declared | Where the incoming link is received |
32
+ |---|---|---|
33
+ | SwiftUI | `NavigationPath` rebuilt from the URL | `.onOpenURL`, plus Associated Domains for universal links |
34
+ | Jetpack Compose | `navDeepLink` on the destination | intent filters in the manifest, verified as App Links |
35
+ | Flutter | route patterns on GoRouter or the router delegate | `onGenerateRoute`, or the platform link plugin |
36
+ | React Native | the `linking` config, with `getStateFromPath` for a synthesized stack | `Linking.getInitialURL` for a cold start, the `url` event while running |
37
+ | Mobile web | the URL itself | the router's own resolution |
38
+
39
+ Two paths to test, always: the app already running, and the app killed. Only the second one builds the stack from nothing, and it is the one that fails.
40
+
41
+ ## State restoration
42
+
43
+ | Stack | Mechanism | Notes |
44
+ |---|---|---|
45
+ | SwiftUI | `@SceneStorage` for per screen state, plus a codable `NavigationPath` | `@State` does not survive process death |
46
+ | Jetpack Compose | `rememberSaveable`, `SavedStateHandle`, and the nav graph's own saved state | pass `saveState` and `restoreState` when switching top-level destinations, or each switch resets that branch |
47
+ | Flutter | `RestorationMixin` with a `restorationScopeId`, and `RestorableProperty` values | restoration is off until a scope id is set |
48
+ | React Native | persist the navigator state from `onStateChange` and feed it back as `initialState` | gate the first render until the saved state has loaded |
49
+ | Mobile web | `history.state` for the route, `sessionStorage` for the screen | `sessionStorage` clears with the tab |
50
+
51
+ Restore the place. Refetch the content.
@@ -0,0 +1,49 @@
1
+ # Search controls
2
+
3
+ Lookup only. The rules live in `heuristics/search.md`. Open this file for the control name in one stack, not as background reading.
4
+
5
+ ## The control per stack
6
+
7
+ | Stack | Field and entry point | Expanded state and results |
8
+ |---|---|---|
9
+ | SwiftUI | `.searchable(text:placement:prompt:)`, placement from `SearchFieldPlacement` | `.searchSuggestions`, `.searchCompletion`, `.searchScopes`, `isSearching`, `dismissSearch` |
10
+ | UIKit | `UISearchController` on `navigationItem.searchController` | `searchResultsUpdater`, `automaticallyShowsCancelButton`, `automaticallyShowsScopeBar` |
11
+ | Compose Material 3 | `SearchBar(state:inputField:)` with `rememberSearchBarState`, or `AppBarWithSearch` | `ExpandedFullScreenSearchBar` on a phone; `ExpandedDockedSearchBar` is the tablet form |
12
+ | Android Views | `com.google.android.material.search.SearchBar` inside the app bar | `com.google.android.material.search.SearchView`, holding history, suggestions and results |
13
+ | Flutter Material | `SearchAnchor`, or `SearchAnchor.bar` for the bar plus view together | `suggestionsBuilder`, `SearchController.openView`, full screen by default on mobile |
14
+ | Flutter Cupertino | `CupertinoSearchTextField` | list of your own below it |
15
+ | React Native | `TextInput` with `returnKeyType="search"`, or the navigator's own search header | list of your own below it |
16
+ | Mobile web | `<input type="search">` | list of your own below it |
17
+
18
+ ## Things the stock control already does
19
+
20
+ - **Clear button.** Present on the iOS search field and on the Material `SearchView`, which shows and hides it with the text. Do not draw a second one.
21
+ - **Focus and keyboard.** The Material `SearchView` raises the keyboard on open by default (`app:autoShowKeyboard`, default true), and the Compose expanded search bar requests focus on first expansion and dismisses the keyboard itself on collapse. On iOS, do not force focus on a surface the user did not open in order to type.
22
+ - **Accessibility.** The Material `SearchView` marks its siblings as unimportant for accessibility while open and restores them on hide. Removing it from the tree while it is still open skips that restore, so call `setModalForAccessibility(false)` by hand in that case.
23
+ - **Predictive back.** Automatic on Android when a `SearchView` is connected to a `SearchBar`.
24
+
25
+ ## Android configuration worth knowing
26
+
27
+ - Soft input mode for a screen using `SearchBar` with `SearchView` is `adjustNothing`. `adjustResize` resizes the window during the expand and collapse animation, and resizing is not useful to somebody who is searching.
28
+ - `SearchBar` does not accept `android:background`. It extends `Toolbar`, so navigation icon and menu APIs work as usual.
29
+ - Under Material 3 Expressive the `SearchBar` goes inside an `AppBarLayout` themed with `ThemeOverlay.Material3Expressive.AppBarWithSearch`.
30
+ - Recent queries have a platform store in the Views stack: a `SearchRecentSuggestionsProvider` with `setupSuggestions(AUTHORITY, MODE)`, then `saveRecentQuery(query, null)` and `clearHistory()`.
31
+ - Material Search needs `com.google.android.material` 1.8.0 or later.
32
+
33
+ ## Compose API drift
34
+
35
+ The `SearchBar(inputField, expanded, onExpandedChange, ...)` and `DockedSearchBar(inputField, expanded, ...)` overloads are deprecated, as are the older `query` and `onQueryChange` forms and `TopSearchBar`, which is now `AppBarWithSearch`. Write the `SearchBarState` form with `ExpandedFullScreenSearchBar`.
36
+
37
+ Full screen expansion also needs the layout to cooperate: no parent may constrain the search bar's size, and the host activity sets `WindowCompat.setDecorFitsSystemWindows(window, false)`.
38
+
39
+ ## Sizes
40
+
41
+ | Value | Number |
42
+ |---|---|
43
+ | collapsed search bar height, Material | 56dp |
44
+ | full-screen search view header height, Material | 72dp |
45
+ | docked search view header height, Material | 56dp |
46
+ | search field text size, Material Views | 16sp |
47
+ | search bar horizontal and vertical margin, Material Views | 16dp |
48
+
49
+ There is no platform-standard debounce interval, suggestion count, minimum query length or retention limit for search history. Those four are the project's own numbers, picked once and written into `STACK.md`.
@@ -0,0 +1,60 @@
1
+ # Type scales
2
+
3
+ Lookup only. The rules live in `heuristics/typography.md`. Open this file for a specific role, size or weight, not as background reading.
4
+
5
+ ## Material 3
6
+
7
+ Fifteen baseline roles. Weight is 400 everywhere except title medium, title small and the three labels, which are 500. Nothing in the baseline scale is 600 or 700.
8
+
9
+ | Role | Size | Line height | Weight | Used for |
10
+ |---|---|---|---|---|
11
+ | displayLarge | 57sp | 64sp | 400 | hero and onboarding text |
12
+ | displayMedium | 45sp | 52sp | 400 | large feature text |
13
+ | displaySmall | 36sp | 44sp | 400 | prominent display |
14
+ | headlineLarge | 32sp | 40sp | 400 | screen titles |
15
+ | headlineMedium | 28sp | 36sp | 400 | section headers |
16
+ | headlineSmall | 24sp | 32sp | 400 | card titles |
17
+ | titleLarge | 22sp | 28sp | 400 | top app bar title |
18
+ | titleMedium | 16sp | 24sp | 500 | tabs, navigation |
19
+ | titleSmall | 14sp | 20sp | 500 | subtitles |
20
+ | bodyLarge | 16sp | 24sp | 400 | primary body text |
21
+ | bodyMedium | 14sp | 20sp | 400 | secondary body text |
22
+ | bodySmall | 12sp | 16sp | 400 | captions |
23
+ | labelLarge | 14sp | 20sp | 500 | buttons, prominent labels |
24
+ | labelMedium | 12sp | 16sp | 500 | chips, smaller labels |
25
+ | labelSmall | 11sp | 16sp | 500 | timestamps, annotations |
26
+
27
+ Floors: body content does not go below 12sp, labels not below 11sp. Reach these through `MaterialTheme.typography`, never as literal sizes, and always in `sp`.
28
+
29
+ M3 Expressive adds fifteen *emphasized* styles that run parallel to these, at heavier weights, for selection, actions, headlines and editorial moments. They are a second scale to reach into deliberately, not permission to raise the weight of the first one. Where the project ships a variable face, the same feature set covers the expressive axes (weight, grade, width, optical size); those belong to display and headline, which are short enough to carry them.
30
+
31
+ ## iOS text styles
32
+
33
+ Sizes at the Large content size, which is the default.
34
+
35
+ | Style | Size | Line height | Weight |
36
+ |---|---|---|---|
37
+ | largeTitle | 34pt | 41pt | Regular |
38
+ | title1 | 28pt | 34pt | Regular |
39
+ | title2 | 22pt | 28pt | Regular |
40
+ | title3 | 20pt | 25pt | Regular |
41
+ | headline | 17pt | 22pt | Semibold |
42
+ | body | 17pt | 22pt | Regular |
43
+ | callout | 16pt | 21pt | Regular |
44
+ | subheadline | 15pt | 20pt | Regular |
45
+ | footnote | 13pt | 18pt | Regular |
46
+ | caption1 | 12pt | 16pt | Regular |
47
+ | caption2 | 11pt | 13pt | Regular |
48
+
49
+ Floor is 11pt (`caption2`), body is 17pt. Reach these through the text styles, so Dynamic Type carries them; a custom face is registered against a style with `relativeTo:` or `UIFontMetrics` rather than given a fixed size.
50
+
51
+ Second-hand tables often print the titles as Bold. They are Regular. The only style that ships semibold is `headline`, and it is the same 17pt as `body`.
52
+
53
+ Letter spacing is not listed here. It comes from the theme, and a guessed tracking value is worse than no value at all.
54
+
55
+ ## What the two scales agree on
56
+
57
+ - Large text is not bold text. Both platforms grow the size and leave the weight at the regular end.
58
+ - Weight above regular is reserved for the small roles that label a control: M3 labels and title medium at 500, iOS `headline` at semibold.
59
+ - The floor is 11 in both, and it is for genuinely peripheral text.
60
+ - Line height sits near 1.2 at the display end and near 1.4 to 1.5 at the body end. It is a ratio that widens as the text gets smaller, not a constant.
@@ -0,0 +1,209 @@
1
+ # Wireframe frame
2
+
3
+ The skeleton every wireframe copies, so two screens in the same project come out in the same drawing and a reviewer compares structure instead of style. Read it when `flow/spec.md` sends you to render a brief, and copy it rather than reinventing a set of conventions per screen.
4
+
5
+ ## The frame
6
+
7
+ 393 by 852 is the reference phone. The status bar occupies the first 59 points and the home indicator the last 34, and both are drawn as occupied area rather than as margin, because the point of the frame is that content cannot use them.
8
+
9
+ Everything is greyscale by construction: four greys, one ink, and nothing else. A wireframe that acquires a colour has stopped being a wireframe, and `flow/spec.md` says why.
10
+
11
+ ## The skeleton
12
+
13
+ Save as `.trunative/screens/<name>.wireframe.html`, one file, no imports, no CDN, no framework.
14
+
15
+ ```html
16
+ <!doctype html>
17
+ <meta charset="utf-8">
18
+ <title>Wireframe</title>
19
+ <style>
20
+ :root {
21
+ --ink: #1c1c1e;
22
+ --mute: #8e8e93;
23
+ --line: #c7c7cc;
24
+ --fill: #e5e5ea;
25
+ --ground: #f2f2f7;
26
+ --paper: #ffffff;
27
+ }
28
+ body {
29
+ margin: 0;
30
+ padding: 24px;
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ gap: 24px;
34
+ background: #d9d9de;
35
+ font: 15px/1.35 -apple-system, "Segoe UI", Roboto, sans-serif;
36
+ color: var(--ink);
37
+ }
38
+ /* The label is a sibling, never a child: the frame clips its own content. */
39
+ .shot { display: flex; flex-direction: column; gap: 6px; }
40
+ .label { font-size: 12px; color: #55555a; }
41
+ .frame {
42
+ width: 393px;
43
+ height: 852px;
44
+ display: flex;
45
+ flex-direction: column;
46
+ background: var(--ground);
47
+ border: 1px solid var(--line);
48
+ border-radius: 44px;
49
+ overflow: hidden;
50
+ position: relative;
51
+ }
52
+ .status, .indicator {
53
+ flex: none;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ color: var(--mute);
58
+ font-size: 11px;
59
+ }
60
+ .status { height: 59px; }
61
+ .indicator { height: 34px; }
62
+ .indicator::after {
63
+ content: "";
64
+ width: 140px;
65
+ height: 5px;
66
+ border-radius: 3px;
67
+ background: var(--line);
68
+ }
69
+ .content {
70
+ flex: 1;
71
+ min-height: 0;
72
+ overflow: hidden;
73
+ padding: 0 16px;
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 16px;
77
+ }
78
+ .bar {
79
+ flex: none;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 12px;
83
+ height: 44px;
84
+ font-size: 22px;
85
+ font-weight: 600;
86
+ }
87
+ .bar .spacer { flex: 1; }
88
+ .row { display: flex; align-items: center; gap: 8px; }
89
+ .scroller { display: flex; gap: 8px; overflow: hidden; }
90
+ .chip {
91
+ flex: none;
92
+ padding: 7px 14px;
93
+ border: 1px solid var(--line);
94
+ border-radius: 999px;
95
+ font-size: 13px;
96
+ color: var(--mute);
97
+ white-space: nowrap;
98
+ }
99
+ .card {
100
+ background: var(--paper);
101
+ border: 1px solid var(--line);
102
+ border-radius: 16px;
103
+ padding: 12px;
104
+ }
105
+ .box {
106
+ background: var(--fill);
107
+ border: 1px solid var(--line);
108
+ border-radius: 10px;
109
+ }
110
+ /* A cross means an image nobody has chosen yet. */
111
+ .image {
112
+ background:
113
+ linear-gradient(to top right, transparent calc(50% - 1px), var(--line) 50%, transparent calc(50% + 1px)),
114
+ linear-gradient(to bottom right, transparent calc(50% - 1px), var(--line) 50%, transparent calc(50% + 1px)),
115
+ var(--fill);
116
+ border: 1px solid var(--line);
117
+ border-radius: 10px;
118
+ }
119
+ .avatar { width: 56px; height: 56px; border-radius: 50%; }
120
+ .text { height: 12px; border-radius: 3px; background: var(--fill); }
121
+ .text.short { width: 40%; }
122
+ .text.half { width: 55%; }
123
+ .caption { font-size: 13px; color: var(--mute); }
124
+ .section { font-size: 17px; font-weight: 600; }
125
+ .button {
126
+ height: 48px;
127
+ border: 1px solid var(--ink);
128
+ border-radius: 12px;
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ font-weight: 600;
133
+ }
134
+ .tabbar {
135
+ flex: none;
136
+ display: flex;
137
+ justify-content: space-around;
138
+ align-items: center;
139
+ height: 64px;
140
+ margin: 0 16px;
141
+ background: var(--paper);
142
+ border: 1px solid var(--line);
143
+ border-radius: 20px;
144
+ font-size: 11px;
145
+ color: var(--mute);
146
+ }
147
+ .tabbar .on { color: var(--ink); font-weight: 600; }
148
+ </style>
149
+
150
+ <div class="shot">
151
+ <div class="label">Memories, default</div>
152
+ <div class="frame">
153
+ <div class="status">status bar</div>
154
+ <div class="content">
155
+ <div class="bar">Memories<span class="spacer"></span><span class="caption">menu</span></div>
156
+ <div class="scroller">
157
+ <div class="chip">Filter</div>
158
+ <div class="chip">You (12)</div>
159
+ <div class="chip">Natalia (20)</div>
160
+ <div class="chip">Favorites (0)</div>
161
+ </div>
162
+ <div class="section">You recently viewed</div>
163
+ <div class="card">
164
+ <div class="row">
165
+ <div class="image" style="width:96px;height:96px"></div>
166
+ <div class="image" style="width:96px;height:96px"></div>
167
+ <div class="box" style="width:96px;height:96px"></div>
168
+ </div>
169
+ <div class="text half" style="margin-top:12px"></div>
170
+ <div class="caption" style="margin-top:6px">29 Memories, 2023 to 2025</div>
171
+ </div>
172
+ <div class="row">
173
+ <div class="section">People</div>
174
+ <span class="spacer" style="flex:1"></span>
175
+ <div class="caption">View all</div>
176
+ </div>
177
+ <div class="row" style="justify-content:space-between">
178
+ <div class="avatar box"></div>
179
+ <div class="avatar box"></div>
180
+ <div class="avatar box"></div>
181
+ <div class="avatar box"></div>
182
+ <div class="avatar box"></div>
183
+ </div>
184
+ <div class="button">Explore Memories</div>
185
+ </div>
186
+ <div class="tabbar">
187
+ <span>Home</span><span class="on">Memories</span><span>Create</span>
188
+ <span>Profiles</span><span>Account</span>
189
+ </div>
190
+ <div class="indicator"></div>
191
+ </div>
192
+ </div>
193
+ ```
194
+
195
+ ## Rules the skeleton encodes
196
+
197
+ - **The frame clips.** `.content` sets `overflow: hidden`, so anything that does not fit is cut off exactly as it would be on the device. A wireframe that scrolls its own frame hides the collision the render exists to find, which is `layout-fold` and `layout-chrome`.
198
+ - **Fixed chrome is a sibling, not an overlay.** The tab bar sits outside `.content`. When a real screen pins something over the content instead, draw it over, and then check what it covers at the end of the scroll.
199
+ - **Real strings only.** Type the words the screen will actually show, including the longest name and the largest count. Lorem text passes every layout and proves none, which is `copy-budget` and `copy-sample-data`.
200
+ - **A cross means an unchosen image**, a plain `.box` means a deliberate empty slot such as an add tile, and neither ever becomes a photograph.
201
+ - **One frame per structurally different state.** Put a second `.shot` beside the first and set its `.label`. They sit side by side on one page, which is how a reviewer sees them in a single capture.
202
+
203
+ ## Rendering
204
+
205
+ ```sh
206
+ chrome --headless --window-size=393,852 --screenshot=wireframe.png .trunative/screens/<name>.wireframe.html
207
+ ```
208
+
209
+ Widen `--window-size` by 440 for every frame past the first, so two frames want about 900 and three about 1340. A window that is a few points short wraps the last frame below the fold and it is missing from the capture with no error, so count the frames rather than guessing the width. Any Chromium binary works, `chromium` and `msedge` included. When none is installed, open the file in whatever browser is and capture it by hand: the HTML is the artefact, and the PNG is a view of it that nothing else depends on.