@hjmds/react 1.30.0 → 1.32.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 (102) hide show
  1. package/dist/auth-screen.d.ts +18 -3
  2. package/dist/auth-screen.d.ts.map +1 -1
  3. package/dist/auth-screen.js +22 -6
  4. package/dist/auth-screen.js.map +1 -1
  5. package/dist/bottom-cta.d.ts +6 -1
  6. package/dist/bottom-cta.d.ts.map +1 -1
  7. package/dist/bottom-cta.js +1 -1
  8. package/dist/bottom-cta.js.map +1 -1
  9. package/dist/bottom-navigation.d.ts +10 -0
  10. package/dist/bottom-navigation.d.ts.map +1 -1
  11. package/dist/bottom-navigation.js +80 -8
  12. package/dist/bottom-navigation.js.map +1 -1
  13. package/dist/carousel.d.ts +29 -1
  14. package/dist/carousel.d.ts.map +1 -1
  15. package/dist/carousel.js +27 -3
  16. package/dist/carousel.js.map +1 -1
  17. package/dist/display.d.ts +8 -2
  18. package/dist/display.d.ts.map +1 -1
  19. package/dist/display.js +12 -7
  20. package/dist/display.js.map +1 -1
  21. package/dist/evidence.d.ts +1 -1
  22. package/dist/evidence.js +1 -1
  23. package/dist/evidence.js.map +1 -1
  24. package/dist/forms.d.ts.map +1 -1
  25. package/dist/forms.js +2 -1
  26. package/dist/forms.js.map +1 -1
  27. package/dist/image-choice.d.ts +20 -2
  28. package/dist/image-choice.d.ts.map +1 -1
  29. package/dist/image-choice.js +60 -7
  30. package/dist/image-choice.js.map +1 -1
  31. package/dist/index.d.ts +3 -3
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/internal/field-error.d.ts +19 -0
  36. package/dist/internal/field-error.d.ts.map +1 -0
  37. package/dist/internal/field-error.js +19 -0
  38. package/dist/internal/field-error.js.map +1 -0
  39. package/dist/internal/initial-focus.d.ts.map +1 -1
  40. package/dist/internal/initial-focus.js +49 -7
  41. package/dist/internal/initial-focus.js.map +1 -1
  42. package/dist/internal/media-album.d.ts +70 -0
  43. package/dist/internal/media-album.d.ts.map +1 -0
  44. package/dist/internal/media-album.js +52 -0
  45. package/dist/internal/media-album.js.map +1 -0
  46. package/dist/internal/message-reactions.d.ts +39 -2
  47. package/dist/internal/message-reactions.d.ts.map +1 -1
  48. package/dist/internal/message-reactions.js +31 -6
  49. package/dist/internal/message-reactions.js.map +1 -1
  50. package/dist/layout.d.ts +19 -3
  51. package/dist/layout.d.ts.map +1 -1
  52. package/dist/layout.js +10 -3
  53. package/dist/layout.js.map +1 -1
  54. package/dist/navigation.d.ts +3 -1
  55. package/dist/navigation.d.ts.map +1 -1
  56. package/dist/navigation.js +4 -2
  57. package/dist/navigation.js.map +1 -1
  58. package/dist/number-field.d.ts.map +1 -1
  59. package/dist/number-field.js +2 -1
  60. package/dist/number-field.js.map +1 -1
  61. package/dist/overlays.d.ts +65 -0
  62. package/dist/overlays.d.ts.map +1 -1
  63. package/dist/overlays.js +36 -3
  64. package/dist/overlays.js.map +1 -1
  65. package/dist/pagination.d.ts +13 -11
  66. package/dist/pagination.d.ts.map +1 -1
  67. package/dist/pagination.js +12 -5
  68. package/dist/pagination.js.map +1 -1
  69. package/dist/public-exports.generated.d.ts +8 -0
  70. package/dist/public-exports.generated.d.ts.map +1 -1
  71. package/dist/public-exports.generated.js +24 -0
  72. package/dist/public-exports.generated.js.map +1 -1
  73. package/dist/screen-flows.d.ts +118 -14
  74. package/dist/screen-flows.d.ts.map +1 -1
  75. package/dist/screen-flows.js +95 -23
  76. package/dist/screen-flows.js.map +1 -1
  77. package/dist/screens.d.ts +69 -10
  78. package/dist/screens.d.ts.map +1 -1
  79. package/dist/screens.js +77 -15
  80. package/dist/screens.js.map +1 -1
  81. package/dist/select.d.ts.map +1 -1
  82. package/dist/select.js +2 -1
  83. package/dist/select.js.map +1 -1
  84. package/dist/selection.d.ts.map +1 -1
  85. package/dist/selection.js +3 -2
  86. package/dist/selection.js.map +1 -1
  87. package/dist/steps.d.ts +11 -1
  88. package/dist/steps.d.ts.map +1 -1
  89. package/dist/steps.js +11 -6
  90. package/dist/steps.js.map +1 -1
  91. package/dist/styles.css +181 -22
  92. package/dist/styles.layered.css +181 -22
  93. package/dist/supplemental-display.d.ts.map +1 -1
  94. package/dist/supplemental-display.js +2 -48
  95. package/dist/supplemental-display.js.map +1 -1
  96. package/dist/theme.d.ts.map +1 -1
  97. package/dist/theme.js +1 -0
  98. package/dist/theme.js.map +1 -1
  99. package/dist/toast.d.ts.map +1 -1
  100. package/dist/toast.js +1 -1
  101. package/dist/toast.js.map +1 -1
  102. package/package.json +3 -3
