@lobehub/ui 5.57.3 → 5.59.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/es/Grid/style.mjs +8 -8
- package/es/Grid/style.mjs.map +1 -1
- package/es/Image/type.d.mts +1 -0
- package/es/Image/viewer/ImageViewer.mjs +1 -0
- package/es/Image/viewer/ImageViewer.mjs.map +1 -1
- package/es/Image/viewer/Toolbar.mjs +19 -6
- package/es/Image/viewer/Toolbar.mjs.map +1 -1
- package/es/Image/viewer/ViewerChrome.mjs.map +1 -1
- package/es/base-ui/Input/style.d.mts +1 -1
- package/es/base-ui/Input/style.mjs +8 -2
- package/es/base-ui/Input/style.mjs.map +1 -1
- package/es/dashboard/ChartGrid/ChartGrid.d.mts +10 -0
- package/es/dashboard/ChartGrid/ChartGrid.mjs +18 -0
- package/es/dashboard/ChartGrid/ChartGrid.mjs.map +1 -0
- package/es/dashboard/ChartGrid/index.d.mts +3 -0
- package/es/dashboard/ChartGrid/style.mjs +32 -0
- package/es/dashboard/ChartGrid/style.mjs.map +1 -0
- package/es/dashboard/ChartGrid/type.d.mts +9 -0
- package/es/dashboard/ConsoleShell/ConsoleShell.d.mts +1 -1
- package/es/dashboard/ConsoleShell/ConsoleShell.mjs +12 -4
- package/es/dashboard/ConsoleShell/ConsoleShell.mjs.map +1 -1
- package/es/dashboard/ConsoleShell/style.mjs +15 -0
- package/es/dashboard/ConsoleShell/style.mjs.map +1 -1
- package/es/dashboard/ConsoleShell/type.d.mts +2 -0
- package/es/dashboard/FilterStrip/FilterStrip.d.mts +11 -0
- package/es/dashboard/FilterStrip/FilterStrip.mjs +51 -0
- package/es/dashboard/FilterStrip/FilterStrip.mjs.map +1 -0
- package/es/dashboard/FilterStrip/index.d.mts +3 -0
- package/es/dashboard/FilterStrip/style.mjs +93 -0
- package/es/dashboard/FilterStrip/style.mjs.map +1 -0
- package/es/dashboard/FilterStrip/type.d.mts +26 -0
- package/es/dashboard/LabelWithKey/LabelWithKey.d.mts +10 -0
- package/es/dashboard/LabelWithKey/LabelWithKey.mjs +18 -0
- package/es/dashboard/LabelWithKey/LabelWithKey.mjs.map +1 -0
- package/es/dashboard/LabelWithKey/index.d.mts +3 -0
- package/es/dashboard/LabelWithKey/type.d.mts +12 -0
- package/es/dashboard/MetricStrip/MetricStrip.d.mts +10 -0
- package/es/dashboard/MetricStrip/MetricStrip.mjs +76 -0
- package/es/dashboard/MetricStrip/MetricStrip.mjs.map +1 -0
- package/es/dashboard/MetricStrip/index.d.mts +3 -0
- package/es/dashboard/MetricStrip/style.mjs +116 -0
- package/es/dashboard/MetricStrip/style.mjs.map +1 -0
- package/es/dashboard/MetricStrip/type.d.mts +25 -0
- package/es/dashboard/PageToolbar/PageToolbar.d.mts +10 -0
- package/es/dashboard/PageToolbar/PageToolbar.mjs +21 -0
- package/es/dashboard/PageToolbar/PageToolbar.mjs.map +1 -0
- package/es/dashboard/PageToolbar/index.d.mts +3 -0
- package/es/dashboard/PageToolbar/style.mjs +34 -0
- package/es/dashboard/PageToolbar/style.mjs.map +1 -0
- package/es/dashboard/PageToolbar/type.d.mts +12 -0
- package/es/dashboard/PanelSurface/PanelSurface.d.mts +10 -0
- package/es/dashboard/PanelSurface/PanelSurface.mjs +36 -0
- package/es/dashboard/PanelSurface/PanelSurface.mjs.map +1 -0
- package/es/dashboard/PanelSurface/index.d.mts +3 -0
- package/es/dashboard/PanelSurface/style.mjs +24 -0
- package/es/dashboard/PanelSurface/style.mjs.map +1 -0
- package/es/dashboard/PanelSurface/type.d.mts +18 -0
- package/es/dashboard/Section/Section.d.mts +10 -0
- package/es/dashboard/Section/Section.mjs +81 -0
- package/es/dashboard/Section/Section.mjs.map +1 -0
- package/es/dashboard/Section/index.d.mts +3 -0
- package/es/dashboard/Section/style.mjs +35 -0
- package/es/dashboard/Section/style.mjs.map +1 -0
- package/es/dashboard/Section/type.d.mts +24 -0
- package/es/dashboard/Surface/Surface.d.mts +4 -3
- package/es/dashboard/Surface/Surface.mjs +67 -7
- package/es/dashboard/Surface/Surface.mjs.map +1 -1
- package/es/dashboard/Surface/index.d.mts +3 -2
- package/es/dashboard/Surface/link.d.mts +14 -0
- package/es/dashboard/Surface/link.mjs +22 -0
- package/es/dashboard/Surface/link.mjs.map +1 -0
- package/es/dashboard/Surface/style.d.mts +23 -0
- package/es/dashboard/Surface/style.mjs +283 -2
- package/es/dashboard/Surface/style.mjs.map +1 -1
- package/es/dashboard/Surface/type.d.mts +30 -2
- package/es/dashboard/TableIconActions/TableIconActions.d.mts +17 -0
- package/es/dashboard/TableIconActions/TableIconActions.mjs +39 -0
- package/es/dashboard/TableIconActions/TableIconActions.mjs.map +1 -0
- package/es/dashboard/TableIconActions/index.d.mts +3 -0
- package/es/dashboard/TableIconActions/type.d.mts +18 -0
- package/es/dashboard/TitleWithTips/TitleWithTips.d.mts +10 -0
- package/es/dashboard/TitleWithTips/TitleWithTips.mjs +28 -0
- package/es/dashboard/TitleWithTips/TitleWithTips.mjs.map +1 -0
- package/es/dashboard/TitleWithTips/index.d.mts +3 -0
- package/es/dashboard/TitleWithTips/type.d.mts +11 -0
- package/es/dashboard/index.d.mts +30 -2
- package/es/dashboard/index.mjs +12 -2
- package/package.json +1 -1
package/es/Grid/style.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { createStaticStyles } from "antd-style";
|
|
2
2
|
//#region src/Grid/style.ts
|
|
3
|
+
/**
|
|
4
|
+
* `--max-item-width` is the column's minimum width. Clamp it to the container,
|
|
5
|
+
* otherwise a value wider than a phone screen overflows the page instead of
|
|
6
|
+
* collapsing to one column.
|
|
7
|
+
*/
|
|
8
|
+
const gridTemplateColumns = "repeat(auto-fill, minmax(max(min(var(--max-item-width), 100%), calc((100% - var(--gap) * (var(--rows) - 1)) / var(--rows))), 1fr))";
|
|
3
9
|
const styles = createStaticStyles(({ css }) => {
|
|
4
10
|
return css`
|
|
5
11
|
--rows: var(--grid-rows, 3);
|
|
@@ -7,16 +13,10 @@ const styles = createStaticStyles(({ css }) => {
|
|
|
7
13
|
--gap: var(--grid-gap, 1em);
|
|
8
14
|
|
|
9
15
|
display: grid !important;
|
|
10
|
-
grid-template-columns:
|
|
11
|
-
auto-fill,
|
|
12
|
-
minmax(
|
|
13
|
-
max(var(--max-item-width), calc((100% - var(--gap) * (var(--rows) - 1)) / var(--rows))),
|
|
14
|
-
1fr
|
|
15
|
-
)
|
|
16
|
-
);
|
|
16
|
+
grid-template-columns: ${gridTemplateColumns};
|
|
17
17
|
`;
|
|
18
18
|
});
|
|
19
19
|
//#endregion
|
|
20
|
-
export { styles };
|
|
20
|
+
export { gridTemplateColumns, styles };
|
|
21
21
|
|
|
22
22
|
//# sourceMappingURL=style.mjs.map
|
package/es/Grid/style.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.mjs","names":[],"sources":["../../src/Grid/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\n\nexport const styles = createStaticStyles(({ css }) => {\n return css`\n --rows: var(--grid-rows, 3);\n --max-item-width: var(--grid-max-item-width, 240px);\n --gap: var(--grid-gap, 1em);\n\n display: grid !important;\n grid-template-columns:
|
|
1
|
+
{"version":3,"file":"style.mjs","names":[],"sources":["../../src/Grid/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\n\n/**\n * `--max-item-width` is the column's minimum width. Clamp it to the container,\n * otherwise a value wider than a phone screen overflows the page instead of\n * collapsing to one column.\n */\nexport const gridTemplateColumns =\n 'repeat(auto-fill, minmax(max(min(var(--max-item-width), 100%), calc((100% - var(--gap) * (var(--rows) - 1)) / var(--rows))), 1fr))';\n\nexport const styles = createStaticStyles(({ css }) => {\n return css`\n --rows: var(--grid-rows, 3);\n --max-item-width: var(--grid-max-item-width, 240px);\n --gap: var(--grid-gap, 1em);\n\n display: grid !important;\n grid-template-columns: ${gridTemplateColumns};\n `;\n});\n"],"mappings":";;;;;;;AAOA,MAAa,sBACX;AAEF,MAAa,SAAS,oBAAoB,EAAE,UAAU;CACpD,OAAO,GAAG;;;;;;6BAMiB,oBAAoB;;AAEjD,CAAC"}
|
package/es/Image/type.d.mts
CHANGED
|
@@ -4,6 +4,7 @@ import { CSSProperties, ComponentProps, ReactNode, Ref } from "react";
|
|
|
4
4
|
interface ImagePreviewOptions {
|
|
5
5
|
autoZoomThreshold?: number;
|
|
6
6
|
defaultZoom?: ZoomPolicy;
|
|
7
|
+
fetchImage?: (source: string) => Promise<Blob>;
|
|
7
8
|
maxScale?: number;
|
|
8
9
|
onDownload?: (source: string) => void | Promise<void>;
|
|
9
10
|
onOpenChange?: (open: boolean) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageViewer.mjs","names":[],"sources":["../../../src/Image/viewer/ImageViewer.tsx"],"sourcesContent":["'use client';\n\nimport { Dialog, type DialogRootProps } from '@base-ui/react/dialog';\nimport {\n memo,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useMergeRefs } from 'react-merge-refs';\n\nimport { ToastHost } from '@/base-ui/Toast';\nimport { useLayerZIndex } from '@/base-ui/zIndex';\nimport { useAppElement } from '@/ThemeProvider/AppElementContext';\n\nimport { styles } from '../style';\nimport { computeFit, type Size, unrotatedRect } from './geometry';\nimport { beginClosePreview, endClosePreview, type PreviewEntry } from './registry';\nimport { useFinalFocus } from './useFinalFocus';\nimport { useFlipTransition } from './useFlipTransition';\nimport { readNatural, useGalleryNav } from './useGalleryNav';\nimport { useRefitTransition } from './useRefitTransition';\nimport { useViewerGestures } from './useViewerGestures';\nimport { useZoomPan } from './useZoomPan';\nimport ViewerChrome from './ViewerChrome';\n\nexport interface ImageViewerProps {\n entries: PreviewEntry[];\n index: number;\n openerFocusElement: HTMLElement | null;\n token: number;\n}\n\nconst readViewport = (): Size => ({ height: window.innerHeight, width: window.innerWidth });\n\nconst prefersReducedMotion = () =>\n window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n\nconst ImageViewer = memo<ImageViewerProps>(({ entries, index, openerFocusElement, token }) => {\n const openerEntry = entries[index];\n const appElement = useAppElement();\n const [animated] = useState(() => !prefersReducedMotion());\n\n const { ref: layerRef, zIndex } = useLayerZIndex<HTMLDivElement>('modal');\n\n const [currentIndex, setCurrentIndex] = useState(index);\n const currentEntry = entries[currentIndex];\n const currentEntryRef = useRef(currentEntry);\n currentEntryRef.current = currentEntry;\n\n const [source, setSource] = useState(openerEntry.src);\n const [natural, setNatural] = useState<Size>(() => readNatural(openerEntry.element));\n const [viewport, setViewport] = useState<Size>(readViewport);\n\n const closeRef = useRef<(() => void) | null>(null);\n const requestClose = useCallback(() => closeRef.current?.(), []);\n\n // useZoomPan needs to gate its wheel/toolbar entry points on close-in-flight,\n // but that state only exists once useFlipTransition (below) has run — and it\n // needs useZoomPan's own transform values first. Break the cycle with a\n // stable indirection: the ref is repointed at the real isClosing once it's\n // available, written directly during render like the other *Ref fields here.\n const isClosingIndirectRef = useRef<() => boolean>(() => false);\n const isClosingIndirect = useCallback(() => isClosingIndirectRef.current(), []);\n\n const isTransitioningIndirectRef = useRef<() => boolean>(() => true);\n const isTransitioningIndirect = useCallback(() => isTransitioningIndirectRef.current(), []);\n\n const fillViewport = Boolean(\n currentEntry.previewSrc && currentEntry.previewSrc !== currentEntry.src,\n );\n\n const {\n canZoomIn,\n canZoomOut,\n dragBy,\n dragEnd,\n escIntent,\n flipHorizontal,\n flipVertical,\n flipX,\n flipY,\n getInitialScale,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n reset,\n resetCloseTracking,\n rotate,\n rotateLeft,\n rotateRight,\n rotation,\n scale,\n setNatural: syncZoomNatural,\n setViewport: syncZoomViewport,\n toggleActualSize,\n x,\n y,\n zoomIn,\n zoomOut,\n } = useZoomPan({\n autoZoomThreshold: currentEntry.options.autoZoomThreshold,\n defaultZoom: currentEntry.options.defaultZoom,\n fillViewport,\n isClosing: isClosingIndirect,\n isTransitioning: isTransitioningIndirect,\n maxScale: currentEntry.options.maxScale,\n natural,\n onCloseRequest: requestClose,\n viewport,\n });\n\n const fitRect = useMemo(\n () => computeFit(natural, viewport, rotation, fillViewport),\n [natural, viewport, rotation, fillViewport],\n );\n const fitRectRef = useRef(fitRect);\n fitRectRef.current = fitRect;\n const getFitRect = useCallback(() => fitRectRef.current, []);\n\n const imageRect = useMemo(() => unrotatedRect(fitRect, rotation), [fitRect, rotation]);\n\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n const getViewportWidth = useCallback(() => viewportRef.current.width, []);\n\n const getCloseSource = useCallback(() => currentEntryRef.current.element, []);\n\n const transform = useMemo(\n () => ({ flipX, flipY, rotate, scale, x, y }),\n [flipX, flipY, rotate, scale, x, y],\n );\n const handleClosed = useCallback(() => endClosePreview(token), [token]);\n\n const {\n backdropRef,\n chromeRef,\n close,\n imageRef: flipImageRef,\n isClosing,\n isTransitioning,\n switchTo,\n } = useFlipTransition({\n animated,\n getCloseSource,\n getFitRect,\n getInitialScale,\n getViewportWidth,\n onClosed: handleClosed,\n source: openerEntry.element,\n transform,\n });\n isClosingIndirectRef.current = isClosing;\n isTransitioningIndirectRef.current = isTransitioning;\n\n useRefitTransition({\n animated,\n fillViewport,\n isTransitioning,\n natural,\n rotation,\n scale,\n viewport,\n x,\n y,\n });\n const finalFocus = useFinalFocus(openerFocusElement);\n\n const isCleanRef = useRef(isClean);\n isCleanRef.current = isClean;\n\n const handleClose = useCallback(() => {\n beginClosePreview(token);\n close({ fade: !isCleanRef.current });\n }, [close, token]);\n closeRef.current = handleClose;\n\n const handleDialogOpenChange = useCallback<NonNullable<DialogRootProps['onOpenChange']>>(\n (open, eventDetails) => {\n if (open) return;\n // Base UI fires this on every Esc regardless of whether we're already\n // mid-close (our Dialog.Root's open prop never actually flips to\n // false), so without this guard a second Esc during the close window\n // reads escIntent() off the close spring's mid-flight scale, decides\n // 'reset', and cancels the close animation out from under itself.\n if (isClosing()) return;\n if (eventDetails.reason === 'escape-key' && escIntent() === 'reset') {\n eventDetails.cancel();\n reset();\n return;\n }\n handleClose();\n },\n [escIntent, handleClose, isClosing, reset],\n );\n\n const gestures = useViewerGestures({\n dragBy,\n dragEnd,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n onClose: handleClose,\n reset,\n zoomIn,\n zoomOut,\n });\n\n const { hasNext, hasPrev, next, prev } = useGalleryNav({\n cancelPendingClose: gestures.cancelPendingClose,\n currentIndex,\n entries,\n flipX,\n flipY,\n getInitialScale,\n resetCloseTracking,\n rotate,\n scale,\n setCurrentIndex,\n setNatural,\n setSource,\n switchTo,\n syncZoomNatural,\n x,\n y,\n });\n\n const popupNodeRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useMergeRefs<HTMLDivElement>([layerRef, gestures.popupRef, popupNodeRef]);\n const imageRef = useMergeRefs<HTMLImageElement>([flipImageRef, gestures.imageRef]);\n\n const handleLoad = useCallback(\n (event: SyntheticEvent<HTMLImageElement>) => {\n const loaded = event.currentTarget;\n if (!loaded.naturalWidth || !loaded.naturalHeight) return;\n const next = { height: loaded.naturalHeight, width: loaded.naturalWidth };\n setNatural((prev) =>\n prev.width === next.width && prev.height === next.height ? prev : next,\n );\n syncZoomNatural(next);\n },\n [syncZoomNatural],\n );\n\n useEffect(() => {\n const handleResize = () => {\n const next = readViewport();\n setViewport(next);\n syncZoomViewport(next);\n };\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, [syncZoomViewport]);\n\n useEffect(() => {\n const { previewSrc, src } = currentEntry;\n if (!previewSrc || previewSrc === src) return;\n let cancelled = false;\n const loader = new window.Image();\n const handleLoaded = () => {\n if (!cancelled) setSource(previewSrc);\n };\n loader.addEventListener('load', handleLoaded);\n loader.src = previewSrc;\n return () => {\n cancelled = true;\n loader.removeEventListener('load', handleLoaded);\n };\n }, [currentEntry]);\n\n useEffect(\n () => () => {\n const thumbnailWasRemoved = !openerEntry.element.isConnected;\n if (thumbnailWasRemoved) endClosePreview(token);\n },\n [openerEntry, token],\n );\n\n return (\n <Dialog.Root modal open onOpenChange={handleDialogOpenChange}>\n <Dialog.Portal container={appElement ?? undefined}>\n {/* Base UI drops a nested dialog's backdrop so overlays don't stack. The viewer is\n not an overlay on the surface it opened from — it replaces the screen — so it\n keeps its own scrim, or a viewer opened from a maskless drawer would show the\n page straight through the letterbox around the image. */}\n <Dialog.Backdrop\n forceRender\n className={styles.viewerBackdrop}\n ref={backdropRef}\n style={zIndex === undefined ? undefined : { zIndex }}\n onClick={gestures.onSurfaceClick}\n />\n <Dialog.Popup\n aria-label={currentEntry.element.alt || undefined}\n className={styles.viewerPopup}\n finalFocus={finalFocus}\n // Focus the popup itself instead of Base UI's default (the first\n // tabbable — the close button): a focus-visible close button pins\n // the idle auto-hide open forever and pops its tooltip on open.\n initialFocus={popupNodeRef}\n ref={popupRef}\n style={zIndex === undefined ? undefined : { zIndex: zIndex + 1 }}\n tabIndex={-1}\n onClick={gestures.onSurfaceClick}\n onPointerCancel={gestures.onPointerFinish}\n onPointerDown={gestures.onPointerDown}\n onPointerMove={gestures.onPointerMove}\n onPointerUp={gestures.onPointerFinish}\n >\n <img\n alt={currentEntry.element.alt}\n className={styles.viewerImage}\n ref={imageRef}\n src={source}\n style={{\n cursor: gestures.cursor,\n height: imageRect.height,\n left: imageRect.x,\n top: imageRect.y,\n width: imageRect.width,\n }}\n onClick={gestures.onImageClick}\n onDoubleClick={gestures.onImageDoubleClick}\n onLoad={handleLoad}\n />\n <ViewerChrome\n canZoomIn={canZoomIn}\n canZoomOut={canZoomOut}\n chromeRef={chromeRef}\n current={currentIndex}\n fitRect={fitRect}\n flipHorizontal={flipHorizontal}\n flipVertical={flipVertical}\n hasNext={hasNext}\n hasPrev={hasPrev}\n natural={natural}\n next={next}\n prev={prev}\n rotateLeft={rotateLeft}\n rotateRight={rotateRight}\n rotation={rotation}\n scale={scale}\n source={source}\n toggleActualSize={toggleActualSize}\n toolbarAddon={currentEntry.options.toolbarAddon}\n total={entries.length}\n zoomIn={zoomIn}\n zoomOut={zoomOut}\n onClose={handleClose}\n onDownload={currentEntry.options.onDownload}\n />\n </Dialog.Popup>\n <ToastHost root={appElement ?? undefined} />\n </Dialog.Portal>\n </Dialog.Root>\n );\n});\n\nImageViewer.displayName = 'ImageViewer';\n\nexport default ImageViewer;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImageViewer.mjs","names":[],"sources":["../../../src/Image/viewer/ImageViewer.tsx"],"sourcesContent":["'use client';\n\nimport { Dialog, type DialogRootProps } from '@base-ui/react/dialog';\nimport {\n memo,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useMergeRefs } from 'react-merge-refs';\n\nimport { ToastHost } from '@/base-ui/Toast';\nimport { useLayerZIndex } from '@/base-ui/zIndex';\nimport { useAppElement } from '@/ThemeProvider/AppElementContext';\n\nimport { styles } from '../style';\nimport { computeFit, type Size, unrotatedRect } from './geometry';\nimport { beginClosePreview, endClosePreview, type PreviewEntry } from './registry';\nimport { useFinalFocus } from './useFinalFocus';\nimport { useFlipTransition } from './useFlipTransition';\nimport { readNatural, useGalleryNav } from './useGalleryNav';\nimport { useRefitTransition } from './useRefitTransition';\nimport { useViewerGestures } from './useViewerGestures';\nimport { useZoomPan } from './useZoomPan';\nimport ViewerChrome from './ViewerChrome';\n\nexport interface ImageViewerProps {\n entries: PreviewEntry[];\n index: number;\n openerFocusElement: HTMLElement | null;\n token: number;\n}\n\nconst readViewport = (): Size => ({ height: window.innerHeight, width: window.innerWidth });\n\nconst prefersReducedMotion = () =>\n window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n\nconst ImageViewer = memo<ImageViewerProps>(({ entries, index, openerFocusElement, token }) => {\n const openerEntry = entries[index];\n const appElement = useAppElement();\n const [animated] = useState(() => !prefersReducedMotion());\n\n const { ref: layerRef, zIndex } = useLayerZIndex<HTMLDivElement>('modal');\n\n const [currentIndex, setCurrentIndex] = useState(index);\n const currentEntry = entries[currentIndex];\n const currentEntryRef = useRef(currentEntry);\n currentEntryRef.current = currentEntry;\n\n const [source, setSource] = useState(openerEntry.src);\n const [natural, setNatural] = useState<Size>(() => readNatural(openerEntry.element));\n const [viewport, setViewport] = useState<Size>(readViewport);\n\n const closeRef = useRef<(() => void) | null>(null);\n const requestClose = useCallback(() => closeRef.current?.(), []);\n\n // useZoomPan needs to gate its wheel/toolbar entry points on close-in-flight,\n // but that state only exists once useFlipTransition (below) has run — and it\n // needs useZoomPan's own transform values first. Break the cycle with a\n // stable indirection: the ref is repointed at the real isClosing once it's\n // available, written directly during render like the other *Ref fields here.\n const isClosingIndirectRef = useRef<() => boolean>(() => false);\n const isClosingIndirect = useCallback(() => isClosingIndirectRef.current(), []);\n\n const isTransitioningIndirectRef = useRef<() => boolean>(() => true);\n const isTransitioningIndirect = useCallback(() => isTransitioningIndirectRef.current(), []);\n\n const fillViewport = Boolean(\n currentEntry.previewSrc && currentEntry.previewSrc !== currentEntry.src,\n );\n\n const {\n canZoomIn,\n canZoomOut,\n dragBy,\n dragEnd,\n escIntent,\n flipHorizontal,\n flipVertical,\n flipX,\n flipY,\n getInitialScale,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n reset,\n resetCloseTracking,\n rotate,\n rotateLeft,\n rotateRight,\n rotation,\n scale,\n setNatural: syncZoomNatural,\n setViewport: syncZoomViewport,\n toggleActualSize,\n x,\n y,\n zoomIn,\n zoomOut,\n } = useZoomPan({\n autoZoomThreshold: currentEntry.options.autoZoomThreshold,\n defaultZoom: currentEntry.options.defaultZoom,\n fillViewport,\n isClosing: isClosingIndirect,\n isTransitioning: isTransitioningIndirect,\n maxScale: currentEntry.options.maxScale,\n natural,\n onCloseRequest: requestClose,\n viewport,\n });\n\n const fitRect = useMemo(\n () => computeFit(natural, viewport, rotation, fillViewport),\n [natural, viewport, rotation, fillViewport],\n );\n const fitRectRef = useRef(fitRect);\n fitRectRef.current = fitRect;\n const getFitRect = useCallback(() => fitRectRef.current, []);\n\n const imageRect = useMemo(() => unrotatedRect(fitRect, rotation), [fitRect, rotation]);\n\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n const getViewportWidth = useCallback(() => viewportRef.current.width, []);\n\n const getCloseSource = useCallback(() => currentEntryRef.current.element, []);\n\n const transform = useMemo(\n () => ({ flipX, flipY, rotate, scale, x, y }),\n [flipX, flipY, rotate, scale, x, y],\n );\n const handleClosed = useCallback(() => endClosePreview(token), [token]);\n\n const {\n backdropRef,\n chromeRef,\n close,\n imageRef: flipImageRef,\n isClosing,\n isTransitioning,\n switchTo,\n } = useFlipTransition({\n animated,\n getCloseSource,\n getFitRect,\n getInitialScale,\n getViewportWidth,\n onClosed: handleClosed,\n source: openerEntry.element,\n transform,\n });\n isClosingIndirectRef.current = isClosing;\n isTransitioningIndirectRef.current = isTransitioning;\n\n useRefitTransition({\n animated,\n fillViewport,\n isTransitioning,\n natural,\n rotation,\n scale,\n viewport,\n x,\n y,\n });\n const finalFocus = useFinalFocus(openerFocusElement);\n\n const isCleanRef = useRef(isClean);\n isCleanRef.current = isClean;\n\n const handleClose = useCallback(() => {\n beginClosePreview(token);\n close({ fade: !isCleanRef.current });\n }, [close, token]);\n closeRef.current = handleClose;\n\n const handleDialogOpenChange = useCallback<NonNullable<DialogRootProps['onOpenChange']>>(\n (open, eventDetails) => {\n if (open) return;\n // Base UI fires this on every Esc regardless of whether we're already\n // mid-close (our Dialog.Root's open prop never actually flips to\n // false), so without this guard a second Esc during the close window\n // reads escIntent() off the close spring's mid-flight scale, decides\n // 'reset', and cancels the close animation out from under itself.\n if (isClosing()) return;\n if (eventDetails.reason === 'escape-key' && escIntent() === 'reset') {\n eventDetails.cancel();\n reset();\n return;\n }\n handleClose();\n },\n [escIntent, handleClose, isClosing, reset],\n );\n\n const gestures = useViewerGestures({\n dragBy,\n dragEnd,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n onClose: handleClose,\n reset,\n zoomIn,\n zoomOut,\n });\n\n const { hasNext, hasPrev, next, prev } = useGalleryNav({\n cancelPendingClose: gestures.cancelPendingClose,\n currentIndex,\n entries,\n flipX,\n flipY,\n getInitialScale,\n resetCloseTracking,\n rotate,\n scale,\n setCurrentIndex,\n setNatural,\n setSource,\n switchTo,\n syncZoomNatural,\n x,\n y,\n });\n\n const popupNodeRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useMergeRefs<HTMLDivElement>([layerRef, gestures.popupRef, popupNodeRef]);\n const imageRef = useMergeRefs<HTMLImageElement>([flipImageRef, gestures.imageRef]);\n\n const handleLoad = useCallback(\n (event: SyntheticEvent<HTMLImageElement>) => {\n const loaded = event.currentTarget;\n if (!loaded.naturalWidth || !loaded.naturalHeight) return;\n const next = { height: loaded.naturalHeight, width: loaded.naturalWidth };\n setNatural((prev) =>\n prev.width === next.width && prev.height === next.height ? prev : next,\n );\n syncZoomNatural(next);\n },\n [syncZoomNatural],\n );\n\n useEffect(() => {\n const handleResize = () => {\n const next = readViewport();\n setViewport(next);\n syncZoomViewport(next);\n };\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, [syncZoomViewport]);\n\n useEffect(() => {\n const { previewSrc, src } = currentEntry;\n if (!previewSrc || previewSrc === src) return;\n let cancelled = false;\n const loader = new window.Image();\n const handleLoaded = () => {\n if (!cancelled) setSource(previewSrc);\n };\n loader.addEventListener('load', handleLoaded);\n loader.src = previewSrc;\n return () => {\n cancelled = true;\n loader.removeEventListener('load', handleLoaded);\n };\n }, [currentEntry]);\n\n useEffect(\n () => () => {\n const thumbnailWasRemoved = !openerEntry.element.isConnected;\n if (thumbnailWasRemoved) endClosePreview(token);\n },\n [openerEntry, token],\n );\n\n return (\n <Dialog.Root modal open onOpenChange={handleDialogOpenChange}>\n <Dialog.Portal container={appElement ?? undefined}>\n {/* Base UI drops a nested dialog's backdrop so overlays don't stack. The viewer is\n not an overlay on the surface it opened from — it replaces the screen — so it\n keeps its own scrim, or a viewer opened from a maskless drawer would show the\n page straight through the letterbox around the image. */}\n <Dialog.Backdrop\n forceRender\n className={styles.viewerBackdrop}\n ref={backdropRef}\n style={zIndex === undefined ? undefined : { zIndex }}\n onClick={gestures.onSurfaceClick}\n />\n <Dialog.Popup\n aria-label={currentEntry.element.alt || undefined}\n className={styles.viewerPopup}\n finalFocus={finalFocus}\n // Focus the popup itself instead of Base UI's default (the first\n // tabbable — the close button): a focus-visible close button pins\n // the idle auto-hide open forever and pops its tooltip on open.\n initialFocus={popupNodeRef}\n ref={popupRef}\n style={zIndex === undefined ? undefined : { zIndex: zIndex + 1 }}\n tabIndex={-1}\n onClick={gestures.onSurfaceClick}\n onPointerCancel={gestures.onPointerFinish}\n onPointerDown={gestures.onPointerDown}\n onPointerMove={gestures.onPointerMove}\n onPointerUp={gestures.onPointerFinish}\n >\n <img\n alt={currentEntry.element.alt}\n className={styles.viewerImage}\n ref={imageRef}\n src={source}\n style={{\n cursor: gestures.cursor,\n height: imageRect.height,\n left: imageRect.x,\n top: imageRect.y,\n width: imageRect.width,\n }}\n onClick={gestures.onImageClick}\n onDoubleClick={gestures.onImageDoubleClick}\n onLoad={handleLoad}\n />\n <ViewerChrome\n canZoomIn={canZoomIn}\n canZoomOut={canZoomOut}\n chromeRef={chromeRef}\n current={currentIndex}\n fetchImage={currentEntry.options.fetchImage}\n fitRect={fitRect}\n flipHorizontal={flipHorizontal}\n flipVertical={flipVertical}\n hasNext={hasNext}\n hasPrev={hasPrev}\n natural={natural}\n next={next}\n prev={prev}\n rotateLeft={rotateLeft}\n rotateRight={rotateRight}\n rotation={rotation}\n scale={scale}\n source={source}\n toggleActualSize={toggleActualSize}\n toolbarAddon={currentEntry.options.toolbarAddon}\n total={entries.length}\n zoomIn={zoomIn}\n zoomOut={zoomOut}\n onClose={handleClose}\n onDownload={currentEntry.options.onDownload}\n />\n </Dialog.Popup>\n <ToastHost root={appElement ?? undefined} />\n </Dialog.Portal>\n </Dialog.Root>\n );\n});\n\nImageViewer.displayName = 'ImageViewer';\n\nexport default ImageViewer;\n"],"mappings":"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"}
|
|
@@ -39,7 +39,10 @@ const usePercentage = (scale, matchScale) => {
|
|
|
39
39
|
const getSnapshot = useCallback(() => Math.round(scale.get() / matchScale * 100), [matchScale, scale]);
|
|
40
40
|
return useSyncExternalStore(subscribe, getSnapshot);
|
|
41
41
|
};
|
|
42
|
-
const
|
|
42
|
+
const defaultFetchImage = async (src) => {
|
|
43
|
+
return (await fetch(src, { mode: "cors" })).blob();
|
|
44
|
+
};
|
|
45
|
+
const Toolbar = memo(({ canZoomIn, canZoomOut, fetchImage, fitRect, flipHorizontal, flipVertical, natural, onDownload, onMoreOpenChange, rotateLeft, rotateRight, rotation, scale, source, toggleActualSize, toolbarAddon, zoomIn, zoomOut }) => {
|
|
43
46
|
const { t } = useTranslation(image_default);
|
|
44
47
|
const [containerEl, setContainerEl] = useState(null);
|
|
45
48
|
const [copyLoading, setCopyLoading] = useState(false);
|
|
@@ -48,9 +51,10 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
48
51
|
const percentage = usePercentage(scale, matchScale);
|
|
49
52
|
const canToggleActualSize = Math.abs(matchScale - 1) > .01;
|
|
50
53
|
const showFitAffordance = canToggleActualSize && percentage === 100;
|
|
54
|
+
const loadImage = fetchImage ?? defaultFetchImage;
|
|
51
55
|
const defaultDownload = useCallback(async (src) => {
|
|
52
56
|
try {
|
|
53
|
-
const blob = await (
|
|
57
|
+
const blob = await loadImage(src);
|
|
54
58
|
const blobUrl = URL.createObjectURL(blob);
|
|
55
59
|
let fileName = getFileNameFromUrl(src);
|
|
56
60
|
const ext = getExtensionFromMimeType(blob.type);
|
|
@@ -59,10 +63,15 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
59
63
|
await downloadBlob(blobUrl, fileName);
|
|
60
64
|
URL.revokeObjectURL(blobUrl);
|
|
61
65
|
toast.success(t("image.downloadSuccess"));
|
|
62
|
-
} catch {
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (fetchImage) throw error;
|
|
63
68
|
window.open(src, "_blank", "noopener,noreferrer");
|
|
64
69
|
}
|
|
65
|
-
}, [
|
|
70
|
+
}, [
|
|
71
|
+
fetchImage,
|
|
72
|
+
loadImage,
|
|
73
|
+
t
|
|
74
|
+
]);
|
|
66
75
|
const handleDownload = useCallback(async () => {
|
|
67
76
|
setDownloadLoading(true);
|
|
68
77
|
try {
|
|
@@ -81,7 +90,7 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
81
90
|
const handleCopy = useCallback(async () => {
|
|
82
91
|
setCopyLoading(true);
|
|
83
92
|
try {
|
|
84
|
-
const blob = await (
|
|
93
|
+
const blob = await loadImage(source);
|
|
85
94
|
const clipboardBlob = await getClipboardBlob(blob);
|
|
86
95
|
await navigator.clipboard.write([new ClipboardItem(clipboardBlob)]);
|
|
87
96
|
toast.success(t("image.copySuccess"));
|
|
@@ -90,7 +99,11 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
90
99
|
} finally {
|
|
91
100
|
setCopyLoading(false);
|
|
92
101
|
}
|
|
93
|
-
}, [
|
|
102
|
+
}, [
|
|
103
|
+
loadImage,
|
|
104
|
+
source,
|
|
105
|
+
t
|
|
106
|
+
]);
|
|
94
107
|
const moreItems = useMemo(() => [
|
|
95
108
|
{
|
|
96
109
|
icon: FlipHorizontal,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toolbar.mjs","names":["imageMessages","Flexbox"],"sources":["../../../src/Image/viewer/Toolbar.tsx"],"sourcesContent":["'use client';\n\nimport {\n Copy,\n Download,\n Ellipsis,\n FlipHorizontal,\n FlipVertical,\n RotateCcw,\n RotateCw,\n ZoomIn,\n ZoomOut,\n} from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type ReactNode, useCallback, useMemo, useState, useSyncExternalStore } from 'react';\n\nimport ActionIcon from '@/base-ui/ActionIcon';\nimport DropdownMenu, { type DropdownItem } from '@/base-ui/DropdownMenu';\nimport { toast } from '@/base-ui/Toast';\nimport { Center, Flexbox } from '@/Flex';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport { TooltipGroup } from '@/Tooltip';\nimport { getClipboardBlob } from '@/utils/blobToPng';\nimport { downloadBlob } from '@/utils/downloadBlob';\n\nimport { styles } from '../style';\nimport ActualSizeIcon from './ActualSizeIcon';\nimport { naturalScale, type Rect, type Rotation, type Size } from './geometry';\n\nconst getFileNameFromUrl = (url: string): string => {\n try {\n const pathname = new URL(url).pathname;\n const match = pathname.match(/\\/([^/]+)$/);\n return match ? decodeURIComponent(match[1]) : 'image';\n } catch {\n return 'image';\n }\n};\n\nconst getExtensionFromMimeType = (mimeType: string): string => {\n const map: Record<string, string> = {\n 'image/svg+xml': 'svg',\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n };\n return map[mimeType?.toLowerCase()] || mimeType?.split('/')[1]?.split('+')[0] || 'png';\n};\n\nconst usePercentage = (scale: MotionValue<number>, matchScale: number): number => {\n const subscribe = useCallback((onChange: () => void) => scale.on('change', onChange), [scale]);\n const getSnapshot = useCallback(\n () => Math.round((scale.get() / matchScale) * 100),\n [matchScale, scale],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot);\n};\n\nexport interface ToolbarProps {\n canZoomIn: boolean;\n canZoomOut: boolean;\n fitRect: Rect;\n flipHorizontal: () => void;\n flipVertical: () => void;\n natural: Size;\n onDownload?: (source: string) => void | Promise<void>;\n onMoreOpenChange?: (open: boolean) => void;\n rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n source: string;\n toggleActualSize: () => void;\n toolbarAddon?: ReactNode;\n zoomIn: () => void;\n zoomOut: () => void;\n}\n\nconst Toolbar = memo<ToolbarProps>(\n ({\n canZoomIn,\n canZoomOut,\n fitRect,\n flipHorizontal,\n flipVertical,\n natural,\n onDownload,\n onMoreOpenChange,\n rotateLeft,\n rotateRight,\n rotation,\n scale,\n source,\n toggleActualSize,\n toolbarAddon,\n zoomIn,\n zoomOut,\n }) => {\n const { t } = useTranslation(imageMessages);\n const [containerEl, setContainerEl] = useState<HTMLElement | null>(null);\n const [copyLoading, setCopyLoading] = useState(false);\n const [downloadLoading, setDownloadLoading] = useState(false);\n\n const matchScale = naturalScale(natural, fitRect, rotation);\n const percentage = usePercentage(scale, matchScale);\n // An image small enough that computeFit never scaled it down is already at\n // 100% when fitted, so the two states the toggle switches between are the\n // same state and the control has nothing to do.\n const canToggleActualSize = Math.abs(matchScale - 1) > 0.01;\n // The control reads as \"go to actual size\" by default, and only flips to\n // the fit affordance when it can actually take you back — an image that\n // fits at 100% is at both states at once and would otherwise offer to\n // \"fit to screen\" while already fitted.\n const showFitAffordance = canToggleActualSize && percentage === 100;\n\n const defaultDownload = useCallback(\n async (src: string) => {\n try {\n const response = await fetch(src, { mode: 'cors' });\n const blob = await response.blob();\n const blobUrl = URL.createObjectURL(blob);\n let fileName = getFileNameFromUrl(src);\n const ext = getExtensionFromMimeType(blob.type);\n if (!fileName.includes('.')) {\n fileName = `${fileName}.${ext}`;\n } else if (fileName.endsWith('.svg+xml')) {\n fileName = fileName.replace(/\\.svg\\+xml$/i, '.svg');\n }\n await downloadBlob(blobUrl, fileName);\n URL.revokeObjectURL(blobUrl);\n toast.success(t('image.downloadSuccess'));\n } catch {\n // A cross-origin source without CORS headers (e.g. a presigned URL reached\n // through a redirect, which sends `Origin: null`) can never be read as a\n // blob; opening it lets the browser handle the save instead of failing.\n window.open(src, '_blank', 'noopener,noreferrer');\n }\n },\n [t],\n );\n\n const handleDownload = useCallback(async () => {\n setDownloadLoading(true);\n try {\n await (onDownload ?? defaultDownload)(source);\n } catch {\n toast.error(t('image.downloadFailed'));\n } finally {\n setDownloadLoading(false);\n }\n }, [defaultDownload, onDownload, source, t]);\n\n const handleCopy = useCallback(async () => {\n setCopyLoading(true);\n try {\n const response = await fetch(source, { mode: 'cors' });\n const blob = await response.blob();\n const clipboardBlob = await getClipboardBlob(blob);\n await navigator.clipboard.write([new ClipboardItem(clipboardBlob)]);\n toast.success(t('image.copySuccess'));\n } catch {\n toast.error(t('image.copyFailed'));\n } finally {\n setCopyLoading(false);\n }\n }, [source, t]);\n\n const moreItems = useMemo<DropdownItem[]>(\n () => [\n {\n icon: FlipHorizontal,\n key: 'flip-horizontal',\n label: t('image.flipHorizontal'),\n onClick: flipHorizontal,\n },\n {\n icon: FlipVertical,\n key: 'flip-vertical',\n label: t('image.flipVertical'),\n onClick: flipVertical,\n },\n { icon: RotateCcw, key: 'rotate-left', label: t('image.rotateLeft'), onClick: rotateLeft },\n {\n icon: RotateCw,\n key: 'rotate-right',\n label: t('image.rotateRight'),\n onClick: rotateRight,\n },\n { icon: Copy, key: 'copy', label: t('image.copy'), onClick: handleCopy },\n ],\n [flipHorizontal, flipVertical, handleCopy, rotateLeft, rotateRight, t],\n );\n\n return (\n <TooltipGroup popupContainer={containerEl ?? undefined}>\n <div className={styles.toolbar} ref={setContainerEl}>\n <Flexbox horizontal align=\"center\" className={styles.toolbarRow} gap={8}>\n <ActionIcon\n disabled={!canZoomOut}\n icon={ZoomOut}\n style={{ borderRadius: 999 }}\n title={t('image.zoomOut')}\n onClick={zoomOut}\n />\n <Center horizontal className={styles.toolbarPercentage}>\n {percentage}%\n </Center>\n <ActionIcon\n disabled={!canZoomIn}\n icon={ZoomIn}\n style={{ borderRadius: 999 }}\n title={t('image.zoomIn')}\n onClick={zoomIn}\n />\n <ActionIcon\n data-actual-size={showFitAffordance ? 'fit' : 'actual'}\n disabled={!canToggleActualSize}\n icon={ActualSizeIcon}\n style={{ borderRadius: 999 }}\n title={showFitAffordance ? t('image.fitToScreen') : t('image.actualSize')}\n onClick={toggleActualSize}\n />\n <ActionIcon\n icon={Download}\n loading={downloadLoading}\n style={{ borderRadius: 999 }}\n title={t('image.download')}\n onClick={handleDownload}\n />\n <DropdownMenu\n items={moreItems}\n placement=\"top\"\n portalProps={{ container: containerEl ?? undefined }}\n onOpenChange={(open) => onMoreOpenChange?.(open)}\n >\n <ActionIcon\n icon={Ellipsis}\n loading={copyLoading}\n style={{ borderRadius: 999 }}\n title={t('image.more')}\n />\n </DropdownMenu>\n {toolbarAddon}\n </Flexbox>\n </div>\n </TooltipGroup>\n );\n },\n);\n\nToolbar.displayName = 'Toolbar';\n\nexport default Toolbar;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Toolbar.mjs","names":["imageMessages","Flexbox"],"sources":["../../../src/Image/viewer/Toolbar.tsx"],"sourcesContent":["'use client';\n\nimport {\n Copy,\n Download,\n Ellipsis,\n FlipHorizontal,\n FlipVertical,\n RotateCcw,\n RotateCw,\n ZoomIn,\n ZoomOut,\n} from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type ReactNode, useCallback, useMemo, useState, useSyncExternalStore } from 'react';\n\nimport ActionIcon from '@/base-ui/ActionIcon';\nimport DropdownMenu, { type DropdownItem } from '@/base-ui/DropdownMenu';\nimport { toast } from '@/base-ui/Toast';\nimport { Center, Flexbox } from '@/Flex';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport { TooltipGroup } from '@/Tooltip';\nimport { getClipboardBlob } from '@/utils/blobToPng';\nimport { downloadBlob } from '@/utils/downloadBlob';\n\nimport { styles } from '../style';\nimport ActualSizeIcon from './ActualSizeIcon';\nimport { naturalScale, type Rect, type Rotation, type Size } from './geometry';\n\nconst getFileNameFromUrl = (url: string): string => {\n try {\n const pathname = new URL(url).pathname;\n const match = pathname.match(/\\/([^/]+)$/);\n return match ? decodeURIComponent(match[1]) : 'image';\n } catch {\n return 'image';\n }\n};\n\nconst getExtensionFromMimeType = (mimeType: string): string => {\n const map: Record<string, string> = {\n 'image/svg+xml': 'svg',\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n };\n return map[mimeType?.toLowerCase()] || mimeType?.split('/')[1]?.split('+')[0] || 'png';\n};\n\nconst usePercentage = (scale: MotionValue<number>, matchScale: number): number => {\n const subscribe = useCallback((onChange: () => void) => scale.on('change', onChange), [scale]);\n const getSnapshot = useCallback(\n () => Math.round((scale.get() / matchScale) * 100),\n [matchScale, scale],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot);\n};\n\nconst defaultFetchImage = async (src: string): Promise<Blob> => {\n const response = await fetch(src, { mode: 'cors' });\n return response.blob();\n};\n\nexport interface ToolbarProps {\n canZoomIn: boolean;\n canZoomOut: boolean;\n fetchImage?: (source: string) => Promise<Blob>;\n fitRect: Rect;\n flipHorizontal: () => void;\n flipVertical: () => void;\n natural: Size;\n onDownload?: (source: string) => void | Promise<void>;\n onMoreOpenChange?: (open: boolean) => void;\n rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n source: string;\n toggleActualSize: () => void;\n toolbarAddon?: ReactNode;\n zoomIn: () => void;\n zoomOut: () => void;\n}\n\nconst Toolbar = memo<ToolbarProps>(\n ({\n canZoomIn,\n canZoomOut,\n fetchImage,\n fitRect,\n flipHorizontal,\n flipVertical,\n natural,\n onDownload,\n onMoreOpenChange,\n rotateLeft,\n rotateRight,\n rotation,\n scale,\n source,\n toggleActualSize,\n toolbarAddon,\n zoomIn,\n zoomOut,\n }) => {\n const { t } = useTranslation(imageMessages);\n const [containerEl, setContainerEl] = useState<HTMLElement | null>(null);\n const [copyLoading, setCopyLoading] = useState(false);\n const [downloadLoading, setDownloadLoading] = useState(false);\n\n const matchScale = naturalScale(natural, fitRect, rotation);\n const percentage = usePercentage(scale, matchScale);\n // An image small enough that computeFit never scaled it down is already at\n // 100% when fitted, so the two states the toggle switches between are the\n // same state and the control has nothing to do.\n const canToggleActualSize = Math.abs(matchScale - 1) > 0.01;\n // The control reads as \"go to actual size\" by default, and only flips to\n // the fit affordance when it can actually take you back — an image that\n // fits at 100% is at both states at once and would otherwise offer to\n // \"fit to screen\" while already fitted.\n const showFitAffordance = canToggleActualSize && percentage === 100;\n\n const loadImage = fetchImage ?? defaultFetchImage;\n\n const defaultDownload = useCallback(\n async (src: string) => {\n try {\n const blob = await loadImage(src);\n const blobUrl = URL.createObjectURL(blob);\n let fileName = getFileNameFromUrl(src);\n const ext = getExtensionFromMimeType(blob.type);\n if (!fileName.includes('.')) {\n fileName = `${fileName}.${ext}`;\n } else if (fileName.endsWith('.svg+xml')) {\n fileName = fileName.replace(/\\.svg\\+xml$/i, '.svg');\n }\n await downloadBlob(blobUrl, fileName);\n URL.revokeObjectURL(blobUrl);\n toast.success(t('image.downloadSuccess'));\n } catch (error) {\n if (fetchImage) throw error;\n // A cross-origin source without CORS headers (e.g. a presigned URL reached\n // through a redirect, which sends `Origin: null`) can never be read as a\n // blob; opening it lets the browser handle the save instead of failing.\n window.open(src, '_blank', 'noopener,noreferrer');\n }\n },\n [fetchImage, loadImage, t],\n );\n\n const handleDownload = useCallback(async () => {\n setDownloadLoading(true);\n try {\n await (onDownload ?? defaultDownload)(source);\n } catch {\n toast.error(t('image.downloadFailed'));\n } finally {\n setDownloadLoading(false);\n }\n }, [defaultDownload, onDownload, source, t]);\n\n const handleCopy = useCallback(async () => {\n setCopyLoading(true);\n try {\n const blob = await loadImage(source);\n const clipboardBlob = await getClipboardBlob(blob);\n await navigator.clipboard.write([new ClipboardItem(clipboardBlob)]);\n toast.success(t('image.copySuccess'));\n } catch {\n toast.error(t('image.copyFailed'));\n } finally {\n setCopyLoading(false);\n }\n }, [loadImage, source, t]);\n\n const moreItems = useMemo<DropdownItem[]>(\n () => [\n {\n icon: FlipHorizontal,\n key: 'flip-horizontal',\n label: t('image.flipHorizontal'),\n onClick: flipHorizontal,\n },\n {\n icon: FlipVertical,\n key: 'flip-vertical',\n label: t('image.flipVertical'),\n onClick: flipVertical,\n },\n { icon: RotateCcw, key: 'rotate-left', label: t('image.rotateLeft'), onClick: rotateLeft },\n {\n icon: RotateCw,\n key: 'rotate-right',\n label: t('image.rotateRight'),\n onClick: rotateRight,\n },\n { icon: Copy, key: 'copy', label: t('image.copy'), onClick: handleCopy },\n ],\n [flipHorizontal, flipVertical, handleCopy, rotateLeft, rotateRight, t],\n );\n\n return (\n <TooltipGroup popupContainer={containerEl ?? undefined}>\n <div className={styles.toolbar} ref={setContainerEl}>\n <Flexbox horizontal align=\"center\" className={styles.toolbarRow} gap={8}>\n <ActionIcon\n disabled={!canZoomOut}\n icon={ZoomOut}\n style={{ borderRadius: 999 }}\n title={t('image.zoomOut')}\n onClick={zoomOut}\n />\n <Center horizontal className={styles.toolbarPercentage}>\n {percentage}%\n </Center>\n <ActionIcon\n disabled={!canZoomIn}\n icon={ZoomIn}\n style={{ borderRadius: 999 }}\n title={t('image.zoomIn')}\n onClick={zoomIn}\n />\n <ActionIcon\n data-actual-size={showFitAffordance ? 'fit' : 'actual'}\n disabled={!canToggleActualSize}\n icon={ActualSizeIcon}\n style={{ borderRadius: 999 }}\n title={showFitAffordance ? t('image.fitToScreen') : t('image.actualSize')}\n onClick={toggleActualSize}\n />\n <ActionIcon\n icon={Download}\n loading={downloadLoading}\n style={{ borderRadius: 999 }}\n title={t('image.download')}\n onClick={handleDownload}\n />\n <DropdownMenu\n items={moreItems}\n placement=\"top\"\n portalProps={{ container: containerEl ?? undefined }}\n onOpenChange={(open) => onMoreOpenChange?.(open)}\n >\n <ActionIcon\n icon={Ellipsis}\n loading={copyLoading}\n style={{ borderRadius: 999 }}\n title={t('image.more')}\n />\n </DropdownMenu>\n {toolbarAddon}\n </Flexbox>\n </div>\n </TooltipGroup>\n );\n },\n);\n\nToolbar.displayName = 'Toolbar';\n\nexport default Toolbar;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ViewerChrome.mjs","names":["imageMessages"],"sources":["../../../src/Image/viewer/ViewerChrome.tsx"],"sourcesContent":["'use client';\n\nimport { X } from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type MouseEvent, type ReactNode, useCallback } from 'react';\n\nimport ActionIcon from '@/base-ui/ActionIcon';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\n\nimport { styles } from '../style';\nimport GalleryNav from './GalleryNav';\nimport type { Rect, Rotation, Size } from './geometry';\nimport Toolbar from './Toolbar';\nimport { useChromeIdle } from './useChromeIdle';\n\nexport interface ViewerChromeProps {\n canZoomIn: boolean;\n canZoomOut: boolean;\n chromeRef: (node: HTMLElement | null) => void;\n current: number;\n fitRect: Rect;\n flipHorizontal: () => void;\n flipVertical: () => void;\n hasNext: boolean;\n hasPrev: boolean;\n natural: Size;\n next: () => void;\n onClose: () => void;\n onDownload?: (source: string) => void | Promise<void>;\n prev: () => void;\n rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n source: string;\n toggleActualSize: () => void;\n toolbarAddon?: ReactNode;\n total: number;\n zoomIn: () => void;\n zoomOut: () => void;\n}\n\nconst ViewerChrome = memo<ViewerChromeProps>(\n ({ chromeRef, current, hasNext, hasPrev, next, onClose, prev, total, ...toolbarProps }) => {\n const { t } = useTranslation(imageMessages);\n const idle = useChromeIdle();\n\n const handleChromeClick = useCallback((event: MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n }, []);\n\n return (\n <div className={styles.viewerChrome} ref={chromeRef} onClick={handleChromeClick}>\n <div\n className={styles.viewerChromeIdle}\n data-idle-hidden={idle.hidden ? '' : undefined}\n ref={idle.ref}\n >\n <ActionIcon\n className={styles.viewerClose}\n icon={X}\n title={t('image.close')}\n onClick={onClose}\n />\n {total > 1 && (\n <GalleryNav\n current={current}\n hasNext={hasNext}\n hasPrev={hasPrev}\n next={next}\n prev={prev}\n total={total}\n />\n )}\n <Toolbar {...toolbarProps} onMoreOpenChange={idle.setHeld} />\n </div>\n </div>\n );\n },\n);\n\nViewerChrome.displayName = 'ViewerChrome';\n\nexport default ViewerChrome;\n"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"ViewerChrome.mjs","names":["imageMessages"],"sources":["../../../src/Image/viewer/ViewerChrome.tsx"],"sourcesContent":["'use client';\n\nimport { X } from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type MouseEvent, type ReactNode, useCallback } from 'react';\n\nimport ActionIcon from '@/base-ui/ActionIcon';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\n\nimport { styles } from '../style';\nimport GalleryNav from './GalleryNav';\nimport type { Rect, Rotation, Size } from './geometry';\nimport Toolbar from './Toolbar';\nimport { useChromeIdle } from './useChromeIdle';\n\nexport interface ViewerChromeProps {\n canZoomIn: boolean;\n canZoomOut: boolean;\n chromeRef: (node: HTMLElement | null) => void;\n current: number;\n fetchImage?: (source: string) => Promise<Blob>;\n fitRect: Rect;\n flipHorizontal: () => void;\n flipVertical: () => void;\n hasNext: boolean;\n hasPrev: boolean;\n natural: Size;\n next: () => void;\n onClose: () => void;\n onDownload?: (source: string) => void | Promise<void>;\n prev: () => void;\n rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n source: string;\n toggleActualSize: () => void;\n toolbarAddon?: ReactNode;\n total: number;\n zoomIn: () => void;\n zoomOut: () => void;\n}\n\nconst ViewerChrome = memo<ViewerChromeProps>(\n ({ chromeRef, current, hasNext, hasPrev, next, onClose, prev, total, ...toolbarProps }) => {\n const { t } = useTranslation(imageMessages);\n const idle = useChromeIdle();\n\n const handleChromeClick = useCallback((event: MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n }, []);\n\n return (\n <div className={styles.viewerChrome} ref={chromeRef} onClick={handleChromeClick}>\n <div\n className={styles.viewerChromeIdle}\n data-idle-hidden={idle.hidden ? '' : undefined}\n ref={idle.ref}\n >\n <ActionIcon\n className={styles.viewerClose}\n icon={X}\n title={t('image.close')}\n onClick={onClose}\n />\n {total > 1 && (\n <GalleryNav\n current={current}\n hasNext={hasNext}\n hasPrev={hasPrev}\n next={next}\n prev={prev}\n total={total}\n />\n )}\n <Toolbar {...toolbarProps} onMoreOpenChange={idle.setHeld} />\n </div>\n </div>\n );\n },\n);\n\nViewerChrome.displayName = 'ViewerChrome';\n\nexport default ViewerChrome;\n"],"mappings":";;;;;;;;;;;;AA4CA,MAAM,eAAe,MAClB,EAAE,WAAW,SAAS,SAAS,SAAS,MAAM,SAAS,MAAM,OAAO,GAAG,mBAAmB;CACzF,MAAM,EAAE,MAAM,eAAeA,aAAa;CAC1C,MAAM,OAAO,cAAc;CAE3B,MAAM,oBAAoB,aAAa,UAAsC;EAC3E,MAAM,gBAAgB;CACxB,GAAG,CAAC,CAAC;CAEL,OACE,oBAAC,OAAD;EAAK,WAAW,OAAO;EAAc,KAAK;EAAW,SAAS;EAC5D,UAAA,qBAAC,OAAD;GACE,WAAW,OAAO;GAClB,oBAAkB,KAAK,SAAS,KAAK,KAAA;GACrC,KAAK,KAAK;GAHZ,UAAA;IAKE,oBAAC,YAAD;KACE,WAAW,OAAO;KAClB,MAAM;KACN,OAAO,EAAE,aAAa;KACtB,SAAS;IACV,CAAA;IACA,QAAQ,KACP,oBAAC,YAAD;KACW;KACA;KACA;KACH;KACA;KACC;IACR,CAAA;IAEH,oBAAC,SAAD;KAAS,GAAI;KAAc,kBAAkB,KAAK;IAAU,CAAA;GACzD;;CACF,CAAA;AAET,CACF;AAEA,aAAa,cAAc"}
|
|
@@ -30,7 +30,7 @@ declare const styles: {
|
|
|
30
30
|
};
|
|
31
31
|
declare const rootVariants: (props?: ({
|
|
32
32
|
shadow?: boolean | null | undefined;
|
|
33
|
-
size?: "
|
|
33
|
+
size?: "large" | "middle" | "small" | null | undefined;
|
|
34
34
|
variant?: "filled" | "outlined" | "borderless" | null | undefined;
|
|
35
35
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
36
36
|
//#endregion
|
|
@@ -3,6 +3,12 @@ import { controlHeight } from "../controlSize.mjs";
|
|
|
3
3
|
import { createStaticStyles } from "antd-style";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
//#region src/base-ui/Input/style.ts
|
|
6
|
+
/**
|
|
7
|
+
* Only the field's own control counts as disabled. NumberField disables its
|
|
8
|
+
* increment / decrement buttons at max / min, and a bare `:has(:disabled)` greyed
|
|
9
|
+
* the whole root whenever the value sat on a bound.
|
|
10
|
+
*/
|
|
11
|
+
const disabledControl = "input:disabled, textarea:disabled";
|
|
6
12
|
const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
7
13
|
clear: css`
|
|
8
14
|
cursor: pointer;
|
|
@@ -140,7 +146,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
140
146
|
box-shadow 150ms ${cssVar.motionEaseOut};
|
|
141
147
|
|
|
142
148
|
&[data-disabled],
|
|
143
|
-
&:has(
|
|
149
|
+
&:has(${disabledControl}) {
|
|
144
150
|
cursor: not-allowed;
|
|
145
151
|
color: ${cssVar.colorTextQuaternary};
|
|
146
152
|
opacity: 0.66;
|
|
@@ -306,6 +312,6 @@ const rootVariants = cva([styles.root, styles.invalid], {
|
|
|
306
312
|
}
|
|
307
313
|
});
|
|
308
314
|
//#endregion
|
|
309
|
-
export { rootVariants, styles };
|
|
315
|
+
export { disabledControl, rootVariants, styles };
|
|
310
316
|
|
|
311
317
|
//# sourceMappingURL=style.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.mjs","names":["lobeStaticStylish"],"sources":["../../../src/base-ui/Input/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\nimport { cva } from 'class-variance-authority';\n\nimport { controlHeight } from '@/base-ui/controlSize';\nimport { lobeStaticStylish } from '@/styles';\n\nexport const styles = createStaticStyles(({ css, cssVar }) => ({\n clear: css`\n cursor: pointer;\n\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n\n width: 16px;\n height: 16px;\n padding: 0;\n border: 0;\n border-radius: 50%;\n\n color: ${cssVar.colorBgContainer};\n\n visibility: hidden;\n background: ${cssVar.colorTextQuaternary};\n\n &:hover {\n background: ${cssVar.colorTextTertiary};\n }\n `,\n count: css`\n pointer-events: none;\n\n position: absolute;\n inset-block-end: 6px;\n inset-inline-end: 8px;\n\n display: inline-flex;\n align-items: center;\n\n height: 20px;\n padding-inline: 8px;\n border-radius: 999px;\n\n font-size: 11px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n color: ${cssVar.colorTextSecondary};\n\n background: ${cssVar.colorFillTertiary};\n\n &[data-over] {\n color: ${cssVar.colorWhite};\n background: ${cssVar.colorError};\n }\n `,\n textareaClear: css`\n position: absolute;\n inset-block-start: 10px;\n inset-inline-end: 10px;\n `,\n textareaWithCount: css`\n padding-block-end: 30px;\n `,\n borderless: css`\n border: 1px solid transparent;\n background: none;\n `,\n filled: css`\n border: 1px solid transparent;\n background: ${cssVar.colorFillTertiary};\n\n &:hover:not(:focus-within, [data-disabled]) {\n background: ${cssVar.colorFillSecondary};\n }\n `,\n input: css`\n flex: 1;\n\n min-width: 0;\n padding: 0;\n border: none;\n\n font: inherit;\n color: inherit;\n\n appearance: none;\n background: transparent;\n outline: none;\n\n &::placeholder {\n color: ${cssVar.colorTextPlaceholder};\n }\n\n /* the root shows focus via its border; outrank global :focus-visible rings */\n &:focus-visible {\n outline: none;\n }\n `,\n invalid: css`\n &:has([data-invalid], [aria-invalid='true']) {\n border-color: ${cssVar.colorError};\n\n &:focus-within {\n border-color: ${cssVar.colorError};\n box-shadow: 0 0 0 2px ${cssVar.colorErrorBg};\n }\n }\n `,\n outlined: css`\n border: 1px solid ${cssVar.colorBorderSecondary};\n background: ${cssVar.colorBgContainer};\n\n &:hover:not(:focus-within, [data-disabled]) {\n border-color: ${cssVar.colorBorder};\n }\n\n &:focus-within {\n border-color: ${cssVar.colorPrimary};\n box-shadow: 0 0 0 2px ${cssVar.colorPrimaryBg};\n }\n `,\n root: css`\n cursor: text;\n\n display: inline-flex;\n gap: 8px;\n align-items: center;\n\n box-sizing: border-box;\n width: 100%;\n padding-inline: 12px;\n border-radius: ${cssVar.borderRadius};\n\n font-size: 14px;\n color: ${cssVar.colorText};\n\n transition:\n background 150ms ${cssVar.motionEaseOut},\n border-color 150ms ${cssVar.motionEaseOut},\n box-shadow 150ms ${cssVar.motionEaseOut};\n\n &[data-disabled],\n &:has(
|
|
1
|
+
{"version":3,"file":"style.mjs","names":["lobeStaticStylish"],"sources":["../../../src/base-ui/Input/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\nimport { cva } from 'class-variance-authority';\n\nimport { controlHeight } from '@/base-ui/controlSize';\nimport { lobeStaticStylish } from '@/styles';\n\n/**\n * Only the field's own control counts as disabled. NumberField disables its\n * increment / decrement buttons at max / min, and a bare `:has(:disabled)` greyed\n * the whole root whenever the value sat on a bound.\n */\nexport const disabledControl = 'input:disabled, textarea:disabled';\n\nexport const styles = createStaticStyles(({ css, cssVar }) => ({\n clear: css`\n cursor: pointer;\n\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n\n width: 16px;\n height: 16px;\n padding: 0;\n border: 0;\n border-radius: 50%;\n\n color: ${cssVar.colorBgContainer};\n\n visibility: hidden;\n background: ${cssVar.colorTextQuaternary};\n\n &:hover {\n background: ${cssVar.colorTextTertiary};\n }\n `,\n count: css`\n pointer-events: none;\n\n position: absolute;\n inset-block-end: 6px;\n inset-inline-end: 8px;\n\n display: inline-flex;\n align-items: center;\n\n height: 20px;\n padding-inline: 8px;\n border-radius: 999px;\n\n font-size: 11px;\n font-weight: 600;\n font-variant-numeric: tabular-nums;\n color: ${cssVar.colorTextSecondary};\n\n background: ${cssVar.colorFillTertiary};\n\n &[data-over] {\n color: ${cssVar.colorWhite};\n background: ${cssVar.colorError};\n }\n `,\n textareaClear: css`\n position: absolute;\n inset-block-start: 10px;\n inset-inline-end: 10px;\n `,\n textareaWithCount: css`\n padding-block-end: 30px;\n `,\n borderless: css`\n border: 1px solid transparent;\n background: none;\n `,\n filled: css`\n border: 1px solid transparent;\n background: ${cssVar.colorFillTertiary};\n\n &:hover:not(:focus-within, [data-disabled]) {\n background: ${cssVar.colorFillSecondary};\n }\n `,\n input: css`\n flex: 1;\n\n min-width: 0;\n padding: 0;\n border: none;\n\n font: inherit;\n color: inherit;\n\n appearance: none;\n background: transparent;\n outline: none;\n\n &::placeholder {\n color: ${cssVar.colorTextPlaceholder};\n }\n\n /* the root shows focus via its border; outrank global :focus-visible rings */\n &:focus-visible {\n outline: none;\n }\n `,\n invalid: css`\n &:has([data-invalid], [aria-invalid='true']) {\n border-color: ${cssVar.colorError};\n\n &:focus-within {\n border-color: ${cssVar.colorError};\n box-shadow: 0 0 0 2px ${cssVar.colorErrorBg};\n }\n }\n `,\n outlined: css`\n border: 1px solid ${cssVar.colorBorderSecondary};\n background: ${cssVar.colorBgContainer};\n\n &:hover:not(:focus-within, [data-disabled]) {\n border-color: ${cssVar.colorBorder};\n }\n\n &:focus-within {\n border-color: ${cssVar.colorPrimary};\n box-shadow: 0 0 0 2px ${cssVar.colorPrimaryBg};\n }\n `,\n root: css`\n cursor: text;\n\n display: inline-flex;\n gap: 8px;\n align-items: center;\n\n box-sizing: border-box;\n width: 100%;\n padding-inline: 12px;\n border-radius: ${cssVar.borderRadius};\n\n font-size: 14px;\n color: ${cssVar.colorText};\n\n transition:\n background 150ms ${cssVar.motionEaseOut},\n border-color 150ms ${cssVar.motionEaseOut},\n box-shadow 150ms ${cssVar.motionEaseOut};\n\n &[data-disabled],\n &:has(${disabledControl}) {\n cursor: not-allowed;\n color: ${cssVar.colorTextQuaternary};\n opacity: 0.66;\n }\n\n &:hover [data-lobe-input-clear],\n &:focus-within [data-lobe-input-clear] {\n visibility: visible;\n }\n `,\n shadow: lobeStaticStylish.shadow,\n sizeLarge: css`\n min-height: ${controlHeight.large}px;\n border-radius: ${cssVar.borderRadiusLG};\n font-size: 16px;\n `,\n sizeMiddle: css`\n min-height: ${controlHeight.middle}px;\n `,\n sizeSmall: css`\n min-height: ${controlHeight.small}px;\n padding-inline: 8px;\n border-radius: ${cssVar.borderRadiusSM};\n font-size: 12px;\n `,\n numberControl: css`\n cursor: pointer;\n\n display: flex;\n align-items: center;\n justify-content: center;\n\n width: 20px;\n height: 13px;\n margin: 0;\n padding: 0;\n border: none;\n border-radius: 4px;\n\n color: ${cssVar.colorTextTertiary};\n\n background: none;\n outline: none;\n\n transition:\n color 150ms ${cssVar.motionEaseOut},\n background 150ms ${cssVar.motionEaseOut};\n\n &:hover:not(:disabled) {\n color: ${cssVar.colorText};\n background: ${cssVar.colorFillSecondary};\n }\n\n &:disabled {\n cursor: not-allowed;\n opacity: 0.3;\n }\n `,\n numberControlLarge: css`\n width: 22px;\n height: 15px;\n `,\n numberControlSmall: css`\n width: 16px;\n height: 10px;\n border-radius: 3px;\n `,\n numberControls: css`\n display: flex;\n flex: none;\n flex-direction: column;\n margin-inline-end: -6px;\n `,\n numberControlsSmall: css`\n margin-inline-end: -4px;\n `,\n numberInput: css`\n font-variant-numeric: tabular-nums;\n `,\n otpCell: css`\n flex: none;\n width: ${controlHeight.middle}px;\n padding-inline: 0;\n text-align: center;\n `,\n otpRoot: css`\n display: inline-flex;\n gap: 8px;\n align-items: center;\n `,\n passwordToggle: css`\n cursor: pointer;\n\n display: inline-flex;\n align-items: center;\n justify-content: center;\n\n margin: 0;\n padding: 0;\n border: none;\n\n color: ${cssVar.colorTextTertiary};\n\n background: none;\n outline: none;\n\n transition: color 150ms ${cssVar.motionEaseOut};\n\n &:hover {\n color: ${cssVar.colorText};\n }\n `,\n slot: css`\n display: inline-flex;\n flex: none;\n align-items: center;\n color: ${cssVar.colorTextTertiary};\n `,\n textarea: css`\n position: relative;\n height: auto;\n padding-block: 8px;\n\n textarea {\n resize: none;\n min-height: calc(1.5em * var(--textarea-min-rows, 2));\n max-height: var(--textarea-max-height, none);\n line-height: 1.5;\n }\n `,\n textareaAutoSize: css`\n textarea {\n field-sizing: content;\n }\n `,\n textareaResize: css`\n textarea {\n resize: vertical;\n }\n `,\n}));\n\nexport const rootVariants = cva([styles.root, styles.invalid], {\n defaultVariants: {\n shadow: false,\n size: 'middle',\n variant: 'outlined',\n },\n variants: {\n shadow: {\n false: null,\n true: styles.shadow,\n },\n size: {\n large: styles.sizeLarge,\n middle: styles.sizeMiddle,\n small: styles.sizeSmall,\n },\n variant: {\n borderless: styles.borderless,\n filled: styles.filled,\n outlined: styles.outlined,\n },\n },\n});\n"],"mappings":";;;;;;;;;;AAWA,MAAa,kBAAkB;AAE/B,MAAa,SAAS,oBAAoB,EAAE,KAAK,cAAc;CAC7D,OAAO,GAAG;;;;;;;;;;;;;;aAcC,OAAO,iBAAiB;;;kBAGnB,OAAO,oBAAoB;;;oBAGzB,OAAO,kBAAkB;;;CAG3C,OAAO,GAAG;;;;;;;;;;;;;;;;;aAiBC,OAAO,mBAAmB;;kBAErB,OAAO,kBAAkB;;;eAG5B,OAAO,WAAW;oBACb,OAAO,WAAW;;;CAGpC,eAAe,GAAG;;;;;CAKlB,mBAAmB,GAAG;;;CAGtB,YAAY,GAAG;;;;CAIf,QAAQ,GAAG;;kBAEK,OAAO,kBAAkB;;;oBAGvB,OAAO,mBAAmB;;;CAG5C,OAAO,GAAG;;;;;;;;;;;;;;;eAeG,OAAO,qBAAqB;;;;;;;;CAQzC,SAAS,GAAG;;sBAEQ,OAAO,WAAW;;;wBAGhB,OAAO,WAAW;gCACV,OAAO,aAAa;;;;CAIlD,UAAU,GAAG;wBACS,OAAO,qBAAqB;kBAClC,OAAO,iBAAiB;;;sBAGpB,OAAO,YAAY;;;;sBAInB,OAAO,aAAa;8BACZ,OAAO,eAAe;;;CAGlD,MAAM,GAAG;;;;;;;;;;qBAUU,OAAO,aAAa;;;aAG5B,OAAO,UAAU;;;yBAGL,OAAO,cAAc;2BACnB,OAAO,cAAc;yBACvB,OAAO,cAAc;;;YAGlC,gBAAgB;;eAEb,OAAO,oBAAoB;;;;;;;;;CASxC,QAAQA,cAAkB;CAC1B,WAAW,GAAG;kBACE,cAAc,MAAM;qBACjB,OAAO,eAAe;;;CAGzC,YAAY,GAAG;kBACC,cAAc,OAAO;;CAErC,WAAW,GAAG;kBACE,cAAc,MAAM;;qBAEjB,OAAO,eAAe;;;CAGzC,eAAe,GAAG;;;;;;;;;;;;;;aAcP,OAAO,kBAAkB;;;;;;oBAMlB,OAAO,cAAc;yBAChB,OAAO,cAAc;;;eAG/B,OAAO,UAAU;oBACZ,OAAO,mBAAmB;;;;;;;;CAQ5C,oBAAoB,GAAG;;;;CAIvB,oBAAoB,GAAG;;;;;CAKvB,gBAAgB,GAAG;;;;;;CAMnB,qBAAqB,GAAG;;;CAGxB,aAAa,GAAG;;;CAGhB,SAAS,GAAG;;aAED,cAAc,OAAO;;;;CAIhC,SAAS,GAAG;;;;;CAKZ,gBAAgB,GAAG;;;;;;;;;;;aAWR,OAAO,kBAAkB;;;;;8BAKR,OAAO,cAAc;;;eAGpC,OAAO,UAAU;;;CAG9B,MAAM,GAAG;;;;aAIE,OAAO,kBAAkB;;CAEpC,UAAU,GAAG;;;;;;;;;;;;CAYb,kBAAkB,GAAG;;;;;CAKrB,gBAAgB,GAAG;;;;;AAKrB,EAAE;AAEF,MAAa,eAAe,IAAI,CAAC,OAAO,MAAM,OAAO,OAAO,GAAG;CAC7D,iBAAiB;EACf,QAAQ;EACR,MAAM;EACN,SAAS;CACX;CACA,UAAU;EACR,QAAQ;GACN,OAAO;GACP,MAAM,OAAO;EACf;EACA,MAAM;GACJ,OAAO,OAAO;GACd,QAAQ,OAAO;GACf,OAAO,OAAO;EAChB;EACA,SAAS;GACP,YAAY,OAAO;GACnB,QAAQ,OAAO;GACf,UAAU,OAAO;EACnB;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ChartGridProps } from "./type.mjs";
|
|
2
|
+
//#region src/dashboard/ChartGrid/ChartGrid.d.ts
|
|
3
|
+
/** Peer charts and ranked lists, two-up, one column below 640px. Not for label/value rows. */
|
|
4
|
+
declare const ChartGrid: {
|
|
5
|
+
({ children, className }: ChartGridProps): import("react").JSX.Element;
|
|
6
|
+
displayName: string;
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { ChartGrid as default };
|
|
10
|
+
//# sourceMappingURL=ChartGrid.d.mts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { styles } from "./style.mjs";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { cx } from "antd-style";
|
|
5
|
+
//#region src/dashboard/ChartGrid/ChartGrid.tsx
|
|
6
|
+
/** Peer charts and ranked lists, two-up, one column below 640px. Not for label/value rows. */
|
|
7
|
+
const ChartGrid = ({ children, className }) => /* @__PURE__ */ jsx("div", {
|
|
8
|
+
className: styles.frame,
|
|
9
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
10
|
+
className: cx(styles.root, className),
|
|
11
|
+
children
|
|
12
|
+
})
|
|
13
|
+
});
|
|
14
|
+
ChartGrid.displayName = "ChartGrid";
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ChartGrid as default };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=ChartGrid.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChartGrid.mjs","names":[],"sources":["../../../src/dashboard/ChartGrid/ChartGrid.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'antd-style';\n\nimport { styles } from './style';\nimport type { ChartGridProps } from './type';\n\n/** Peer charts and ranked lists, two-up, one column below 640px. Not for label/value rows. */\nconst ChartGrid = ({ children, className }: ChartGridProps) => (\n <div className={styles.frame}>\n <div className={cx(styles.root, className)}>{children}</div>\n </div>\n);\n\nChartGrid.displayName = 'ChartGrid';\n\nexport default ChartGrid;\n"],"mappings":";;;;;;AAQA,MAAM,aAAa,EAAE,UAAU,gBAC7B,oBAAC,OAAD;CAAK,WAAW,OAAO;CACrB,UAAA,oBAAC,OAAD;EAAK,WAAW,GAAG,OAAO,MAAM,SAAS;EAAI;CAAc,CAAA;AACxD,CAAA;AAGP,UAAU,cAAc"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createStaticStyles } from "antd-style";
|
|
2
|
+
//#region src/dashboard/ChartGrid/style.ts
|
|
3
|
+
const styles = createStaticStyles(({ css }) => ({
|
|
4
|
+
/**
|
|
5
|
+
* The frame is the container. A query on the grid itself would not see its own width.
|
|
6
|
+
* Charts stay two-up so a trend and its switch stay readable; one child does not
|
|
7
|
+
* stretch across the page.
|
|
8
|
+
*/
|
|
9
|
+
frame: css`
|
|
10
|
+
container-type: inline-size;
|
|
11
|
+
|
|
12
|
+
@container (max-width: 640px) {
|
|
13
|
+
& > * {
|
|
14
|
+
grid-template-columns: minmax(0, 1fr);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
`,
|
|
18
|
+
root: css`
|
|
19
|
+
display: grid;
|
|
20
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
21
|
+
gap: 16px;
|
|
22
|
+
align-items: stretch;
|
|
23
|
+
|
|
24
|
+
&:has(> :only-child) {
|
|
25
|
+
grid-template-columns: minmax(0, min(100%, 560px));
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
}));
|
|
29
|
+
//#endregion
|
|
30
|
+
export { styles };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=style.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.mjs","names":[],"sources":["../../../src/dashboard/ChartGrid/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\n\nexport const styles = createStaticStyles(({ css }) => ({\n /**\n * The frame is the container. A query on the grid itself would not see its own width.\n * Charts stay two-up so a trend and its switch stay readable; one child does not\n * stretch across the page.\n */\n frame: css`\n container-type: inline-size;\n\n @container (max-width: 640px) {\n & > * {\n grid-template-columns: minmax(0, 1fr);\n }\n }\n `,\n root: css`\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 16px;\n align-items: stretch;\n\n &:has(> :only-child) {\n grid-template-columns: minmax(0, min(100%, 560px));\n }\n `,\n}));\n"],"mappings":";;AAEA,MAAa,SAAS,oBAAoB,EAAE,WAAW;;;;;;CAMrD,OAAO,GAAG;;;;;;;;;CASV,MAAM,GAAG;;;;;;;;;;AAUX,EAAE"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConsoleShellProps } from "./type.mjs";
|
|
2
2
|
//#region src/dashboard/ConsoleShell/ConsoleShell.d.ts
|
|
3
|
-
declare function ConsoleShell({ brand, breadcrumb, children, className, collapsed, collapseLabel, defaultCollapsed, expandLabel, footer, hotkey, navigation, onCollapsedChange, openNavigationLabel, skipToContentLabel, storageKey, tools, ...rest }: ConsoleShellProps): import("react").JSX.Element;
|
|
3
|
+
declare function ConsoleShell({ brand, breadcrumb, children, className, collapsed, closeNavigationLabel, collapseLabel, defaultCollapsed, expandLabel, footer, hotkey, navigation, onCollapsedChange, openNavigationLabel, skipToContentLabel, storageKey, tools, ...rest }: ConsoleShellProps): import("react").JSX.Element;
|
|
4
4
|
declare namespace ConsoleShell {
|
|
5
5
|
var displayName: string;
|
|
6
6
|
}
|
|
@@ -7,14 +7,14 @@ import { useIsCompact, useLocalStorage, usePrefersReducedMotion } from "../hooks
|
|
|
7
7
|
import { styles } from "./style.mjs";
|
|
8
8
|
import { useEffect, useId, useRef, useState } from "react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
import { PanelLeft } from "lucide-react";
|
|
10
|
+
import { PanelLeft, X } from "lucide-react";
|
|
11
11
|
//#region src/dashboard/ConsoleShell/ConsoleShell.tsx
|
|
12
12
|
function isTypingTarget(target) {
|
|
13
13
|
if (!(target instanceof HTMLElement)) return false;
|
|
14
14
|
const tag = target.tagName;
|
|
15
15
|
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable;
|
|
16
16
|
}
|
|
17
|
-
function ConsoleShell({ brand, breadcrumb, children, className, collapsed, collapseLabel = "Collapse sidebar", defaultCollapsed = false, expandLabel = "Expand sidebar", footer, hotkey = true, navigation, onCollapsedChange, openNavigationLabel = "Open navigation", skipToContentLabel = "Skip to content", storageKey, tools, ...rest }) {
|
|
17
|
+
function ConsoleShell({ brand, breadcrumb, children, className, collapsed, closeNavigationLabel = "Close navigation", collapseLabel = "Collapse sidebar", defaultCollapsed = false, expandLabel = "Expand sidebar", footer, hotkey = true, navigation, onCollapsedChange, openNavigationLabel = "Open navigation", skipToContentLabel = "Skip to content", storageKey, tools, ...rest }) {
|
|
18
18
|
const mainId = `console-main-${useId().replaceAll(":", "")}`;
|
|
19
19
|
const shellRef = useRef(null);
|
|
20
20
|
const isCompact = useIsCompact();
|
|
@@ -60,10 +60,16 @@ function ConsoleShell({ brand, breadcrumb, children, className, collapsed, colla
|
|
|
60
60
|
const sidebar = (rail) => /* @__PURE__ */ jsxs(ConsoleShellContext, {
|
|
61
61
|
value: stateFor(rail),
|
|
62
62
|
children: [
|
|
63
|
-
brand ? /* @__PURE__ */
|
|
63
|
+
brand || isCompact ? /* @__PURE__ */ jsxs("div", {
|
|
64
64
|
className: styles.brand,
|
|
65
65
|
"data-collapsed": rail,
|
|
66
|
-
children: brand
|
|
66
|
+
children: [brand, isCompact ? /* @__PURE__ */ jsx(Button, {
|
|
67
|
+
"aria-label": closeNavigationLabel,
|
|
68
|
+
className: styles.brandClose,
|
|
69
|
+
icon: X,
|
|
70
|
+
type: "text",
|
|
71
|
+
onClick: closeNavigation
|
|
72
|
+
}) : null]
|
|
67
73
|
}) : null,
|
|
68
74
|
navigation ? /* @__PURE__ */ jsx("div", {
|
|
69
75
|
className: styles.navSlot,
|
|
@@ -99,6 +105,8 @@ function ConsoleShell({ brand, breadcrumb, children, className, collapsed, colla
|
|
|
99
105
|
}),
|
|
100
106
|
isCompact ? /* @__PURE__ */ jsx(Drawer, {
|
|
101
107
|
noHeader: true,
|
|
108
|
+
classNames: { bodyContent: styles.drawerBody },
|
|
109
|
+
closable: false,
|
|
102
110
|
open: drawerOpen,
|
|
103
111
|
placement: "left",
|
|
104
112
|
width: 272,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConsoleShell.mjs","names":["Flexbox"],"sources":["../../../src/dashboard/ConsoleShell/ConsoleShell.tsx"],"sourcesContent":["'use client';\n\nimport { PanelLeft } from 'lucide-react';\nimport { useEffect, useId, useRef, useState } from 'react';\n\nimport Button from '@/base-ui/Button';\nimport { Drawer } from '@/base-ui/Drawer';\nimport { Flexbox } from '@/Flex';\n\nimport { useIsCompact, useLocalStorage, usePrefersReducedMotion } from '../hooks';\nimport { ConsoleShellContext } from './context';\nimport { styles } from './style';\nimport type { ConsoleShellProps, ConsoleShellState } from './type';\n\nfunction isTypingTarget(target: EventTarget | null) {\n if (!(target instanceof HTMLElement)) return false;\n const tag = target.tagName;\n return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable;\n}\n\nfunction ConsoleShell({\n brand,\n breadcrumb,\n children,\n className,\n collapsed,\n collapseLabel = 'Collapse sidebar',\n defaultCollapsed = false,\n expandLabel = 'Expand sidebar',\n footer,\n hotkey = true,\n navigation,\n onCollapsedChange,\n openNavigationLabel = 'Open navigation',\n skipToContentLabel = 'Skip to content',\n storageKey,\n tools,\n ...rest\n}: ConsoleShellProps) {\n const mainId = `console-main-${useId().replaceAll(':', '')}`;\n const shellRef = useRef<HTMLDivElement>(null);\n const isCompact = useIsCompact();\n const reducedMotion = usePrefersReducedMotion();\n const [storedCollapsed, setStoredCollapsed] = useLocalStorage(storageKey, defaultCollapsed);\n const [drawerOpen, setDrawerOpen] = useState(false);\n const isControlled = collapsed !== undefined;\n const railCollapsed = isControlled ? collapsed : storedCollapsed;\n const railed = railCollapsed && !isCompact;\n\n const setCollapsed = (next: boolean) => {\n if (!isControlled) setStoredCollapsed(next);\n onCollapsedChange?.(next);\n };\n\n const closeNavigation = () => setDrawerOpen(false);\n const toggleNavigation = () => {\n if (isCompact) {\n setDrawerOpen((open) => !open);\n return;\n }\n setCollapsed(!railCollapsed);\n };\n const toggleRef = useRef(toggleNavigation);\n toggleRef.current = toggleNavigation;\n\n useEffect(() => {\n if (!hotkey) return;\n const node = shellRef.current;\n if (!node) return;\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key.toLowerCase() !== 'b' || !(event.metaKey || event.ctrlKey) || event.altKey) {\n return;\n }\n if (event.repeat || isTypingTarget(event.target)) return;\n if (!node.contains(event.target as Node)) return;\n event.preventDefault();\n toggleRef.current();\n };\n\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [hotkey]);\n\n const stateFor = (rail: boolean): ConsoleShellState => ({\n closeNavigation,\n collapsed: rail,\n compact: isCompact,\n });\n\n const sidebar = (rail: boolean) => (\n <ConsoleShellContext value={stateFor(rail)}>\n {brand ? (\n <div className={styles.brand} data-collapsed={rail}>\n {brand}\n </div>\n ) : null}\n {navigation ? <div className={styles.navSlot}>{navigation}</div> : null}\n {footer ? <div className={styles.sidebarBottom}>{footer}</div> : null}\n </ConsoleShellContext>\n );\n\n const toggleLabel = isCompact ? openNavigationLabel : railed ? expandLabel : collapseLabel;\n\n return (\n <div\n className={className ? `${styles.shell} ${className}` : styles.shell}\n data-console-shell=\"\"\n ref={shellRef}\n {...rest}\n >\n <a\n className={styles.skipLink}\n href={`#${mainId}`}\n onClick={(event) => {\n event.preventDefault();\n document.getElementById(mainId)?.focus();\n }}\n >\n {skipToContentLabel}\n </a>\n <aside className={styles.sidebar} data-collapsed={railed} data-instant={reducedMotion}>\n {!isCompact && sidebar(railed)}\n </aside>\n {isCompact ? (\n <Drawer
|
|
1
|
+
{"version":3,"file":"ConsoleShell.mjs","names":["Flexbox"],"sources":["../../../src/dashboard/ConsoleShell/ConsoleShell.tsx"],"sourcesContent":["'use client';\n\nimport { PanelLeft, X } from 'lucide-react';\nimport { useEffect, useId, useRef, useState } from 'react';\n\nimport Button from '@/base-ui/Button';\nimport { Drawer } from '@/base-ui/Drawer';\nimport { Flexbox } from '@/Flex';\n\nimport { useIsCompact, useLocalStorage, usePrefersReducedMotion } from '../hooks';\nimport { ConsoleShellContext } from './context';\nimport { styles } from './style';\nimport type { ConsoleShellProps, ConsoleShellState } from './type';\n\nfunction isTypingTarget(target: EventTarget | null) {\n if (!(target instanceof HTMLElement)) return false;\n const tag = target.tagName;\n return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable;\n}\n\nfunction ConsoleShell({\n brand,\n breadcrumb,\n children,\n className,\n collapsed,\n closeNavigationLabel = 'Close navigation',\n collapseLabel = 'Collapse sidebar',\n defaultCollapsed = false,\n expandLabel = 'Expand sidebar',\n footer,\n hotkey = true,\n navigation,\n onCollapsedChange,\n openNavigationLabel = 'Open navigation',\n skipToContentLabel = 'Skip to content',\n storageKey,\n tools,\n ...rest\n}: ConsoleShellProps) {\n const mainId = `console-main-${useId().replaceAll(':', '')}`;\n const shellRef = useRef<HTMLDivElement>(null);\n const isCompact = useIsCompact();\n const reducedMotion = usePrefersReducedMotion();\n const [storedCollapsed, setStoredCollapsed] = useLocalStorage(storageKey, defaultCollapsed);\n const [drawerOpen, setDrawerOpen] = useState(false);\n const isControlled = collapsed !== undefined;\n const railCollapsed = isControlled ? collapsed : storedCollapsed;\n const railed = railCollapsed && !isCompact;\n\n const setCollapsed = (next: boolean) => {\n if (!isControlled) setStoredCollapsed(next);\n onCollapsedChange?.(next);\n };\n\n const closeNavigation = () => setDrawerOpen(false);\n const toggleNavigation = () => {\n if (isCompact) {\n setDrawerOpen((open) => !open);\n return;\n }\n setCollapsed(!railCollapsed);\n };\n const toggleRef = useRef(toggleNavigation);\n toggleRef.current = toggleNavigation;\n\n useEffect(() => {\n if (!hotkey) return;\n const node = shellRef.current;\n if (!node) return;\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key.toLowerCase() !== 'b' || !(event.metaKey || event.ctrlKey) || event.altKey) {\n return;\n }\n if (event.repeat || isTypingTarget(event.target)) return;\n if (!node.contains(event.target as Node)) return;\n event.preventDefault();\n toggleRef.current();\n };\n\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [hotkey]);\n\n const stateFor = (rail: boolean): ConsoleShellState => ({\n closeNavigation,\n collapsed: rail,\n compact: isCompact,\n });\n\n const sidebar = (rail: boolean) => (\n <ConsoleShellContext value={stateFor(rail)}>\n {brand || isCompact ? (\n <div className={styles.brand} data-collapsed={rail}>\n {brand}\n {isCompact ? (\n // In the drawer the close button sits in the brand row's flow, so it never\n // overlaps a brand switcher the way the drawer's floating close button did.\n <Button\n aria-label={closeNavigationLabel}\n className={styles.brandClose}\n icon={X}\n type=\"text\"\n onClick={closeNavigation}\n />\n ) : null}\n </div>\n ) : null}\n {navigation ? <div className={styles.navSlot}>{navigation}</div> : null}\n {footer ? <div className={styles.sidebarBottom}>{footer}</div> : null}\n </ConsoleShellContext>\n );\n\n const toggleLabel = isCompact ? openNavigationLabel : railed ? expandLabel : collapseLabel;\n\n return (\n <div\n className={className ? `${styles.shell} ${className}` : styles.shell}\n data-console-shell=\"\"\n ref={shellRef}\n {...rest}\n >\n <a\n className={styles.skipLink}\n href={`#${mainId}`}\n onClick={(event) => {\n event.preventDefault();\n document.getElementById(mainId)?.focus();\n }}\n >\n {skipToContentLabel}\n </a>\n <aside className={styles.sidebar} data-collapsed={railed} data-instant={reducedMotion}>\n {!isCompact && sidebar(railed)}\n </aside>\n {isCompact ? (\n <Drawer\n noHeader\n classNames={{ bodyContent: styles.drawerBody }}\n closable={false}\n open={drawerOpen}\n placement=\"left\"\n width={272}\n onClose={closeNavigation}\n >\n <Flexbox height=\"100%\">{sidebar(false)}</Flexbox>\n </Drawer>\n ) : null}\n <div className={styles.workspace}>\n <header className={styles.topbar}>\n <Button\n aria-label={toggleLabel}\n icon={PanelLeft}\n type=\"text\"\n onClick={toggleNavigation}\n />\n <span aria-hidden className={styles.topbarDivider} />\n <div className={styles.topbarMain}>{breadcrumb}</div>\n {tools ? <div className={styles.tools}>{tools}</div> : null}\n </header>\n <main className={styles.main} id={mainId} tabIndex={-1}>\n {children}\n </main>\n </div>\n </div>\n );\n}\n\nConsoleShell.displayName = 'ConsoleShell';\n\nexport default ConsoleShell;\n"],"mappings":"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"}
|
|
@@ -23,6 +23,11 @@ const styles = createStaticStyles(({ css, cssVar, responsive }) => ({
|
|
|
23
23
|
${responsive.laptop} {
|
|
24
24
|
margin-block-start: 0;
|
|
25
25
|
}
|
|
26
|
+
`,
|
|
27
|
+
/** Close button in the drawer's brand row: right edge, vertically centred with the brand. */
|
|
28
|
+
brandClose: css`
|
|
29
|
+
flex: none;
|
|
30
|
+
margin-inline: auto -7px;
|
|
26
31
|
`,
|
|
27
32
|
brandAnchor: css`
|
|
28
33
|
display: flex;
|
|
@@ -87,6 +92,16 @@ const styles = createStaticStyles(({ css, cssVar, responsive }) => ({
|
|
|
87
92
|
gap: 16px;
|
|
88
93
|
padding: 16px;
|
|
89
94
|
}
|
|
95
|
+
`,
|
|
96
|
+
/**
|
|
97
|
+
* The drawer body is the scroll container. Without its default padding the scrollbar sits on
|
|
98
|
+
* the panel edge; the brand row, nav and footer keep their own insets.
|
|
99
|
+
*/
|
|
100
|
+
drawerBody: css`
|
|
101
|
+
&& {
|
|
102
|
+
min-block-size: 100%;
|
|
103
|
+
padding: 0;
|
|
104
|
+
}
|
|
90
105
|
`,
|
|
91
106
|
navSlot: css`
|
|
92
107
|
display: flex;
|