@wix/himalaya-cli 0.1318.0 → 0.1320.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 (84) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +2 -1
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/theme/src/index.d.ts +21 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview-page/src/capabilities.generated.d.ts +2 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/brand-color-math.d.ts +98 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +1 -1
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +9 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -0
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/brand-color-math.mjs +183 -0
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -0
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -0
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -0
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -0
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -0
  17. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/theme.mjs +189 -0
  18. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +8 -0
  19. package/dist/cli.mjs +574 -404
  20. package/dist/preview-assets.generated.bin +0 -0
  21. package/guides/app-config.md +1 -1
  22. package/guides/capabilities.md +17 -0
  23. package/guides/designing-himi-screens.md +2 -0
  24. package/guides/importable-sdks.md +1 -0
  25. package/guides/qa-builds.md +34 -8
  26. package/guides/sdk-reference.json +39 -2
  27. package/guides/web-page-sdk-reference.md +1 -0
  28. package/package.json +2 -2
  29. package/templates/beauty-salon-owner/config.ts +1 -1
  30. package/templates/beauty-salon-owner/dev/index.ts +125 -24
  31. package/templates/beauty-salon-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  32. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +59 -31
  33. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  34. package/templates/brandy/README.md +5 -3
  35. package/templates/community-club-owner/config.ts +1 -1
  36. package/templates/community-club-owner/dev/index.ts +125 -24
  37. package/templates/community-club-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  38. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +59 -31
  39. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  40. package/templates/events-meetups-owner/config.ts +1 -1
  41. package/templates/events-meetups-owner/dev/index.ts +125 -24
  42. package/templates/events-meetups-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  43. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +59 -31
  44. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  45. package/templates/gym-fitness-owner/config.ts +1 -1
  46. package/templates/gym-fitness-owner/dev/index.ts +125 -24
  47. package/templates/gym-fitness-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  48. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +59 -31
  49. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  50. package/templates/nonprofit-cause-owner/config.ts +1 -1
  51. package/templates/nonprofit-cause-owner/dev/index.ts +125 -24
  52. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  53. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +59 -31
  54. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  55. package/templates/owner-lite/config.ts +1 -1
  56. package/templates/owner-lite/dev/index.ts +125 -24
  57. package/templates/owner-lite/tier5-src/_shared/aria-attachments.ts +41 -2
  58. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +59 -31
  59. package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  60. package/templates/podcast-creator-owner/config.ts +1 -1
  61. package/templates/podcast-creator-owner/dev/index.ts +125 -24
  62. package/templates/podcast-creator-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  63. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +59 -31
  64. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  65. package/templates/portfolio-creative-owner/config.ts +1 -1
  66. package/templates/portfolio-creative-owner/dev/index.ts +125 -24
  67. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  68. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +59 -31
  69. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  70. package/templates/restaurant-owner/config.ts +1 -1
  71. package/templates/restaurant-owner/dev/index.ts +125 -24
  72. package/templates/restaurant-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  73. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +59 -31
  74. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  75. package/templates/retail-store-owner/config.ts +1 -1
  76. package/templates/retail-store-owner/dev/index.ts +125 -24
  77. package/templates/retail-store-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  78. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +59 -31
  79. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
  80. package/templates/yoga-studio-owner/config.ts +1 -1
  81. package/templates/yoga-studio-owner/dev/index.ts +125 -24
  82. package/templates/yoga-studio-owner/tier5-src/_shared/aria-attachments.ts +41 -2
  83. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +59 -31
  84. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +154 -13
@@ -28,7 +28,7 @@ import { defineService } from "@himalaya/state";
28
28
  import { open } from "@himalaya/open-url";
29
29
  import { writeText } from "@himalaya/clipboard";
30
30
  import { availability, recognizeOnce, stop as stopSpeech } from "@himalaya/speech";
31
- import { capturePhoto } from "@himalaya/camera";
31
+ import { capturePhoto, pickDocument } from "@himalaya/camera";
32
32
  import { put } from "@himalaya/presigned-upload";