@@ -1,9 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { createContext, useContext } from "react";
4
+ import { TextArea } from "./forms.js";
3
5
  import { CompositionLayout, ScreenLayout } from "./screens.js";
4
6
  import { ScreenExitButton } from "./internal/screen-exit.js";
5
7
  import { Button, IconButton } from "./actions.js";
6
8
  import { MessageReactions } from "./internal/message-reactions.js";
9
+ import { MediaAlbumView, useMediaAlbumIndex } from "./internal/media-album.js";
10
+ import { validateMediaSetChoice } from "@hjmds/design-contracts/screen-patterns";
7
11
  import { Stack, Text, Grid, VisuallyHidden } from "./layout.js";
8
12
  import { SearchField } from "./forms.js";
9
13
  import { Chip, RadioGroup } from "./selection.js";
@@ -11,7 +15,7 @@ import { AlertDialog, Menu, Sheet } from "./overlays.js";
11
15
  import { UploadItem } from "./upload-item.js";
12
16
  import {} from "@hjmds/design-contracts/components/upload-item";
13
17
  import {} from "@hjmds/design-contracts/components/alert-dialog";
14
- import { initialFocusDefaults, validateCommentThread, resolvePermissionAction, resolveOnboardingStep, resolveSearchCommit, resolveSearchDebounceMs, resolveSearchEmptyCause, resolveSearchScreenPhase, resolveFocusAfterRemoval, screenPatternRecipe, commentThreadRecipe, profileSummaryRecipe, searchScreenRecipe, resolveCommentMenuItems } from "@hjmds/design-contracts/screen-patterns";
18
+ import { initialFocusDefaults, validateCommentThread, resolvePermissionAction, resolveOnboardingStep, resolveSearchCommit, resolveSearchDebounceMs, resolveSearchEmptyCause, resolveSearchFilterRailVisible, resolveSearchScreenPhase, resolveFocusAfterRemoval, screenPatternRecipe, commentThreadRecipe, profileSummaryRecipe, searchScreenRecipe, resolveCommentMenuItems } from "@hjmds/design-contracts/screen-patterns";
15
19
  import { resolveWindowClass } from "@hjmds/design-contracts/responsive";
16
20
  import { containerRecipe } from "@hjmds/design-contracts/components/container";
17
21
  import { ListRow } from "./display.js";
