@everfur/sdk 0.1.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 (89) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +161 -0
  4. package/animations/package.json +8 -0
  5. package/chat/package.json +8 -0
  6. package/client/package.json +8 -0
  7. package/core/package.json +8 -0
  8. package/dist/CameraPort-Cv31Pz7g.d.cts +29 -0
  9. package/dist/CameraPort-Cv31Pz7g.d.ts +29 -0
  10. package/dist/ChatController-CKdBvPj2.d.ts +146 -0
  11. package/dist/ChatController-CpUMvvZf.d.cts +146 -0
  12. package/dist/EverfurResult-D92-uL82.d.cts +240 -0
  13. package/dist/EverfurResult-D92-uL82.d.ts +240 -0
  14. package/dist/FilePort-BabWrv7I.d.cts +22 -0
  15. package/dist/FilePort-BabWrv7I.d.ts +22 -0
  16. package/dist/PhotoController-BItt5M7u.d.cts +43 -0
  17. package/dist/PhotoController-D8zMTdcW.d.ts +43 -0
  18. package/dist/TelemetryPort-BDNr00hu.d.cts +12 -0
  19. package/dist/TelemetryPort-BDNr00hu.d.ts +12 -0
  20. package/dist/animations/index.cjs +1997 -0
  21. package/dist/animations/index.d.cts +517 -0
  22. package/dist/animations/index.d.ts +517 -0
  23. package/dist/animations/index.js +1972 -0
  24. package/dist/cacheEpoch-DknKn3S0.d.cts +36 -0
  25. package/dist/cacheEpoch-DknKn3S0.d.ts +36 -0
  26. package/dist/chat/index.cjs +1170 -0
  27. package/dist/chat/index.d.cts +59 -0
  28. package/dist/chat/index.d.ts +59 -0
  29. package/dist/chat/index.js +1167 -0
  30. package/dist/client/index.cjs +2317 -0
  31. package/dist/client/index.d.cts +65 -0
  32. package/dist/client/index.d.ts +65 -0
  33. package/dist/client/index.js +2217 -0
  34. package/dist/config-BSjBdxrZ.d.cts +501 -0
  35. package/dist/config-CiJ0PVBB.d.ts +501 -0
  36. package/dist/core/index.cjs +3175 -0
  37. package/dist/core/index.d.cts +70 -0
  38. package/dist/core/index.d.ts +70 -0
  39. package/dist/core/index.js +3163 -0
  40. package/dist/identity-Brl-lDd6.d.cts +91 -0
  41. package/dist/identity-DK9zORrG.d.ts +91 -0
  42. package/dist/ids-CJ1S6adf.d.cts +46 -0
  43. package/dist/ids-CJ1S6adf.d.ts +46 -0
  44. package/dist/index.cjs +4488 -0
  45. package/dist/index.d.cts +156 -0
  46. package/dist/index.d.ts +156 -0
  47. package/dist/index.js +4464 -0
  48. package/dist/petsRepository-BEGb97M9.d.cts +326 -0
  49. package/dist/petsRepository-Bu18r2kK.d.ts +326 -0
  50. package/dist/photo/index.cjs +2189 -0
  51. package/dist/photo/index.d.cts +43 -0
  52. package/dist/photo/index.d.ts +43 -0
  53. package/dist/photo/index.js +2186 -0
  54. package/dist/projection-CeIUsbSk.d.cts +8 -0
  55. package/dist/projection-CeIUsbSk.d.ts +8 -0
  56. package/dist/records/index.cjs +2840 -0
  57. package/dist/records/index.d.cts +224 -0
  58. package/dist/records/index.d.ts +224 -0
  59. package/dist/records/index.js +2834 -0
  60. package/dist/requestFunnel-DuUH-kAe.d.cts +28 -0
  61. package/dist/requestFunnel-dio5OmR9.d.ts +28 -0
  62. package/dist/resolve-Dq_4_agU.d.cts +86 -0
  63. package/dist/resolve-Dq_4_agU.d.ts +86 -0
  64. package/dist/runtime-BgQnA594.d.cts +349 -0
  65. package/dist/runtime-CBA-LvdM.d.ts +349 -0
  66. package/dist/server/index.cjs +533 -0
  67. package/dist/server/index.d.cts +48 -0
  68. package/dist/server/index.d.ts +48 -0
  69. package/dist/server/index.js +530 -0
  70. package/dist/testing/index.cjs +825 -0
  71. package/dist/testing/index.d.cts +113 -0
  72. package/dist/testing/index.d.ts +113 -0
  73. package/dist/testing/index.js +822 -0
  74. package/dist/testing/rn/index.cjs +449 -0
  75. package/dist/testing/rn/index.d.cts +49 -0
  76. package/dist/testing/rn/index.d.ts +49 -0
  77. package/dist/testing/rn/index.js +444 -0
  78. package/dist/video/index.cjs +1941 -0
  79. package/dist/video/index.d.cts +66 -0
  80. package/dist/video/index.d.ts +66 -0
  81. package/dist/video/index.js +1938 -0
  82. package/package.json +311 -0
  83. package/photo/package.json +8 -0
  84. package/records/package.json +8 -0
  85. package/server/device-blocked.cjs +15 -0
  86. package/server/package.json +9 -0
  87. package/testing/package.json +8 -0
  88. package/testing/rn/package.json +8 -0
  89. package/video/package.json +8 -0