33
33
  import { alert } from "@himalaya/ui";
34
34
  import { auth } from "@himalaya/wix-platform";
@@ -250,12 +250,13 @@ const core = createAriaChat({
250
250
  widgetItemsUnavailable: () => copyParam("ownerAria.widget.itemsUnavailable", "Items aren't available here."),
251
251
  attachUploading: () => copyParam("ownerAria.attach.uploading", "Uploading…"),
252
252
  attachFailed: () => copyParam("ownerAria.attach.failed", "Couldn't upload"),
253
- attachTooMany: (max) => copyTemplate("ownerAria.attach.tooMany", "You can attach up to {max} photos.", { max }),
253
+ attachTooMany: (max) => copyTemplate("ownerAria.attach.tooMany", "You can attach up to {max} files.", { max }),
254
254
  attachUnsupported: () => copyParam("ownerAria.attach.unsupported", "That file type can't be attached."),
255
- attachTooLarge: (mb) => copyTemplate("ownerAria.attach.tooLarge", "That photo is too large to attach (up to {mb} MB).", { mb }),
256
- attachWaitUploading: () => copyParam("ownerAria.attach.waitUploading", "Wait for your photo to finish uploading."),
257
- attachWaitFailed: () => copyParam("ownerAria.attach.waitFailed", "Retry or remove the photo that didn't upload."),
258
- attachNeedsText: () => copyParam("ownerAria.attach.needsText", "Add a message to send with your photo."),
255
+ attachTooLarge: (mb) => copyTemplate("ownerAria.attach.tooLarge", "That file is too large to attach (up to {mb} MB).", { mb }),
256
+ attachWaitUploading: () => copyParam("ownerAria.attach.waitUploading", "Wait for your file to finish uploading."),
257
+ attachWaitFailed: () => copyParam("ownerAria.attach.waitFailed", "Retry or remove the file that didn't upload."),
258
+ attachWaitRemove: () => copyParam("ownerAria.attach.waitRemove", "Remove the file that didn't upload, then add it again."),
259
+ attachNeedsText: () => copyParam("ownerAria.attach.needsText", "Add a message to send with your file."),
259
260
  toolFailed: () => copyParam("ownerAria.toolFailed", "Aria couldn't finish that. Try again."),
260
261
  },
261
262
  clientTools: { confirm: confirmWithOwner, userAgent },
@@ -305,6 +306,10 @@ const core = createAriaChat({
305
306
  // which go to a self-authorizing url with no Wix credential attached.
306
307
  attachments: {
307
308
  pick: async (source) => {
309
+ if (source === "document") {
310
+ const file = await pickDocument();
311
+ return file ? { dataUri: file.uri, name: file.name, mimeType: file.mime } : null;
312
+ }
308
313
  const asset = await capturePhoto({ source, maxEdgePixels: 2048 });
309
314
  return asset ? { dataUri: asset.uri, mimeType: asset.mime } : null;
310
315
  },
@@ -318,6 +323,92 @@ const core = createAriaChat({
318
323
  },
319
324
  });
320
325
 
326
+ // Focused native TextFields keep their local non-empty edit rather than adopting
327
+ // worker normalization (the shared shouldAdoptWorkerValue rule). Remember only
328
+ // prefixes where THIS draft actually selected a reference, so later editor echoes and
329
+ // the Form.message submission cannot restore those triggers into the wire text.
330
+ // Email addresses and newly authored @ characters are never blanket-stripped.
331
+ let mentionTriggerPrefixes: string[] = [];
332
+ let pendingMentionTrigger: { rawPrefix: string; prefix: string } | null = null;
333
+ let mentionDraftRevision = 0;
334
+ let previousMentionRaw = "";
335
+ // Set by a selection that consumed a typed trigger: the normalized draft the
336
+ // worker now holds. A field that blurs adopts that value WITHOUT dispatching
337
+ // typing, so the next keystroke is an edit of it, not of previousMentionRaw.
338
+ // The first typing after a selection whose raw value is byte-identical to the
339
+ // pre-selection one cannot be an edit of the focused raw text (nothing changed),
340
+ // so it is read as an edit of the adopted draft: that field holds no consumed
341
+ // marker any more, and a standalone @ there is a new trigger.
342
+ let adoptedMentionDraft: string | null = null;
343
+ function reconcileMentionDraft(raw: string): string {
344
+ // Move consumed trigger positions with an edit anywhere in the focused field.
345
+ // A replaced/deleted trigger loses its marker, so newly authored @ text stays literal.
346
+ let start = 0;
347
+ while (start < previousMentionRaw.length && start < raw.length && previousMentionRaw[start] === raw[start]) start += 1;
348
+ let suffix = 0;
349
+ while (suffix < previousMentionRaw.length - start && suffix < raw.length - start
350
+ && previousMentionRaw[previousMentionRaw.length - suffix - 1] === raw[raw.length - suffix - 1]) suffix += 1;
351
+ const end = previousMentionRaw.length - suffix;
352
+ const delta = raw.length - previousMentionRaw.length;
353
+ const move = (position: number): number | null => position < start ? position : position >= end ? position + delta : null;
354
+ mentionTriggerPrefixes = mentionTriggerPrefixes.flatMap(prefix => {
355
+ let position = move(prefix.length);
356
+ // A whole-draft replacement ("Ask @" → "New @") shares only whitespace and the @
357
+ // with the old text, so the common suffix lines the NEW @ up with the old marker.
358
+ // When every word before the marker was replaced, its context is gone: the @ is
359
+ // newly authored and must stay (and may open the picker again).
360
+ if (position !== null && start === 0 && prefix.length >= end && prefix.trim()
361
+ && !previousMentionRaw.slice(end, prefix.length).trim()) return [];
362
+ // Coalesced edits may add text on both sides of the marker. Keep it when
363
+ // its complete previous context still occurs exactly once in the new value.
364
+ if (position === null && prefix) {
365
+ const anchor = `${prefix}@`; const found = raw.indexOf(anchor);
366
+ if (found >= 0 && raw.indexOf(anchor, found + 1) < 0) position = found + prefix.length;
367
+ }
368
+ // A coalesced focused-field edit can replace context on both sides of a
369
+ // selected marker. If this is the sole consumed marker and the new draft
370
+ // has one standalone @, its position is unambiguous even without the old
371
+ // prefix. Email addresses and authored @suffixes are never candidates.
372
+ if (position === null && mentionTriggerPrefixes.length === 1) {
373
+ const candidates = [...raw.matchAll(/(^|\s)@(?=\s|$)/g)].map(match => match.index! + match[1].length);
374
+ if (candidates.length === 1) position = candidates[0];
375
+ }
376
+ // Only the standalone trigger that opened the picker is consumed. Adding
377
+ // a literal suffix (or joining it to the preceding word) authors ordinary
378
+ // @ text even when the focused editor retains the selected trigger.
379
+ const standalone = position !== null && raw[position] === "@"
380
+ && (position === 0 || /\s/.test(raw[position - 1]))
381
+ && (position + 1 === raw.length || /\s/.test(raw[position + 1]));
382
+ return standalone ? [raw.slice(0, position!)] : [];
383
+ });
384
+ if (pendingMentionTrigger) {
385
+ const position = move(pendingMentionTrigger.rawPrefix.length);
386
+ const standalone = position !== null && raw[position] === "@"
387
+ && (position === 0 || /\s/.test(raw[position - 1]))
388
+ && (position + 1 === raw.length || /\s/.test(raw[position + 1]));
389
+ pendingMentionTrigger = standalone
390
+ ? { rawPrefix: raw.slice(0, position!), prefix: raw.slice(0, position!) } : null;
391
+ if (pendingMentionTrigger) {
392
+ for (const prefix of [...mentionTriggerPrefixes].sort((a, b) => b.length - a.length)) {
393
+ if (prefix.length < pendingMentionTrigger.rawPrefix.length) pendingMentionTrigger.prefix = pendingMentionTrigger.prefix.slice(0, prefix.length) + pendingMentionTrigger.prefix.slice(prefix.length + 1);
394
+ }
395
+ }
396
+ }
397
+ previousMentionRaw = raw;
398
+ let text = raw;
399
+ for (const prefix of [...mentionTriggerPrefixes].sort((a, b) => b.length - a.length)) {
400
+ text = text.slice(0, prefix.length) + text.slice(prefix.length + 1);
401
+ }
402
+ return text;
403
+ }
404
+ function reconciledMentionArg(arg?: AriaActionArg): AriaActionArg | undefined {
405
+ if (typeof arg === "string") return reconcileMentionDraft(arg);
406
+ if (!arg) return arg;
407
+ const keys: Array<"message" | "text" | "value" | "intent" | "id"> = ["message", "text", "value", "intent", "id"];
408
+ const key = keys.find(k => typeof arg[k] === "string" && arg[k]);
409
+ return key ? { ...arg, [key]: reconcileMentionDraft(String(arg[key])) } : arg;
410
+ }
411
+
321
412
  const svc = defineService<AriaChatState, {
322
413
  load: (s: AriaChatState, arg?: AriaActionArg) => Promise<AriaChatState>;
323
414
  submit: (s: AriaChatState, arg?: AriaActionArg) => Promise<AriaChatState>;
@@ -374,13 +465,51 @@ const svc = defineService<AriaChatState, {
374
465
  state: core.initialState,
375
466
  actions: {
376
467
  // `load` is the onAppear dispatch, so it is where the copy pack arrives.
377
- load: (s, arg) => { stashCopy(arg); stashTargets(arg); stashRealtimeUrl(arg); return core.load(s); },
378
- submit: core.submit,
379
- submitPrompt: core.submitPrompt,
380
- newChat: core.newChat,
468
+ load: (s, arg) => { mentionDraftRevision += 1; mentionTriggerPrefixes = []; pendingMentionTrigger = null; previousMentionRaw = ""; adoptedMentionDraft = null; stashCopy(arg); stashTargets(arg); stashRealtimeUrl(arg); return core.load(s); },
469
+ submit: async (s, arg) => {
470
+ const normalized = reconciledMentionArg(arg);
471
+ const previousPrefixes = mentionTriggerPrefixes; const previousTrigger = pendingMentionTrigger;
472
+ const revision = mentionDraftRevision;
473
+ const next = await core.submit(s, normalized);
474
+ // A refusal keeps this draft; a later keystroke/new-chat belongs to its own.
475
+ if (revision === mentionDraftRevision) {
476
+ if (next.input && next.input === s.input) { mentionTriggerPrefixes = previousPrefixes; pendingMentionTrigger = previousTrigger; }
477
+ else { mentionTriggerPrefixes = []; pendingMentionTrigger = null; previousMentionRaw = ""; adoptedMentionDraft = null; }
478
+ }
479
+ return next;
480
+ },
481
+ submitPrompt: async (s, arg) => {
482
+ const previousPrefixes = mentionTriggerPrefixes; const previousTrigger = pendingMentionTrigger;
483
+ const revision = mentionDraftRevision;
484
+ const next = await core.submitPrompt(s, arg);
485
+ if (revision === mentionDraftRevision) {
486
+ if (next.input && next.input === s.input) { mentionTriggerPrefixes = previousPrefixes; pendingMentionTrigger = previousTrigger; }
487
+ else { mentionTriggerPrefixes = []; pendingMentionTrigger = null; previousMentionRaw = ""; adoptedMentionDraft = null; }
488
+ }
489
+ return next;
490
+ },
491
+ newChat: (s) => { mentionDraftRevision += 1; mentionTriggerPrefixes = []; pendingMentionTrigger = null; previousMentionRaw = ""; adoptedMentionDraft = null; return core.newChat(s); },
381
492
  // The composer's onChange. Mirrors the draft into state so `submit` can
382
493
  // empty it and the field actually clears.
383
- typing: core.typing,
494
+ typing: (s, arg) => {
495
+ mentionDraftRevision += 1;
496
+ const raw = typeof arg === "string" ? arg : ([arg?.value, arg?.text, arg?.message].find((v): v is string => typeof v === "string") ?? "");
497
+ if (adoptedMentionDraft !== null) {
498
+ if (raw === previousMentionRaw) { mentionTriggerPrefixes = []; previousMentionRaw = adoptedMentionDraft; }
499
+ adoptedMentionDraft = null;
500
+ }
501
+ const next = core.typing(s, { value: reconcileMentionDraft(raw) });
502
+ // A standalone @ asks for a reference; emails and embedded @ characters
503
+ // remain ordinary text. A successful selection consumes its trigger.
504
+ if (next.input !== s.input && /(?:^|\s)@$/.test(next.input) && !next.mention.openHidden) {
505
+ // Keep the fragment editable until a reference is selected. Cancelling
506
+ // the picker must not turn a literal @example.com into example.com.
507
+ pendingMentionTrigger = { rawPrefix: raw.slice(0, -1), prefix: next.input.slice(0, -1) };
508
+ void svc.mentionOpen();
509
+ return next;
510
+ }
511
+ return next;
512
+ },
384
513
  // Named by the `_poll` directive above. A name that does not resolve here is
385
514
  // a `poll-action-missing` error in `himi test --content`.
386
515
  tick: core.tick,
@@ -419,11 +548,23 @@ const svc = defineService<AriaChatState, {
419
548
  // chips above the composer. Every one is named by the descriptor, so every
420
549
  // one has to be exported here — see the note on the picker family above.
421
550
  mentionOpen: core.mentionOpen,
422
- mentionClose: core.mentionClose,
551
+ mentionClose: (s) => { pendingMentionTrigger = null; return core.mentionClose(s); },
423
552
  mentionBack: core.mentionBack,
424
553
  mentionProvider: core.mentionProvider,
425
554
  mentionQuery: core.mentionQuery,
426
- mentionPick: core.mentionPick,
555
+ mentionPick: (s, arg) => {
556
+ const next = core.mentionPick(s, arg);
557
+ const trigger = pendingMentionTrigger;
558
+ if (trigger && !s.mention.hidden && next.mention.hidden && next.input.startsWith(`${trigger.prefix}@`)) {
559
+ mentionDraftRevision += 1;
560
+ pendingMentionTrigger = null;
561
+ if (!mentionTriggerPrefixes.includes(trigger.rawPrefix)) mentionTriggerPrefixes.push(trigger.rawPrefix);
562
+ const normalized = next.input.slice(0, trigger.prefix.length) + next.input.slice(trigger.prefix.length + 1);
563
+ adoptedMentionDraft = normalized;
564
+ return core.typing(next, { value: normalized });
565
+ }
566
+ return next;
567
+ },
427
568
  mentionRemove: core.mentionRemove,
428
569
  // The composer's photos. `attachmentSettled` is dispatched by the upload
429
570
  // itself (see `attachments.settled` above), never by the descriptor.
@@ -37,7 +37,7 @@ export default defineApp({
37
37
  actions: "http://localhost:8190/v1/actions",
38
38
  sync: "http://localhost:8190/v1/sync",
39
39
  },
40
- capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard","ui","speech","camera","presigned_upload","app_info","biometrics"],
40
+ capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard","ui","speech","camera","presigned_upload","app_info","biometrics","wix_platform"],
41
41
  designTokensPath: "./tokens.json",
42
42
  navigation: {
43
43
  "navKind": "responsive",
@@ -61615,6 +61615,65 @@ const SCREENS = runtimeSetting({
61615
61615
  },
61616
61616
  {
61617
61617
  "id": "",
61618
+ "body": {
61619
+ "package": "himalaya",
61620
+ "type": "VStack",
61621
+ "props": {
61622
+ "spacing": 4,
61623
+ "children": [
61624
+ {
61625
+ "id": "",
61626
+ "body": {
61627
+ "package": "himalaya",
61628
+ "type": "Icon",
61629
+ "props": {
61630
+ "name": "doc",
61631
+ "size": 24,
61632
+ "tint": "textSecondary"
61633
+ }
61634
+ },
61635
+ "modifiers": {
61636
+ "accessibilityRole": "decorative"
61637
+ }
61638
+ },
61639
+ {
61640
+ "id": "",
61641
+ "body": {
61642
+ "package": "himalaya",
61643
+ "type": "Text",
61644
+ "props": {
61645
+ "text": "$item.name",
61646
+ "style": "caption",
61647
+ "color": "textPrimary",
61648
+ "maxLines": 2
61649
+ }
61650
+ },
61651
+ "modifiers": {
61652
+ "height": 44
61653
+ }
61654
+ },
61655
+ {
61656
+ "id": "",
61657
+ "body": {
61658
+ "package": "himalaya",
61659
+ "type": "Text",
61660
+ "props": {
61661
+ "text": "$item.typeLabel",
61662
+ "style": "caption",
61663
+ "color": "textSecondary",
61664
+ "maxLines": 1
61665
+ }
61666
+ }
61667
+ }
61668
+ ]
61669
+ }
61670
+ },
61671
+ "modifiers": {
61672
+ "hiddenPath": "$item.documentHidden"
61673
+ }
61674
+ },
61675
+ {
61676
+ "id": "himi_owner_lite_aria.attachment_status_$item.id",
61618
61677
  "body": {
61619
61678
  "package": "himalaya",
61620
61679
  "type": "Text",
@@ -61626,6 +61685,7 @@ const SCREENS = runtimeSetting({
61626
61685
  }
61627
61686
  },
61628
61687
  "modifiers": {
61688
+ "height": 44,
61629
61689
  "hiddenPath": "$item.statusHidden"
61630
61690
  }
61631
61691
  },
@@ -61641,21 +61701,22 @@ const SCREENS = runtimeSetting({
61641
61701
  "id": "himi_owner_lite_aria.attachment_retry_$item.id",
61642
61702
  "body": {
61643
61703
  "package": "himalaya",
61644
- "type": "Text",
61704
+ "type": "Button",
61645
61705
  "props": {
61646
- "text": "Retry",
61647
- "style": "caption",
61648
- "color": "accentPrimary",
61649
- "maxLines": 1
61706
+ "title": "Retry",
61707
+ "action": {
61708
+ "actionId": "tier5.himiOwnerLiteAria.retryAttachment",
61709
+ "params": {
61710
+ "id": "$item.id"
61711
+ }
61712
+ },
61713
+ "style": "tertiary",
61714
+ "size": "sm"
61650
61715
  }
61651
61716
  },
61652
61717
  "modifiers": {
61653
- "onTap": {
61654
- "actionId": "tier5.himiOwnerLiteAria.retryAttachment",
61655
- "params": {
61656
- "id": "$item.id"
61657
- }
61658
- },
61718
+ "width": 92,
61719
+ "height": 48,
61659
61720
  "hiddenPath": "$item.retryHidden"
61660
61721
  }
61661
61722
  },
@@ -61663,32 +61724,51 @@ const SCREENS = runtimeSetting({
61663
61724
  "id": "himi_owner_lite_aria.attachment_remove_$item.id",
61664
61725
  "body": {
61665
61726
  "package": "himalaya",
61666
- "type": "Icon",
61727
+ "type": "Button",
61667
61728
  "props": {
61668
- "name": "close",
61669
- "size": 14,
61670
- "tint": "textSecondary"
61729
+ "title": "Remove attachment",
61730
+ "action": {
61731
+ "actionId": "tier5.himiOwnerLiteAria.removeAttachment",
61732
+ "params": {
61733
+ "id": "$item.id"
61734
+ }
61735
+ },
61736
+ "style": "tertiary",
61737
+ "iconOnly": true,
61738
+ "leadingIcon": "close",
61739
+ "cornerRadius": 24,
61740
+ "minHeight": 48
61671
61741
  }
61672
61742
  },
61673
61743
  "modifiers": {
61674
- "onTap": {
61675
- "actionId": "tier5.himiOwnerLiteAria.removeAttachment",
61676
- "params": {
61677
- "id": "$item.id"
61678
- }
61679
- },
61680
- "accessibilityLabel": "Remove photo"
61744
+ "width": 48,
61745
+ "height": 48,
61746
+ "accessibilityLabel": "Remove attachment",
61747
+ "accessibilityRole": "button"
61681
61748
  }
61682
61749
  }
61683
61750
  ]
61684
61751
  }
61752
+ },
61753
+ "modifiers": {
61754
+ "height": 48
61685
61755
  }
61686
61756
  }
61687
61757
  ]
61688
61758
  }
61689
61759
  },
61690
61760
  "modifiers": {
61691
- "width": 72
61761
+ "width": 160,
61762
+ "padding": {
61763
+ "top": 8,
61764
+ "bottom": 8,
61765
+ "leading": 8,
61766
+ "trailing": 8
61767
+ },
61768
+ "background": {
61769
+ "token": "surfaceSecondary"
61770
+ },
61771
+ "cornerRadius": 12
61692
61772
  }
61693
61773
  }
61694
61774
  ]
@@ -61700,7 +61780,7 @@ const SCREENS = runtimeSetting({
61700
61780
  "leading": 0,
61701
61781
  "trailing": 0,
61702
61782
  "top": 8,
61703
- "bottom": 0
61783
+ "bottom": 8
61704
61784
  }
61705
61785
  }
61706
61786
  },
@@ -61753,6 +61833,27 @@ const SCREENS = runtimeSetting({
61753
61833
  },
61754
61834
  "accessibilityLabel": "Take a photo"
61755
61835
  }
61836
+ },
61837
+ {
61838
+ "id": "himi_owner_lite_aria.attach.document",
61839
+ "body": {
61840
+ "package": "himalaya",
61841
+ "type": "Icon",
61842
+ "props": {
61843
+ "name": "document",
61844
+ "size": 22,
61845
+ "tint": "accentPrimary"
61846
+ }
61847
+ },
61848
+ "modifiers": {
61849
+ "onTap": {
61850
+ "actionId": "tier5.himiOwnerLiteAria.attach",
61851
+ "params": {
61852
+ "source": "document"
61853
+ }
61854
+ },
61855
+ "accessibilityLabel": "Add files"
61856
+ }
61756
61857
  }
61757
61858
  ]
61758
61859
  }