@@ -46,34 +50,82 @@ export function ListDetailScreen({ list, detail, back, refresh, loadMore, layout
46
50
  // "Load more" ends the list inside the scroll body, centered like LoadMore; the fixed footer is the bottom CTA slot (2026-10-17 review).
47
51
  return _jsxs("div", { style: { height: "100%", ...layoutStyle }, children: [_jsx("div", { ref: listHost, tabIndex: -1, hidden: open, style: { height: "100%" }, onFocusCapture: event => { returnFocus.current = event.target; }, children: _jsx(ScreenLayout, { ...screen, ...(refresh || screen.actions ? { actions: _jsxs(Stack, { axis: "inline", gap: "xs", align: "center", children: [screen.actions, refresh ? _jsx(Action, { action: refresh, secondary: true, edge: "end" }) : null] }) } : {}), children: loadMore ? _jsxs("div", { style: { display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0, gap: screenPatternRecipe.itemGap }, children: [list, _jsx("div", { style: { display: "flex", justifyContent: "center" }, children: _jsx(Action, { action: loadMore, secondary: true }) })] }) : list }) }), detail ? _jsx("div", { ref: detailHost, style: { height: "100%" }, children: _jsx(ScreenLayout, { title: detail.title, ...(detail.titleVisibility === undefined ? {} : { titleVisibility: detail.titleVisibility }), ...(detail.headerAlignment === undefined ? {} : { headerAlignment: detail.headerAlignment }), ...(detail.state === undefined ? {} : { state: detail.state }), stateAction: detail.stateAction, notice: detail.notice, exit: { kind: "back", label: back.label, onClick: back.onAction, disabled: !!(back.disabled || back.pending) }, children: detail.content }) }) : null] });
48
52
  }
49
- export function EditorScreen({ children, dirty, submitPlacement = "footer", submit, cancel, discard, draftStatus, initialFocus = initialFocusDefaults.editor, ...screen }) {
53
+ export function EditorScreen({ children, dirty, submitPlacement = "footer", submit, cancel, discard, draftStatus, stepBack, initialFocus = initialFocusDefaults.editor, ...screen }) {
50
54
  const [confirm, setConfirm] = useState(false);
51
55
  const cancelLocked = !!(cancel.disabled || cancel.pending || submit.pending);
52
56
  const submitAction = { ...submit, disabled: !!(submit.disabled || screen.state && screen.state.kind !== "ready") };
53
57
  // A prompt opened before an external save must not bypass the current leave lock
54
58
  // or reappear once the save finishes. Keep the draft mounted throughout.
55
- useEffect(() => { if (cancelLocked)
56
- setConfirm(false); }, [cancelLocked]);
59
+ const stepping = !!stepBack, stepLocked = !!(stepBack?.disabled || stepBack?.pending || submit.pending);
60
+ // A step-back exit replaces the cancel exit, so a discard prompt opened from the cancel exit cannot outlive it.
61
+ useEffect(() => { if (cancelLocked || stepping)
62
+ setConfirm(false); }, [cancelLocked, stepping]);
57
63
  // Preserve an async host result so AlertDialog can retain failure and retry.
58
64
  const leave = () => { if (!cancelLocked)
59
65
  return cancel.onAction(); };
60
66
  // Writing is the screen's purpose: focus the first field on entry unless the host opts out (2026-10-18).
61
- return _jsxs(_Fragment, { children: [_jsx(ScreenLayout, { ...screen, initialFocus: initialFocus, exit: { kind: "close", label: cancel.label, disabled: cancelLocked, onClick: () => { if (cancelLocked)
62
- return; if (dirty)
63
- setConfirm(true);
64
- else
65
- leave(); } }, actions: submitPlacement === "header" ? _jsx(Action, { action: submitAction }) : screen.actions, footer: submitPlacement === "header" ? draftStatus : _jsxs(Stack, { gap: "sm", children: [draftStatus, _jsx(Action, { action: submitAction })] }), children: children }), _jsx(AlertDialog, { open: confirm && !cancelLocked, onOpenChange: setConfirm, request: { ...discard, mode: "confirm", onConfirm: leave } })] });
67
+ return _jsxs(_Fragment, { children: [_jsx(ScreenLayout, { ...screen, initialFocus: initialFocus, exit: stepBack ? { kind: "back", label: stepBack.label, disabled: stepLocked, onClick: () => { if (!stepLocked)
68
+ stepBack.onAction(); } }
69
+ : { kind: "close", label: cancel.label, disabled: cancelLocked, onClick: () => { if (cancelLocked)
70
+ return; if (dirty)
71
+ setConfirm(true);
72
+ else
73
+ leave(); } }, actions: submitPlacement === "header" ? _jsx(Action, { action: submitAction }) : screen.actions, footer: submitPlacement === "header" ? draftStatus : _jsxs(Stack, { gap: "sm", children: [draftStatus, _jsx(Action, { action: submitAction })] }), children: children }), _jsx(AlertDialog, { open: confirm && !cancelLocked && !stepping, onOpenChange: setConfirm, request: { ...discard, mode: "confirm", onConfirm: leave } })] });
66
74
  }
67
- export function ProfileScreen({ summary, edit, children, accountActions, ...screen }) { return _jsx(ScreenLayout, { ...screen, children: _jsxs(Stack, { gap: "xl", children: [_jsx(CompositionLayout, { footer: _jsx(Action, { action: edit, secondary: true }), children: summary }), children, accountActions] }) }); }
68
- export function ModerationScreen({ reasonPicker, reasons, reason, onReasonChange, reasonLabel, children, submit, block, ...screen }) {
75
+ // Hands ProfileScreen's edit action to the ProfileSummary inside its summary slot, wherever the product nests it.
76
+ const ProfileEditContext = createContext(undefined);
77
+ export function ProfileScreen({ summary, edit, children, accountActions, editPlacement = "below-summary", ...screen }) { return _jsx(ScreenLayout, { ...screen, children: _jsxs(Stack, { gap: "xl", children: [editPlacement === "summary" ? _jsx(ProfileEditContext.Provider, { value: edit, children: summary }) : _jsx(CompositionLayout, { footer: _jsx(Action, { action: edit, secondary: true }), children: summary }), children, accountActions] }) }); }
78
+ /** Body, stacked actions and confirmations shared by ModerationScreen (screen chrome) and ModerationForm (none). */
79
+ function useModeration(fields, ready, cancel) {
69
80
  const [confirm, setConfirm] = useState(false);
70
- const hasBlock = !!block;
81
+ const [confirmId, setConfirmId] = useState(null);
82
+ const review = fields.variant === "review" ? fields : null, report = fields.variant === "review" ? null : fields;
83
+ const hasBlock = !!report?.block;
71
84
  // Withdrawing the action ends that confirmation session; a later action needs
72
85
  // a new explicit press even if the screen itself stays mounted.
73
86
  useEffect(() => { if (!hasBlock)
74
87
  setConfirm(false); }, [hasBlock]);
75
- return _jsxs(_Fragment, { children: [_jsx(ScreenLayout, { ...screen, footer: _jsxs(Stack, { gap: "xs", children: [_jsx(Action, { action: { ...submit, disabled: submit.disabled || !!screen.state && screen.state.kind !== "ready" || !reasons.some(item => item.value === reason) } }), block ? _jsx(Action, { action: { ...block.action, onAction: () => setConfirm(true) }, secondary: true }) : null] }), children: _jsxs(Stack, { gap: "lg", children: [reasonPicker ?? (reasons.length ? _jsx(RadioGroup, { accessibilityLabel: reasonLabel, orientation: "vertical", items: reasons, value: reason, onValueChange: value => { if (value)
76
- onReasonChange(value); } }) : null), children] }) }), block ? _jsx(AlertDialog, { open: confirm, onOpenChange: setConfirm, request: block.confirmation }) : null] });
88
+ const confirming = review && !review.resolution ? review.decisions.find(decision => decision.id === confirmId && decision.confirmation) : undefined;
89
+ // Same rule for a decision: its confirmation closes when the decision or its confirmation is withdrawn or resolved.
90
+ useEffect(() => { if (confirmId !== null && !confirming)
91
+ setConfirmId(null); }, [confirmId, confirming]);
92
+ const cancelAction = cancel ? _jsx(Action, { action: cancel, secondary: true }) : null;
93
+ if (review) {
94
+ const busy = review.decisions.some(decision => decision.pending);
95
+ const reasonMissing = (review.decisionReason.required ?? true) && !review.decisionReason.value.trim();
96
+ const field = review.resolution ?? _jsx(TextArea, { label: review.decisionReason.label, value: review.decisionReason.value, onChange: event => review.decisionReason.onValueChange(event.currentTarget.value), ...(review.decisionReason.description === undefined ? {} : { description: review.decisionReason.description }), ...(review.decisionReason.maxLength === undefined ? {} : { maxLength: review.decisionReason.maxLength }), required: review.decisionReason.required ?? true, disabled: !!review.decisionReason.disabled || busy });
97
+ // One decision at a time: a pending decision locks the others and the reason it was taken with.
98
+ const decisions = review.resolution ? [] : review.decisions.map(decision => {
99
+ const locked = !!decision.disabled || busy || !ready || reasonMissing;
100
+ return _jsx(Button, { tone: decision.tone ?? "secondary", disabled: locked, loading: decision.pending ?? false, onClick: () => { if (locked)
101
+ return; if (decision.confirmation)
102
+ setConfirmId(decision.id);
103
+ else
104
+ decision.onAction(); }, children: decision.label }, decision.id);
105
+ });
106
+ return { body: _jsxs(Stack, { gap: "lg", children: [review.children, field] }),
107
+ actions: decisions.length || cancelAction ? _jsxs(Stack, { gap: "xs", children: [decisions, cancelAction] }) : null,
108
+ dialogs: confirming?.confirmation ? _jsx(AlertDialog, { open: true, onOpenChange: open => { if (!open)
109
+ setConfirmId(null); }, request: confirming.confirmation }) : null };
110
+ }
111
+ const r = report;
112
+ return { body: _jsxs(Stack, { gap: "lg", children: [r.reasonPicker ?? (r.reasons.length ? _jsx(RadioGroup, { accessibilityLabel: r.reasonLabel, orientation: "vertical", items: r.reasons, value: r.reason, onValueChange: value => { if (value)
113
+ r.onReasonChange(value); } }) : null), r.children] }),
114
+ actions: _jsxs(Stack, { gap: "xs", children: [_jsx(Action, { action: { ...r.submit, disabled: r.submit.disabled || !ready || !r.reasons.some(item => item.value === r.reason) } }), r.block ? _jsx(Action, { action: { ...r.block.action, onAction: () => setConfirm(true) }, secondary: true }) : null, cancelAction] }),
115
+ dialogs: r.block ? _jsx(AlertDialog, { open: confirm, onOpenChange: setConfirm, request: r.block.confirmation }) : null };
116
+ }
117
+ // Every key of either variant, so the rest of the props reach ScreenLayout unchanged whichever variant is used.
118
+ const moderationFieldKeys = ["variant", "reasonPicker", "reasons", "reason", "onReasonChange", "reasonLabel", "children", "submit", "block", "decisionReason", "decisions", "resolution"];
119
+ export function ModerationScreen(props) {
120
+ const screen = { ...props };
121
+ for (const key of moderationFieldKeys)
122
+ delete screen[key];
123
+ const { body, actions, dialogs } = useModeration(props, !props.state || props.state.kind === "ready");
124
+ return _jsxs(_Fragment, { children: [_jsx(ScreenLayout, { ...screen, footer: actions, children: body }), dialogs] });
125
+ }
126
+ export function ModerationForm(props) {
127
+ const { body, actions, dialogs } = useModeration(props, true, props.cancel);
128
+ return _jsxs(_Fragment, { children: [_jsx(CompositionLayout, { ...(props.title === undefined ? {} : { title: props.title }), ...(props.headingLevel === undefined ? {} : { headingLevel: props.headingLevel }), ...(props.description === undefined ? {} : { description: props.description }), ...(props.actions === undefined ? {} : { actions: props.actions }), ...(props.layoutStyle === undefined ? {} : { layoutStyle: props.layoutStyle }), footer: actions, children: body }), dialogs] });
77
129
  }
78
130
  export function PhotoSourceSheet({ open, onOpenChange, onSelect, labels, disabled = false, cameraAvailable = true }) {
79
131
  // File-input click must stay in the user's activation stack. Deferring until the
@@ -82,8 +134,21 @@ export function PhotoSourceSheet({ open, onOpenChange, onSelect, labels, disable
82
134
  return; onOpenChange(false); onSelect(source); };
83
135
  return _jsx(Sheet, { open: open, onOpenChange: onOpenChange, title: labels.title, closeLabel: labels.cancel, children: _jsxs(Stack, { gap: "xs", children: [_jsx(Button, { tone: "secondary", disabled: disabled, onClick: () => select("library"), children: labels.library }), cameraAvailable ? _jsx(Button, { tone: "secondary", disabled: disabled, onClick: () => select("camera"), children: labels.camera }) : null] }) });
84
136
  }
137
+ /**
138
+ * One picked item: preview, upload status, then move/rotate/remove. Exported (2026-10-18, Utilverse photo tools) so a
139
+ * virtualized host (VirtualList, FlatList renderItem) renders the same cell as the screen grid instead of a copy.
140
+ * Web keyboard focus recovery after removal belongs to the screen grid; a virtualized host owns its own.
141
+ */
142
+ export function MediaSelectionCell({ item, index, count, labels, actionLabels, removeLabel, moveUpLabel, moveDownLabel, onRemove, onMove, onRetry, onCancel, rotate, disabled = false }) {
143
+ const id = item.descriptor.id;
144
+ // The job lock is temporary, so it keeps the focused action focusable (aria-disabled; Button drops the click) instead
145
+ // of throwing keyboard focus to <body> for the length of the job. The list boundaries stay natively disabled.
146
+ const lock = disabled ? { "aria-disabled": true } : {};
147
+ return _jsxs(Stack, { "data-hjm-media-item": id, gap: "xs", children: [item.preview, _jsx(UploadItem, { descriptor: item.descriptor, labels: labels, onRetry: onRetry, onCancel: onCancel }), _jsxs(Stack, { axis: "inline", gap: "xxs", layoutStyle: { flexWrap: "wrap" }, children: [_jsx(Button, { "data-media-action": "up", "aria-label": moveUpLabel(item), size: "small", tone: "ghost", disabled: index === 0, ...lock, onClick: () => onMove(id, -1), children: actionLabels.moveUp }), _jsx(Button, { "data-media-action": "down", "aria-label": moveDownLabel(item), size: "small", tone: "ghost", disabled: index === count - 1, ...lock, onClick: () => onMove(id, 1), children: actionLabels.moveDown }), rotate ? _jsx(Button, { "data-media-action": "rotate", "aria-label": rotate.itemLabel(item), size: "small", tone: "ghost", ...lock, onClick: () => rotate.onRotate(id), children: rotate.label }) : null, _jsx(Button, { "data-media-action": "remove", "aria-label": removeLabel(item), size: "small", tone: "ghost", ...lock, onClick: () => onRemove(id), children: actionLabels.remove })] })] });
148
+ }
85
149
  /** A photo is the primary content; upload status stays below it instead of replacing the thumbnail. */
86
- export function MediaSelectionScreen({ library, selectionSummary, items, add, done, labels, actionLabels, removeLabel, moveUpLabel, moveDownLabel, onRemove, onMove, onRetry, onCancel, ...screen }) {
150
+ export function MediaSelectionScreen({ library, selectionSummary, items, add, done, labels, actionLabels, removeLabel, moveUpLabel, moveDownLabel, onRemove, onMove, onRetry, onCancel, rotate, disabled, ...screen }) {
151
+ const itemActions = { labels, actionLabels, removeLabel, moveUpLabel, moveDownLabel, onRemove, onMove, onRetry, onCancel, ...(rotate === undefined ? {} : { rotate }), ...(disabled === undefined ? {} : { disabled }) };
87
152
  const grid = useRef(null), addButton = useRef(null);
88
153
  const focusedItem = useRef(null);
89
154
  useLayoutEffect(() => {
@@ -144,7 +209,7 @@ export function MediaSelectionScreen({ library, selectionSummary, items, add, do
144
209
  // disabled move button retain it until the layout effect chooses a successor.
145
210
  if (event.relatedTarget ? !event.currentTarget.contains(event.relatedTarget) : node.isConnected && !node.matches(":disabled"))
146
211
  focusedItem.current = null;
147
- }, children: items.map((item, index) => _jsxs(Stack, { "data-hjm-media-item": item.descriptor.id, gap: "xs", children: [item.preview, _jsx(UploadItem, { descriptor: item.descriptor, labels: labels, onRetry: onRetry, onCancel: onCancel }), _jsxs(Stack, { axis: "inline", gap: "xxs", layoutStyle: { flexWrap: "wrap" }, children: [_jsx(Button, { "data-media-action": "up", "aria-label": moveUpLabel(item), size: "small", tone: "ghost", disabled: index === 0, onClick: () => onMove(item.descriptor.id, -1), children: actionLabels.moveUp }), _jsx(Button, { "data-media-action": "down", "aria-label": moveDownLabel(item), size: "small", tone: "ghost", disabled: index === items.length - 1, onClick: () => onMove(item.descriptor.id, 1), children: actionLabels.moveDown }), _jsx(Button, { "data-media-action": "remove", "aria-label": removeLabel(item), size: "small", tone: "ghost", onClick: () => onRemove(item.descriptor.id), children: actionLabels.remove })] })] }, item.descriptor.id)) }) });
212
+ }, children: items.map((item, index) => _jsx(MediaSelectionCell, { item: item, index: index, count: items.length, ...itemActions }, item.descriptor.id)) }) });
148
213
  }
