feat(blocks): add reusable application shell blocks
- add appearance state, theme presets, preference controls, and preview components - add responsive primary, nested, mobile, and flyout navigation with route-aware breadcrumbs - add application layout, sidebar state, header actions, user menu, query refresh, and scroll utilities - add configurable chat and notification surfaces backed by consumer-provided data and queries - expose package entry points and cover theme, state, and command behavior with tests
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import * as React from "react"
|
||||
import { getOverflowAncestors, isOverflowElement } from "@floating-ui/utils/dom"
|
||||
import { addEventListener } from "@base-ui/utils/addEventListener"
|
||||
import { mergeCleanups } from "@base-ui/utils/mergeCleanups"
|
||||
import { ownerWindow } from "@base-ui/utils/owner"
|
||||
|
||||
type ScrollContainer = HTMLElement | React.RefObject<HTMLElement | null>
|
||||
type ScrollTarget = Element | VisualViewport | Window
|
||||
|
||||
function getScrollTargets(element: HTMLElement): ScrollTarget[] {
|
||||
const targets: ScrollTarget[] = []
|
||||
|
||||
if (isOverflowElement(element)) {
|
||||
targets.push(element)
|
||||
}
|
||||
|
||||
targets.push(...getOverflowAncestors(element, [], false))
|
||||
|
||||
if (targets.length === 0) {
|
||||
targets.push(ownerWindow(element))
|
||||
}
|
||||
|
||||
return Array.from(new Set(targets))
|
||||
}
|
||||
|
||||
export function useOnScroll(listener: VoidFunction): void
|
||||
export function useOnScroll(
|
||||
container: ScrollContainer,
|
||||
listener: VoidFunction
|
||||
): void
|
||||
export function useOnScroll(
|
||||
containerOrListener: ScrollContainer | VoidFunction,
|
||||
listener?: VoidFunction
|
||||
) {
|
||||
const container =
|
||||
typeof containerOrListener === "function" ? null : containerOrListener
|
||||
const scrollListener =
|
||||
typeof containerOrListener === "function" ? containerOrListener : listener
|
||||
|
||||
if (!scrollListener) {
|
||||
throw new Error(
|
||||
"useOnScroll requires a listener when a scroll container is provided."
|
||||
)
|
||||
}
|
||||
|
||||
const onScroll = React.useEffectEvent(scrollListener)
|
||||
|
||||
React.useEffect(() => {
|
||||
const targets =
|
||||
container === null
|
||||
? [window]
|
||||
: "current" in container
|
||||
? container.current && getScrollTargets(container.current)
|
||||
: getScrollTargets(container)
|
||||
|
||||
if (!targets) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
onScroll()
|
||||
}
|
||||
|
||||
return mergeCleanups(
|
||||
...targets.map((target) =>
|
||||
addEventListener(target, "scroll", handleScroll, { passive: true })
|
||||
)
|
||||
)
|
||||
}, [container])
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from "react"
|
||||
import { useIsFetching, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
const ACTIVE_QUERY_FILTERS = { type: "active" } as const
|
||||
|
||||
export function useRefresh() {
|
||||
const queryClient = useQueryClient()
|
||||
const queryCache = queryClient.getQueryCache()
|
||||
const isRefreshing = useIsFetching(ACTIVE_QUERY_FILTERS) > 0
|
||||
const subscribe = React.useCallback(
|
||||
(listener: VoidFunction) => queryCache.subscribe(listener),
|
||||
[queryCache]
|
||||
)
|
||||
const getSnapshot = React.useCallback(
|
||||
() => queryCache.findAll(ACTIVE_QUERY_FILTERS).length > 0,
|
||||
[queryCache]
|
||||
)
|
||||
const canRefresh = React.useSyncExternalStore(
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
() => false
|
||||
)
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
if (!canRefresh || isRefreshing) {
|
||||
return
|
||||
}
|
||||
|
||||
void queryClient.refetchQueries(ACTIVE_QUERY_FILTERS)
|
||||
}, [canRefresh, isRefreshing, queryClient])
|
||||
|
||||
return { canRefresh, isRefreshing, refresh }
|
||||
}
|
||||
Reference in New Issue
Block a user