@@ -27,6 +27,14 @@ export const ARIA_IMAGE_TYPES: readonly string[] = [
27
27
  "image/heic",
28
28
  ];
29
29
 
30
+ /** Document MIME types supported by the native Aria reference. */
31
+ export const ARIA_DOCUMENT_TYPES: readonly string[] = [
32
+ "text/csv", "text/plain", "application/pdf", "application/msword",
33
+ "application/vnd.ms-excel", "application/vnd.ms-powerpoint",
34
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
35
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
36
+ ];
37
+
30
38
  /** At most this many attachments ride one message. The native Aria app caps at
31
39
  * 10; nothing on the wire says otherwise. */
32
40
  export const ARIA_MAX_ATTACHMENTS = 10;
@@ -40,6 +48,9 @@ export const ARIA_MAX_ATTACHMENTS = 10;
40
48
  * Checking it HERE is what turns that refusal into a sentence the owner can act
41
49
  * on, instead of a chip that fails for no stated reason. */
42
50
  export const ARIA_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
51
+ // Inline documents include base64 and JSON overhead under the 4 MiB frame bridge.
52
+ // Larger documents use the file-backed attachment follow-up.
53
+ export const ARIA_MAX_DOCUMENT_BYTES = 2 * 1024 * 1024;
43
54
 
