@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.
- package/dist/auth-screen.d.ts +18 -3
- package/dist/auth-screen.d.ts.map +1 -1
- package/dist/auth-screen.js +22 -6
- package/dist/auth-screen.js.map +1 -1
- package/dist/bottom-cta.d.ts +6 -1
- package/dist/bottom-cta.d.ts.map +1 -1
- package/dist/bottom-cta.js +1 -1
- package/dist/bottom-cta.js.map +1 -1
- package/dist/bottom-navigation.d.ts +10 -0
- package/dist/bottom-navigation.d.ts.map +1 -1
- package/dist/bottom-navigation.js +80 -8
- package/dist/bottom-navigation.js.map +1 -1
- package/dist/carousel.d.ts +29 -1
- package/dist/carousel.d.ts.map +1 -1
- package/dist/carousel.js +27 -3
- package/dist/carousel.js.map +1 -1
- package/dist/display.d.ts +8 -2
- package/dist/display.d.ts.map +1 -1
- package/dist/display.js +12 -7
- package/dist/display.js.map +1 -1
- package/dist/evidence.d.ts +1 -1
- package/dist/evidence.js +1 -1
- package/dist/evidence.js.map +1 -1
- package/dist/forms.d.ts.map +1 -1
- package/dist/forms.js +2 -1
- package/dist/forms.js.map +1 -1
- package/dist/image-choice.d.ts +20 -2
- package/dist/image-choice.d.ts.map +1 -1
- package/dist/image-choice.js +60 -7
- package/dist/image-choice.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/field-error.d.ts +19 -0
- package/dist/internal/field-error.d.ts.map +1 -0
- package/dist/internal/field-error.js +19 -0
- package/dist/internal/field-error.js.map +1 -0
- package/dist/internal/initial-focus.d.ts.map +1 -1
- package/dist/internal/initial-focus.js +49 -7
- package/dist/internal/initial-focus.js.map +1 -1
- package/dist/internal/media-album.d.ts +70 -0
- package/dist/internal/media-album.d.ts.map +1 -0
- package/dist/internal/media-album.js +52 -0
- package/dist/internal/media-album.js.map +1 -0
- package/dist/internal/message-reactions.d.ts +39 -2
- package/dist/internal/message-reactions.d.ts.map +1 -1
- package/dist/internal/message-reactions.js +31 -6
- package/dist/internal/message-reactions.js.map +1 -1
- package/dist/layout.d.ts +19 -3
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +10 -3
- package/dist/layout.js.map +1 -1
- package/dist/navigation.d.ts +3 -1
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +4 -2
- package/dist/navigation.js.map +1 -1
- package/dist/number-field.d.ts.map +1 -1
- package/dist/number-field.js +2 -1
- package/dist/number-field.js.map +1 -1
- package/dist/overlays.d.ts +65 -0
- package/dist/overlays.d.ts.map +1 -1
- package/dist/overlays.js +36 -3
- package/dist/overlays.js.map +1 -1
- package/dist/pagination.d.ts +13 -11
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +12 -5
- package/dist/pagination.js.map +1 -1
- package/dist/public-exports.generated.d.ts +8 -0
- package/dist/public-exports.generated.d.ts.map +1 -1
- package/dist/public-exports.generated.js +24 -0
- package/dist/public-exports.generated.js.map +1 -1
- package/dist/screen-flows.d.ts +118 -14
- package/dist/screen-flows.d.ts.map +1 -1
- package/dist/screen-flows.js +95 -23
- package/dist/screen-flows.js.map +1 -1
- package/dist/screens.d.ts +69 -10
- package/dist/screens.d.ts.map +1 -1
- package/dist/screens.js +77 -15
- package/dist/screens.js.map +1 -1
- package/dist/select.d.ts.map +1 -1
- package/dist/select.js +2 -1
- package/dist/select.js.map +1 -1
- package/dist/selection.d.ts.map +1 -1
- package/dist/selection.js +3 -2
- package/dist/selection.js.map +1 -1
- package/dist/steps.d.ts +11 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +11 -6
- package/dist/steps.js.map +1 -1
- package/dist/styles.css +181 -22
- package/dist/styles.layered.css +181 -22
- package/dist/supplemental-display.d.ts.map +1 -1
- package/dist/supplemental-display.js +2 -48
- package/dist/supplemental-display.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +1 -0
- package/dist/theme.js.map +1 -1
- package/dist/toast.d.ts.map +1 -1
- package/dist/toast.js +1 -1
- package/dist/toast.js.map +1 -1
- package/package.json +3 -3
package/dist/screen-flows.js
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
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: "
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
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) =>
|
|
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
|
|
279
|
-
//
|
|
280
|
-
const showRail = (committedQuery
|
|
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.
|
|
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 ?
|
|
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
|