@jini-ai/ui 0.5.4 → 0.5.6
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/CHANGELOG.md +10 -0
- package/dist/core/theme/validation.js +40 -38
- package/dist/features/a2ui/renderer.js +15 -13
- package/dist/features/about/react/components/AboutTab.js +64 -34
- package/dist/features/about/rules.d.ts +2 -2
- package/dist/features/about/rules.js +156 -129
- package/dist/features/admin-widgets/components/ImagePreviewModal.d.ts +4 -3
- package/dist/features/admin-widgets/components/ImagePreviewModal.js +6 -10
- package/dist/features/admin-widgets/components/InfoTip.d.ts +3 -2
- package/dist/features/admin-widgets/components/InfoTip.hooks.js +2 -0
- package/dist/features/admin-widgets/components/InfoTip.js +34 -7
- package/dist/features/admin-widgets/components/InfoTip.rules.d.ts +13 -0
- package/dist/features/admin-widgets/components/InfoTip.rules.js +43 -10
- package/dist/features/admin-widgets/components/Lightbox/Lightbox.d.ts +30 -0
- package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.d.ts +40 -0
- package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +128 -0
- package/dist/features/admin-widgets/components/Lightbox/Lightbox.js +18 -0
- package/dist/features/admin-widgets/components/MiddleTruncate.d.ts +14 -0
- package/dist/features/admin-widgets/components/MiddleTruncate.js +12 -0
- package/dist/features/admin-widgets/components/MiddleTruncate.rules.d.ts +20 -0
- package/dist/features/admin-widgets/components/MiddleTruncate.rules.js +21 -0
- package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.d.ts +18 -0
- package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.d.ts +16 -0
- package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +45 -0
- package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.js +18 -0
- package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.rules.d.ts +32 -0
- package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.rules.js +26 -0
- package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +21 -0
- package/dist/features/admin-widgets/components/Select/Select.hooks.js +24 -2
- package/dist/features/admin-widgets/components/Select/Select.js +4 -3
- package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.js +29 -14
- package/dist/features/admin-widgets/index.d.ts +14 -2
- package/dist/features/admin-widgets/index.js +7 -1
- package/dist/features/admin-widgets/styles/admin-widgets.css +1 -0
- package/dist/features/asset-grid/react/components/AssetCard.js +7 -1
- package/dist/features/asset-grid/react/components/AssetGrid.d.ts +1 -1
- package/dist/features/asset-grid/react/components/AssetGrid.js +38 -18
- package/dist/features/asset-grid/react/components/useAssetGridFilters.d.ts +21 -0
- package/dist/features/asset-grid/react/components/useAssetGridFilters.js +19 -0
- package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +16 -11
- package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.d.ts +2 -0
- package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +35 -11
- package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.d.ts +6 -6
- package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.js +10 -129
- package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.d.ts +50 -0
- package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.js +95 -0
- package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.d.ts +56 -0
- package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.js +80 -0
- package/dist/features/asset-tree-browser/rules.js +40 -22
- package/dist/features/browser-chrome/rules.js +74 -70
- package/dist/features/charts/browser.js +11 -5
- package/dist/features/charts/react.js +2 -2
- package/dist/features/charts/table.js +2 -2
- package/dist/features/command-palette/react/hooks/useCommandPalette.js +3 -9
- package/dist/features/connectors/ConnectorsBrowser.d.ts +4 -4
- package/dist/features/connectors/ConnectorsBrowser.js +26 -14
- package/dist/features/connectors/components/ConnectorCard.js +67 -38
- package/dist/features/connectors/components/ConnectorDetailDrawer.js +62 -31
- package/dist/features/connectors/components/ConnectorGrid.js +11 -6
- package/dist/features/connectors/hooks/useConnectorAuthorization.js +6 -0
- package/dist/features/connectors/hooks/useConnectorCatalog.d.ts +2 -0
- package/dist/features/connectors/hooks/useConnectorCatalog.js +32 -13
- package/dist/features/connectors/hooks/useWiredConnectorsBrowser.d.ts +3 -3
- package/dist/features/connectors/rules.d.ts +8 -1
- package/dist/features/connectors/rules.js +42 -20
- package/dist/features/connectors/types.d.ts +11 -0
- package/dist/features/execution/constants.d.ts +4 -10
- package/dist/features/execution/constants.js +9 -94
- package/dist/features/execution/react/components/ByokProviderForm.d.ts +2 -4
- package/dist/features/execution/react/components/ByokProviderForm.js +140 -43
- package/dist/features/execution/react/components/ExecutionTab.d.ts +1 -2
- package/dist/features/execution/react/components/ExecutionTab.js +52 -30
- package/dist/features/execution/react/components/LocalCliAgentCard.js +12 -3
- package/dist/features/execution/react/components/LocalCliAgentList.js +14 -3
- package/dist/features/execution/react/components/LocalCliAgentSettings.js +14 -3
- package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +54 -36
- package/dist/features/execution/react/hooks/useReasoningControl.d.ts +1 -1
- package/dist/features/execution/react/hooks/useReasoningControl.js +116 -75
- package/dist/features/execution/rules.d.ts +13 -2
- package/dist/features/execution/rules.js +51 -11
- package/dist/features/execution/types.d.ts +6 -42
- package/dist/features/execution/types.js +0 -17
- package/dist/features/external-mcp/react/components/ExternalMcpTab.d.ts +6 -2
- package/dist/features/external-mcp/react/components/ExternalMcpTab.js +9 -2
- package/dist/features/file-dropzone/react/components/FileDropzone.js +24 -25
- package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.d.ts +2 -2
- package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.js +35 -30
- package/dist/features/file-dropzone/react/components/FileDropzoneThumbnailGrid.js +30 -24
- package/dist/features/file-dropzone/react/components/FileDropzoneZone.d.ts +14 -0
- package/dist/features/file-dropzone/react/components/FileDropzoneZone.js +26 -0
- package/dist/features/html-editor/canvas-content-wrapper.js +13 -13
- package/dist/features/html-editor/canvas-embed-placeholders.js +46 -37
- package/dist/features/html-editor/css.js +76 -69
- package/dist/features/html-editor/editor-blocks.js +47 -33
- package/dist/features/html-editor/editor-serialization.js +6 -5
- package/dist/features/html-editor/protected-elements.js +39 -28
- package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +4 -4
- package/dist/features/html-editor/source-splice.js +51 -30
- package/dist/features/i18n/dictionaries/settings-dialog.ja.js +1 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +1 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +1 -0
- package/dist/features/i18n/locale.js +20 -16
- package/dist/features/iframe-pool/react/dom-sync.js +8 -6
- package/dist/features/integrations/react/components/CodexInstallToggleButton.js +19 -4
- package/dist/features/integrations/react/components/IntegrationsTab.js +60 -14
- package/dist/features/interactive-ui/interactive-ui.css +1 -1
- package/dist/features/interactive-ui/providers/native/data-table.manifest.d.ts +6 -6
- package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -7
- package/dist/features/interactive-ui/providers/shadcn/data-table.manifest.d.ts +6 -6
- package/dist/features/lexical-rich-text-editor/mention-parser.js +60 -57
- package/dist/features/lexical-rich-text-editor/react/hooks/useKeyboardCommands.js +33 -38
- package/dist/features/lexical-rich-text-editor/rules.js +87 -74
- package/dist/features/lexical-rich-text-editor/serialize.js +27 -25
- package/dist/features/mcp-ui/secret-card.js +111 -61
- package/dist/features/mcp-ui/surfaces/confirmation.js +39 -21
- package/dist/features/mcp-ui/surfaces/document.d.ts +1 -1
- package/dist/features/mcp-ui/surfaces/document.js +10 -2
- package/dist/features/mcp-ui/surfaces/fields.js +30 -50
- package/dist/features/mcp-ui/surfaces/form.d.ts +6 -0
- package/dist/features/mcp-ui/surfaces/form.js +43 -21
- package/dist/features/mcp-ui/surfaces/outcome.js +30 -20
- package/dist/features/mcp-ui/surfaces/text-input.js +33 -21
- package/dist/features/mcp-ui/surfaces/tokens.d.ts +6 -1
- package/dist/features/mcp-ui/surfaces/tokens.js +5 -2
- package/dist/features/media-providers/constants.d.ts +1 -0
- package/dist/features/media-providers/constants.js +2 -81
- package/dist/features/media-providers/index.d.ts +1 -1
- package/dist/features/media-providers/ports.d.ts +2 -0
- package/dist/features/media-providers/react/components/MediaProviderCard.d.ts +22 -0
- package/dist/features/media-providers/react/components/MediaProviderCard.js +50 -0
- package/dist/features/media-providers/react/components/MediaProvidersTab.d.ts +5 -6
- package/dist/features/media-providers/react/components/MediaProvidersTab.js +40 -72
- package/dist/features/media-providers/react/components/media-providers-tab-labels.d.ts +11 -0
- package/dist/features/media-providers/react/components/media-providers-tab-labels.js +57 -0
- package/dist/features/media-providers/react/hooks/useMediaProvidersTab.d.ts +4 -2
- package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +37 -23
- package/dist/features/media-providers/rules.js +17 -10
- package/dist/features/media-providers/types.d.ts +4 -22
- package/dist/features/media-providers/types.js +3 -8
- package/dist/features/memory/formatters.js +127 -92
- package/dist/features/memory/react/components/MemoryConnectedPanel.js +92 -36
- package/dist/features/memory/react/components/MemoryList.js +37 -18
- package/dist/features/memory/react/hooks/useMemoryExtractions.store.js +42 -36
- package/dist/features/notifications/react/components/NotificationsTab.js +28 -28
- package/dist/features/observability/long-task.d.ts +0 -12
- package/dist/features/observability/long-task.js +15 -19
- package/dist/features/observability/resource-error.d.ts +0 -10
- package/dist/features/observability/resource-error.js +18 -13
- package/dist/features/observability/stuck-run.js +10 -6
- package/dist/features/observability/telemetry-url.d.ts +6 -0
- package/dist/features/observability/telemetry-url.js +16 -0
- package/dist/features/observability/white-screen.js +12 -24
- package/dist/features/panel-kit/fetch-query/adapter.react.d.ts +2 -2
- package/dist/features/panel-kit/fetch-query/adapter.react.js +4 -2
- package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +46 -33
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +3 -1
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +3 -1
- package/dist/features/privacy/react/components/PrivacyTab.js +24 -13
- package/dist/features/progress-card/components/ProgressCard.js +7 -3
- package/dist/features/progress-card/reference-adapters.js +59 -54
- package/dist/features/project-locations/react/components/ProjectLocationsTab.js +24 -15
- package/dist/features/project-locations/react/hooks/useProjectLocationsTab.d.ts +8 -0
- package/dist/features/project-locations/react/hooks/useProjectLocationsTab.js +31 -23
- package/dist/features/resource-dashboard/react/components/ResourceBoard.js +6 -47
- package/dist/features/resource-dashboard/react/components/ResourceBoardView.d.ts +2 -2
- package/dist/features/resource-dashboard/react/components/ResourceBoardView.js +3 -6
- package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.d.ts +15 -0
- package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.js +35 -0
- package/dist/features/resource-dashboard/react/components/ResourceCard.js +5 -16
- package/dist/features/resource-dashboard/react/components/ResourceCardParts.d.ts +34 -0
- package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +34 -0
- package/dist/features/resource-dashboard/react/components/ResourceRowList.js +3 -1
- package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +2 -2
- package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.d.ts +19 -0
- package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +11 -0
- package/dist/features/resource-dashboard/react/components/ResourceRowListView.d.ts +1 -1
- package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +4 -4
- package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.d.ts +10 -0
- package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.js +20 -0
- package/dist/features/resource-dashboard/react/components/resourceBoardProps.d.ts +48 -0
- package/dist/features/resource-dashboard/react/components/resourceBoardProps.js +70 -0
- package/dist/features/resource-dashboard/react/components/resourceRowListProps.d.ts +19 -0
- package/dist/features/resource-dashboard/react/components/resourceRowListProps.js +14 -0
- package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.d.ts +16 -0
- package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.js +13 -0
- package/dist/features/resource-dashboard/rules.js +2 -8
- package/dist/features/sketch-editor/dom.js +150 -99
- package/dist/features/sketch-editor/react/components/SketchEditor.js +26 -15
- package/dist/features/skills/react/components/SkillDraftForm.js +13 -19
- package/dist/features/skills/react/components/SkillDraftForm.labels.d.ts +31 -0
- package/dist/features/skills/react/components/SkillDraftForm.labels.js +54 -0
- package/dist/features/skills/react/components/SkillRow.js +38 -18
- package/dist/features/skills/react/components/SkillRow.labels.d.ts +7 -0
- package/dist/features/skills/react/components/SkillRow.labels.js +18 -0
- package/dist/features/skills/react/components/SkillsTab.js +24 -14
- package/dist/features/skills/react/components/SkillsTab.labels.d.ts +7 -0
- package/dist/features/skills/react/components/SkillsTab.labels.js +11 -0
- package/dist/features/skills/rules.js +32 -23
- package/dist/features/source-config-list/agent-handles.js +2 -1
- package/dist/features/source-config-list/dependencies.js +53 -49
- package/dist/features/source-config-list/index.d.ts +3 -3
- package/dist/features/source-config-list/react/components/SourceConfigAddForm.d.ts +6 -5
- package/dist/features/source-config-list/react/components/SourceConfigAddForm.js +38 -7
- package/dist/features/source-config-list/react/components/SourceConfigField.js +69 -25
- package/dist/features/source-config-list/react/components/SourceConfigItemCard.d.ts +6 -5
- package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +76 -20
- package/dist/features/source-config-list/react/components/SourceConfigListView.d.ts +5 -3
- package/dist/features/source-config-list/react/components/SourceConfigListView.js +33 -3
- package/dist/features/source-config-list/react/components/SourceConfigTestControl.js +17 -1
- package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.d.ts +11 -0
- package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.js +12 -0
- package/dist/features/source-config-list/types.d.ts +14 -0
- package/dist/features/tab-launcher-menu/react/components/TabLauncherMenu.js +25 -1
- package/dist/features/tab-launcher-menu/react/hooks/useTabLauncherMenu.js +15 -16
- package/dist/features/tab-strip/react/components/TabBar.js +8 -1
- package/dist/features/tabbed-dialog/react/components/TabbedDialog.js +8 -30
- package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.d.ts +15 -0
- package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.js +8 -0
- package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.d.ts +12 -0
- package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.js +6 -0
- package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.d.ts +27 -0
- package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.js +12 -0
- package/dist/features/tabbed-dialog/react/components/tabbedDialogView.d.ts +43 -0
- package/dist/features/tabbed-dialog/react/components/tabbedDialogView.js +59 -0
- package/dist/features/version-manager/dependencies.d.ts +3 -1
- package/dist/features/version-manager/dependencies.js +3 -2
- package/dist/features/version-manager/react/components/VersionManagerModal.js +10 -6
- package/dist/features/version-manager/react/hooks/useVersionManager.js +7 -4
- package/dist/features/viewer-shell/react/components/CommentSidePanel.js +103 -50
- package/dist/features/viewer-shell/react/components/MarkdownSplitPane.js +28 -8
- package/dist/features/viewer-shell/react/hooks/useCommentReorder.js +22 -16
- package/dist/features/viewer-shell/rules.js +19 -23
- package/dist/react/components/CustomSelect.d.ts +38 -1
- package/dist/react/components/CustomSelect.js +94 -38
- package/dist/react/components/OnboardingDropdown.js +20 -8
- package/dist/react/components/RemixIcon.stylesheet.js +3 -1
- package/dist/react/components/Toast.js +13 -2
- package/dist/react/components/TooltipLayer.js +5 -14
- package/dist/react/components/WorkingDirPicker.js +78 -50
- package/dist/react/components/remixicon-font-recovery.js +38 -30
- package/dist/react/components/select-popover.d.ts +8 -6
- package/dist/react/components/select-popover.js +33 -29
- package/dist/react/mcp-ui/McpUiHost.d.ts +1 -1
- package/dist/react/mcp-ui/McpUiHost.js +10 -11
- package/dist/react/mcp-ui/host-message-source.d.ts +14 -2
- package/dist/react/mcp-ui/host-message-source.js +15 -5
- package/dist/react/mcp-ui/mcp-ui-host-frame.d.ts +41 -0
- package/dist/react/mcp-ui/mcp-ui-host-frame.js +39 -0
- package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +97 -69
- package/dist/renderers/preview-modal-shell/react/components/PreviewModalShell.js +59 -30
- package/dist/renderers/preview-modal-shell/react/components/icons.js +4 -1
- package/dist/renderers/react/components/ArtifactView.js +9 -6
- package/dist/renderers/srcdoc/build.d.ts +4 -1
- package/dist/renderers/srcdoc/build.js +62 -48
- package/dist/styles/admin.css +29 -14
- package/dist/styles/lightbox.css +142 -0
- package/dist/styles/variables.css +128 -0
- package/dist/theme/apply.js +9 -5
- package/dist/utils/auto-open-file.d.ts +2 -1
- package/dist/utils/auto-open-file.js +69 -85
- package/dist/utils/markdown-scroll-sync.js +108 -87
- package/dist/utils/polygon-selection.js +25 -26
- package/dist/utils/smooth-scroll-to-top.d.ts +1 -1
- package/dist/utils/smooth-scroll-to-top.js +2 -32
- package/dist/utils/sse.js +26 -26
- package/dist/utils/unit-bezier.d.ts +7 -0
- package/dist/utils/unit-bezier.js +38 -0
- package/package.json +7 -10
- package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.d.ts +0 -13
- package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.js +0 -46
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** A page must be at least this many viewports tall before the jump buttons are worth their space. */
|
|
2
|
+
export const SCROLL_JUMP_MIN_VIEWPORTS = 2;
|
|
3
|
+
/** Within this many px of an edge counts as AT that edge (fractional scroll positions never hit 0 exactly). */
|
|
4
|
+
export const SCROLL_JUMP_EDGE_SLACK_PX = 8;
|
|
5
|
+
/**
|
|
6
|
+
* Which jump buttons a scroll container shows: none on a short page, ↑ only once scrolled away from
|
|
7
|
+
* the top, ↓ only while there is more below.
|
|
8
|
+
* @complexity O(1).
|
|
9
|
+
*/
|
|
10
|
+
export function scrollJumpVisibility({ scrollTop, scrollHeight, clientHeight }, { minViewports = SCROLL_JUMP_MIN_VIEWPORTS, edgeSlackPx = SCROLL_JUMP_EDGE_SLACK_PX } = {}) {
|
|
11
|
+
if (clientHeight <= 0 || scrollHeight < clientHeight * minViewports)
|
|
12
|
+
return { showTop: false, showBottom: false };
|
|
13
|
+
return {
|
|
14
|
+
showTop: scrollTop > edgeSlackPx,
|
|
15
|
+
showBottom: scrollHeight - scrollTop - clientHeight > edgeSlackPx,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Reduced-motion users get an instant jump instead of a long animated scroll. */
|
|
19
|
+
export function scrollJumpBehavior({ reducedMotion }) {
|
|
20
|
+
return reducedMotion ? "auto" : "smooth";
|
|
21
|
+
}
|
|
22
|
+
/** The `scrollTo` target for an edge: 0 for the top, the full content height for the bottom. */
|
|
23
|
+
export function scrollJumpTarget({ edge, scrollHeight }) {
|
|
24
|
+
return edge === "top" ? 0 : scrollHeight;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=ScrollJump.rules.js.map
|
|
@@ -2,7 +2,20 @@ export { focusableInDomOrder, resolveTabTarget } from '../../../../react/compone
|
|
|
2
2
|
export interface SelectOption {
|
|
3
3
|
value: string;
|
|
4
4
|
label: string;
|
|
5
|
+
/** Heading shown above the first option of each run with this value (e.g. "Pages", "Posts").
|
|
6
|
+
* Callers pass options already ordered by group; search narrows rows, headings follow. */
|
|
7
|
+
group?: string | undefined;
|
|
8
|
+
/** Short state shown as a muted pill after the label (e.g. "draft"); omit for the normal state. */
|
|
9
|
+
badge?: string | undefined;
|
|
10
|
+
/** Muted secondary text after the label, e.g. a slug telling two same-titled rows apart.
|
|
11
|
+
* Search matches it too. */
|
|
12
|
+
detail?: string | undefined;
|
|
5
13
|
}
|
|
14
|
+
/** The group heading to paint before `options[index]`, or null when it continues the previous run. */
|
|
15
|
+
export declare function groupHeadingAt({ options, index }: {
|
|
16
|
+
options: readonly SelectOption[];
|
|
17
|
+
index: number;
|
|
18
|
+
}): string | null;
|
|
6
19
|
export interface PanelPosition {
|
|
7
20
|
top?: number | undefined;
|
|
8
21
|
bottom?: number | undefined;
|
|
@@ -18,6 +31,14 @@ export interface PanelPosition {
|
|
|
18
31
|
export declare function computePosition({ trigger }: {
|
|
19
32
|
trigger: HTMLElement;
|
|
20
33
|
}): PanelPosition;
|
|
34
|
+
/** Top edge (viewport px, never negative) of the nearest scrollable PANE around the trigger, or 0.
|
|
35
|
+
* A pane is an `overflow-y: auto|scroll` ancestor at least half the viewport tall — whether or not
|
|
36
|
+
* it currently overflows (a short form still sits under the app bar), while a small scrolling
|
|
37
|
+
* wrapper such as a table or chip strip does not count and leaves placement as it was.
|
|
38
|
+
* @complexity O(depth) computed-style reads per measurement. */
|
|
39
|
+
export declare function scrollPaneTop({ trigger }: {
|
|
40
|
+
trigger: HTMLElement;
|
|
41
|
+
}, _optional?: Record<string, never>): number;
|
|
21
42
|
export declare function buildOptionId({ listboxId, index }: {
|
|
22
43
|
listboxId: string;
|
|
23
44
|
index: number;
|
|
@@ -13,6 +13,11 @@ const SEARCH_VISIBILITY_THRESHOLD = 8;
|
|
|
13
13
|
// 280px is only a placement heuristic; the panel actual maxHeight and scroll limit use
|
|
14
14
|
// the remaining viewport space so a low trigger does not open a mostly off-screen panel.
|
|
15
15
|
const ESTIMATED_PANEL_HEIGHT = 280;
|
|
16
|
+
/** The group heading to paint before `options[index]`, or null when it continues the previous run. */
|
|
17
|
+
export function groupHeadingAt({ options, index }) {
|
|
18
|
+
const group = options[index]?.group;
|
|
19
|
+
return group && group !== options[index - 1]?.group ? group : null;
|
|
20
|
+
}
|
|
16
21
|
/**
|
|
17
22
|
* Use fixed viewport-relative coordinates to escape scrolling/overflow-hidden ancestors
|
|
18
23
|
* without coupling to their layout. maxHeight uses actual remaining space, not the estimate,
|
|
@@ -23,13 +28,30 @@ export function computePosition({ trigger }) {
|
|
|
23
28
|
const gap = 4;
|
|
24
29
|
const viewportHeight = window.innerHeight;
|
|
25
30
|
const spaceBelow = viewportHeight - rect.bottom;
|
|
26
|
-
|
|
31
|
+
// Space above ends at the trigger's scrolling pane, not the viewport: an app bar above that pane
|
|
32
|
+
// is not room for the panel (owner, 2026-10-15: on a short phone the list flipped up and covered
|
|
33
|
+
// the top bar). With nothing scrolling but the page this is 0 and placement is unchanged.
|
|
34
|
+
const spaceAbove = rect.top - scrollPaneTop({ trigger });
|
|
27
35
|
const openUpward = spaceBelow < ESTIMATED_PANEL_HEIGHT && spaceAbove > spaceBelow;
|
|
28
36
|
if (openUpward) {
|
|
29
37
|
return { bottom: viewportHeight - rect.top + gap, left: rect.left, width: rect.width, maxHeight: Math.max(120, spaceAbove - gap * 2) };
|
|
30
38
|
}
|
|
31
39
|
return { top: rect.bottom + gap, left: rect.left, width: rect.width, maxHeight: Math.max(120, spaceBelow - gap * 2) };
|
|
32
40
|
}
|
|
41
|
+
/** Top edge (viewport px, never negative) of the nearest scrollable PANE around the trigger, or 0.
|
|
42
|
+
* A pane is an `overflow-y: auto|scroll` ancestor at least half the viewport tall — whether or not
|
|
43
|
+
* it currently overflows (a short form still sits under the app bar), while a small scrolling
|
|
44
|
+
* wrapper such as a table or chip strip does not count and leaves placement as it was.
|
|
45
|
+
* @complexity O(depth) computed-style reads per measurement. */
|
|
46
|
+
export function scrollPaneTop({ trigger }, _optional = {}) {
|
|
47
|
+
for (let el = trigger.parentElement; el && el !== document.body && el !== document.documentElement; el = el.parentElement) {
|
|
48
|
+
const overflowY = getComputedStyle(el).overflowY;
|
|
49
|
+
if ((overflowY === "auto" || overflowY === "scroll") && el.clientHeight * 2 >= window.innerHeight) {
|
|
50
|
+
return Math.max(0, el.getBoundingClientRect().top);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
33
55
|
export function buildOptionId({ listboxId, index }) {
|
|
34
56
|
return `${listboxId}-option-${index}`;
|
|
35
57
|
}
|
|
@@ -118,7 +140,7 @@ export function useSelectDropdown({ value, onChange, options, }, { disabled } =
|
|
|
118
140
|
if (!showSearch || !query.trim())
|
|
119
141
|
return options;
|
|
120
142
|
const q = query.trim().toLowerCase();
|
|
121
|
-
return options.filter((o) => o.label.toLowerCase().includes(q));
|
|
143
|
+
return options.filter((o) => o.label.toLowerCase().includes(q) || Boolean(o.detail?.toLowerCase().includes(q)));
|
|
122
144
|
}, [options, query, showSearch]);
|
|
123
145
|
const selectedOption = options.find((o) => o.value === value) ?? null;
|
|
124
146
|
function openPanel() {
|
|
@@ -3,12 +3,13 @@ import { resolveSelectPortalContainer } from '../../../../react/components/selec
|
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { agentHandle } from "@jini-ai/agentic";
|
|
5
5
|
import { buildAgentListHandles } from "@jini-ai/agentic";
|
|
6
|
-
import {
|
|
6
|
+
import { Fragment } from "react";
|
|
7
|
+
import { useSelectDropdown, groupHeadingAt } from "./Select.hooks.js";
|
|
7
8
|
function SelectOptionRow({ option, index, isSelected, isHighlighted, optionId, setOptionRef, onHighlight, onSelect, agentHandle: optionHandle, }) {
|
|
8
|
-
return (_jsxs("li", { ref: (el) => setOptionRef(index, el), id: optionId, role: "option", "aria-selected": isSelected, className: `select-option${isSelected ? " is-selected" : ""}${isHighlighted ? " is-highlighted" : ""}`, onMouseEnter: () => onHighlight(index), onClick: () => onSelect(option), ...(optionHandle ? agentHandle({ handle: optionHandle }, { role: "button", label: option.label }) : {}), children: [_jsx("span", { className: "select-option-label", children: option.label }), isSelected ? (_jsx("svg", { className: "select-option-check", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M3.5 8.5 6.5 11.5 12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })) : null] }));
|
|
9
|
+
return (_jsxs("li", { ref: (el) => setOptionRef(index, el), id: optionId, role: "option", "aria-selected": isSelected, className: `select-option${isSelected ? " is-selected" : ""}${isHighlighted ? " is-highlighted" : ""}`, onMouseEnter: () => onHighlight(index), onClick: () => onSelect(option), ...(optionHandle ? agentHandle({ handle: optionHandle }, { role: "button", label: option.label }) : {}), children: [_jsx("span", { className: "select-option-label", children: option.label }), option.detail ? _jsx("span", { className: "select-option-detail", children: option.detail }) : null, option.badge ? _jsx("span", { className: "select-option-badge", children: option.badge }) : null, isSelected ? (_jsx("svg", { className: "select-option-check", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M3.5 8.5 6.5 11.5 12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })) : null] }));
|
|
9
10
|
}
|
|
10
11
|
function SelectPanel({ panelRef, position, showSearch, searchInputRef, query, setQuery, filtered, value, highlightedIndex, listboxId, optionId, setOptionRef, onHighlight, onSelect, onKeyDown, searchHandle, optionHandles, t, }) {
|
|
11
|
-
return (_jsxs("div", { ref: panelRef, className: "select-panel", style: { left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight }, tabIndex: -1, onKeyDown: onKeyDown, children: [showSearch ? (_jsx("input", { ref: searchInputRef, type: "text", className: "select-search", "aria-label": t("Search options"), placeholder: t("Search…"), value: query, onChange: (e) => setQuery(e.target.value), ...(searchHandle ? agentHandle({ handle: searchHandle }, { role: "field", label: "Search the option list" }) : {}) })) : null, _jsx("ul", { className: "select-list", role: "listbox", id: listboxId, children: filtered.length === 0 ? (_jsx("li", { className: "select-empty", role: "presentation", children: t("No matches") })) : (filtered.map((option, index) => (_jsx(SelectOptionRow, { option: option, index: index, isSelected: option.value === value, isHighlighted: index === highlightedIndex, optionId: optionId(index), setOptionRef: setOptionRef, onHighlight: onHighlight, onSelect: onSelect, agentHandle: optionHandles?.[index] }, option.value)))) })] }));
|
|
12
|
+
return (_jsxs("div", { ref: panelRef, className: "select-panel", style: { left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight }, tabIndex: -1, onKeyDown: onKeyDown, children: [showSearch ? (_jsx("input", { ref: searchInputRef, type: "text", className: "select-search", "aria-label": t("Search options"), placeholder: t("Search…"), value: query, onChange: (e) => setQuery(e.target.value), ...(searchHandle ? agentHandle({ handle: searchHandle }, { role: "field", label: "Search the option list" }) : {}) })) : null, _jsx("ul", { className: "select-list", role: "listbox", id: listboxId, children: filtered.length === 0 ? (_jsx("li", { className: "select-empty", role: "presentation", children: t("No matches") })) : (filtered.map((option, index) => (_jsxs(Fragment, { children: [groupHeadingAt({ options: filtered, index }) ? (_jsx("li", { className: "select-group-label", role: "presentation", children: option.group })) : null, _jsx(SelectOptionRow, { option: option, index: index, isSelected: option.value === value, isHighlighted: index === highlightedIndex, optionId: optionId(index), setOptionRef: setOptionRef, onHighlight: onHighlight, onSelect: onSelect, agentHandle: optionHandles?.[index] })] }, option.value)))) })] }));
|
|
12
13
|
}
|
|
13
14
|
export function resolveSelectTriggerLabel({ selectedOption, placeholder }, { t = (key) => key } = {}) {
|
|
14
15
|
if (selectedOption)
|
|
@@ -13,6 +13,13 @@ export function useSuggestInput({ value, suggestions }, { limit } = {}) {
|
|
|
13
13
|
const matches = matchSuggestions({ typed: value, suggestions }, { limit });
|
|
14
14
|
const visible = open && matches.length > 0;
|
|
15
15
|
const current = highlighted < matches.length ? highlighted : -1;
|
|
16
|
+
const close = () => { setOpen(false); setHighlighted(-1); };
|
|
17
|
+
const move = (step) => {
|
|
18
|
+
setOpen(true);
|
|
19
|
+
const next = nextHighlight({ current, step, count: matches.length });
|
|
20
|
+
setHighlighted(next);
|
|
21
|
+
setScrollTarget({ index: next });
|
|
22
|
+
};
|
|
16
23
|
return {
|
|
17
24
|
open: visible,
|
|
18
25
|
matches,
|
|
@@ -20,33 +27,41 @@ export function useSuggestInput({ value, suggestions }, { limit } = {}) {
|
|
|
20
27
|
setHighlighted,
|
|
21
28
|
scrollTarget,
|
|
22
29
|
openList: () => setOpen(true),
|
|
23
|
-
closeList:
|
|
30
|
+
closeList: close,
|
|
24
31
|
handleKey: (key) => {
|
|
25
|
-
|
|
32
|
+
const step = arrowStep(key);
|
|
33
|
+
if (step !== undefined) {
|
|
26
34
|
if (matches.length === 0)
|
|
27
35
|
return { consumed: false };
|
|
28
|
-
|
|
29
|
-
const step = key === 'ArrowDown' ? 1 : -1;
|
|
30
|
-
const next = (current + step + matches.length + (current === -1 && step === -1 ? 1 : 0)) % matches.length;
|
|
31
|
-
setHighlighted(next);
|
|
32
|
-
setScrollTarget({ index: next });
|
|
36
|
+
move(step);
|
|
33
37
|
return { consumed: true };
|
|
34
38
|
}
|
|
35
39
|
if (key === 'Escape' && visible) {
|
|
36
|
-
|
|
37
|
-
setHighlighted(-1);
|
|
40
|
+
close();
|
|
38
41
|
return { consumed: true };
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
setHighlighted(-1);
|
|
44
|
-
return { consumed: true, picked };
|
|
43
|
+
if (key === 'Enter' && visible && current >= 0) {
|
|
44
|
+
close();
|
|
45
|
+
return { consumed: true, picked: matches[current] };
|
|
45
46
|
}
|
|
46
47
|
return { consumed: false };
|
|
47
48
|
},
|
|
48
49
|
};
|
|
49
50
|
}
|
|
51
|
+
/** +1 for ArrowDown, -1 for ArrowUp, undefined for every other key. */
|
|
52
|
+
function arrowStep(key) {
|
|
53
|
+
if (key === 'ArrowDown')
|
|
54
|
+
return 1;
|
|
55
|
+
if (key === 'ArrowUp')
|
|
56
|
+
return -1;
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The row an arrow key moves to, wrapping at either end; from no highlight, ArrowUp lands on the last row. */
|
|
60
|
+
function nextHighlight({ current, step, count }) {
|
|
61
|
+
if (current === -1)
|
|
62
|
+
return step === 1 ? 0 : count - 1;
|
|
63
|
+
return (current + step + count) % count;
|
|
64
|
+
}
|
|
50
65
|
/** Default scroller: `block: 'nearest'` moves the list only as far as needed. Optional call because
|
|
51
66
|
* non-browser DOMs (jsdom in host test suites) don't implement `scrollIntoView`. */
|
|
52
67
|
export function scrollOptionIntoView(option) {
|
|
@@ -14,8 +14,10 @@ export type { SeeMoreProps, SeeMoreView } from './components/SeeMore/SeeMore.js'
|
|
|
14
14
|
export { useSeeMoreClamp } from './components/SeeMore/SeeMore.hooks.js';
|
|
15
15
|
export { ImagePreviewModal } from './components/ImagePreviewModal.js';
|
|
16
16
|
export type { ImagePreviewModalProps } from './components/ImagePreviewModal.js';
|
|
17
|
-
export {
|
|
18
|
-
export type {
|
|
17
|
+
export { Lightbox } from './components/Lightbox/Lightbox.js';
|
|
18
|
+
export type { LightboxProps } from './components/Lightbox/Lightbox.js';
|
|
19
|
+
export { useLightbox, openLightboxDialog, closeLightboxDialog } from './components/Lightbox/Lightbox.hooks.js';
|
|
20
|
+
export type { LightboxController, LightboxHookInput } from './components/Lightbox/Lightbox.hooks.js';
|
|
19
21
|
export { ComingSoonPanel } from './components/ComingSoonPanel.js';
|
|
20
22
|
export type { ComingSoonPanelProps } from './components/ComingSoonPanel.js';
|
|
21
23
|
export { ComingSoonNotice } from './components/ComingSoonNotice.js';
|
|
@@ -26,4 +28,14 @@ export { useSuggestInput, matchSuggestions } from './components/SuggestInput/Sug
|
|
|
26
28
|
export type { SuggestInputController } from './components/SuggestInput/SuggestInput.hooks.js';
|
|
27
29
|
export { useDebouncedHtmlPreview } from './components/useDebouncedHtmlPreview.hooks.js';
|
|
28
30
|
export type { HtmlPreviewPort, HtmlPreviewState } from './components/useDebouncedHtmlPreview.hooks.js';
|
|
31
|
+
export { MiddleTruncate } from './components/MiddleTruncate.js';
|
|
32
|
+
export type { MiddleTruncateProps } from './components/MiddleTruncate.js';
|
|
33
|
+
export { splitMiddleTruncation } from './components/MiddleTruncate.rules.js';
|
|
34
|
+
export type { MiddleTruncationParts } from './components/MiddleTruncate.rules.js';
|
|
35
|
+
export { ScrollJumpButtons } from './components/ScrollJump/ScrollJump.js';
|
|
36
|
+
export type { ScrollJumpButtonsProps } from './components/ScrollJump/ScrollJump.js';
|
|
37
|
+
export { useScrollJump } from './components/ScrollJump/ScrollJump.hooks.js';
|
|
38
|
+
export type { ScrollJumpController } from './components/ScrollJump/ScrollJump.hooks.js';
|
|
39
|
+
export { scrollJumpVisibility, scrollJumpBehavior, scrollJumpTarget } from './components/ScrollJump/ScrollJump.rules.js';
|
|
40
|
+
export type { ScrollJumpVisibility, ScrollMetrics } from './components/ScrollJump/ScrollJump.rules.js';
|
|
29
41
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -7,10 +7,16 @@ export { placeInfoTipBubble, infoTipBubbleStyle } from './components/InfoTip.rul
|
|
|
7
7
|
export { SeeMore, resolveSeeMoreView } from './components/SeeMore/SeeMore.js';
|
|
8
8
|
export { useSeeMoreClamp } from './components/SeeMore/SeeMore.hooks.js';
|
|
9
9
|
export { ImagePreviewModal } from './components/ImagePreviewModal.js';
|
|
10
|
-
export {
|
|
10
|
+
export { Lightbox } from './components/Lightbox/Lightbox.js';
|
|
11
|
+
export { useLightbox, openLightboxDialog, closeLightboxDialog } from './components/Lightbox/Lightbox.hooks.js';
|
|
11
12
|
export { ComingSoonPanel } from './components/ComingSoonPanel.js';
|
|
12
13
|
export { ComingSoonNotice } from './components/ComingSoonNotice.js';
|
|
13
14
|
export { SuggestInput } from './components/SuggestInput/SuggestInput.js';
|
|
14
15
|
export { useSuggestInput, matchSuggestions } from './components/SuggestInput/SuggestInput.hooks.js';
|
|
15
16
|
export { useDebouncedHtmlPreview } from './components/useDebouncedHtmlPreview.hooks.js';
|
|
17
|
+
export { MiddleTruncate } from './components/MiddleTruncate.js';
|
|
18
|
+
export { splitMiddleTruncation } from './components/MiddleTruncate.rules.js';
|
|
19
|
+
export { ScrollJumpButtons } from './components/ScrollJump/ScrollJump.js';
|
|
20
|
+
export { useScrollJump } from './components/ScrollJump/ScrollJump.hooks.js';
|
|
21
|
+
export { scrollJumpVisibility, scrollJumpBehavior, scrollJumpTarget } from './components/ScrollJump/ScrollJump.rules.js';
|
|
16
22
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,9 +3,15 @@ import { useCallback } from 'react';
|
|
|
3
3
|
import { useT } from '../../../i18n/index.js';
|
|
4
4
|
import { useInView } from '../../../../react/hooks/useInView.js';
|
|
5
5
|
import { resolveCheckboxClickAction, resolvePreviewClickAction } from '../../rules.js';
|
|
6
|
+
function renderCardActions({ asset, renderCardExtra, onDeleteAsset, t }, _options = {}) {
|
|
7
|
+
if (!renderCardExtra && !onDeleteAsset)
|
|
8
|
+
return null;
|
|
9
|
+
return (_jsxs("div", { className: "asset-grid-card-actions", children: [renderCardExtra ? renderCardExtra(asset) : _jsx("span", {}), onDeleteAsset ? (_jsx("button", { type: "button", className: "asset-grid-card-delete-btn", onClick: () => onDeleteAsset(asset.id), children: t('Remove') })) : null] }));
|
|
10
|
+
}
|
|
6
11
|
export function AssetCard({ asset, index, selected, title, subtitle, kindLabel, sourceLabel, renderThumbnail, renderThumbnailPlaceholder, onToggle, onRange, onPreview, onDeleteAsset, renderCardExtra, }) {
|
|
7
12
|
const t = useT();
|
|
8
13
|
const { ref, inView } = useInView({ once: true, rootMargin: '300px' });
|
|
14
|
+
const selectionValue = selected ? 'true' : 'false';
|
|
9
15
|
const handlePreviewClick = useCallback((e) => {
|
|
10
16
|
const action = resolvePreviewClickAction(e);
|
|
11
17
|
if (action === 'toggle')
|
|
@@ -22,6 +28,6 @@ export function AssetCard({ asset, index, selected, title, subtitle, kindLabel,
|
|
|
22
28
|
else
|
|
23
29
|
onToggle(asset.id, index);
|
|
24
30
|
}, [asset.id, index, onToggle, onRange]);
|
|
25
|
-
return (_jsxs("figure", { className: "asset-grid-card", "data-asset-grid-id": asset.id, "data-selected":
|
|
31
|
+
return (_jsxs("figure", { className: "asset-grid-card", "data-asset-grid-id": asset.id, "data-selected": selectionValue, children: [_jsxs("div", { className: "asset-grid-card-thumb", children: [_jsx("div", { ref: ref, className: "asset-grid-card-thumb-lazy", children: inView ? renderThumbnail(asset) : (renderThumbnailPlaceholder?.(asset) ?? null) }), _jsx("button", { type: "button", className: "asset-grid-card-preview-btn", onClick: handlePreviewClick, "aria-label": t('Preview {title}', { title }), children: _jsx("span", { className: "asset-grid-card-preview-overlay", "aria-hidden": true, children: _jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "11", cy: "11", r: "7" }), _jsx("path", { d: "m21 21-4.3-4.3" })] }) }) }), _jsx("button", { type: "button", className: "asset-grid-card-select-check", "data-checked": selectionValue, "aria-pressed": selected, "aria-label": selected ? t('Deselect asset') : t('Select asset'), onClick: handleCheckboxClick, children: selected ? (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: _jsx("path", { d: "M20 6 9 17l-5-5" }) })) : null }), sourceLabel ? (_jsx("span", { className: "asset-grid-card-badge", "data-badge": "source", children: sourceLabel })) : null, kindLabel ? (_jsx("span", { className: "asset-grid-card-badge", "data-badge": "kind", children: kindLabel })) : null] }), _jsxs("figcaption", { className: "asset-grid-card-meta", children: [_jsx("button", { type: "button", className: "asset-grid-card-title", title: title, onClick: () => onPreview(asset.id), children: title }), subtitle ? _jsx("span", { className: "asset-grid-card-subtitle", children: subtitle }) : null] }), renderCardActions({ asset, renderCardExtra, onDeleteAsset, t })] }));
|
|
26
32
|
}
|
|
27
33
|
//# sourceMappingURL=AssetCard.js.map
|
|
@@ -41,5 +41,5 @@ export interface AssetGridProps<TAsset extends AssetGridItem> {
|
|
|
41
41
|
* pattern behind a photo library / DAM. See `packages/ui/source-map.md` for
|
|
42
42
|
* the full retained-behavior manifest against the original `LibrarySection.tsx`.
|
|
43
43
|
*/
|
|
44
|
-
export declare function AssetGrid<TAsset extends AssetGridItem>({ active, selectors, dependencies, kindFacets, sourceFacets, renderThumbnail, renderThumbnailPlaceholder, renderCardExtra, renderBulkActions, onDeleteAsset, onDeleteSelected, onPreview, isPreviewOpen, toolbarActions, emptyState, searchPlaceholder, searchDebounceMs, liveUpdateCoalesceMs, initialViewMode, useWiredAssetGridData: useWiredAssetGridDataHook, useAssetGridSelection: useAssetGridSelectionHook, }: AssetGridProps<TAsset>): import("react").JSX.Element;
|
|
44
|
+
export declare function AssetGrid<TAsset extends AssetGridItem>({ active, selectors, dependencies, kindFacets: configuredKindFacets, sourceFacets: configuredSourceFacets, renderThumbnail, renderThumbnailPlaceholder, renderCardExtra, renderBulkActions, onDeleteAsset, onDeleteSelected, onPreview, isPreviewOpen, toolbarActions, emptyState, searchPlaceholder, searchDebounceMs, liveUpdateCoalesceMs, initialViewMode, useWiredAssetGridData: useWiredAssetGridDataHook, useAssetGridSelection: useAssetGridSelectionHook, }: AssetGridProps<TAsset>): import("react").JSX.Element;
|
|
45
45
|
//# sourceMappingURL=AssetGrid.d.ts.map
|
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { useT } from '../../../i18n/index.js';
|
|
4
|
-
import {
|
|
5
|
-
import { DEFAULT_SEARCH_DEBOUNCE_MS } from '../../constants.js';
|
|
6
|
-
import { buildFacetLabelMap, dayKeyFromTimestamp, resolveFacetLabel } from '../../rules.js';
|
|
4
|
+
import { dayKeyFromTimestamp, resolveFacetLabel } from '../../rules.js';
|
|
7
5
|
import { useWiredAssetGridData } from '../hooks/useAssetGridData.js';
|
|
8
6
|
import { useWiredAssetGridLiveUpdates } from '../hooks/useAssetGridLiveUpdates.js';
|
|
9
7
|
import { useAssetGridSelection } from '../hooks/useAssetGridSelection.js';
|
|
10
8
|
import { useRubberBandDrag } from '../hooks/useRubberBandDrag.js';
|
|
11
9
|
import { useAssetGridKeyboardShortcuts } from '../hooks/useAssetGridKeyboardShortcuts.js';
|
|
10
|
+
import { useAssetGridFilters } from './useAssetGridFilters.js';
|
|
12
11
|
import { AssetCard } from './AssetCard.js';
|
|
13
12
|
import { AssetGridBody } from './AssetGridBody.js';
|
|
14
13
|
import { AssetGridToolbar } from './AssetGridToolbar.js';
|
|
15
14
|
import { SelectionActionBar } from './SelectionActionBar.js';
|
|
16
15
|
import { SelectionBand } from './SelectionBand.js';
|
|
17
16
|
import { DeleteConfirmDialog } from './DeleteConfirmDialog.js';
|
|
17
|
+
function renderGridContent({ loading, emptyState, t, ...bodyProps }, _options = {}) {
|
|
18
|
+
if (loading && bodyProps.assets.length === 0)
|
|
19
|
+
return _jsx("p", { className: "asset-grid-empty", children: t('Loading…') });
|
|
20
|
+
if (bodyProps.assets.length === 0)
|
|
21
|
+
return emptyState ?? _jsx("div", { className: "asset-grid-empty", children: t('No assets yet.') });
|
|
22
|
+
return _jsx(AssetGridBody, { ...bodyProps });
|
|
23
|
+
}
|
|
18
24
|
/**
|
|
19
25
|
* A live-updating, filterable, multi-selectable asset grid — the generic
|
|
20
26
|
* pattern behind a photo library / DAM. See `packages/ui/source-map.md` for
|
|
21
27
|
* the full retained-behavior manifest against the original `LibrarySection.tsx`.
|
|
22
28
|
*/
|
|
23
|
-
export function AssetGrid({ active = true, selectors, dependencies, kindFacets
|
|
29
|
+
export function AssetGrid({ active = true, selectors, dependencies, kindFacets: configuredKindFacets, sourceFacets: configuredSourceFacets, renderThumbnail, renderThumbnailPlaceholder, renderCardExtra, renderBulkActions, onDeleteAsset, onDeleteSelected, onPreview, isPreviewOpen = false, toolbarActions, emptyState, searchPlaceholder, searchDebounceMs, liveUpdateCoalesceMs, initialViewMode = 'grid', useWiredAssetGridData: useWiredAssetGridDataHook = useWiredAssetGridData, useAssetGridSelection: useAssetGridSelectionHook = useAssetGridSelection, }) {
|
|
24
30
|
const t = useT();
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
const { kind, setKind, source, setSource, search, setSearch, debouncedSearch, filtersActive, kindFacets, sourceFacets, kindFacetLabels, sourceFacetLabels, } = useAssetGridFilters({
|
|
32
|
+
kindFacets: configuredKindFacets,
|
|
33
|
+
sourceFacets: configuredSourceFacets,
|
|
34
|
+
searchDebounceMs,
|
|
35
|
+
});
|
|
28
36
|
const [viewMode, setViewMode] = useState(initialViewMode);
|
|
29
37
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
|
30
38
|
/** Message from a delete the host rejected. Rendered inline — see `runDelete`. */
|
|
31
39
|
const [deleteError, setDeleteError] = useState(null);
|
|
32
|
-
const debouncedSearch = useDebouncedValue({ value: search, delayMs: searchDebounceMs });
|
|
33
|
-
const filtersActive = !!(kind || source || debouncedSearch.trim());
|
|
34
40
|
// `dependencies` (host-supplied, or omitted to fall back to the package's
|
|
35
41
|
// in-memory fake) is resolved by each wirer independently — neither this
|
|
36
42
|
// component nor the real `useAssetGridData`/`useAssetGridLiveUpdates`
|
|
@@ -102,11 +108,16 @@ export function AssetGrid({ active = true, selectors, dependencies, kindFacets =
|
|
|
102
108
|
setSelectedIds(new Set());
|
|
103
109
|
});
|
|
104
110
|
}, [selectedIds, onDeleteSelected, setAssets, setSelectedIds, runDelete]);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
// Built only when the host supplied `onDeleteAsset`: the card shows its per-asset delete control
|
|
112
|
+
// only when it receives a handler, so there is no "no handler" path to route through `runDelete`
|
|
113
|
+
// here (the old inline `onDeleteAsset ? … : null` could never see `null` from a rendered card).
|
|
114
|
+
const handleDeleteAsset = useMemo(() => onDeleteAsset
|
|
115
|
+
? async (id) => {
|
|
116
|
+
await runDelete((batch) => onDeleteAsset(batch[0]), [id], () => {
|
|
117
|
+
setAssets((prev) => prev.filter((a) => a.id !== id));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
: undefined, [onDeleteAsset, setAssets, runDelete]);
|
|
110
121
|
const handlePreview = useCallback((id) => {
|
|
111
122
|
const asset = assets.find((a) => a.id === id);
|
|
112
123
|
if (asset)
|
|
@@ -122,13 +133,11 @@ export function AssetGrid({ active = true, selectors, dependencies, kindFacets =
|
|
|
122
133
|
onClearSelection: clearSelection,
|
|
123
134
|
onRequestDeleteSelected: requestDeleteSelected,
|
|
124
135
|
});
|
|
125
|
-
const kindFacetLabels = useMemo(() => buildFacetLabelMap(kindFacets), [kindFacets]);
|
|
126
|
-
const sourceFacetLabels = useMemo(() => buildFacetLabelMap(sourceFacets), [sourceFacets]);
|
|
127
136
|
const getDayKey = useMemo(() => selectors.getDayKey ?? ((asset) => dayKeyFromTimestamp(selectors.getTimestamp(asset))), [selectors]);
|
|
128
137
|
const renderCard = useCallback((asset, index) => {
|
|
129
138
|
const kindValue = selectors.getKind(asset);
|
|
130
139
|
const sourceValue = selectors.getSource?.(asset);
|
|
131
|
-
return (_jsx(AssetCard, { asset: asset, index: index, selected: selectedIds.has(asset.id), title: selectors.getTitle(asset), subtitle: selectors.getSubtitle?.(asset), kindLabel: resolveFacetLabel(kindValue, kindFacetLabels), sourceLabel: sourceValue ? resolveFacetLabel(sourceValue, sourceFacetLabels) : undefined, renderThumbnail: renderThumbnail, renderThumbnailPlaceholder: renderThumbnailPlaceholder, onToggle: toggleOne, onRange: rangeTo, onPreview: handlePreview, onDeleteAsset:
|
|
140
|
+
return (_jsx(AssetCard, { asset: asset, index: index, selected: selectedIds.has(asset.id), title: selectors.getTitle(asset), subtitle: selectors.getSubtitle?.(asset), kindLabel: resolveFacetLabel(kindValue, kindFacetLabels), sourceLabel: sourceValue ? resolveFacetLabel(sourceValue, sourceFacetLabels) : undefined, renderThumbnail: renderThumbnail, renderThumbnailPlaceholder: renderThumbnailPlaceholder, onToggle: toggleOne, onRange: rangeTo, onPreview: handlePreview, onDeleteAsset: handleDeleteAsset, renderCardExtra: renderCardExtra }, asset.id));
|
|
132
141
|
}, [
|
|
133
142
|
selectedIds,
|
|
134
143
|
selectors,
|
|
@@ -143,6 +152,17 @@ export function AssetGrid({ active = true, selectors, dependencies, kindFacets =
|
|
|
143
152
|
handleDeleteAsset,
|
|
144
153
|
renderCardExtra,
|
|
145
154
|
]);
|
|
146
|
-
return (_jsxs("div", { className: "asset-grid-root", children: [_jsx(AssetGridToolbar, { search: search, onSearchChange: setSearch, searchPlaceholder: searchPlaceholder, kind: kind, onKindChange: setKind, kindFacets: kindFacets, source: source, onSourceChange: setSource, sourceFacets: sourceFacets, viewMode: viewMode, onViewModeChange: setViewMode, onRefresh: () => void reload(), loading: loading, toolbarActions: toolbarActions }), selectedIds.size > 0 && !dragging ? (_jsx(SelectionActionBar, { selectedIds: [...selectedIds], onSelectAll: selectAll, onClear: clearSelection, onRequestDelete: requestDeleteSelected, renderBulkActions: renderBulkActions })) : null,
|
|
155
|
+
return (_jsxs("div", { className: "asset-grid-root", children: [_jsx(AssetGridToolbar, { search: search, onSearchChange: setSearch, searchPlaceholder: searchPlaceholder, kind: kind, onKindChange: setKind, kindFacets: kindFacets, source: source, onSourceChange: setSource, sourceFacets: sourceFacets, viewMode: viewMode, onViewModeChange: setViewMode, onRefresh: () => void reload(), loading: loading, toolbarActions: toolbarActions }), selectedIds.size > 0 && !dragging ? (_jsx(SelectionActionBar, { selectedIds: [...selectedIds], onSelectAll: selectAll, onClear: clearSelection, onRequestDelete: requestDeleteSelected, renderBulkActions: renderBulkActions })) : null, renderGridContent({
|
|
156
|
+
loading,
|
|
157
|
+
emptyState,
|
|
158
|
+
t,
|
|
159
|
+
viewMode,
|
|
160
|
+
assets,
|
|
161
|
+
getDayKey,
|
|
162
|
+
containerRef,
|
|
163
|
+
onMouseDown,
|
|
164
|
+
selecting: selectedIds.size > 0,
|
|
165
|
+
renderCard,
|
|
166
|
+
}), _jsx(SelectionBand, { band: band }), deleteError ? (_jsx("p", { className: "jini-hint jini-hint-error", role: "alert", children: deleteError })) : null, confirmDeleteOpen ? (_jsx(DeleteConfirmDialog, { count: selectedIds.size, onCancel: () => setConfirmDeleteOpen(false), onConfirm: () => void confirmDelete() })) : null] }));
|
|
147
167
|
}
|
|
148
168
|
//# sourceMappingURL=AssetGrid.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AssetGridFacetOption } from '../../types.js';
|
|
2
|
+
/** Owns the editable filters and the debounced membership used by data/live updates. */
|
|
3
|
+
export declare function useAssetGridFilters({ kindFacets, sourceFacets, searchDebounceMs, }: {
|
|
4
|
+
kindFacets: AssetGridFacetOption[] | undefined;
|
|
5
|
+
sourceFacets: AssetGridFacetOption[] | undefined;
|
|
6
|
+
searchDebounceMs: number | undefined;
|
|
7
|
+
}, _options?: Record<string, never>): {
|
|
8
|
+
kind: string;
|
|
9
|
+
setKind: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
10
|
+
source: string;
|
|
11
|
+
setSource: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
12
|
+
search: string;
|
|
13
|
+
setSearch: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
14
|
+
debouncedSearch: string;
|
|
15
|
+
filtersActive: boolean;
|
|
16
|
+
kindFacets: AssetGridFacetOption[];
|
|
17
|
+
sourceFacets: AssetGridFacetOption[];
|
|
18
|
+
kindFacetLabels: Map<string, string>;
|
|
19
|
+
sourceFacetLabels: Map<string, string>;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=useAssetGridFilters.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { useDebouncedValue } from '../../../../react/hooks/useDebouncedValue.js';
|
|
3
|
+
import { DEFAULT_SEARCH_DEBOUNCE_MS } from '../../constants.js';
|
|
4
|
+
import { buildFacetLabelMap } from '../../rules.js';
|
|
5
|
+
/** Owns the editable filters and the debounced membership used by data/live updates. */
|
|
6
|
+
export function useAssetGridFilters({ kindFacets = [], sourceFacets = [], searchDebounceMs = DEFAULT_SEARCH_DEBOUNCE_MS, }, _options = {}) {
|
|
7
|
+
const [kind, setKind] = useState('');
|
|
8
|
+
const [source, setSource] = useState('');
|
|
9
|
+
const [search, setSearch] = useState('');
|
|
10
|
+
const debouncedSearch = useDebouncedValue({ value: search, delayMs: searchDebounceMs });
|
|
11
|
+
const filtersActive = !!(kind || source || debouncedSearch.trim());
|
|
12
|
+
const kindFacetLabels = useMemo(() => buildFacetLabelMap(kindFacets), [kindFacets]);
|
|
13
|
+
const sourceFacetLabels = useMemo(() => buildFacetLabelMap(sourceFacets), [sourceFacets]);
|
|
14
|
+
return {
|
|
15
|
+
kind, setKind, source, setSource, search, setSearch,
|
|
16
|
+
debouncedSearch, filtersActive, kindFacets, sourceFacets, kindFacetLabels, sourceFacetLabels,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=useAssetGridFilters.js.map
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { useGlobalKeydown } from '../../../../browser/index.js';
|
|
2
2
|
import { isTypingTarget } from '../../rules.js';
|
|
3
|
+
function handleSelectionKey({ event, typing, hasSelection, isPreviewOpen, onClearSelection, onRequestDeleteSelected }, _options = {}) {
|
|
4
|
+
if (event.key === 'Escape') {
|
|
5
|
+
if (isPreviewOpen)
|
|
6
|
+
return; // the preview surface owns Escape while it's open
|
|
7
|
+
if (hasSelection)
|
|
8
|
+
onClearSelection();
|
|
9
|
+
}
|
|
10
|
+
else if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
11
|
+
if (typing || isPreviewOpen || !hasSelection)
|
|
12
|
+
return;
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
onRequestDeleteSelected();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
/** Cmd/Ctrl+A select-all, Escape clear-selection, Delete/Backspace triggers the bulk-delete confirm flow. */
|
|
4
18
|
export function useAssetGridKeyboardShortcuts(params) {
|
|
5
19
|
const { active, enabled, hasAssets, hasSelection, isPreviewOpen, onSelectAll, onClearSelection, onRequestDeleteSelected, } = params;
|
|
@@ -14,17 +28,8 @@ export function useAssetGridKeyboardShortcuts(params) {
|
|
|
14
28
|
e.preventDefault();
|
|
15
29
|
onSelectAll();
|
|
16
30
|
}
|
|
17
|
-
else
|
|
18
|
-
|
|
19
|
-
return; // the preview surface owns Escape while it's open
|
|
20
|
-
if (hasSelection)
|
|
21
|
-
onClearSelection();
|
|
22
|
-
}
|
|
23
|
-
else if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
24
|
-
if (typing || isPreviewOpen || !hasSelection)
|
|
25
|
-
return;
|
|
26
|
-
e.preventDefault();
|
|
27
|
-
onRequestDeleteSelected();
|
|
31
|
+
else {
|
|
32
|
+
handleSelectionKey({ event: e, typing, hasSelection, isPreviewOpen, onClearSelection, onRequestDeleteSelected });
|
|
28
33
|
}
|
|
29
34
|
},
|
|
30
35
|
}, { enabled: active });
|
|
@@ -19,6 +19,8 @@ export interface UseAssetGridLiveUpdatesParams<TAsset extends AssetGridItem> {
|
|
|
19
19
|
* id all fall back to a single full reload for that window — see
|
|
20
20
|
* `packages/ui/archived provenance ledger` for why (the original OD comment: "a filtered
|
|
21
21
|
* view can't predict membership client-side").
|
|
22
|
+
* Disposed subscriptions discard in-flight incremental reads. Failed passes report a
|
|
23
|
+
* fixed diagnostic and request a full reconciliation on the next live event.
|
|
22
24
|
*/
|
|
23
25
|
export declare function useAssetGridLiveUpdates<TAsset extends AssetGridItem>(params: UseAssetGridLiveUpdatesParams<TAsset>): void;
|
|
24
26
|
export type UseWiredAssetGridLiveUpdatesParams<TAsset extends AssetGridItem> = Omit<UseAssetGridLiveUpdatesParams<TAsset>, 'liveUpdates' | 'fetchAssetById'> & {
|
|
@@ -2,6 +2,18 @@ import { useEffect, useMemo, useRef } from 'react';
|
|
|
2
2
|
import { DEFAULT_LIVE_UPDATE_COALESCE_MS } from '../../constants.js';
|
|
3
3
|
import { mergeIngestedAssets } from '../../rules.js';
|
|
4
4
|
import { createFakeAssetGridDependencies } from '../../dependencies.js';
|
|
5
|
+
/** Drains pending deletes and cancels matching ingests before the pass can fetch.
|
|
6
|
+
* Mutates the pending sets and schedules the asset update. @complexity O(d + a) deletes and assets. */
|
|
7
|
+
function applyPendingDeletes({ pendingDelete, pendingIngest, setAssets }, _options = {}) {
|
|
8
|
+
// Deletes are free (no fetch); apply them first.
|
|
9
|
+
if (pendingDelete.size) {
|
|
10
|
+
const del = new Set(pendingDelete);
|
|
11
|
+
pendingDelete.clear();
|
|
12
|
+
for (const id of del)
|
|
13
|
+
pendingIngest.delete(id);
|
|
14
|
+
setAssets((prev) => prev.filter((a) => !del.has(a.id)));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
5
17
|
/**
|
|
6
18
|
* Subscribes to a live-update transport and reconciles incremental
|
|
7
19
|
* ingest/delete events into the current asset list. Events are coalesced
|
|
@@ -11,6 +23,8 @@ import { createFakeAssetGridDependencies } from '../../dependencies.js';
|
|
|
11
23
|
* id all fall back to a single full reload for that window — see
|
|
12
24
|
* `packages/ui/archived provenance ledger` for why (the original OD comment: "a filtered
|
|
13
25
|
* view can't predict membership client-side").
|
|
26
|
+
* Disposed subscriptions discard in-flight incremental reads. Failed passes report a
|
|
27
|
+
* fixed diagnostic and request a full reconciliation on the next live event.
|
|
14
28
|
*/
|
|
15
29
|
export function useAssetGridLiveUpdates(params) {
|
|
16
30
|
const { active, liveUpdates, filtersActive, fetchAssetById, setAssets, reload, coalesceMs = DEFAULT_LIVE_UPDATE_COALESCE_MS } = params;
|
|
@@ -33,19 +47,17 @@ export function useAssetGridLiveUpdates(params) {
|
|
|
33
47
|
useEffect(() => {
|
|
34
48
|
if (!active || !liveUpdates)
|
|
35
49
|
return;
|
|
50
|
+
let disposed = false;
|
|
36
51
|
let timer = null;
|
|
37
52
|
const pendingIngest = new Set();
|
|
38
53
|
const pendingDelete = new Set();
|
|
39
54
|
let pendingFull = false;
|
|
55
|
+
// No `disposed` check on entry: a pass only ever starts from `flush`, whose first pass runs
|
|
56
|
+
// synchronously from a timer that cleanup cancels, and whose later passes are gated by the loop's
|
|
57
|
+
// own `!disposed` condition — so a pass can never begin after disposal. The post-fetch check
|
|
58
|
+
// below still matters, because disposal can land while a pass is awaiting.
|
|
40
59
|
const runFlushPass = async () => {
|
|
41
|
-
|
|
42
|
-
if (pendingDelete.size) {
|
|
43
|
-
const del = new Set(pendingDelete);
|
|
44
|
-
pendingDelete.clear();
|
|
45
|
-
for (const id of del)
|
|
46
|
-
pendingIngest.delete(id);
|
|
47
|
-
setAssets((prev) => prev.filter((a) => !del.has(a.id)));
|
|
48
|
-
}
|
|
60
|
+
applyPendingDeletes({ pendingDelete, pendingIngest, setAssets });
|
|
49
61
|
if (pendingFull || filtersActiveRef.current) {
|
|
50
62
|
pendingFull = false;
|
|
51
63
|
pendingIngest.clear();
|
|
@@ -61,6 +73,9 @@ export function useAssetGridLiveUpdates(params) {
|
|
|
61
73
|
return;
|
|
62
74
|
}
|
|
63
75
|
const fetched = await Promise.all(ids.map((id) => fetchById(id)));
|
|
76
|
+
// The same state setter can outlive this subscription (for example after deactivation).
|
|
77
|
+
if (disposed)
|
|
78
|
+
return;
|
|
64
79
|
// A missing fetch is ambiguous (filtered out? race?) — reload instead.
|
|
65
80
|
if (fetched.some((a) => a === null)) {
|
|
66
81
|
await reloadRef.current();
|
|
@@ -105,15 +120,23 @@ export function useAssetGridLiveUpdates(params) {
|
|
|
105
120
|
try {
|
|
106
121
|
do {
|
|
107
122
|
rerun = false;
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
try {
|
|
124
|
+
await runFlushPass();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
if (!disposed) {
|
|
128
|
+
pendingFull = true;
|
|
129
|
+
console.error('[asset-grid] Live update failed.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} while (rerun && !disposed);
|
|
110
133
|
}
|
|
111
134
|
finally {
|
|
112
135
|
flushing = false;
|
|
113
136
|
}
|
|
114
137
|
};
|
|
115
138
|
const schedule = () => {
|
|
116
|
-
if (timer)
|
|
139
|
+
if (disposed || timer)
|
|
117
140
|
return;
|
|
118
141
|
timer = setTimeout(() => void flush(), coalesceMs);
|
|
119
142
|
};
|
|
@@ -132,6 +155,7 @@ export function useAssetGridLiveUpdates(params) {
|
|
|
132
155
|
},
|
|
133
156
|
});
|
|
134
157
|
return () => {
|
|
158
|
+
disposed = true;
|
|
135
159
|
if (timer)
|
|
136
160
|
clearTimeout(timer);
|
|
137
161
|
unsubscribe();
|