149
214
  function Highlighted({ text, match }) {
150
215
  if (!match || match.start < 0 || match.end <= match.start || match.end > text.length)
@@ -240,7 +305,7 @@ function SearchFilterSheetView({ sheet }) {
240
305
  return _jsx(Sheet, { open: sheet.open, onOpenChange: open => sheet.onOpenChange(open), title: sheet.title, closeLabel: sheet.labels.close, placement: session?.side ? "end" : "bottom", size: sheet.size ?? "large", footer: footer, children: sheet.renderContent(draft, setDraft) });
241
306
  }
242
307
  /** Abort is supplied to the host request; the host must ignore aborted responses before committing results. */
243
- export function SearchScreen({ query, queryLabel, queryField, queryClearLabel, onQueryChange, onSearch, debounceMs, filters, recentSearches, children, onSubmit, committedQuery, filtersOverflow = "wrap", hostGutter = "none", queryLabelVisibility = "visible", searching, searchingLabel, recentQueries, suggestedQueries, suggestions, resultSummary, appliedFilters, filterSheet, initialFocus = initialFocusDefaults.search, ...screen }) {
308
+ export function SearchScreen({ query, queryLabel, queryField, queryClearLabel, onQueryChange, onSearch, debounceMs, filters, recentSearches, children, onSubmit, committedQuery, filtersOverflow = "wrap", filtersRole = "narrow", hostGutter = "none", queryLabelVisibility = "visible", searching, searchingLabel, recentQueries, suggestedQueries, suggestions, resultSummary, appliedFilters, filterSheet, initialFocus = initialFocusDefaults.search, ...screen }) {
244
309
  // The host owns localized copy. A custom queryField owns its own clear affordance.
245
310
  if (queryField == null && !queryClearLabel?.trim())
246
311
  throw new TypeError("SearchScreen requires a localized queryClearLabel for its default search field");
@@ -275,9 +340,9 @@ export function SearchScreen({ query, queryLabel, queryField, queryClearLabel, o
275
340
  const phase = resolveSearchScreenPhase(query, committedQuery);
276
341
  const appliedCount = appliedFilters?.items.length ?? 0;
277
342
  const cause = phase === "results" && resultSummary ? resolveSearchEmptyCause(resultSummary.count, appliedCount) : "none";
278
- // Two-step search shows the rail only beside results (typing keeps the pinned area short), and a
279
- // query-caused zero hides it because no filter can fix that; a filter-caused zero keeps it to undo the filter.
280
- const showRail = (committedQuery === undefined || phase === "results") && cause !== "query";
343
+ // Two-step search shows the rail only beside results; a query-caused zero hides narrowing filters (none can fix it)
344
+ // but keeps scope filters, which are that zero's recovery (resolveSearchFilterRailVisible, Native twin).
345
+ const showRail = resolveSearchFilterRailVisible(phase, committedQuery !== undefined, cause, filtersRole);
281
346
  const trigger = filterSheet?.trigger ? _jsx(SearchFilterTrigger, { sheet: filterSheet, appliedCount: appliedCount, triggerRef: focus.filterTrigger }) : null;
282
347
  const rail = !showRail ? null : trigger && filters != null ? _jsxs(Stack, { axis: "inline", gap: "xs", wrap: filtersOverflow === "wrap", children: [trigger, filters] }) : trigger ?? filters;
283
348
  // Wrapping chips grow the pinned area line by line at large text; the scroll rail caps it at one row.
@@ -332,15 +397,19 @@ function CommentMenuButton({ item, menu, items }) {
332
397
  }
333
398
  /** Virtualized hosts reuse the same row as CommentThreadScreen; product slots add content, not a second layout. */
334
399
  export function CommentThreadRow({ item, onLike, onReply, replyLabel, layoutStyle, commentMenu }) {
400
+ validateMediaSetChoice(item.media != null, item.mediaAlbum !== undefined);
401
+ const [albumIndex, setAlbumIndex] = useMediaAlbumIndex(item.mediaAlbum);
402
+ // bodyText inlines the author, so its time always moves under the body; rich bodies opt in with timePlacement.
403
+ const timeInActions = item.bodyText !== undefined || item.timePlacement === "actions";
335
404
  const avatarSize = item.parentId === null ? commentThreadRecipe.avatarSize : commentThreadRecipe.replyAvatarSize;
336
405
  const canReply = item.canReply ?? item.parentId === null;
337
406
  const menuItems = commentMenu ? resolveCommentMenuItems(commentMenu.items(item)) : [];
338
407
  const time = item.timeLabel ? _jsx("time", { className: "hjm-comment-row__time", dateTime: item.timeDateTime, children: _jsx(Text, { variant: "caption", tone: "muted", children: item.timeLabel }) }) : null;
339
408
  // The like count sits under the heart so the text column carries only identity, body and reply.
340
409
  return _jsxs(Stack, { axis: "inline", align: "start", gap: "xs", ...(layoutStyle === undefined ? {} : { layoutStyle }), children: [item.avatar ? _jsx("div", { className: "hjm-comment-row__avatar", style: { inlineSize: avatarSize }, children: item.avatar }) : null, _jsxs("div", { className: "hjm-comment-row__text", style: { gap: commentThreadRecipe.lineGap }, children: [(() => {
341
- const identity = _jsxs(_Fragment, { children: [item.bodyText !== undefined ? _jsxs(Text, { children: [_jsx(Text, { emphasis: "strong", children: item.author }), " ", item.bodyText] }) : (item.author || item.timeLabel) ? _jsxs(Stack, { axis: "inline", gap: "xxs", align: "center", layoutStyle: { flexWrap: "wrap" }, children: [item.author ? _jsx(Text, { variant: "caption", emphasis: "strong", children: item.author }) : null, time] }) : null, item.body, item.media ? _jsx("div", { className: "hjm-comment-row__media", style: { maxInlineSize: commentThreadRecipe.mediaMaxWidth, borderRadius: commentThreadRecipe.mediaRadius }, children: item.media }) : null] });
410
+ const identity = _jsxs(_Fragment, { children: [item.bodyText !== undefined ? _jsxs(Text, { children: [_jsx(Text, { emphasis: "strong", children: item.author }), " ", item.bodyText] }) : (item.author || (item.timeLabel && !timeInActions)) ? _jsxs(Stack, { axis: "inline", gap: "xxs", align: "center", layoutStyle: { flexWrap: "wrap" }, children: [item.author ? _jsx(Text, { variant: "caption", emphasis: "strong", children: item.author }) : null, timeInActions ? null : time] }) : null, item.body, item.media ? _jsx("div", { className: "hjm-comment-row__media", style: { maxInlineSize: commentThreadRecipe.mediaMaxWidth, borderRadius: commentThreadRecipe.mediaRadius }, children: item.media }) : null] });
342
411
  return item.reactions ? _jsx(MessageReactions, { ...item.reactions, ...((item.canReply ?? item.parentId === null) && !item.replyDisabled ? { replyAction: { label: replyLabel, onPress: () => onReply(item.id), ...(item.reactions.replyIcon === undefined ? {} : { icon: item.reactions.replyIcon }) } } : {}), triggerClassName: "hjm-comment-row__reactions", children: identity }) : identity;
343
- })(), (item.bodyText !== undefined && time) || canReply || item.reactionSummary ? _jsxs(Stack, { axis: "inline", gap: "sm", align: "center", layoutStyle: { flexWrap: "wrap" }, children: [item.bodyText !== undefined ? time : null, item.reactionSummary, canReply ? _jsx(CommentTextAction, { label: replyLabel, disabled: item.replyDisabled ?? false, onPress: () => onReply(item.id) }) : null] }) : null] }), _jsxs("div", { className: "hjm-comment-row__like", ...(menuItems.length || item.actions == null && item.likeAction === undefined ? { "data-edge": "end" } : {}), style: { "--hjm-comment-anchor-line": `var(--hjm-type-${item.bodyText !== undefined ? commentThreadRecipe.actionAnchor.inlineTextVariant : commentThreadRecipe.actionAnchor.authorTextVariant}-line-height)` }, children: [_jsxs(Stack, { axis: "inline", gap: "xxs", align: "center", layoutStyle: { flexShrink: 0 }, children: [item.likeAction !== undefined ? item.likeAction : _jsx(IconButton, { label: item.likeLabel, tone: "ghost", onClick: () => onLike(item.id), children: item.likeIcon }), item.actions, commentMenu && menuItems.length ? _jsx(CommentMenuButton, { item: item, menu: commentMenu, items: menuItems }) : null] }), item.likeCountLabel ? _jsx("div", { className: "hjm-comment-row__like-count", style: { inlineSize: commentThreadRecipe.likeColumnWidth }, children: _jsx(Text, { variant: "caption", tone: "muted", children: item.likeCountLabel }) }) : null] })] }, item.id);
412
+ })(), item.mediaAlbum ? _jsx(MediaAlbumView, { className: "hjm-comment-row__album", album: item.mediaAlbum, index: albumIndex, onIndexChange: setAlbumIndex, width: commentThreadRecipe.mediaMaxWidth, radius: commentThreadRecipe.mediaRadius }) : null, (timeInActions && time) || canReply || item.reactionSummary ? _jsxs(Stack, { axis: "inline", gap: "sm", align: "center", layoutStyle: { flexWrap: "wrap" }, children: [timeInActions ? time : null, item.reactionSummary, canReply ? _jsx(CommentTextAction, { label: replyLabel, disabled: item.replyDisabled ?? false, onPress: () => onReply(item.id) }) : null] }) : null, item.panel != null ? _jsx("div", { className: "hjm-comment-row__panel", style: { marginBlockStart: commentThreadRecipe.panelGap - commentThreadRecipe.lineGap }, children: item.panel }) : null] }), _jsxs("div", { className: "hjm-comment-row__like", ...(menuItems.length || item.actions == null && item.likeAction === undefined ? { "data-edge": "end" } : {}), style: { "--hjm-comment-anchor-line": `var(--hjm-type-${item.bodyText !== undefined ? commentThreadRecipe.actionAnchor.inlineTextVariant : commentThreadRecipe.actionAnchor.authorTextVariant}-line-height)` }, children: [_jsxs(Stack, { axis: "inline", gap: "xxs", align: "center", layoutStyle: { flexShrink: 0 }, children: [item.likeAction !== undefined ? item.likeAction : _jsx(IconButton, { label: item.likeLabel, tone: "ghost", onClick: () => onLike(item.id), children: item.likeIcon }), item.actions, commentMenu && menuItems.length ? _jsx(CommentMenuButton, { item: item, menu: commentMenu, items: menuItems }) : null] }), item.likeCountLabel ? _jsx("div", { className: "hjm-comment-row__like-count", style: { inlineSize: commentThreadRecipe.likeColumnWidth }, children: _jsx(Text, { variant: "caption", tone: "muted", children: item.likeCountLabel }) }) : null] })] }, item.id);
344
413
  }
