@wireai/activation 0.8.0 → 0.9.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/AGENTS.md +29 -5
- package/CHANGELOG.md +163 -6
- package/README.md +105 -10
- package/dist/analytics/index.d.mts +6 -6
- package/dist/analytics/index.d.ts +6 -6
- package/dist/analytics/index.js +447 -49
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +447 -49
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +2 -2
- package/dist/coachmarks/index.d.ts +2 -2
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-f7LWcdWG.d.ts → currentSession-61dcm3V-.d.ts} +10 -2
- package/dist/{currentSession-d9CrBxwe.d.mts → currentSession-Bs2JfTJ8.d.mts} +10 -2
- package/dist/{decision-BzbiKwk3.d.mts → decision-Bl_M2y3r.d.mts} +1 -1
- package/dist/{decision-plDEOCkt.d.ts → decision-Cau5KmP6.d.ts} +1 -1
- package/dist/index.d.mts +558 -15
- package/dist/index.d.ts +558 -15
- package/dist/index.js +1144 -375
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +969 -207
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +69 -23
- package/dist/questionnaire/index.d.ts +69 -23
- package/dist/questionnaire/index.js +643 -172
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +608 -137
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +24 -12
- package/dist/reviews/index.d.ts +24 -12
- package/dist/reviews/index.js +507 -173
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +462 -129
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.d.mts +2 -2
- package/dist/showcase/index.d.ts +2 -2
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/transport-DzU-TqZz.d.ts +86 -0
- package/dist/transport-f5VVB5hH.d.mts +86 -0
- package/dist/{types-GL_hQ0TN.d.mts → types-BcmagF6K.d.mts} +1 -1
- package/dist/{types-GL_hQ0TN.d.ts → types-BcmagF6K.d.ts} +1 -1
- package/dist/{types-BhpXJGlg.d.ts → types-Buj9Lw9t.d.ts} +15 -3
- package/dist/{types-CMuOexw0.d.mts → types-CKFhyrMu.d.mts} +1 -1
- package/dist/{types-CMuOexw0.d.ts → types-CKFhyrMu.d.ts} +1 -1
- package/dist/{types-A6pTxIZV.d.mts → types-CNUqMK0D.d.mts} +15 -3
- package/metro/index.d.ts +18 -1
- package/metro/index.js +80 -0
- package/package.json +9 -1
- package/src/WireOnboarding.tsx +4 -0
- package/src/analytics/currentSession.ts +1 -1
- package/src/cards/CardGridSelectCard.tsx +253 -0
- package/src/cards/ChipSelectCard.tsx +1 -1
- package/src/cards/SelectionCard.tsx +15 -19
- package/src/cards/index.ts +13 -1
- package/src/cards/optionSchema.ts +65 -0
- package/src/coachmarks/types.ts +1 -1
- package/src/components/CardHandoff.tsx +39 -7
- package/src/components/DoneCheck.tsx +94 -0
- package/src/components/ModalPaneHost.tsx +141 -0
- package/src/components/StepDots.tsx +112 -0
- package/src/device/appVersion.ts +29 -8
- package/src/device/deviceModel.ts +21 -8
- package/src/features/cache.ts +1 -1
- package/src/haptics/expo-haptics.d.ts +31 -0
- package/src/haptics/haptics.ts +58 -0
- package/src/icons/IconRegistry.tsx +53 -0
- package/src/icons/WireIcon.tsx +85 -0
- package/src/icons/expoIcons.ts +165 -0
- package/src/icons/index.ts +18 -0
- package/src/icons/vocabulary.ts +154 -0
- package/src/index.ts +20 -0
- package/src/questionnaire/QuestionnaireGate.tsx +315 -164
- package/src/questionnaire/decision.ts +4 -2
- package/src/questionnaire/index.ts +3 -2
- package/src/questionnaire/steps.ts +261 -0
- package/src/questionnaire/types.ts +33 -12
- package/src/reviews/ReviewGate.tsx +250 -152
- package/src/reviews/index.ts +7 -3
- package/src/reviews/transport.ts +77 -3
- package/src/reviews/types.ts +13 -0
- package/src/session/persistedSession.ts +1 -1
- package/src/session-analytics/lifecycle.ts +1 -1
- package/src/session-analytics/useSessionStart.ts +2 -2
- package/src/showcase/FeatureShowcase.tsx +1 -1
- package/src/showcase/showcaseColors.ts +1 -1
- package/src/theme/mergeThemeOver.ts +27 -0
- package/src/types.ts +8 -0
- package/dist/transport-BeO_Brcu.d.mts +0 -40
- package/dist/transport-DLpd1v5_.d.ts +0 -40
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient type for the OPTIONAL peer `expo-haptics`.
|
|
3
|
+
*
|
|
4
|
+
* The kit never hard-depends on it: it is loaded lazily via a guarded dynamic import
|
|
5
|
+
* and externalized from the build, so the core kit installs nothing. This declaration
|
|
6
|
+
* lets the module typecheck whether or not the host has the package installed. When the
|
|
7
|
+
* host does install it, Metro/the bundler resolves the real module at runtime; when it
|
|
8
|
+
* doesn't, the guarded import rejects and every haptic becomes a silent no-op.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors `src/reviews/expo-store-review.d.ts` exactly — same optional-peer contract.
|
|
11
|
+
*/
|
|
12
|
+
declare module "expo-haptics" {
|
|
13
|
+
/** Impact strengths (a physical "tap" against the UI). */
|
|
14
|
+
export enum ImpactFeedbackStyle {
|
|
15
|
+
Light = "light",
|
|
16
|
+
Medium = "medium",
|
|
17
|
+
Heavy = "heavy",
|
|
18
|
+
}
|
|
19
|
+
/** Notification tones (the outcome of a task). */
|
|
20
|
+
export enum NotificationFeedbackType {
|
|
21
|
+
Success = "success",
|
|
22
|
+
Warning = "warning",
|
|
23
|
+
Error = "error",
|
|
24
|
+
}
|
|
25
|
+
/** The "value changed" tick — used for picking a star. */
|
|
26
|
+
export function selectionAsync(): Promise<void>;
|
|
27
|
+
/** A physical impact tap of the given strength. */
|
|
28
|
+
export function impactAsync(style?: ImpactFeedbackStyle): Promise<void>;
|
|
29
|
+
/** The task-outcome tone — used for the submit payoff. */
|
|
30
|
+
export function notificationAsync(type?: NotificationFeedbackType): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* haptics.ts — the physical half of the kit's feedback layer: a tap you feel.
|
|
3
|
+
*
|
|
4
|
+
* `expo-haptics` is an OPTIONAL peer, loaded lazily via a guarded dynamic import so the
|
|
5
|
+
* core kit never depends on it — the exact contract `reviews/storeReview.ts` uses for
|
|
6
|
+
* `expo-store-review`. A host that has not installed it (or a platform without a taptic
|
|
7
|
+
* engine) simply feels nothing: every call resolves to `false` and NOTHING throws. A
|
|
8
|
+
* missed haptic must never break a gate.
|
|
9
|
+
*
|
|
10
|
+
* The module is deliberately fire-and-forget. Callers `void play(...)` from a press
|
|
11
|
+
* handler and never await it, so a slow/absent native module can't delay a tap.
|
|
12
|
+
*
|
|
13
|
+
* Tones map to intent, not to a device API:
|
|
14
|
+
* • "selection" — a value changed (picking a star).
|
|
15
|
+
* • "light" — a light impact (moving to the next question).
|
|
16
|
+
* • "success" — a task completed (the submit payoff, under the done check).
|
|
17
|
+
*
|
|
18
|
+
* NOT gated on reduce-motion: `prefers-reduced-motion` is a VESTIBULAR accommodation about
|
|
19
|
+
* on-screen movement. Haptics are a non-visual channel and are often the accessibility
|
|
20
|
+
* affordance a reduce-motion user relies on, so silencing them here would be the wrong
|
|
21
|
+
* trade. Hosts that want no haptics leave `expo-haptics` uninstalled.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** What a haptic is for, in product terms. Mapped to the native API inside `play`. */
|
|
25
|
+
export type HapticTone = "selection" | "light" | "success";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Play a haptic when the optional peer is available. Resolves `true` when a haptic was
|
|
29
|
+
* actually requested, `false` when the peer is absent or the native call failed. NEVER
|
|
30
|
+
* throws and never rejects — the result is advisory (tests/analytics), not control flow.
|
|
31
|
+
*/
|
|
32
|
+
export const play = async (tone: HapticTone): Promise<boolean> => {
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import("expo-haptics");
|
|
35
|
+
if (!mod) return false;
|
|
36
|
+
if (tone === "selection") {
|
|
37
|
+
await mod.selectionAsync();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (tone === "light") {
|
|
41
|
+
await mod.impactAsync(mod.ImpactFeedbackStyle.Light);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
await mod.notificationAsync(mod.NotificationFeedbackType.Success);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
// Peer not installed, unsupported platform, or the native call failed — stay silent.
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fire-and-forget `play`. The call site reads as the intent ("a star was tapped"), and a
|
|
54
|
+
* rejected/absent native module can never surface as an unhandled rejection.
|
|
55
|
+
*/
|
|
56
|
+
export const playHaptic = (tone: HapticTone): void => {
|
|
57
|
+
void play(tone);
|
|
58
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon registry — the host's escape hatch, and the FIRST step of icon resolution.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately the same shape and philosophy as the `illustrations` registry
|
|
5
|
+
* (components/Illustration.tsx): name → ReactNode, held in context, so an app can ship brand
|
|
6
|
+
* SVG / Lottie / Image nodes the kit never imports. Here it means a host can:
|
|
7
|
+
*
|
|
8
|
+
* • override any vocabulary name with its own brand mark (`{ instagram: <MyIgGlyph/> }`), or
|
|
9
|
+
* • supply icons WITHOUT installing `@expo/vector-icons` at all, or
|
|
10
|
+
* • extend the vocabulary with names of its own — the AI emits `"my-thing"`, the host renders
|
|
11
|
+
* it, and the kit needs no release.
|
|
12
|
+
*
|
|
13
|
+
* <WireOnboarding icons={{ instagram: <BrandIg/> }} ... />
|
|
14
|
+
*
|
|
15
|
+
* A name the registry doesn't have falls through to `@expo/vector-icons`, then to no icon at
|
|
16
|
+
* all. See `WireIcon`.
|
|
17
|
+
*/
|
|
18
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
19
|
+
|
|
20
|
+
export type WireIconRegistry = Record<string, React.ReactNode>;
|
|
21
|
+
|
|
22
|
+
/** One shared empty registry so an unconfigured provider never allocates a fresh {} per render. */
|
|
23
|
+
const EMPTY_REGISTRY: WireIconRegistry = {};
|
|
24
|
+
|
|
25
|
+
const IconContext = createContext<WireIconRegistry>(EMPTY_REGISTRY);
|
|
26
|
+
|
|
27
|
+
export type IconRegistryProviderProps = {
|
|
28
|
+
registry?: WireIconRegistry;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const IconRegistryProvider: React.FC<IconRegistryProviderProps> = ({
|
|
33
|
+
registry,
|
|
34
|
+
children,
|
|
35
|
+
}) => {
|
|
36
|
+
const value = useMemo(() => registry ?? EMPTY_REGISTRY, [registry]);
|
|
37
|
+
return <IconContext.Provider value={value}>{children}</IconContext.Provider>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Look up a host-supplied icon node by name. Returns undefined if absent.
|
|
42
|
+
*
|
|
43
|
+
* The OWN-property check matters here for the same reason it does in `vocabulary.ts`: the name
|
|
44
|
+
* is LLM-authored, and a bare `registry[name]` answers `"toString"` with an inherited function
|
|
45
|
+
* — which is truthy, so it would be handed to React as a child and blow up the card. An icon
|
|
46
|
+
* name must never be able to do that.
|
|
47
|
+
*/
|
|
48
|
+
export const useHostIcon = (name?: string): React.ReactNode | undefined => {
|
|
49
|
+
const registry = useContext(IconContext);
|
|
50
|
+
if (!name) return undefined;
|
|
51
|
+
if (!Object.prototype.hasOwnProperty.call(registry, name)) return undefined;
|
|
52
|
+
return registry[name];
|
|
53
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WireIcon — render one semantic icon name, or nothing at all.
|
|
3
|
+
*
|
|
4
|
+
* RESOLUTION ORDER (each step falls through to the next, never throws):
|
|
5
|
+
* 1. the host `icons` registry — brand artwork, zero kit deps (IconRegistry.tsx)
|
|
6
|
+
* 2. `@expo/vector-icons` — if the optional peer resolves (expoIcons.ts)
|
|
7
|
+
* 3. NOTHING — render null
|
|
8
|
+
*
|
|
9
|
+
* Step 3 is a FEATURE, not a failure mode. The AI emits icon names from a vocabulary that will
|
|
10
|
+
* keep growing, against production apps that ship on their own schedule (new store builds only,
|
|
11
|
+
* never OTA, so they cannot be force-updated). So an unknown, misspelled, hallucinated, or
|
|
12
|
+
* simply newer-than-this-client name MUST be a no-op: no crash, no broken-glyph tofu box, and
|
|
13
|
+
* no layout shift. Returning `null` (rather than a spacer or a placeholder) is what gives the
|
|
14
|
+
* last property — a row with an unresolvable icon lays out EXACTLY like a row with no icon,
|
|
15
|
+
* because the parent's flex `gap` adds no space for a null child.
|
|
16
|
+
*
|
|
17
|
+
* An icon is always DECORATIVE: the label carries the meaning, so the icon is hidden from
|
|
18
|
+
* assistive tech and never becomes the accessible name of its option.
|
|
19
|
+
*/
|
|
20
|
+
import React from "react";
|
|
21
|
+
import { StyleSheet, View } from "react-native";
|
|
22
|
+
|
|
23
|
+
import { resolveIconFamily, type OptionalRequire } from "./expoIcons";
|
|
24
|
+
import { useHostIcon } from "./IconRegistry";
|
|
25
|
+
import { lookupIconGlyph } from "./vocabulary";
|
|
26
|
+
|
|
27
|
+
export type WireIconProps = {
|
|
28
|
+
/** A semantic name from the vocabulary (see vocabulary.ts). Unknown → renders nothing. */
|
|
29
|
+
name?: string;
|
|
30
|
+
/** Glyph size in px. */
|
|
31
|
+
size?: number;
|
|
32
|
+
/** Glyph color. Callers pass a THEME token — never a raw hex. */
|
|
33
|
+
color?: string;
|
|
34
|
+
/** Injectable resolver. TEST-ONLY seam; production uses the guarded runtime require. */
|
|
35
|
+
requireModule?: OptionalRequire;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const _WireIcon: React.FC<WireIconProps> = ({ name, size = 20, color, requireModule }) => {
|
|
39
|
+
// 1) Host artwork wins — it can also cover names outside the vocabulary entirely.
|
|
40
|
+
const hostIcon = useHostIcon(name);
|
|
41
|
+
if (hostIcon) {
|
|
42
|
+
return (
|
|
43
|
+
<View
|
|
44
|
+
style={styles.decorative}
|
|
45
|
+
accessible={false}
|
|
46
|
+
importantForAccessibility="no-hide-descendants"
|
|
47
|
+
>
|
|
48
|
+
{hostIcon}
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2) The vocabulary → a concrete glyph, then the optional peer. Both may miss; both degrade.
|
|
54
|
+
const glyph = lookupIconGlyph(name);
|
|
55
|
+
if (!glyph) return null;
|
|
56
|
+
|
|
57
|
+
const Family = resolveIconFamily(glyph.family, requireModule);
|
|
58
|
+
if (!Family) return null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View
|
|
62
|
+
style={styles.decorative}
|
|
63
|
+
accessible={false}
|
|
64
|
+
importantForAccessibility="no-hide-descendants"
|
|
65
|
+
>
|
|
66
|
+
<Family
|
|
67
|
+
name={glyph.name}
|
|
68
|
+
size={size}
|
|
69
|
+
color={color}
|
|
70
|
+
// Icons are sized by the layout, not the text scale — a large accessibility font must
|
|
71
|
+
// not blow up a card grid. The LABEL still scales, which is what carries the meaning.
|
|
72
|
+
allowFontScaling={false}
|
|
73
|
+
/>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const WireIcon = React.memo(_WireIcon);
|
|
79
|
+
WireIcon.displayName = "WireIcon";
|
|
80
|
+
|
|
81
|
+
const styles = StyleSheet.create({
|
|
82
|
+
// No size/margin of its own: the glyph decides its footprint, so an absent icon and a
|
|
83
|
+
// present one differ by exactly the glyph — never by a wrapper.
|
|
84
|
+
decorative: {},
|
|
85
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* expoIcons — resolve an `@expo/vector-icons` family, lazily and optionally.
|
|
3
|
+
*
|
|
4
|
+
* ── WHY THIS DOES NOT BREAK THE NO-NATIVE-DEPENDENCY POLICY ──────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The policy (see `questionnaire/QuestionnaireGate.tsx`'s back-chevron note, `AnimatedSparkle`,
|
|
7
|
+
* `coachmarks/GestureHint`, `cards/StatusCard`'s Unicode STATUS_ICONS) says: no vector library
|
|
8
|
+
* is worth FORCING EVERY HOST INTO A NATIVE REBUILD. Read it precisely — what it forbids is
|
|
9
|
+
* forcing hosts, not the existence of an icon.
|
|
10
|
+
*
|
|
11
|
+
* `@expo/vector-icons` is an OPTIONAL peer:
|
|
12
|
+
*
|
|
13
|
+
* • A host WITHOUT it installs the kit fine (optional peer → npm never fetches it) and renders
|
|
14
|
+
* fine (icon → null, layout unchanged). To BUNDLE fine it must let Metro know the module is
|
|
15
|
+
* allowed to be missing — see "OPTIONALITY IS A BUILD-TIME CONTRACT" below.
|
|
16
|
+
* • A host WITH it gets real brand icons. Both live host apps already ship it
|
|
17
|
+
* (`@expo/vector-icons ^15.0.3`), so neither needs a rebuild — which matters because
|
|
18
|
+
* neither can be force-updated: both ship new store builds only and never OTA.
|
|
19
|
+
* • A host that wants neither can override every name with its own artwork through the
|
|
20
|
+
* `icons` registry, so the library is never on the critical path at all.
|
|
21
|
+
*
|
|
22
|
+
* ── THE SPECIFIER MUST BE A STRING LITERAL — THIS IS THE WHOLE BUG ───────────────────────
|
|
23
|
+
*
|
|
24
|
+
* DO NOT "restore" the old `const req = require; req(moduleName)` shape. It could never work,
|
|
25
|
+
* and it shipped broken in 0.8.0. What that shape actually did, proven in a real `expo export`
|
|
26
|
+
* bundle and in Expo SDK 55's runtime:
|
|
27
|
+
*
|
|
28
|
+
* 1. Metro collects dependencies STATICALLY, by matching a call whose callee is literally the
|
|
29
|
+
* identifier `require` and whose argument is a STRING LITERAL. Aliasing `require` to a local
|
|
30
|
+
* hid the call from the collector, so this module's dependencyMap was emitted EMPTY (`[]`)
|
|
31
|
+
* and `@expo/vector-icons` was never put in the bundle at all.
|
|
32
|
+
* 2. At runtime the alias did NOT point at Node's `require`. Metro renames the free `require`
|
|
33
|
+
* identifier to the module factory's require param, so `req` WAS `metroRequire`, whose
|
|
34
|
+
* module registry is keyed by NUMERIC module ids. Handing it the string "@expo/vector-icons"
|
|
35
|
+
* could never match a key, so it threw `Requiring unknown module "@expo/vector-icons"` —
|
|
36
|
+
* even in a bundle that contained the module.
|
|
37
|
+
* 3. That throw never reached the `catch` below. Metro's own `guardedLoadModule` wraps the
|
|
38
|
+
* load, catches the error FIRST, and routes it to `global.ErrorUtils.reportFatalError(e)`
|
|
39
|
+
* instead of rethrowing (Expo SDK 55: `@expo/cli/build/metro-require/require.js:151-165`).
|
|
40
|
+
* So the call returned `undefined` — icon silently null — AND logged a red fatal error. A
|
|
41
|
+
* try/catch cannot guard a require that Metro never lets throw.
|
|
42
|
+
*
|
|
43
|
+
* A LITERAL `require("@expo/vector-icons")` is collected, is bundled, and resolves SYNCHRONOUSLY
|
|
44
|
+
* through the numeric dependencyMap — the same reason `reviews/storeReview.ts`'s literal
|
|
45
|
+
* `await import("expo-store-review")` has always worked. (The old comment here claimed the alias
|
|
46
|
+
* worked "exactly like expo-store-review". It was the opposite: storeReview's specifier is a
|
|
47
|
+
* literal, and that is precisely why it resolves and this one did not.)
|
|
48
|
+
*
|
|
49
|
+
* ── OPTIONALITY IS A BUILD-TIME CONTRACT, AND THE TRY/CATCH IS LOAD-BEARING ──────────────
|
|
50
|
+
*
|
|
51
|
+
* Metro has NO runtime resolution: every specifier is resolved when the bundle is built. It does
|
|
52
|
+
* have a real optional-dependency mechanism, and it needs BOTH halves of what is written below:
|
|
53
|
+
* a LITERAL specifier (so the dep is collected at all) AND the call inside a TRY/CATCH — that is
|
|
54
|
+
* literally how Metro marks a dependency `isOptional`. When the peer is absent, Metro then puts
|
|
55
|
+
* `null` in this module's dependencyMap and `metroRequire(null, "@expo/vector-icons")` throws
|
|
56
|
+
* "Cannot find module" from its own `moduleId === null` branch, BEFORE `guardedLoadModule` — so
|
|
57
|
+
* unlike the string-id path in the autopsy above, that throw really does land in the catch below.
|
|
58
|
+
* So do NOT "simplify" the try/catch away: it is what keeps the peer optional, not just tidy.
|
|
59
|
+
*
|
|
60
|
+
* That mechanism rides on the transformer's `allowOptionalDependencies`, which Expo enables by
|
|
61
|
+
* default but bare `metro-config` defaults to FALSE. For hosts in the second camp, the kit ships
|
|
62
|
+
* a safety net: `withWireOnboarding` (metro/index.js) installs a `resolveRequest` that maps an
|
|
63
|
+
* absent optional peer to an EMPTY module, landing on exactly the degradation path below (no
|
|
64
|
+
* families on the namespace → `undefined` → icon renders null).
|
|
65
|
+
*/
|
|
66
|
+
import type { WireIconFamily } from "./vocabulary";
|
|
67
|
+
|
|
68
|
+
// Metro injects a module-scoped `require`; it is ABSENT in a pure-ESM runtime (the kit's own
|
|
69
|
+
// tests run under `node --test` as ESM). Declared locally so this type-checks without ambient
|
|
70
|
+
// Node types; the `typeof` guard keeps the reference ESM-safe.
|
|
71
|
+
declare const require: ((id: string) => unknown) | undefined;
|
|
72
|
+
|
|
73
|
+
/** A `require`-like resolver. Injectable in tests; production uses the guarded literal require. */
|
|
74
|
+
export type OptionalRequire = (moduleName: string) => unknown;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The minimal shape the kit uses from a vector-icons family. A COMPONENT TYPE, not a plain
|
|
78
|
+
* function: these are real React components (createIconSet output) and must be mounted via
|
|
79
|
+
* JSX/createElement, never invoked directly — calling one bypasses the reconciler and breaks
|
|
80
|
+
* the moment the family uses hooks or is a class.
|
|
81
|
+
*/
|
|
82
|
+
export type IconComponent = import("react").ComponentType<{
|
|
83
|
+
name: string;
|
|
84
|
+
size?: number;
|
|
85
|
+
color?: string;
|
|
86
|
+
allowFontScaling?: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The production resolver. The specifier is a LITERAL so Metro collects it (see the header); the
|
|
91
|
+
* `moduleName` parameter exists only to keep the `OptionalRequire` seam shape, so anything other
|
|
92
|
+
* than the one module this file owns resolves to undefined.
|
|
93
|
+
*/
|
|
94
|
+
const runtimeRequire: OptionalRequire = (moduleName) => {
|
|
95
|
+
if (moduleName !== "@expo/vector-icons") return undefined;
|
|
96
|
+
if (typeof require !== "function") return undefined;
|
|
97
|
+
try {
|
|
98
|
+
return require("@expo/vector-icons");
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Read a module's `default` (Expo modules are consumed as default exports) or the namespace. */
|
|
105
|
+
const interop = (mod: unknown): Record<string, unknown> | undefined => {
|
|
106
|
+
if (!mod || typeof mod !== "object") return undefined;
|
|
107
|
+
const def = (mod as { default?: unknown }).default;
|
|
108
|
+
// `@expo/vector-icons` exposes the families as NAMED exports; prefer the namespace when it
|
|
109
|
+
// already carries them, and only fall back to `default` for a CJS-interop wrapper.
|
|
110
|
+
const ns = mod as Record<string, unknown>;
|
|
111
|
+
if (ns.MaterialCommunityIcons || ns.Ionicons) return ns;
|
|
112
|
+
if (def && typeof def === "object") return def as Record<string, unknown>;
|
|
113
|
+
return ns;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Narrow an unknown export to something mountable. A vector-icons family is either a function
|
|
118
|
+
* component or a class (`createIconSet` has returned both across versions), and React.memo /
|
|
119
|
+
* forwardRef wrappers are objects — so accept an object carrying a `$$typeof` too, and reject
|
|
120
|
+
* anything else rather than handing the reconciler a non-component.
|
|
121
|
+
*/
|
|
122
|
+
const asComponent = (value: unknown): IconComponent | undefined => {
|
|
123
|
+
if (typeof value === "function") return value as IconComponent;
|
|
124
|
+
if (value && typeof value === "object" && "$$typeof" in (value as object)) {
|
|
125
|
+
return value as IconComponent;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Module-level memo. `null` = "we looked and it is not there" (distinct from "not looked yet"),
|
|
132
|
+
* so an absent peer costs exactly one failed require per process, not one per rendered icon.
|
|
133
|
+
*/
|
|
134
|
+
let cached: Record<string, unknown> | null | undefined;
|
|
135
|
+
|
|
136
|
+
/** Reset the memo. TEST-ONLY seam — production never calls it. */
|
|
137
|
+
export const resetIconModuleCache = (): void => {
|
|
138
|
+
cached = undefined;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve one icon family component, or undefined when the peer is absent/unresolvable.
|
|
143
|
+
* Never throws: an icon is decorative, so it must never be able to break a card.
|
|
144
|
+
*
|
|
145
|
+
* `requireModule` is injectable so tests can exercise BOTH the found and absent paths without
|
|
146
|
+
* installing the native peer (same convention as `detectAppVersion`).
|
|
147
|
+
*/
|
|
148
|
+
export const resolveIconFamily = (
|
|
149
|
+
family: WireIconFamily,
|
|
150
|
+
requireModule: OptionalRequire = runtimeRequire,
|
|
151
|
+
): IconComponent | undefined => {
|
|
152
|
+
try {
|
|
153
|
+
if (cached === undefined || requireModule !== runtimeRequire) {
|
|
154
|
+
const resolved = interop(requireModule("@expo/vector-icons"));
|
|
155
|
+
// Don't poison the module memo from an injected test require.
|
|
156
|
+
if (requireModule === runtimeRequire) cached = resolved ?? null;
|
|
157
|
+
if (!resolved) return undefined;
|
|
158
|
+
return asComponent(resolved[family]);
|
|
159
|
+
}
|
|
160
|
+
if (cached === null) return undefined;
|
|
161
|
+
return asComponent(cached[family]);
|
|
162
|
+
} catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icon module — a named semantic vocabulary the AI emits, resolved to real glyphs through
|
|
3
|
+
* an OPTIONAL peer, with a host override registry in front and a no-op behind. See
|
|
4
|
+
* `vocabulary.ts` for the contract and `expoIcons.ts` for why the optional peer honors the
|
|
5
|
+
* kit's no-forced-native-dependency policy rather than breaking it.
|
|
6
|
+
*/
|
|
7
|
+
export { WireIcon } from "./WireIcon";
|
|
8
|
+
export type { WireIconProps } from "./WireIcon";
|
|
9
|
+
export { IconRegistryProvider, useHostIcon } from "./IconRegistry";
|
|
10
|
+
export type { WireIconRegistry, IconRegistryProviderProps } from "./IconRegistry";
|
|
11
|
+
export {
|
|
12
|
+
WIRE_ICON_GLYPHS,
|
|
13
|
+
WIRE_ICON_NAMES,
|
|
14
|
+
lookupIconGlyph,
|
|
15
|
+
} from "./vocabulary";
|
|
16
|
+
export type { WireIconName, WireIconFamily, WireIconGlyph } from "./vocabulary";
|
|
17
|
+
export { resolveIconFamily, resetIconModuleCache } from "./expoIcons";
|
|
18
|
+
export type { OptionalRequire, IconComponent } from "./expoIcons";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Wire icon VOCABULARY — the stable, semantic contract between the AI and the kit.
|
|
3
|
+
*
|
|
4
|
+
* The model emits a plain semantic string (`"instagram"`, `"friend"`, `"wedding"`), never a
|
|
5
|
+
* raw icon-library glyph name. That indirection is the whole point:
|
|
6
|
+
*
|
|
7
|
+
* • The model is never coupled to a library version. `@expo/vector-icons` renaming a glyph,
|
|
8
|
+
* or a host swapping the library out for brand SVGs, changes THIS map — not the prompt,
|
|
9
|
+
* not the server, and not a single stored answer.
|
|
10
|
+
* • A host can override any name with its own artwork (see `IconRegistry`), so brand icons
|
|
11
|
+
* need no kit change.
|
|
12
|
+
* • An unknown name degrades to no icon (see `WireIcon`), which is why the vocabulary can
|
|
13
|
+
* grow without a coordinated kit+server release.
|
|
14
|
+
*
|
|
15
|
+
* `WIRE_ICON_NAMES` is exported so the server/AI side can be taught the list verbatim — paste
|
|
16
|
+
* it into the prompt as the allowed `icon` values. Keep names SEMANTIC (what the option means)
|
|
17
|
+
* rather than visual (what the glyph looks like): the model reasons about meaning.
|
|
18
|
+
*
|
|
19
|
+
* Every glyph below was verified to exist in the `@expo/vector-icons` 15.0.3 glyphmaps that
|
|
20
|
+
* both live host apps already ship. MaterialCommunityIcons is the default family (the broadest
|
|
21
|
+
* single set); Ionicons covers the few brand logos MCI lacks (TikTok, Discord).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The icon families this kit resolves out of `@expo/vector-icons`. Both are root exports. */
|
|
25
|
+
export type WireIconFamily = "MaterialCommunityIcons" | "Ionicons";
|
|
26
|
+
|
|
27
|
+
/** One vocabulary entry: the concrete glyph a semantic name resolves to. */
|
|
28
|
+
export interface WireIconGlyph {
|
|
29
|
+
family: WireIconFamily;
|
|
30
|
+
/** The library's own glyph name — an INTERNAL detail. The AI never sees or emits this. */
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The semantic name → concrete glyph map. Grouped by the onboarding questions these actually
|
|
36
|
+
* serve: acquisition channels ("where did you hear about us"), intent/occasion ("what brings
|
|
37
|
+
* you here"), and a general-purpose set for everything else.
|
|
38
|
+
*/
|
|
39
|
+
export const WIRE_ICON_GLYPHS = {
|
|
40
|
+
// ── Acquisition channels ("where did you hear about us?") ──────────────────
|
|
41
|
+
instagram: { family: "MaterialCommunityIcons", name: "instagram" },
|
|
42
|
+
tiktok: { family: "Ionicons", name: "logo-tiktok" },
|
|
43
|
+
youtube: { family: "MaterialCommunityIcons", name: "youtube" },
|
|
44
|
+
facebook: { family: "MaterialCommunityIcons", name: "facebook" },
|
|
45
|
+
twitter: { family: "MaterialCommunityIcons", name: "twitter" },
|
|
46
|
+
reddit: { family: "MaterialCommunityIcons", name: "reddit" },
|
|
47
|
+
linkedin: { family: "MaterialCommunityIcons", name: "linkedin" },
|
|
48
|
+
discord: { family: "Ionicons", name: "logo-discord" },
|
|
49
|
+
whatsapp: { family: "MaterialCommunityIcons", name: "whatsapp" },
|
|
50
|
+
twitch: { family: "MaterialCommunityIcons", name: "twitch" },
|
|
51
|
+
snapchat: { family: "MaterialCommunityIcons", name: "snapchat" },
|
|
52
|
+
pinterest: { family: "MaterialCommunityIcons", name: "pinterest" },
|
|
53
|
+
google: { family: "MaterialCommunityIcons", name: "google" },
|
|
54
|
+
search: { family: "MaterialCommunityIcons", name: "magnify" },
|
|
55
|
+
friend: { family: "MaterialCommunityIcons", name: "account-heart" },
|
|
56
|
+
podcast: { family: "MaterialCommunityIcons", name: "podcast" },
|
|
57
|
+
blog: { family: "MaterialCommunityIcons", name: "post" },
|
|
58
|
+
newsletter: { family: "MaterialCommunityIcons", name: "email-outline" },
|
|
59
|
+
news: { family: "MaterialCommunityIcons", name: "newspaper" },
|
|
60
|
+
"app-store": { family: "MaterialCommunityIcons", name: "apple" },
|
|
61
|
+
ad: { family: "MaterialCommunityIcons", name: "bullhorn" },
|
|
62
|
+
|
|
63
|
+
// ── Intent / occasion ("what brings you here?") ────────────────────────────
|
|
64
|
+
wedding: { family: "MaterialCommunityIcons", name: "ring" },
|
|
65
|
+
birthday: { family: "MaterialCommunityIcons", name: "cake-variant" },
|
|
66
|
+
baby: { family: "MaterialCommunityIcons", name: "baby-carriage" },
|
|
67
|
+
travel: { family: "MaterialCommunityIcons", name: "airplane" },
|
|
68
|
+
work: { family: "MaterialCommunityIcons", name: "briefcase" },
|
|
69
|
+
celebration: { family: "MaterialCommunityIcons", name: "party-popper" },
|
|
70
|
+
store: { family: "MaterialCommunityIcons", name: "storefront" },
|
|
71
|
+
school: { family: "MaterialCommunityIcons", name: "school" },
|
|
72
|
+
team: { family: "MaterialCommunityIcons", name: "account-multiple" },
|
|
73
|
+
growth: { family: "MaterialCommunityIcons", name: "trending-up" },
|
|
74
|
+
|
|
75
|
+
// ── General purpose ────────────────────────────────────────────────────────
|
|
76
|
+
sparkle: { family: "MaterialCommunityIcons", name: "star-four-points" },
|
|
77
|
+
heart: { family: "MaterialCommunityIcons", name: "heart" },
|
|
78
|
+
star: { family: "MaterialCommunityIcons", name: "star" },
|
|
79
|
+
fire: { family: "MaterialCommunityIcons", name: "fire" },
|
|
80
|
+
target: { family: "MaterialCommunityIcons", name: "target" },
|
|
81
|
+
trophy: { family: "MaterialCommunityIcons", name: "trophy" },
|
|
82
|
+
rocket: { family: "MaterialCommunityIcons", name: "rocket-launch" },
|
|
83
|
+
clock: { family: "MaterialCommunityIcons", name: "clock-outline" },
|
|
84
|
+
calendar: { family: "MaterialCommunityIcons", name: "calendar-star" },
|
|
85
|
+
camera: { family: "MaterialCommunityIcons", name: "camera" },
|
|
86
|
+
image: { family: "MaterialCommunityIcons", name: "image" },
|
|
87
|
+
video: { family: "MaterialCommunityIcons", name: "video" },
|
|
88
|
+
mic: { family: "MaterialCommunityIcons", name: "microphone" },
|
|
89
|
+
music: { family: "MaterialCommunityIcons", name: "music" },
|
|
90
|
+
book: { family: "MaterialCommunityIcons", name: "book-open-variant" },
|
|
91
|
+
food: { family: "MaterialCommunityIcons", name: "silverware-fork-knife" },
|
|
92
|
+
coffee: { family: "MaterialCommunityIcons", name: "coffee" },
|
|
93
|
+
fitness: { family: "MaterialCommunityIcons", name: "dumbbell" },
|
|
94
|
+
health: { family: "MaterialCommunityIcons", name: "heart-pulse" },
|
|
95
|
+
meditation: { family: "MaterialCommunityIcons", name: "meditation" },
|
|
96
|
+
sleep: { family: "MaterialCommunityIcons", name: "sleep" },
|
|
97
|
+
brain: { family: "MaterialCommunityIcons", name: "brain" },
|
|
98
|
+
idea: { family: "MaterialCommunityIcons", name: "lightbulb-on" },
|
|
99
|
+
money: { family: "MaterialCommunityIcons", name: "cash" },
|
|
100
|
+
shopping: { family: "MaterialCommunityIcons", name: "cart" },
|
|
101
|
+
gift: { family: "MaterialCommunityIcons", name: "gift" },
|
|
102
|
+
home: { family: "MaterialCommunityIcons", name: "home" },
|
|
103
|
+
car: { family: "MaterialCommunityIcons", name: "car" },
|
|
104
|
+
pet: { family: "MaterialCommunityIcons", name: "paw" },
|
|
105
|
+
plant: { family: "MaterialCommunityIcons", name: "sprout" },
|
|
106
|
+
sun: { family: "MaterialCommunityIcons", name: "white-balance-sunny" },
|
|
107
|
+
moon: { family: "MaterialCommunityIcons", name: "weather-night" },
|
|
108
|
+
globe: { family: "MaterialCommunityIcons", name: "earth" },
|
|
109
|
+
"map-pin": { family: "MaterialCommunityIcons", name: "map-marker" },
|
|
110
|
+
compass: { family: "MaterialCommunityIcons", name: "compass" },
|
|
111
|
+
flag: { family: "MaterialCommunityIcons", name: "flag" },
|
|
112
|
+
bookmark: { family: "MaterialCommunityIcons", name: "bookmark" },
|
|
113
|
+
bell: { family: "MaterialCommunityIcons", name: "bell" },
|
|
114
|
+
mail: { family: "MaterialCommunityIcons", name: "email" },
|
|
115
|
+
phone: { family: "MaterialCommunityIcons", name: "phone" },
|
|
116
|
+
chat: { family: "MaterialCommunityIcons", name: "chat" },
|
|
117
|
+
user: { family: "MaterialCommunityIcons", name: "account" },
|
|
118
|
+
users: { family: "MaterialCommunityIcons", name: "account-group" },
|
|
119
|
+
settings: { family: "MaterialCommunityIcons", name: "cog" },
|
|
120
|
+
tools: { family: "MaterialCommunityIcons", name: "tools" },
|
|
121
|
+
puzzle: { family: "MaterialCommunityIcons", name: "puzzle" },
|
|
122
|
+
chart: { family: "MaterialCommunityIcons", name: "chart-line" },
|
|
123
|
+
shield: { family: "MaterialCommunityIcons", name: "shield-check" },
|
|
124
|
+
lock: { family: "MaterialCommunityIcons", name: "lock" },
|
|
125
|
+
cloud: { family: "MaterialCommunityIcons", name: "cloud" },
|
|
126
|
+
pencil: { family: "MaterialCommunityIcons", name: "pencil" },
|
|
127
|
+
check: { family: "MaterialCommunityIcons", name: "check-circle" },
|
|
128
|
+
plus: { family: "MaterialCommunityIcons", name: "plus-circle" },
|
|
129
|
+
question: { family: "MaterialCommunityIcons", name: "help-circle" },
|
|
130
|
+
other: { family: "MaterialCommunityIcons", name: "dots-horizontal-circle" },
|
|
131
|
+
} as const satisfies Record<string, WireIconGlyph>;
|
|
132
|
+
|
|
133
|
+
/** A name in the vocabulary. The card schemas accept any string and degrade — see `WireIcon`. */
|
|
134
|
+
export type WireIconName = keyof typeof WIRE_ICON_GLYPHS;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Every semantic name, as a plain array — the list to paste into the server/AI prompt as the
|
|
138
|
+
* allowed `icon` values. Sorted for a stable, diffable prompt.
|
|
139
|
+
*/
|
|
140
|
+
export const WIRE_ICON_NAMES: readonly string[] = Object.keys(WIRE_ICON_GLYPHS).sort();
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Look up a semantic name. Returns undefined for anything outside the vocabulary.
|
|
144
|
+
*
|
|
145
|
+
* The OWN-property check is load-bearing, not defensive noise: the name comes from an LLM, and
|
|
146
|
+
* a plain object lookup answers `"toString"` / `"constructor"` / `"valueOf"` with an inherited
|
|
147
|
+
* `Object.prototype` member. That would hand back a function where a `{family, name}` is
|
|
148
|
+
* expected — a non-glyph masquerading as one. Caught by the vocabulary canary.
|
|
149
|
+
*/
|
|
150
|
+
export const lookupIconGlyph = (name: string | undefined): WireIconGlyph | undefined => {
|
|
151
|
+
if (!name) return undefined;
|
|
152
|
+
if (!Object.prototype.hasOwnProperty.call(WIRE_ICON_GLYPHS, name)) return undefined;
|
|
153
|
+
return (WIRE_ICON_GLYPHS as Record<string, WireIconGlyph>)[name];
|
|
154
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -52,11 +52,31 @@ export {
|
|
|
52
52
|
ChipSelectCard,
|
|
53
53
|
TextInputCard,
|
|
54
54
|
SelectionCard,
|
|
55
|
+
CardGridSelectCard,
|
|
55
56
|
StatusCard,
|
|
56
57
|
NumberStepperCard,
|
|
57
58
|
InterstitialCard,
|
|
58
59
|
onboardingComponents,
|
|
59
60
|
} from "./cards";
|
|
61
|
+
export type { CardOption } from "./cards";
|
|
62
|
+
|
|
63
|
+
// ─── Icons (semantic vocabulary → optional @expo/vector-icons → nothing) ──────
|
|
64
|
+
// WIRE_ICON_NAMES is the list to paste into the server/AI prompt as the allowed `icon` values.
|
|
65
|
+
export {
|
|
66
|
+
WireIcon,
|
|
67
|
+
IconRegistryProvider,
|
|
68
|
+
useHostIcon,
|
|
69
|
+
WIRE_ICON_GLYPHS,
|
|
70
|
+
WIRE_ICON_NAMES,
|
|
71
|
+
lookupIconGlyph,
|
|
72
|
+
} from "./icons";
|
|
73
|
+
export type {
|
|
74
|
+
WireIconProps,
|
|
75
|
+
WireIconRegistry,
|
|
76
|
+
WireIconName,
|
|
77
|
+
WireIconFamily,
|
|
78
|
+
WireIconGlyph,
|
|
79
|
+
} from "./icons";
|
|
60
80
|
|
|
61
81
|
// ─── Flow internals (advanced) ────────────────────────────────────────────────
|
|
62
82
|
export { OnboardingFlow } from "./OnboardingFlow";
|