@@ -0,0 +1,1170 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactNativeSafeAreaContext = require('react-native-safe-area-context');
5
+ var reactNative = require('react-native');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/rn/chat/useChat.ts
9
+ var CONTRACT_RETRYABILITY = Object.freeze([
10
+ Object.freeze({ wireCode: "AccessDeniedException", envelope: "http_envelope", httpStatus: 403, retryable: false }),
11
+ Object.freeze({ wireCode: "AuthorizationException", envelope: "http_envelope", httpStatus: 401, retryable: false }),
12
+ Object.freeze({ wireCode: "ConflictException", envelope: "http_envelope", httpStatus: 409, retryable: true }),
13
+ Object.freeze({ wireCode: "DAILY_QUOTA_EXCEEDED", envelope: "http_envelope", httpStatus: 429, retryable: true }),
14
+ Object.freeze({ wireCode: "IDEMPOTENCY_KEY_IN_FLIGHT", envelope: "http_envelope", httpStatus: 409, retryable: true }),
15
+ Object.freeze({ wireCode: "IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST", envelope: "http_envelope", httpStatus: 409, retryable: false }),
16
+ Object.freeze({ wireCode: "InternalFailureException", envelope: "http_envelope", httpStatus: 500, retryable: true }),
17
+ Object.freeze({ wireCode: "NotImplementedException", envelope: "http_envelope", httpStatus: 501, retryable: false }),
18
+ Object.freeze({ wireCode: "PaymentRequiredException", envelope: "http_envelope", httpStatus: 402, retryable: false }),
19
+ Object.freeze({ wireCode: "ResourceNotFoundException", envelope: "http_envelope", httpStatus: 404, retryable: false }),
20
+ Object.freeze({ wireCode: "ServiceUnavailableException", envelope: "http_envelope", httpStatus: 503, retryable: true }),
21
+ Object.freeze({ wireCode: "TIER_UPGRADE_REQUIRED", envelope: "http_envelope", httpStatus: 402, retryable: false }),
22
+ Object.freeze({ wireCode: "ThrottlingException", envelope: "http_envelope", httpStatus: 429, retryable: true }),
23
+ Object.freeze({ wireCode: "ValidationException", envelope: "http_envelope", httpStatus: 400, retryable: false }),
24
+ Object.freeze({ wireCode: "accessDenied", envelope: "http_envelope", httpStatus: 403, retryable: false }),
25
+ Object.freeze({ wireCode: "authExpired", envelope: "http_envelope", httpStatus: 401, retryable: true }),
26
+ Object.freeze({ wireCode: "internal_error", envelope: "sse_frame", httpStatus: 200, retryable: false }),
27
+ Object.freeze({ wireCode: "invalidRequest", envelope: "http_envelope", httpStatus: 400, retryable: false }),
28
+ Object.freeze({ wireCode: "not_found", envelope: "sse_frame", httpStatus: 200, retryable: false }),
29
+ Object.freeze({ wireCode: "session_required", envelope: "http_envelope", httpStatus: 403, retryable: false }),
30
+ Object.freeze({ wireCode: "timeout", envelope: "sse_frame", httpStatus: 200, retryable: true }),
31
+ Object.freeze({ wireCode: "unavailable", envelope: "http_envelope", httpStatus: 503, retryable: true })
32
+ ]);
33
+ Object.freeze(
34
+ CONTRACT_RETRYABILITY.map((row) => row.wireCode)
35
+ );
36
+
37
+ // src/client/errors/contract.ts
38
+ var NORMALIZED_FOLD = Object.freeze({
39
+ // the default status-mapped envelope
40
+ ValidationException: "validationFailed",
41
+ AuthorizationException: "authRejected",
42
+ PaymentRequiredException: "paymentRequired",
43
+ AccessDeniedException: "accessDenied",
44
+ ResourceNotFoundException: "resourceNotFound",
45
+ ConflictException: "conflict",
46
+ ThrottlingException: "rateLimited",
47
+ InternalFailureException: "internalError",
48
+ NotImplementedException: "notImplemented",
49
+ ServiceUnavailableException: "serviceUnavailable",
50
+ // dict-detail codes on the widget surface
51
+ authExpired: "authExpired",
52
+ unavailable: "serviceUnavailable",
53
+ session_required: "sessionRequired",
54
+ accessDenied: "accessDenied",
55
+ invalidRequest: "sdkUpdateRequired",
56
+ // typed codes
57
+ IDEMPOTENCY_KEY_IN_FLIGHT: "idempotencyInFlight",
58
+ IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST: "idempotencyConflict",
59
+ DAILY_QUOTA_EXCEEDED: "quotaExceeded",
60
+ TIER_UPGRADE_REQUIRED: "paymentRequired",
61
+ // SSE error frames
62
+ not_found: "conversationNotFound",
63
+ timeout: "streamTimeout",
64
+ internal_error: "internalError"
65
+ });
66
+ var CONTRACT_ERROR_ROWS = Object.freeze(
67
+ CONTRACT_RETRYABILITY.map(
68
+ (generated) => Object.freeze({
69
+ wireCode: generated.wireCode,
70
+ envelope: generated.envelope,
71
+ retryable: generated.retryable,
72
+ normalized: NORMALIZED_FOLD[generated.wireCode]
73
+ })
74
+ )
75
+ );
76
+ var BY_UPPER = new Map(
77
+ CONTRACT_ERROR_ROWS.map((r) => [r.wireCode.toUpperCase(), r])
78
+ );
79
+ function contractErrorFor(wireCode) {
80
+ if (typeof wireCode !== "string") return void 0;
81
+ const key = wireCode.trim().toUpperCase();
82
+ return key.length > 0 ? BY_UPPER.get(key) : void 0;
83
+ }
84
+ var CATEGORY_OF = Object.freeze({
85
+ // the caller's sign-in state is wrong and a credential change is the remedy
86
+ authExpired: "authentication",
87
+ authRejected: "authentication",
88
+ sessionRequired: "authentication",
89
+ // signed in, correctly identified, and still not allowed
90
+ accessDenied: "authorization",
91
+ // a plan gate, not a permission failure: the remedy is commercial
92
+ paymentRequired: "entitlement",
93
+ // slow down / come back later
94
+ quotaExceeded: "rateLimit",
95
+ rateLimited: "rateLimit",
96
+ // the request itself was wrong; retrying it unchanged can never succeed
97
+ validationFailed: "invalidRequest",
98
+ sdkUpdateRequired: "invalidRequest",
99
+ // the target is gone, hidden, or was never visible to this caller
100
+ conversationNotFound: "notFound",
101
+ petNotFound: "notFound",
102
+ resourceNotFound: "notFound",
103
+ // something else already holds the thing
104
+ conflict: "conflict",
105
+ idempotencyConflict: "conflict",
106
+ idempotencyInFlight: "conflict",
107
+ // the pipe, not the request: timeouts, drops, a briefly unreachable dependency
108
+ streamTimeout: "connection",
109
+ streamIncomplete: "connection",
110
+ upstreamLlmTimeout: "connection",
111
+ serviceUnavailable: "connection",
112
+ // the server admits fault
113
+ internalError: "server",
114
+ notImplemented: "server",
115
+ mediaUploadFailed: "media",
116
+ moderationBlocked: "policy",
117
+ unknown: "unknown"
118
+ });
119
+ function categoryFor(code) {
120
+ return CATEGORY_OF[code];
121
+ }
122
+
123
+ // src/client/errors/EverfurError.ts
124
+ var USER_FACING_COPY = Object.freeze({
125
+ authExpired: "Please sign in again to continue.",
126
+ authRejected: "Please sign in again to continue.",
127
+ sessionRequired: "This app needs to be updated to keep using Everfur.",
128
+ accessDenied: "This feature isn't available on your account right now.",
129
+ paymentRequired: "This is part of a paid plan.",
130
+ quotaExceeded: "You have used today's messages. This resets tomorrow.",
131
+ rateLimited: "You are sending messages quickly. Try again in a few seconds.",
132
+ validationFailed: "Something in that request was not right. Check the details and try again.",
133
+ sdkUpdateRequired: "This app needs to be updated to keep using Everfur.",
134
+ conflict: "That is already running. Wait for it to finish, then try again.",
135
+ idempotencyConflict: "That message was already delivered.",
136
+ idempotencyInFlight: "That is still processing. Give it a moment.",
137
+ conversationNotFound: "This conversation is no longer available.",
138
+ petNotFound: "We could not find that pet. Please pick a pet and try again.",
139
+ resourceNotFound: "We could not find that.",
140
+ streamTimeout: "The response timed out. Tap retry to try again.",
141
+ mediaUploadFailed: "We could not upload that photo. Please attach it again.",
142
+ moderationBlocked: "This request cannot be answered here.",
143
+ upstreamLlmTimeout: "The response timed out. Tap retry to try again.",
144
+ internalError: "We hit a snag on our end. Please try again.",
145
+ notImplemented: "That is not available yet.",
146
+ serviceUnavailable: "That service is briefly unavailable. Try again shortly.",
147
+ streamIncomplete: "The connection dropped mid-answer. Tap retry to continue.",
148
+ unknown: "Something went wrong. Please try again."
149
+ });
150
+ function userFacingError(code) {
151
+ return USER_FACING_COPY[code];
152
+ }
153
+ function defineHidden(target, key, value) {
154
+ Object.defineProperty(target, key, {
155
+ value,
156
+ enumerable: false,
157
+ writable: false,
158
+ configurable: false
159
+ });
160
+ }
161
+ function makeEverfurError(code, opts) {
162
+ contractErrorFor(opts?.wireCode);
163
+ const error = {
164
+ code,
165
+ category: categoryFor(code),
166
+ retryable: opts?.retryable,
167
+ displayMessage: userFacingError(code)
168
+ };
169
+ if (opts?.httpStatus !== void 0) error.httpStatus = opts.httpStatus;
170
+ if (opts?.requestId) error.requestId = opts.requestId;
171
+ if (opts?.wireMessage) defineHidden(error, "wireMessage", opts.wireMessage);
172
+ if (opts?.raw !== void 0 && opts.raw !== null) defineHidden(error, "raw", opts.raw);
173
+ return Object.freeze(error);
174
+ }
175
+
176
+ // src/client/errors/EverfurResult.ts
177
+ function ok(value, warning) {
178
+ return Object.freeze(
179
+ { ok: true, value }
180
+ );
181
+ }
182
+ function err(error) {
183
+ return Object.freeze({ ok: false, error });
184
+ }
185
+ var EverfurConfigError = class _EverfurConfigError extends Error {
186
+ constructor(kind, message) {
187
+ super(message);
188
+ this.name = "EverfurConfigError";
189
+ this.kind = kind;
190
+ Object.setPrototypeOf(this, _EverfurConfigError.prototype);
191
+ }
192
+ };
193
+
194
+ // src/client/protocol/frames.ts
195
+ function assertNever(x) {
196
+ throw new Error(
197
+ `assertNever: unhandled discriminated-union member ${JSON.stringify(x)}`
198
+ );
199
+ }
200
+
201
+ // src/core/chat/ChatController.ts
202
+ var MESSAGE_MAX_CHARS = 4e3;
203
+
204
+ // src/client/entitlements/resolve.ts
205
+ var REQUIRES = Object.freeze({
206
+ chat: Object.freeze(["chat.message.create"]),
207
+ records: Object.freeze(["records.upload.create", "records.record.read"]),
208
+ video: Object.freeze(["video.gait.execute"]),
209
+ photo: Object.freeze(["photo.checkup.execute"]),
210
+ animations: Object.freeze(["animations.state.read"])
211
+ });
212
+ function requires(name) {
213
+ return REQUIRES[name];
214
+ }
215
+ function decideCapability(name, decisions, opts) {
216
+ if (opts?.isPending === true) {
217
+ return freezeDecision({ name, enabled: false, isPending: true, limits: {} });
218
+ }
219
+ const keys = requires(name);
220
+ const byResource = /* @__PURE__ */ new Map();
221
+ for (const decision of decisions) byResource.set(decision.resource, decision);
222
+ const limits = mergeLimits(keys, byResource);
223
+ const missing = keys.filter((key) => byResource.get(key)?.granted !== true);
224
+ if (missing.length === 0) {
225
+ return freezeDecision({ name, enabled: true, isPending: false, limits });
226
+ }
227
+ return freezeDecision({
228
+ name,
229
+ enabled: false,
230
+ isPending: false,
231
+ limits,
232
+ disabled: disabledFor(keys, byResource)
233
+ });
234
+ }
235
+ function mergeLimits(keys, byResource) {
236
+ const out = {};
237
+ for (const key of keys) {
238
+ const decisionLimits = byResource.get(key)?.limits;
239
+ if (!decisionLimits) continue;
240
+ for (const [limitKey, limitValue] of Object.entries(decisionLimits)) {
241
+ const existing = out[limitKey];
242
+ out[limitKey] = existing === void 0 ? limitValue : Math.min(existing, limitValue);
243
+ }
244
+ }
245
+ return out;
246
+ }
247
+ function disabledFor(keys, byResource) {
248
+ for (const key of keys) {
249
+ const explicit = byResource.get(key)?.disabled;
250
+ if (explicit) {
251
+ return Object.freeze({
252
+ reason: explicit.reason,
253
+ level: explicit.level,
254
+ recoverable: explicit.recoverable
255
+ });
256
+ }
257
+ }
258
+ return Object.freeze({
259
+ reason: userFacingError("accessDenied"),
260
+ level: "user_entitlement",
261
+ recoverable: false
262
+ });
263
+ }
264
+ function freezeDecision(d) {
265
+ const base = {
266
+ name: d.name,
267
+ enabled: d.enabled,
268
+ isPending: d.isPending,
269
+ limits: Object.freeze({ ...d.limits })
270
+ };
271
+ if (d.disabled !== void 0) base.disabled = d.disabled;
272
+ return Object.freeze(base);
273
+ }
274
+
275
+ // src/client/theme/tokens.ts
276
+ function deepFreeze(value) {
277
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
278
+ Object.freeze(value);
279
+ for (const key of Object.keys(value)) {
280
+ deepFreeze(value[key]);
281
+ }
282
+ }
283
+ return value;
284
+ }
285
+ var SPACE = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, xxl: 32 };
286
+ var RADIUS = { tile: 5, xs: 6, sm: 10, md: 14, lg: 20, xl: 28, pill: 500 };
287
+ var TYPE_SIZE = {
288
+ nano: 8,
289
+ small: 9,
290
+ micro: 10,
291
+ tiny: 11,
292
+ caption: 12,
293
+ bodyTight: 13,
294
+ body: 14,
295
+ subtitle: 16,
296
+ heading: 18,
297
+ verdict: 24
298
+ };
299
+ var LINE_HEIGHT = { tight: 1.15, snug: 1.3, normal: 1.4, relaxed: 1.55 };
300
+ var WEIGHT = { regular: "400", medium: "500", bold: "700" };
301
+ var LETTER_SPACING = {
302
+ eyebrow: 1,
303
+ monoWide: 0.9,
304
+ headerBrand: -0.42,
305
+ tight: -0.35
306
+ };
307
+ function baseType() {
308
+ return {
309
+ family: "System",
310
+ mono: "monospace",
311
+ fontUrl: null,
312
+ size: TYPE_SIZE,
313
+ lineHeight: LINE_HEIGHT,
314
+ weight: WEIGHT,
315
+ letterSpacing: LETTER_SPACING
316
+ };
317
+ }
318
+ var MOTION = {
319
+ duration: { instant: 80, fast: 140, normal: 220, slow: 320 },
320
+ spring: { gentle: { damping: 18, stiffness: 200 } },
321
+ easing: { standard: "cubic-bezier(0.4, 0, 0.2, 1)" },
322
+ reducedMotion: false
323
+ };
324
+ var LIGHT_COLOR = {
325
+ brand: "#10462E",
326
+ brandSoft: "rgba(16,70,46,0.10)",
327
+ brandSofter: "rgba(16,70,46,0.15)",
328
+ brandBorder: "rgba(16,70,46,0.18)",
329
+ borderHairlineGreen: "rgba(16,70,46,0.25)",
330
+ surface: "#FFFFFF",
331
+ surfaceCream: "#FDFBF8",
332
+ surfaceSubtle: "#EFE9E3",
333
+ background: "#FFFFFF",
334
+ borderRingCream: "#D8D2C8",
335
+ text: {
336
+ primary: "#111111",
337
+ secondary: "#0E462E",
338
+ muted: "#6E6660",
339
+ placeholder: "rgba(0,0,0,0.30)",
340
+ onBrand: "#FFFFFF",
341
+ metaCool: "#3D3D3D"
342
+ },
343
+ primary: "#10462E",
344
+ onPrimary: "#FFFFFF",
345
+ accent: "#10462E",
346
+ onAccent: "#FFFFFF",
347
+ accentEdge: "#07C168",
348
+ success: "#07C168",
349
+ danger: "#B91C1C",
350
+ errorRed: "#FF1B00",
351
+ warningStrong: "#A45610",
352
+ cautionAccent: "#D9821D",
353
+ riskConcern: "#EFA700",
354
+ badgeBgPositive: "rgba(7,193,104,0.12)",
355
+ badgeBgCaution: "rgba(217,130,29,0.14)",
356
+ badgeBgCritical: "rgba(185,28,28,0.12)",
357
+ badgeBgNeutral: "rgba(110,102,96,0.12)",
358
+ bubbleUser: "#10462E",
359
+ bubbleAssistant: "#FFFFFF",
360
+ skeleton: "#EFE9E3",
361
+ scanButtonPink: "#FFC3BC",
362
+ checkupCardLowRiskBg: "rgba(7,193,104,0.10)",
363
+ checkupCardConcernBg: "rgba(239,167,0,0.10)"
364
+ };
365
+ var EVERFUR_LIGHT = deepFreeze({
366
+ brightness: "light",
367
+ meta: { displayName: "Everfur", logoUrl: null, placeholder: null, hidePoweredBy: false },
368
+ color: LIGHT_COLOR,
369
+ space: SPACE,
370
+ radius: RADIUS,
371
+ type: baseType(),
372
+ motion: MOTION
373
+ });
374
+ var DARK_COLOR = {
375
+ brand: "#6BD39A",
376
+ brandSoft: "rgba(107,211,154,0.12)",
377
+ brandSofter: "rgba(107,211,154,0.18)",
378
+ brandBorder: "rgba(107,211,154,0.22)",
379
+ borderHairlineGreen: "rgba(107,211,154,0.28)",
380
+ surface: "#1C1F23",
381
+ surfaceCream: "#23272C",
382
+ surfaceSubtle: "#23272C",
383
+ background: "#15171A",
384
+ borderRingCream: "#3A3F45",
385
+ text: {
386
+ primary: "#F5F3F0",
387
+ secondary: "#A9E4C4",
388
+ muted: "#9A938C",
389
+ placeholder: "rgba(255,255,255,0.35)",
390
+ onBrand: "#0E1F17",
391
+ metaCool: "#C9C9C9"
392
+ },
393
+ primary: "#6BD39A",
394
+ onPrimary: "#0E1F17",
395
+ accent: "#6BD39A",
396
+ onAccent: "#0E1F17",
397
+ accentEdge: "#34D888",
398
+ success: "#07C168",
399
+ danger: "#B91C1C",
400
+ errorRed: "#FF1B00",
401
+ warningStrong: "#A45610",
402
+ cautionAccent: "#D9821D",
403
+ riskConcern: "#EFA700",
404
+ badgeBgPositive: "rgba(7,193,104,0.18)",
405
+ badgeBgCaution: "rgba(217,130,29,0.20)",
406
+ badgeBgCritical: "rgba(185,28,28,0.22)",
407
+ badgeBgNeutral: "rgba(255,255,255,0.10)",
408
+ bubbleUser: "#6BD39A",
409
+ bubbleAssistant: "#1C1F23",
410
+ skeleton: "#23272C",
411
+ scanButtonPink: "#FFC3BC",
412
+ checkupCardLowRiskBg: "rgba(7,193,104,0.16)",
413
+ checkupCardConcernBg: "rgba(239,167,0,0.16)"
414
+ };
415
+ deepFreeze({
416
+ brightness: "dark",
417
+ meta: { displayName: "Everfur", logoUrl: null, placeholder: null, hidePoweredBy: false },
418
+ color: DARK_COLOR,
419
+ space: SPACE,
420
+ radius: RADIUS,
421
+ type: baseType(),
422
+ motion: MOTION
423
+ });
424
+ var EVERFUR_THEME_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@everfur/sdk#EverfurThemeContext@v1");
425
+ var globalThemeHost = globalThis;
426
+ var EverfurThemeContext = globalThemeHost[EVERFUR_THEME_CONTEXT_SYMBOL] ?? (globalThemeHost[EVERFUR_THEME_CONTEXT_SYMBOL] = react.createContext(EVERFUR_LIGHT));
427
+ EverfurThemeContext.displayName ??= "EverfurThemeContext";
428
+ function useEverfurTheme() {
429
+ return react.useContext(EverfurThemeContext);
430
+ }
431
+
432
+ // src/rn/facade.ts
433
+ var EVERFUR_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@everfur/sdk#EverfurContext@v1");
434
+ var globalContextHost = globalThis;
435
+ var EverfurContext = globalContextHost[EVERFUR_CONTEXT_SYMBOL] ?? (globalContextHost[EVERFUR_CONTEXT_SYMBOL] = react.createContext(null));
436
+ EverfurContext.displayName ??= "EverfurContext";
437
+ function useEverfurContext() {
438
+ const ctx = react.useContext(EverfurContext);
439
+ if (ctx === null) {
440
+ throw new EverfurConfigError(
441
+ "not-initialized",
442
+ "Everfur hooks must be called inside <EverfurProvider>. Wrap your tree once, high."
443
+ );
444
+ }
445
+ return ctx;
446
+ }
447
+
448
+ // src/rn/chat/useChat.ts
449
+ var BOOTSTRAP_SNAPSHOT = Object.freeze({
450
+ status: "idle",
451
+ messages: Object.freeze([]),
452
+ conversationId: null,
453
+ error: null,
454
+ isStreaming: false,
455
+ isBootstrapping: true,
456
+ suggestedPrompts: Object.freeze([])
457
+ });
458
+ function useEverfurChat(opts) {
459
+ const { runtime, userRef, activePet } = useEverfurContext();
460
+ const petRef = opts?.petRef ?? activePet ?? null;
461
+ const conversationId = opts?.conversationId ?? null;
462
+ const [controller, setController] = react.useState(null);
463
+ react.useEffect(() => {
464
+ const acquired = runtime.registry.acquireChat({ userRef, petRef, conversationId });
465
+ setController(acquired);
466
+ return () => {
467
+ setController((current) => current === acquired ? null : current);
468
+ runtime.registry.releaseChat(acquired);
469
+ };
470
+ }, [runtime, userRef, petRef, conversationId]);
471
+ const subscribe = react.useCallback(
472
+ (onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
473
+ [controller]
474
+ );
475
+ const getSnapshot = react.useCallback(
476
+ () => controller ? controller.getSnapshot() : BOOTSTRAP_SNAPSHOT,
477
+ [controller]
478
+ );
479
+ const snapshot = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
480
+ const send = react.useCallback(
481
+ (text) => controller ? driveToResult(controller, controller.send(text)) : Promise.resolve(notReadyError()),
482
+ [controller]
483
+ );
484
+ const retryLast = react.useCallback(
485
+ () => controller ? driveToResult(controller, controller.retryLast()) : Promise.resolve(notReadyError()),
486
+ [controller]
487
+ );
488
+ const stop = react.useCallback(() => {
489
+ controller?.stop();
490
+ }, [controller]);
491
+ return react.useMemo(
492
+ () => ({
493
+ status: snapshot.status,
494
+ messages: snapshot.messages,
495
+ conversationId: snapshot.conversationId,
496
+ error: snapshot.error,
497
+ isStreaming: snapshot.isStreaming,
498
+ isBootstrapping: snapshot.isBootstrapping,
499
+ suggestedPrompts: snapshot.suggestedPrompts,
500
+ send,
501
+ retryLast,
502
+ stop
503
+ }),
504
+ [snapshot, send, retryLast, stop]
505
+ );
506
+ }
507
+ var NOOP_UNSUBSCRIBE = () => {
508
+ };
509
+ async function driveToResult(controller, iterable) {
510
+ for await (const _frame of iterable) {
511
+ }
512
+ const snapshot = controller.getSnapshot();
513
+ if (snapshot.status === "error" && snapshot.error !== null) return err(snapshot.error);
514
+ return ok(lastAssistantResponseId(snapshot.messages));
515
+ }
516
+ function lastAssistantResponseId(messages) {
517
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
518
+ const message = messages[i];
519
+ if (message && message.role === "assistant" && message.responseId !== void 0) {
520
+ return message.responseId;
521
+ }
522
+ }
523
+ return null;
524
+ }
525
+ function notReadyError() {
526
+ return err(
527
+ makeEverfurError("internalError", { retryable: true })
528
+ );
529
+ }
530
+ function useCapability(name) {
531
+ const { runtime } = useEverfurContext();
532
+ const store = runtime.entitlements;
533
+ const snapshot = react.useSyncExternalStore(
534
+ store.subscribe,
535
+ store.getSnapshot,
536
+ store.getSnapshot
537
+ );
538
+ return react.useMemo(
539
+ () => decideCapability(name, snapshot.decisions, { isPending: snapshot.isPending }),
540
+ [name, snapshot]
541
+ );
542
+ }
543
+ var MIN_OFFSTATE_HEIGHT = 48;
544
+ function selectView(decision) {
545
+ if (decision.isPending) return "pending";
546
+ return decision.enabled ? "enabled" : "disabled";
547
+ }
548
+ function CapabilityGate(props) {
549
+ const { capability, children, renderDisabled, renderPending, style } = props;
550
+ const decision = useCapability(capability);
551
+ const view = react.useMemo(() => selectView(decision), [decision]);
552
+ switch (view) {
553
+ case "enabled":
554
+ return children;
555
+ case "pending":
556
+ return renderPending ? renderPending() : /* @__PURE__ */ jsxRuntime.jsx(PendingSkeleton, { capability, style });
557
+ case "disabled":
558
+ return renderDisabled ? renderDisabled(decision) : /* @__PURE__ */ jsxRuntime.jsx(OffState, { decision, style });
559
+ default:
560
+ return assertNever(view);
561
+ }
562
+ }
563
+ function PendingSkeleton({
564
+ capability,
565
+ style
566
+ }) {
567
+ const theme = useEverfurTheme();
568
+ const styles = react.useMemo(() => makeStyles(theme), [theme]);
569
+ return /* @__PURE__ */ jsxRuntime.jsx(
570
+ reactNative.View,
571
+ {
572
+ style: [styles.offstate, style],
573
+ accessible: true,
574
+ accessibilityState: { busy: true },
575
+ accessibilityLabel: `Loading ${capability}`,
576
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.skeletonBar })
577
+ }
578
+ );
579
+ }
580
+ function OffState({
581
+ decision,
582
+ style
583
+ }) {
584
+ const theme = useEverfurTheme();
585
+ const styles = react.useMemo(() => makeStyles(theme), [theme]);
586
+ const message = decision.disabled?.reason ?? "";
587
+ return /* @__PURE__ */ jsxRuntime.jsx(
588
+ reactNative.View,
589
+ {
590
+ style: [styles.offstate, style],
591
+ accessible: true,
592
+ accessibilityRole: "text",
593
+ accessibilityLabel: message,
594
+ children: message.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.offstateText, children: message }) : null
595
+ }
596
+ );
597
+ }
598
+ function makeStyles(theme) {
599
+ const { color, space, radius, type } = theme;
600
+ return reactNative.StyleSheet.create({
601
+ // A deliberate cream card with a soft brand hairline, never a bare grey slab.
602
+ offstate: {
603
+ minHeight: MIN_OFFSTATE_HEIGHT,
604
+ justifyContent: "center",
605
+ paddingHorizontal: space.lg,
606
+ paddingVertical: space.md,
607
+ backgroundColor: color.surfaceCream,
608
+ borderRadius: radius.md,
609
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
610
+ borderColor: color.brandBorder
611
+ },
612
+ offstateText: {
613
+ fontFamily: type.family,
614
+ fontSize: type.size.body,
615
+ lineHeight: Math.round(type.size.body * type.lineHeight.normal),
616
+ color: color.text.muted
617
+ },
618
+ // A soft green shimmer bar (brand wash) that stays legible on the cream card in both light and dark,
619
+ // where the neutral `skeleton` token would collide with the dark cream surface.
620
+ skeletonBar: {
621
+ height: space.md,
622
+ borderRadius: radius.xs,
623
+ backgroundColor: color.brandSofter
624
+ }
625
+ });
626
+ }
627
+ var CLINICAL_DISCLAIMER = Object.freeze({
628
+ title: "For educational use only",
629
+ body: "Everfur is for educational purposes only. It helps you track visible changes through AI checkups. It does not diagnose, treat, prescribe, or replace veterinary care. For urgent symptoms or emergencies, contact a veterinarian immediately."
630
+ });
631
+ function ClinicalDisclaimer(props) {
632
+ const theme = useEverfurTheme();
633
+ const styles = react.useMemo(() => makeStyles2(theme), [theme]);
634
+ return /* @__PURE__ */ jsxRuntime.jsxs(
635
+ reactNative.View,
636
+ {
637
+ style: [styles.container, props.style],
638
+ accessible: true,
639
+ accessibilityRole: "text",
640
+ accessibilityLabel: `${CLINICAL_DISCLAIMER.title}. ${CLINICAL_DISCLAIMER.body}`,
641
+ children: [
642
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.title, children: CLINICAL_DISCLAIMER.title }),
643
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.body, children: CLINICAL_DISCLAIMER.body })
644
+ ]
645
+ }
646
+ );
647
+ }
648
+ function makeStyles2(theme) {
649
+ const { color, space, type } = theme;
650
+ return reactNative.StyleSheet.create({
651
+ container: {
652
+ paddingHorizontal: space.lg,
653
+ paddingVertical: space.md,
654
+ borderTopWidth: reactNative.StyleSheet.hairlineWidth,
655
+ borderTopColor: color.borderHairlineGreen,
656
+ backgroundColor: color.surfaceCream,
657
+ gap: space.xs
658
+ },
659
+ title: {
660
+ fontFamily: type.family,
661
+ fontSize: type.size.caption,
662
+ lineHeight: Math.round(type.size.caption * type.lineHeight.snug),
663
+ color: color.text.secondary,
664
+ fontWeight: type.weight.bold,
665
+ letterSpacing: type.letterSpacing.eyebrow
666
+ },
667
+ body: {
668
+ fontFamily: type.family,
669
+ fontSize: type.size.caption,
670
+ lineHeight: Math.round(type.size.caption * type.lineHeight.snug),
671
+ color: color.text.muted,
672
+ fontWeight: type.weight.regular
673
+ }
674
+ });
675
+ }
676
+ var INLINE_TOKEN = /\*\*([^*]+?)\*\*|\*([^*]+?)\*|`([^`]+?)`/g;
677
+ function renderInlineMarkdown(text, styles) {
678
+ if (!text.includes("*") && !text.includes("`")) return text;
679
+ const nodes = [];
680
+ let cursor = 0;
681
+ let key = 0;
682
+ INLINE_TOKEN.lastIndex = 0;
683
+ for (let match = INLINE_TOKEN.exec(text); match !== null; match = INLINE_TOKEN.exec(text)) {
684
+ if (match.index > cursor) nodes.push(text.slice(cursor, match.index));
685
+ const bold = match[1];
686
+ const italic = match[2];
687
+ const code = match[3];
688
+ if (bold !== void 0) {
689
+ nodes.push(/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.bold, children: bold }, key++));
690
+ } else if (italic !== void 0) {
691
+ nodes.push(styles.italic ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.italic, children: italic }, key++) : italic);
692
+ } else if (code !== void 0) {
693
+ nodes.push(styles.code ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.code, children: code }, key++) : code);
694
+ }
695
+ cursor = match.index + match[0].length;
696
+ }
697
+ if (cursor < text.length) nodes.push(text.slice(cursor));
698
+ return nodes;
699
+ }
700
+ var DOT_SIZE = 6;
701
+ var LOOP_MS = 520;
702
+ var STAGGER_MS = Math.round(LOOP_MS * 0.18);
703
+ var OPACITY_REST = 0.44;
704
+ var OPACITY_STATIC = 0.5;
705
+ var TRI_IN = [0, 0.5, 1];
706
+ var DOT_INDICES = [0, 1, 2];
707
+ function TypingIndicator() {
708
+ const theme = useEverfurTheme();
709
+ const styles = react.useMemo(() => makeStyles3(theme), [theme]);
710
+ const animate = !theme.motion.reducedMotion;
711
+ return /* @__PURE__ */ jsxRuntime.jsx(
712
+ reactNative.View,
713
+ {
714
+ style: styles.card,
715
+ accessible: true,
716
+ accessibilityRole: "text",
717
+ accessibilityLabel: "Assistant is typing",
718
+ accessibilityLiveRegion: "polite",
719
+ accessibilityState: { busy: true },
720
+ children: DOT_INDICES.map(
721
+ (index) => animate ? /* @__PURE__ */ jsxRuntime.jsx(AnimatedDot, { index, style: styles.dot }, index) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.dot, styles.dotStatic] }, index)
722
+ )
723
+ }
724
+ );
725
+ }
726
+ function AnimatedDot({ index, style }) {
727
+ const t = react.useRef(new reactNative.Animated.Value(0)).current;
728
+ react.useEffect(() => {
729
+ const anim = reactNative.Animated.sequence([
730
+ reactNative.Animated.delay(index * STAGGER_MS),
731
+ reactNative.Animated.loop(reactNative.Animated.timing(t, { toValue: 1, duration: LOOP_MS, useNativeDriver: true }))
732
+ ]);
733
+ anim.start();
734
+ return () => anim.stop();
735
+ }, [t, index]);
736
+ const opacity = t.interpolate({ inputRange: TRI_IN, outputRange: [OPACITY_REST, 1, OPACITY_REST] });
737
+ const translateY = t.interpolate({ inputRange: TRI_IN, outputRange: [0, -3.5, 0] });
738
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: [style, { opacity, transform: [{ translateY }] }] });
739
+ }
740
+ function makeStyles3(theme) {
741
+ const c = theme.color;
742
+ const s = theme.space;
743
+ const r = theme.radius;
744
+ return reactNative.StyleSheet.create({
745
+ card: {
746
+ alignSelf: "flex-start",
747
+ flexDirection: "row",
748
+ alignItems: "center",
749
+ gap: s.xs,
750
+ paddingHorizontal: s.md,
751
+ paddingVertical: s.sm,
752
+ backgroundColor: c.surfaceCream,
753
+ borderRadius: r.md,
754
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
755
+ borderColor: c.brandBorder
756
+ },
757
+ dot: { width: DOT_SIZE, height: DOT_SIZE, borderRadius: r.pill, backgroundColor: c.brand },
758
+ dotStatic: { opacity: OPACITY_STATIC }
759
+ });
760
+ }
761
+ var SPINNER_DELAY_MS = 300;
762
+ var DEFAULT_PLACEHOLDER = "Message";
763
+ var DEFAULT_SEND_LABEL = "Send";
764
+ var DEFAULT_STOP_LABEL = "Stop";
765
+ var FOLLOW_UPS_LABEL = "Follow up questions";
766
+ var POWERED_BY_LABEL = "powered by everfur";
767
+ var MIN_SURFACE_HEIGHT = 120;
768
+ var MIN_TOUCH_TARGET = 44;
769
+ var INPUT_MAX_HEIGHT = 120;
770
+ var EMPTY_GLYPH_SIZE = 40;
771
+ var BUBBLE_MAX_WIDTH = "82%";
772
+ var COMPOSER_BORDER_WIDTH = 1.25;
773
+ var BUBBLE_EDGE_WIDTH = 0.5;
774
+ var PENDING_OPACITY = 0.6;
775
+ var COMPOSER_SHADOW_OPACITY = 0.1;
776
+ var COMPOSER_SHADOW_OFFSET_Y = 2;
777
+ var COMPOSER_SHADOW_ELEVATION = 2;
778
+ function EverfurChat(props) {
779
+ const { petRef, conversationId, style } = props;
780
+ return /* @__PURE__ */ jsxRuntime.jsx(CapabilityGate, { capability: "chat", style, children: /* @__PURE__ */ jsxRuntime.jsx(ChatBody, { petRef, conversationId, style }) });
781
+ }
782
+ function selectView2(state) {
783
+ if (state.isBootstrapping) return "loading";
784
+ if (state.status === "error" && state.error) return "error";
785
+ if (state.messages.length === 0) return "empty";
786
+ return "populated";
787
+ }
788
+ function ChatBody({
789
+ petRef,
790
+ conversationId,
791
+ style
792
+ }) {
793
+ const { theme, styles } = useChatTheme();
794
+ const chatOpts = react.useMemo(
795
+ () => buildChatOpts(petRef, conversationId),
796
+ [petRef, conversationId]
797
+ );
798
+ const chat = useEverfurChat(chatOpts);
799
+ const view = selectView2(chat);
800
+ const insets = reactNativeSafeAreaContext.useSafeAreaInsets();
801
+ const onSelectPrompt = react.useCallback(
802
+ (prompt) => {
803
+ void chat.send(prompt);
804
+ },
805
+ [chat.send]
806
+ );
807
+ return (
808
+ // KeyboardAvoidingView so the composer + disclaimer lift above the software keyboard while the user types.
809
+ /* @__PURE__ */ jsxRuntime.jsxs(
810
+ reactNative.KeyboardAvoidingView,
811
+ {
812
+ style: [styles.container, { paddingTop: insets.top }, style],
813
+ behavior: reactNative.Platform.OS === "ios" ? "padding" : void 0,
814
+ children: [
815
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.body, children: renderView(view, chat, onSelectPrompt) }),
816
+ /* @__PURE__ */ jsxRuntime.jsx(Composer, { send: chat.send, stop: chat.stop, isStreaming: chat.isStreaming }),
817
+ /* @__PURE__ */ jsxRuntime.jsx(ClinicalDisclaimer, {}),
818
+ theme.meta.hidePoweredBy ? null : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.poweredBy, accessibilityRole: "text", accessibilityLabel: POWERED_BY_LABEL, children: POWERED_BY_LABEL })
819
+ ]
820
+ }
821
+ )
822
+ );
823
+ }
824
+ function Composer({
825
+ send,
826
+ stop,
827
+ isStreaming
828
+ }) {
829
+ const { theme, styles } = useChatTheme();
830
+ const [draft, setDraft] = react.useState("");
831
+ const placeholder = theme.meta.placeholder ?? DEFAULT_PLACEHOLDER;
832
+ const onSend = react.useCallback(() => {
833
+ if (draft.trim().length === 0) return;
834
+ if ([...draft].length > MESSAGE_MAX_CHARS) return;
835
+ void send(draft);
836
+ setDraft("");
837
+ }, [draft, send]);
838
+ const onStop = react.useCallback(() => {
839
+ stop();
840
+ }, [stop]);
841
+ const isOverLimit = react.useMemo(() => [...draft].length > MESSAGE_MAX_CHARS, [draft]);
842
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.composer, children: [
843
+ /* @__PURE__ */ jsxRuntime.jsx(
844
+ reactNative.TextInput,
845
+ {
846
+ style: styles.input,
847
+ value: draft,
848
+ onChangeText: setDraft,
849
+ placeholder,
850
+ placeholderTextColor: theme.color.text.placeholder,
851
+ accessibilityLabel: placeholder,
852
+ onSubmitEditing: onSend,
853
+ returnKeyType: "send",
854
+ blurOnSubmit: false,
855
+ multiline: true,
856
+ editable: true
857
+ }
858
+ ),
859
+ isStreaming ? /* @__PURE__ */ jsxRuntime.jsx(
860
+ reactNative.Pressable,
861
+ {
862
+ onPress: onStop,
863
+ accessibilityRole: "button",
864
+ accessibilityLabel: DEFAULT_STOP_LABEL,
865
+ style: styles.actionButton,
866
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.actionLabel, children: DEFAULT_STOP_LABEL })
867
+ }
868
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
869
+ reactNative.Pressable,
870
+ {
871
+ onPress: onSend,
872
+ disabled: isOverLimit,
873
+ accessibilityRole: "button",
874
+ accessibilityLabel: DEFAULT_SEND_LABEL,
875
+ accessibilityState: { disabled: isOverLimit },
876
+ style: [styles.actionButton, isOverLimit ? styles.actionButtonDisabled : null],
877
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.actionLabel, isOverLimit ? styles.actionLabelDisabled : null], children: DEFAULT_SEND_LABEL })
878
+ }
879
+ )
880
+ ] });
881
+ }
882
+ function renderView(view, chat, onSelectPrompt) {
883
+ switch (view) {
884
+ case "loading":
885
+ return /* @__PURE__ */ jsxRuntime.jsx(LoadingState, {});
886
+ case "empty":
887
+ return /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { prompts: chat.suggestedPrompts, onSelectPrompt });
888
+ case "error":
889
+ return /* @__PURE__ */ jsxRuntime.jsx(ErrorState, { error: chat.error, onRetry: chat.retryLast });
890
+ case "populated":
891
+ return /* @__PURE__ */ jsxRuntime.jsx(
892
+ PopulatedState,
893
+ {
894
+ messages: chat.messages,
895
+ prompts: chat.suggestedPrompts,
896
+ onSelectPrompt,
897
+ isStreaming: chat.isStreaming
898
+ }
899
+ );
900
+ default:
901
+ return assertNever(view);
902
+ }
903
+ }
904
+ function buildChatOpts(petRef, conversationId) {
905
+ const opts = {};
906
+ if (petRef !== void 0) opts.petRef = petRef;
907
+ if (conversationId !== void 0) opts.conversationId = conversationId;
908
+ return opts;
909
+ }
910
+ function LoadingState() {
911
+ const { theme, styles } = useChatTheme();
912
+ const [visible, setVisible] = react.useState(false);
913
+ react.useEffect(() => {
914
+ const id = setTimeout(() => setVisible(true), SPINNER_DELAY_MS);
915
+ return () => clearTimeout(id);
916
+ }, []);
917
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.centered, accessible: true, accessibilityState: { busy: true }, accessibilityLabel: "Loading", children: visible ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: theme.color.brand }) : null });
918
+ }
919
+ function EmptyState({
920
+ prompts,
921
+ onSelectPrompt
922
+ }) {
923
+ const { styles } = useChatTheme();
924
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.ScrollView, { contentContainerStyle: styles.emptyContent, keyboardShouldPersistTaps: "handled", children: [
925
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.emptyGlyphWrap, accessible: true, accessibilityRole: "summary", accessibilityLabel: "No messages yet", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.emptyGlyph }) }),
926
+ /* @__PURE__ */ jsxRuntime.jsx(FollowUps, { prompts, onSelectPrompt })
927
+ ] });
928
+ }
929
+ function FollowUps({
930
+ prompts,
931
+ onSelectPrompt
932
+ }) {
933
+ const { styles } = useChatTheme();
934
+ if (prompts.length === 0) return null;
935
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.followUps, children: [
936
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.followUpsLabel, accessibilityRole: "header", children: FOLLOW_UPS_LABEL }),
937
+ prompts.map((prompt, index) => /* @__PURE__ */ jsxRuntime.jsx(
938
+ reactNative.Pressable,
939
+ {
940
+ onPress: () => onSelectPrompt(prompt),
941
+ accessibilityRole: "button",
942
+ accessibilityLabel: prompt,
943
+ style: [styles.followUpRow, index > 0 ? styles.followUpDivider : null],
944
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.followUpText, children: prompt })
945
+ },
946
+ prompt
947
+ ))
948
+ ] });
949
+ }
950
+ function ErrorState({ error, onRetry }) {
951
+ const { styles } = useChatTheme();
952
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.centered, accessible: true, accessibilityRole: "alert", children: [
953
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.errorText, children: error.displayMessage }),
954
+ error.retryable ? /* @__PURE__ */ jsxRuntime.jsx(
955
+ reactNative.Pressable,
956
+ {
957
+ onPress: onRetry,
958
+ accessibilityRole: "button",
959
+ accessibilityLabel: "Try again",
960
+ style: styles.retryButton,
961
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: styles.retryLabel, children: "Try again" })
962
+ }
963
+ ) : null
964
+ ] });
965
+ }
966
+ function PopulatedState({
967
+ messages,
968
+ prompts,
969
+ onSelectPrompt,
970
+ isStreaming
971
+ }) {
972
+ const { styles } = useChatTheme();
973
+ const lastMessage = messages[messages.length - 1];
974
+ const awaitingAssistant = isStreaming && (lastMessage === void 0 || lastMessage.role === "user");
975
+ return /* @__PURE__ */ jsxRuntime.jsxs(
976
+ reactNative.ScrollView,
977
+ {
978
+ style: styles.list,
979
+ contentContainerStyle: styles.listContent,
980
+ accessibilityLabel: "Conversation",
981
+ keyboardShouldPersistTaps: "handled",
982
+ children: [
983
+ messages.map((message) => /* @__PURE__ */ jsxRuntime.jsx(MessageRow, { message }, message.id)),
984
+ awaitingAssistant ? /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, {}) : null,
985
+ !isStreaming ? /* @__PURE__ */ jsxRuntime.jsx(FollowUps, { prompts, onSelectPrompt }) : null
986
+ ]
987
+ }
988
+ );
989
+ }
990
+ function MessageRow({ message }) {
991
+ const { styles } = useChatTheme();
992
+ const isUser = message.role === "user";
993
+ const sender = isUser ? "You" : "Assistant";
994
+ return /* @__PURE__ */ jsxRuntime.jsx(
995
+ reactNative.View,
996
+ {
997
+ style: [styles.row, isUser ? styles.rowUser : styles.rowAssistant],
998
+ accessible: true,
999
+ accessibilityRole: "text",
1000
+ accessibilityLabel: `${sender}: ${message.text}`,
1001
+ accessibilityState: message.pending === true ? { busy: true } : void 0,
1002
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.bubble, isUser ? styles.bubbleUser : styles.bubbleAssistant, message.pending ? styles.bubblePending : null], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: isUser ? styles.bubbleTextUser : styles.bubbleTextAssistant, children: isUser ? message.text : renderInlineMarkdown(message.text, {
1003
+ bold: styles.bubbleBold,
1004
+ italic: styles.bubbleItalic,
1005
+ code: styles.bubbleCode
1006
+ }) }) })
1007
+ }
1008
+ );
1009
+ }
1010
+ function useChatTheme() {
1011
+ const theme = useEverfurTheme();
1012
+ const styles = react.useMemo(() => makeStyles4(theme), [theme]);
1013
+ return { theme, styles };
1014
+ }
1015
+ function makeStyles4(theme) {
1016
+ const c = theme.color;
1017
+ const s = theme.space;
1018
+ const r = theme.radius;
1019
+ const t = theme.type;
1020
+ const weightLabel = t.weight.medium;
1021
+ return reactNative.StyleSheet.create({
1022
+ // Cream ground so the branded bubbles / bare assistant text sit on an Everfur surface (host `style` still wins).
1023
+ container: { flex: 1, minHeight: MIN_SURFACE_HEIGHT, backgroundColor: c.surfaceCream },
1024
+ body: { flex: 1 },
1025
+ centered: { minHeight: MIN_SURFACE_HEIGHT, flexGrow: 1, alignItems: "center", justifyContent: "center", padding: s.xl },
1026
+ // Deliberate empty-state placeholder: a soft brand-wash disc (never a blank region).
1027
+ emptyGlyph: {
1028
+ width: EMPTY_GLYPH_SIZE,
1029
+ height: EMPTY_GLYPH_SIZE,
1030
+ borderRadius: r.pill,
1031
+ backgroundColor: c.brandSoft,
1032
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
1033
+ borderColor: c.brandBorder
1034
+ },
1035
+ // Empty state scrolls (glyph centered, then the follow-up list) so nothing overflows a short surface.
1036
+ emptyContent: { flexGrow: 1, justifyContent: "center", padding: s.lg, gap: s.lg },
1037
+ emptyGlyphWrap: { alignItems: "center", justifyContent: "center", paddingVertical: s.xl },
1038
+ errorText: {
1039
+ fontFamily: t.family,
1040
+ fontSize: t.size.subtitle,
1041
+ lineHeight: t.size.subtitle * t.lineHeight.snug,
1042
+ color: c.danger,
1043
+ textAlign: "center",
1044
+ marginBottom: s.md
1045
+ },
1046
+ // Brand-green pill retry CTA with the WCAG-legible on-brand foreground.
1047
+ retryButton: {
1048
+ minHeight: MIN_TOUCH_TARGET,
1049
+ minWidth: MIN_TOUCH_TARGET,
1050
+ alignItems: "center",
1051
+ justifyContent: "center",
1052
+ paddingHorizontal: s.xl,
1053
+ paddingVertical: s.sm,
1054
+ borderRadius: r.pill,
1055
+ backgroundColor: c.brand
1056
+ },
1057
+ retryLabel: { fontFamily: t.family, color: c.onPrimary, fontSize: t.size.body, fontWeight: weightLabel },
1058
+ list: { flex: 1 },
1059
+ listContent: { padding: s.lg, gap: s.md },
1060
+ row: { flexDirection: "row" },
1061
+ rowUser: { justifyContent: "flex-end" },
1062
+ rowAssistant: { justifyContent: "flex-start" },
1063
+ bubble: { maxWidth: BUBBLE_MAX_WIDTH, paddingHorizontal: s.md, paddingVertical: s.sm, borderRadius: r.lg },
1064
+ // User = brand accent fill + 0.5px accent edge, tail corner tightened (the mobile asymmetry).
1065
+ bubbleUser: {
1066
+ backgroundColor: c.bubbleUser,
1067
+ borderBottomRightRadius: r.tile,
1068
+ borderWidth: BUBBLE_EDGE_WIDTH,
1069
+ borderColor: c.accentEdge
1070
+ },
1071
+ // Assistant = bare text block (no fill, flush-left) so the reply reads as plain, on-surface prose.
1072
+ bubbleAssistant: { paddingHorizontal: 0 },
1073
+ bubblePending: { opacity: PENDING_OPACITY },
1074
+ bubbleTextUser: {
1075
+ fontFamily: t.family,
1076
+ color: c.onAccent,
1077
+ fontSize: t.size.body,
1078
+ lineHeight: t.size.body * t.lineHeight.normal
1079
+ },
1080
+ bubbleTextAssistant: {
1081
+ fontFamily: t.family,
1082
+ color: c.text.primary,
1083
+ fontSize: t.size.body,
1084
+ lineHeight: t.size.body * t.lineHeight.normal
1085
+ },
1086
+ // Inline-markdown runs inside the assistant reply (nested <Text>): bold entities + optional italic / code.
1087
+ bubbleBold: { fontWeight: t.weight.bold },
1088
+ bubbleItalic: { fontStyle: "italic" },
1089
+ bubbleCode: { fontFamily: t.mono },
1090
+ // "Follow up questions": a labeled, list-with-dividers block of tappable, ready-to-send backend prompts.
1091
+ followUps: { marginTop: s.sm, borderRadius: r.md, backgroundColor: c.brandSoft, borderWidth: reactNative.StyleSheet.hairlineWidth, borderColor: c.brandBorder },
1092
+ followUpsLabel: {
1093
+ fontFamily: t.family,
1094
+ color: c.text.secondary,
1095
+ fontSize: t.size.caption,
1096
+ fontWeight: weightLabel,
1097
+ letterSpacing: t.letterSpacing.eyebrow,
1098
+ textTransform: "uppercase",
1099
+ paddingHorizontal: s.md,
1100
+ paddingTop: s.md,
1101
+ paddingBottom: s.sm
1102
+ },
1103
+ followUpRow: { minHeight: MIN_TOUCH_TARGET, justifyContent: "center", paddingHorizontal: s.md, paddingVertical: s.sm },
1104
+ followUpDivider: { borderTopWidth: reactNative.StyleSheet.hairlineWidth, borderTopColor: c.brandBorder },
1105
+ followUpText: {
1106
+ fontFamily: t.family,
1107
+ color: c.text.primary,
1108
+ fontSize: t.size.body,
1109
+ lineHeight: t.size.body * t.lineHeight.snug
1110
+ },
1111
+ // Composer = inset cream pill with a brand border and a soft brand-tinted lift.
1112
+ composer: {
1113
+ flexDirection: "row",
1114
+ alignItems: "flex-end",
1115
+ gap: s.sm,
1116
+ marginHorizontal: s.md,
1117
+ marginTop: s.xs,
1118
+ marginBottom: s.sm,
1119
+ paddingHorizontal: s.md,
1120
+ paddingVertical: s.sm,
1121
+ backgroundColor: c.surfaceCream,
1122
+ borderWidth: COMPOSER_BORDER_WIDTH,
1123
+ borderColor: c.brandBorder,
1124
+ borderRadius: r.pill,
1125
+ shadowColor: c.brand,
1126
+ shadowOpacity: COMPOSER_SHADOW_OPACITY,
1127
+ shadowRadius: s.sm,
1128
+ shadowOffset: { width: 0, height: COMPOSER_SHADOW_OFFSET_Y },
1129
+ elevation: COMPOSER_SHADOW_ELEVATION
1130
+ },
1131
+ // Bare input inside the pill (the composer owns the border / fill); placeholder color set on the element.
1132
+ input: {
1133
+ flex: 1,
1134
+ minHeight: MIN_TOUCH_TARGET,
1135
+ maxHeight: INPUT_MAX_HEIGHT,
1136
+ paddingHorizontal: s.md,
1137
+ paddingVertical: s.sm,
1138
+ fontFamily: t.family,
1139
+ fontSize: t.size.body,
1140
+ lineHeight: t.size.body * t.lineHeight.normal,
1141
+ color: c.text.primary
1142
+ },
1143
+ // Brand-green pill send / stop control.
1144
+ actionButton: {
1145
+ minHeight: MIN_TOUCH_TARGET,
1146
+ minWidth: MIN_TOUCH_TARGET,
1147
+ alignItems: "center",
1148
+ justifyContent: "center",
1149
+ paddingHorizontal: s.lg,
1150
+ borderRadius: r.pill,
1151
+ backgroundColor: c.brand
1152
+ },
1153
+ actionLabel: { fontFamily: t.family, color: c.onPrimary, fontSize: t.size.body, fontWeight: weightLabel },
1154
+ // The over-length state. Opacity alone would carry the meaning by appearance only, so the control also
1155
+ // sets `disabled` and `accessibilityState.disabled`, which is what a screen reader announces.
1156
+ actionButtonDisabled: { opacity: 0.45 },
1157
+ actionLabelDisabled: { color: c.onPrimary },
1158
+ // Everfur's own attribution: small, muted, centered under the disclaimer.
1159
+ poweredBy: {
1160
+ fontFamily: t.family,
1161
+ color: c.text.muted,
1162
+ fontSize: t.size.micro,
1163
+ textAlign: "center",
1164
+ paddingBottom: s.xs
1165
+ }
1166
+ });
1167
+ }
1168
+
1169
+ exports.EverfurChat = EverfurChat;
1170
+ exports.useEverfurChat = useEverfurChat;