345
414
  /** Controlled thread: server ordering, permission checks and receipt-based draft clearing belong to the product. */
346
415
  export function CommentThreadScreen({ items, expandedIds, onExpandedChange, onLike, onReply, replyLabel, repliesLabel, composer, threadHeader, threadFooter, commentMenu, ...screen }) {
@@ -366,7 +435,10 @@ export function CommentThreadScreen({ items, expandedIds, onExpandedChange, onLi
366
435
  }
367
436
  /** Profile identity and counts are common; routing and mutations stay controlled by the product. */
368
437
  export function ProfileSummary({ name, headingLevel = 2, avatar, badge, nameActions, stats = [], actions, children }) {
438
+ const edit = useContext(ProfileEditContext);
439
+ const { tone: editTone, size: editSize } = profileSummaryRecipe.editAction;
440
+ const editButton = edit ? _jsx(Button, { tone: editTone, size: editSize, disabled: !!(edit.disabled || edit.pending), loading: edit.pending ?? false, onClick: edit.onAction, children: edit.label }) : null;
369
441
  return _jsxs(CompositionLayout, { title: name, headingLevel: headingLevel, leading: _jsx("span", { style: { display: "flex", width: profileSummaryRecipe.avatarSize, height: profileSummaryRecipe.avatarSize, flexShrink: 0, borderRadius: "50%", overflow: "hidden" }, children: avatar }), badge: badge, actions: nameActions, footer: children, children: [stats.length ? _jsx(Stack, { axis: "inline", gap: "md", align: "stretch", layoutStyle: { width: "100%" }, children: stats.map(stat => _jsx(Stack, { gap: "xxs", align: "center", justify: "center", layoutStyle: { flex: 1, minWidth: 0 }, children: stat.onPress ? _jsx(Button, { tone: "ghost", "aria-label": `${stat.value} ${stat.label}`, disabled: stat.disabled ?? false, onClick: stat.onPress, layoutStyle: { width: "100%" }, children: _jsxs(Stack, { gap: "xxs", align: "center", children: [_jsx(Text, { emphasis: "strong", children: stat.value }), _jsx(Text, { variant: "caption", tone: "muted", children: stat.label })] }) })
370
- : _jsxs(_Fragment, { children: [_jsx(Text, { emphasis: "strong", children: stat.value }), _jsx(Text, { variant: "caption", tone: "muted", children: stat.label })] }) }, stat.id)) }) : null, actions ? _jsx(Stack, { axis: "inline", gap: "sm", wrap: true, children: actions }) : null] });
442
+ : _jsxs(_Fragment, { children: [_jsx(Text, { emphasis: "strong", children: stat.value }), _jsx(Text, { variant: "caption", tone: "muted", children: stat.label })] }) }, stat.id)) }) : null, editButton || actions ? _jsxs(Stack, { axis: "inline", gap: "sm", wrap: true, children: [editButton, actions] }) : null] });
371
443
  }
372
444
  //# sourceMappingURL=screen-flows.js.map