44
55
  /** A chip in the composer. Every field is always present — a `$item.` path that
45
56
  * is missing on some rows does not substitute — and each `…Hidden` flag
@@ -58,6 +69,9 @@ export interface AriaAttachmentChip {
58
69
  * runtime's frame limit. */
59
70
  thumbUrl: string;
60
71
  thumbHidden: boolean;
72
+ /** Filename/type card for documents (images keep the 72pt preview). */
73
+ documentHidden: boolean;
74
+ typeLabel: string;
61
75
  /** "Uploading…" / "Couldn't upload" / "" — collapses when empty. */
62
76
  statusText: string;
63
77
  statusHidden: boolean;
@@ -113,6 +127,21 @@ export function normalizeMime(mime: string): string {
113
127
  }
114
128
  }
115
129
 
130
+ /** Native providers sometimes report a generic MIME; match the reference's
131
+ * extension fallback only for those values, not a conflicting explicit type. */
132
+ export function attachmentMime(mime: string, name = ""): string {
133
+ const normalized = normalizeMime(mime);
134
+ if (normalized === "text/comma-separated-values") return "text/csv";
135
+ if (normalized && normalized !== "application/octet-stream") return normalized;
136
+ const extension = name.split(".").pop()?.toLowerCase() ?? "";
137
+ const types: Record<string, string> = {
138
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif", heic: "image/heic", heif: "image/heif",
139
+ csv: "text/csv", txt: "text/plain", pdf: "application/pdf", doc: "application/msword", xls: "application/vnd.ms-excel", ppt: "application/vnd.ms-powerpoint",
140
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
141
+ };
142
+ return types[extension] ?? normalized;
143
+ }
144
+
116
145
  /** A file name for an image the picker did not name, by type. The service
117
146
  * requires one (1–255 characters), and it is what the transcript shows. */
118
147
  export function imageFileName(mimeType: string, n: number, given?: string): string {
@@ -131,8 +160,8 @@ export type AttachRefusal = "" | "type" | "size" | "count";
131
160
 
132
161
  export function refusalFor(parsed: ParsedDataUri | null, mimeType: string, attached: number): AttachRefusal {
133
162
  if (attached >= ARIA_MAX_ATTACHMENTS) return "count";
134
- if (!parsed || !ARIA_IMAGE_TYPES.includes(mimeType)) return "type";
135
- if (parsed.bytes > ARIA_MAX_IMAGE_BYTES) return "size";
163
+ if (!parsed || ![...ARIA_IMAGE_TYPES, ...ARIA_DOCUMENT_TYPES].includes(mimeType)) return "type";
164
+ if (parsed.bytes > (mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES)) return "size";
136
165
  return "";
137
166
  }
138
167
 
@@ -146,10 +175,20 @@ export function chipIn(
146
175
  ref = "",
147
176
  ): AriaAttachmentChip {
148
177
  const statusText = state === "uploading" ? labels.uploading : state === "failed" ? labels.failed : "";
178
+ const dot = base.name.lastIndexOf(".");
179
+ const extension = dot > 0 && dot < base.name.length - 1 ? base.name.slice(dot + 1).toUpperCase() : "";
180
+ const mimeLabels: Record<string, string> = {
181
+ "application/pdf": "PDF", "text/csv": "CSV", "text/plain": "TXT", "application/msword": "DOC",
182
+ "application/vnd.ms-excel": "XLS", "application/vnd.ms-powerpoint": "PPT",
183
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
184
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
185
+ };
149
186
  return {
150
187
  ...base,
151
188
  state,
152
189
  thumbHidden: !base.thumbUrl,
190
+ documentHidden: base.mimeType.startsWith("image/"),
191
+ typeLabel: extension || mimeLabels[base.mimeType] || base.mimeType,
153
192
  statusText,
154
193
  statusHidden: !statusText,
155
194
  retryHidden: state !== "failed",