@lobehub/ui 6.0.0-beta.4 → 6.0.0-beta.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/es/ActionIcon/style.mjs +1 -1
- package/es/ActionIcon/style.mjs.map +1 -1
- package/es/Anchor/Anchor.mjs +2 -2
- package/es/Anchor/Anchor.mjs.map +1 -1
- package/es/Carousel/Carousel.mjs +1 -1
- package/es/Carousel/Carousel.mjs.map +1 -1
- package/es/CodeDiff/CodeDiff.mjs.map +1 -1
- package/es/CodeDiff/PatchDiff.mjs.map +1 -1
- package/es/CodeDiff/theme.mjs.map +1 -1
- package/es/CodeDiff/type.d.mts +2 -2
- package/es/CodeEditor/CodeEditor.mjs +1 -1
- package/es/CodeEditor/CodeEditor.mjs.map +1 -1
- package/es/ColorPicker/ColorPicker.mjs +1 -1
- package/es/ColorPicker/ColorPicker.mjs.map +1 -1
- package/es/ColorSwatches/ColorSwatches.mjs +18 -23
- package/es/ColorSwatches/ColorSwatches.mjs.map +1 -1
- package/es/ColorSwatches/CustomPicker.mjs +101 -0
- package/es/ColorSwatches/CustomPicker.mjs.map +1 -0
- package/es/ColorSwatches/index.d.mts +2 -2
- package/es/ColorSwatches/marker.stylex.mjs +9 -0
- package/es/ColorSwatches/marker.stylex.mjs.map +1 -0
- package/es/ColorSwatches/style.mjs +36 -0
- package/es/ColorSwatches/style.mjs.map +1 -1
- package/es/ColorSwatches/type.d.mts +9 -0
- package/es/ConfigProvider/cdnAssets.mjs +6 -0
- package/es/ConfigProvider/cdnAssets.mjs.map +1 -0
- package/es/ConfigProvider/index.mjs +2 -1
- package/es/ConfigProvider/index.mjs.map +1 -1
- package/es/ContextMenu/store.d.mts +0 -1
- package/es/ContextMenu/store.mjs.map +1 -1
- package/es/DatePicker/DatePicker.mjs +1 -1
- package/es/DatePicker/DatePicker.mjs.map +1 -1
- package/es/DatePicker/DateRangePicker.mjs +1 -1
- package/es/DatePicker/DateRangePicker.mjs.map +1 -1
- package/es/DraggablePanel/atoms.mjs +1 -1
- package/es/DraggablePanel/atoms.mjs.map +1 -1
- package/es/DraggableSideNav/DraggableSideNav.mjs +2 -2
- package/es/DraggableSideNav/DraggableSideNav.mjs.map +1 -1
- package/es/Drawer/atoms.mjs +1 -1
- package/es/Drawer/atoms.mjs.map +1 -1
- package/es/DropdownMenu/atoms.mjs +1 -1
- package/es/DropdownMenu/atoms.mjs.map +1 -1
- package/es/EditableText/ControlInput.mjs +42 -29
- package/es/EditableText/ControlInput.mjs.map +1 -1
- package/es/EditableText/EditableText.mjs +1 -1
- package/es/EditableText/EditableText.mjs.map +1 -1
- package/es/EmojiPicker/EmojiPicker.mjs +3 -9
- package/es/EmojiPicker/EmojiPicker.mjs.map +1 -1
- package/es/EmojiPicker/useEmojiMartI18n.mjs +37 -0
- package/es/EmojiPicker/useEmojiMartI18n.mjs.map +1 -0
- package/es/FormKit/engine/tanstack.mjs +1 -1
- package/es/FormKit/engine/tanstack.mjs.map +1 -1
- package/es/Grid/style.mjs +1 -1
- package/es/Grid/style.mjs.map +1 -1
- package/es/HotkeyInput/HotkeyInput.mjs +1 -1
- package/es/HotkeyInput/HotkeyInput.mjs.map +1 -1
- package/es/Image/type.d.mts +1 -0
- package/es/Image/viewer/ImageViewer.mjs +2 -1
- 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/ImageSelect/ImageSelect.mjs +1 -1
- package/es/ImageSelect/ImageSelect.mjs.map +1 -1
- package/es/Input/Input.mjs +7 -5
- package/es/Input/Input.mjs.map +1 -1
- package/es/Input/TextArea.mjs +1 -1
- package/es/Input/TextArea.mjs.map +1 -1
- package/es/Input/style.mjs +3 -3
- package/es/Input/style.mjs.map +1 -1
- package/es/Modal/atoms.mjs +3 -3
- package/es/Modal/atoms.mjs.map +1 -1
- package/es/Pagination/Pagination.mjs +1 -1
- package/es/Pagination/Pagination.mjs.map +1 -1
- package/es/Popover/PopoverInGroup.mjs +1 -1
- package/es/Popover/PopoverInGroup.mjs.map +1 -1
- package/es/Popover/atoms.mjs +1 -1
- package/es/Popover/atoms.mjs.map +1 -1
- package/es/Rate/Rate.mjs +1 -1
- package/es/Rate/Rate.mjs.map +1 -1
- package/es/ScrollShadow/ScrollShadow.mjs +1 -1
- package/es/ScrollShadow/ScrollShadow.mjs.map +1 -1
- package/es/SearchBar/SearchBar.mjs +31 -25
- package/es/SearchBar/SearchBar.mjs.map +1 -1
- package/es/Segmented/Segmented.mjs +1 -1
- package/es/Segmented/Segmented.mjs.map +1 -1
- package/es/Select/atoms.mjs +1 -1
- package/es/Select/atoms.mjs.map +1 -1
- package/es/Switch/atoms.mjs +1 -1
- package/es/Switch/atoms.mjs.map +1 -1
- package/es/Tabs/Tabs.mjs +35 -21
- package/es/Tabs/Tabs.mjs.map +1 -1
- package/es/ToggleGroup/ToggleGroup.mjs +1 -1
- package/es/ToggleGroup/ToggleGroup.mjs.map +1 -1
- package/es/Tooltip/TooltipInGroup.mjs +1 -1
- package/es/Tooltip/TooltipInGroup.mjs.map +1 -1
- package/es/Tooltip/TooltipStandalone.mjs +1 -1
- package/es/Tooltip/TooltipStandalone.mjs.map +1 -1
- package/es/Tree/Tree.mjs +1 -1
- package/es/Tree/Tree.mjs.map +1 -1
- package/es/awesome/Giscus/style.mjs +9 -9
- package/es/awesome/Giscus/style.mjs.map +1 -1
- package/es/awesome/Grainient/Grainient.d.mts +11 -0
- package/es/awesome/Grainient/Grainient.mjs +314 -0
- package/es/awesome/Grainient/Grainient.mjs.map +1 -0
- package/es/awesome/Grainient/index.d.mts +3 -0
- package/es/awesome/Grainient/style.mjs +23 -0
- package/es/awesome/Grainient/style.mjs.map +1 -0
- package/es/awesome/Grainient/type.d.mts +121 -0
- package/es/awesome/GridBackground/GridBackground.mjs +1 -1
- package/es/awesome/GridBackground/GridBackground.mjs.map +1 -1
- package/es/awesome/GridBackground/GridShowcase.mjs +4 -4
- package/es/awesome/GridBackground/GridShowcase.mjs.map +1 -1
- package/es/awesome/Spline/ParentSize.mjs +1 -1
- package/es/awesome/Spline/ParentSize.mjs.map +1 -1
- package/es/awesome/index.d.mts +4 -1
- package/es/awesome/index.mjs +2 -1
- package/es/chat/BackBottom/BackBottom.mjs +1 -1
- package/es/chat/BackBottom/BackBottom.mjs.map +1 -1
- package/es/chat/BackBottom/type.d.mts +2 -2
- package/es/chat/EditableMessage/EditableMessage.mjs +1 -1
- package/es/chat/EditableMessage/EditableMessage.mjs.map +1 -1
- package/es/chat/EditableMessageList/EditableMessageList.mjs +1 -1
- package/es/chat/EditableMessageList/EditableMessageList.mjs.map +1 -1
- package/es/chat/EditableMessageList/messageReducer.mjs +30 -23
- package/es/chat/EditableMessageList/messageReducer.mjs.map +1 -1
- package/es/chat/MessageModal/MessageModal.mjs +1 -1
- package/es/chat/MessageModal/MessageModal.mjs.map +1 -1
- package/es/chat/TokenTag/TokenTag.mjs +2 -3
- package/es/chat/TokenTag/TokenTag.mjs.map +1 -1
- package/es/dashboard/AuthLayout/Aside.d.mts +10 -0
- package/es/dashboard/AuthLayout/Aside.mjs +31 -0
- package/es/dashboard/AuthLayout/Aside.mjs.map +1 -0
- package/es/dashboard/AuthLayout/AuthLayout.d.mts +3 -1
- package/es/dashboard/AuthLayout/AuthLayout.mjs +93 -37
- package/es/dashboard/AuthLayout/AuthLayout.mjs.map +1 -1
- package/es/dashboard/AuthLayout/index.d.mts +3 -2
- package/es/dashboard/AuthLayout/style.mjs +98 -1
- package/es/dashboard/AuthLayout/style.mjs.map +1 -1
- package/es/dashboard/AuthLayout/type.d.mts +38 -2
- package/es/dashboard/AuthTools/AuthLocaleSwitch.d.mts +10 -0
- package/es/dashboard/AuthTools/AuthLocaleSwitch.mjs +33 -0
- package/es/dashboard/AuthTools/AuthLocaleSwitch.mjs.map +1 -0
- package/es/dashboard/AuthTools/AuthThemeSwitch.d.mts +10 -0
- package/es/dashboard/AuthTools/AuthThemeSwitch.mjs +59 -0
- package/es/dashboard/AuthTools/AuthThemeSwitch.mjs.map +1 -0
- package/es/dashboard/AuthTools/index.d.mts +4 -0
- package/es/dashboard/AuthTools/style.mjs +27 -0
- package/es/dashboard/AuthTools/style.mjs.map +1 -0
- package/es/dashboard/AuthTools/type.d.mts +29 -0
- package/es/dashboard/ChartGrid/ChartGrid.d.mts +10 -0
- package/es/dashboard/ChartGrid/ChartGrid.mjs +19 -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 +18 -0
- package/es/dashboard/ChartGrid/style.mjs.map +1 -0
- package/es/dashboard/ChartGrid/type.d.mts +8 -0
- package/es/dashboard/ConsoleNav/Skeleton.d.mts +15 -0
- package/es/dashboard/ConsoleNav/Skeleton.mjs +82 -0
- package/es/dashboard/ConsoleNav/Skeleton.mjs.map +1 -0
- package/es/dashboard/ConsoleNav/index.d.mts +3 -2
- package/es/dashboard/ConsoleNav/style.mjs +18 -0
- package/es/dashboard/ConsoleNav/style.mjs.map +1 -1
- package/es/dashboard/ConsoleNav/type.d.mts +13 -0
- package/es/dashboard/ConsoleShell/ConsoleShell.d.mts +1 -1
- package/es/dashboard/ConsoleShell/ConsoleShell.mjs +26 -6
- package/es/dashboard/ConsoleShell/ConsoleShell.mjs.map +1 -1
- package/es/dashboard/ConsoleShell/style.mjs +10 -0
- package/es/dashboard/ConsoleShell/style.mjs.map +1 -1
- package/es/dashboard/ConsoleShell/type.d.mts +11 -0
- package/es/dashboard/FilterSidebar/FilterSidebar.mjs +18 -17
- package/es/dashboard/FilterSidebar/FilterSidebar.mjs.map +1 -1
- package/es/dashboard/FilterStrip/FilterStrip.d.mts +11 -0
- package/es/dashboard/FilterStrip/FilterStrip.mjs +55 -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 +78 -0
- package/es/dashboard/FilterStrip/style.mjs.map +1 -0
- package/es/dashboard/FilterStrip/type.d.mts +25 -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 +11 -0
- package/es/dashboard/MetricStrip/MetricCell.mjs +81 -0
- package/es/dashboard/MetricStrip/MetricCell.mjs.map +1 -0
- package/es/dashboard/MetricStrip/MetricStrip.d.mts +13 -0
- package/es/dashboard/MetricStrip/MetricStrip.mjs +47 -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 +156 -0
- package/es/dashboard/MetricStrip/style.mjs.map +1 -0
- package/es/dashboard/MetricStrip/type.d.mts +36 -0
- package/es/dashboard/PageSkeleton/PageSkeleton.mjs +20 -18
- package/es/dashboard/PageSkeleton/PageSkeleton.mjs.map +1 -1
- package/es/dashboard/PageSkeleton/style.mjs +1 -1
- package/es/dashboard/PageSkeleton/style.mjs.map +1 -1
- package/es/dashboard/PageToolbar/PageToolbar.d.mts +10 -0
- package/es/dashboard/PageToolbar/PageToolbar.mjs +22 -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 +36 -0
- package/es/dashboard/PageToolbar/style.mjs.map +1 -0
- package/es/dashboard/PageToolbar/type.d.mts +11 -0
- package/es/dashboard/PanelSurface/PanelSurface.d.mts +10 -0
- package/es/dashboard/PanelSurface/PanelSurface.mjs +40 -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 +25 -0
- package/es/dashboard/PanelSurface/style.mjs.map +1 -0
- package/es/dashboard/PanelSurface/type.d.mts +17 -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 +31 -0
- package/es/dashboard/Section/style.mjs.map +1 -0
- package/es/dashboard/Section/type.d.mts +23 -0
- package/es/dashboard/StatCard/StatCard.d.mts +2 -1
- package/es/dashboard/StatCard/StatCard.mjs +28 -91
- package/es/dashboard/StatCard/StatCard.mjs.map +1 -1
- package/es/dashboard/StatCard/type.d.mts +16 -5
- package/es/dashboard/Surface/Surface.d.mts +4 -3
- package/es/dashboard/Surface/Surface.mjs +91 -8
- 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 +16 -0
- package/es/dashboard/Surface/link.mjs +40 -0
- package/es/dashboard/Surface/link.mjs.map +1 -0
- package/es/dashboard/Surface/marker.stylex.mjs +9 -0
- package/es/dashboard/Surface/marker.stylex.mjs.map +1 -0
- package/es/dashboard/Surface/style.d.mts +2 -0
- package/es/dashboard/Surface/style.mjs +155 -14
- package/es/dashboard/Surface/style.mjs.map +1 -1
- package/es/dashboard/Surface/type.d.mts +29 -1
- 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 +17 -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 +10 -0
- package/es/dashboard/index.d.mts +38 -4
- package/es/dashboard/index.mjs +17 -3
- package/es/hooks/useControlledState.mjs +20 -0
- package/es/hooks/useControlledState.mjs.map +1 -0
- package/es/hooks/useHighlight.mjs +2 -2
- package/es/hooks/useHighlight.mjs.map +1 -1
- package/es/hooks/useHover.mjs +22 -0
- package/es/hooks/useHover.mjs.map +1 -0
- package/es/hooks/useScroll.d.mts +8 -0
- package/es/hooks/useScroll.mjs +33 -0
- package/es/hooks/useScroll.mjs.map +1 -0
- package/es/hooks/useSize.mjs +26 -0
- package/es/hooks/useSize.mjs.map +1 -0
- package/es/internal/virtual/VirtualScrollArea.mjs +1 -1
- package/es/internal/virtual/VirtualScrollArea.mjs.map +1 -1
- package/es/mobile/ChatInputArea/ChatInputArea.mjs +1 -1
- package/es/mobile/ChatInputArea/ChatInputArea.mjs.map +1 -1
- package/es/mobile/TabBar/TabBar.mjs +1 -1
- package/es/mobile/TabBar/TabBar.mjs.map +1 -1
- package/es/node_modules/.pnpm/@eslint_core@1.2.1/node_modules/@eslint/core/dist/esm/types.d.mts +2 -46
- package/es/node_modules/.pnpm/@eslint_plugin-kit@0.7.3/node_modules/@eslint/plugin-kit/dist/cjs/index.d.mts +1 -1
- package/es/node_modules/.pnpm/@types_estree@1.0.9/node_modules/@types/estree/index.d.mts +1 -4
- package/es/node_modules/.pnpm/eslint@10.12.0_jiti@2.6.1_supports-color@10.2.2/node_modules/eslint/lib/types/index.d.mts +1 -1
- package/es/style.css +339 -32
- package/es/styles/theme/token/neutral.mjs +5 -5
- package/es/styles/theme/token/neutral.mjs.map +1 -1
- package/es/utils/colorMath.mjs +52 -0
- package/es/utils/colorMath.mjs.map +1 -0
- package/es/utils/formatNumber.mjs +7 -0
- package/es/utils/formatNumber.mjs.map +1 -0
- package/es/utils/genCdnUrl.mjs +6 -3
- package/es/utils/genCdnUrl.mjs.map +1 -1
- package/es/utils/hashString.mjs +19 -0
- package/es/utils/hashString.mjs.map +1 -0
- package/es/utils/mergeRefs.mjs +20 -0
- package/es/utils/mergeRefs.mjs.map +1 -0
- package/es/utils/placement.d.mts +5 -5
- package/es/utils/placement.mjs.map +1 -1
- package/package.json +16 -42
- package/es/dashboard/StatCard/style.mjs +0 -105
- package/es/dashboard/StatCard/style.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tanstack.mjs","names":[],"sources":["../../../src/FormKit/engine/tanstack.ts"],"sourcesContent":["import { FormApi, getBy, setBy } from '@tanstack/form-core';\nimport isEqual from 'fast-deep-equal';\n\nimport type {\n ChangeSource,\n CreateEngine,\n FieldRegistration,\n FieldSnapshot,\n FormEngine,\n FormStatus,\n FormValidator,\n ValidateResult,\n ValidateTrigger,\n ValueChange,\n} from './types';\n\nconst toEnginePath = (path: string) => path.replaceAll(/\\.(\\d+)(?=\\.|$)/g, '[$1]');\n\nconst isSameOrNested = (a: string, b: string) =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`);\n\nconst isUnder = (path: string, root: string) => path === root || path.startsWith(`${root}.`);\n\ninterface FieldRuntime {\n registration: FieldRegistration;\n timer?: ReturnType<typeof setTimeout>;\n token: number;\n}\n\nexport const createTanstackEngine: CreateEngine = (initialValues) => {\n const form = new FormApi({ defaultValues: initialValues });\n\n const fields = new Map<string, FieldRuntime>();\n const fieldErrors = new Map<string, string>();\n const formErrors = new Map<string, string>();\n const serverErrors = new Map<string, string>();\n const validating = new Set<string>();\n const touched = new Set<string>();\n const listeners = new Set<() => void>();\n const valueListeners = new Set<(change: ValueChange) => void>();\n const snapshots = new Map<string, FieldSnapshot>();\n\n let formValidator: FormValidator | undefined;\n let baseline = initialValues;\n let dirtyCache: { baseline: unknown; dirty: boolean; values: unknown } | undefined;\n let inflightSubmit: Promise<ValidateResult> | undefined;\n let submitting = false;\n let submitCount = 0;\n let status: FormStatus = { dirty: false, submitCount: 0, submitting: false };\n\n const notify = () => {\n for (const listener of listeners) listener();\n };\n const storeSubscription = form.store.subscribe(notify);\n\n const getValue = (path: string) => form.getFieldValue(toEnginePath(path) as never) as unknown;\n const getValues = () => form.state.values as Record<string, unknown>;\n const isTouched = (path: string) => {\n for (const entry of touched) if (isSameOrNested(entry, path)) return true;\n return false;\n };\n const errorOf = (path: string) =>\n serverErrors.get(path) ?? fieldErrors.get(path) ?? formErrors.get(path);\n\n const getField = (path: string): FieldSnapshot => {\n const next: FieldSnapshot = {\n error: errorOf(path),\n touched: isTouched(path),\n validating: validating.has(path),\n value: getValue(path),\n };\n const prev = snapshots.get(path);\n if (\n prev &&\n prev.error === next.error &&\n prev.touched === next.touched &&\n prev.validating === next.validating &&\n Object.is(prev.value, next.value)\n )\n return prev;\n snapshots.set(path, next);\n return next;\n };\n\n const getStatus = (): FormStatus => {\n const values = getValues();\n if (dirtyCache?.values !== values || dirtyCache.baseline !== baseline)\n dirtyCache = { baseline, dirty: !isEqual(values, baseline), values };\n const { dirty } = dirtyCache;\n if (\n status.dirty !== dirty ||\n status.submitting !== submitting ||\n status.submitCount !== submitCount\n )\n status = { dirty, submitCount, submitting };\n return status;\n };\n\n const shouldValidate = (path: string, trigger: ValidateTrigger) => {\n const mode = fields.get(path)?.registration.validateOn ?? 'auto';\n if (trigger === 'submit') return true;\n if (mode === 'change') return true;\n if (mode === 'blur') return trigger === 'blur';\n if (mode === 'submit') return false;\n return trigger === 'blur' || errorOf(path) !== undefined;\n };\n\n const setOrDelete = (map: Map<string, string>, path: string, error: string | undefined) => {\n if (error === undefined) map.delete(path);\n else map.set(path, error);\n };\n\n const runField = (path: string, withFormValidator: boolean): Promise<void> => {\n const runtime = fields.get(path);\n if (!runtime) return Promise.resolve();\n runtime.token += 1;\n const token = runtime.token;\n const { validate } = runtime.registration;\n if (!validate && !(withFormValidator && formValidator)) return Promise.resolve();\n\n validating.add(path);\n notify();\n return (async () => {\n const values = getValues();\n const [fieldError, formResult] = await Promise.all([\n validate?.(getValue(path), values),\n withFormValidator ? formValidator?.(values) : undefined,\n ]);\n if (runtime.token !== token || fields.get(path) !== runtime) return;\n setOrDelete(fieldErrors, path, fieldError);\n if (withFormValidator) setOrDelete(formErrors, path, formResult?.[path]);\n validating.delete(path);\n notify();\n })();\n };\n\n const schedule = (path: string, trigger: ValidateTrigger) => {\n const runtime = fields.get(path);\n if (!runtime || !shouldValidate(path, trigger)) return;\n clearTimeout(runtime.timer);\n const debounceMs = runtime.registration.debounceMs ?? 0;\n if (trigger === 'change' && debounceMs > 0) {\n runtime.token += 1;\n runtime.timer = setTimeout(() => void runField(path, true), debounceMs);\n return;\n }\n void runField(path, true);\n };\n\n const clearErrorsUnder = (path: string, inclusive = false) => {\n for (const map of [fieldErrors, formErrors, serverErrors])\n for (const key of map.keys())\n if (inclusive ? isUnder(key, path) : key.startsWith(`${path}.`)) map.delete(key);\n };\n\n const resetTo = (values: Record<string, unknown>) => {\n clearRuntimeState();\n baseline = values;\n form.reset(values);\n notify();\n };\n\n const emitValueChange = (path: string, source: ChangeSource) => {\n for (const listener of valueListeners) listener({ path, source });\n };\n\n const afterUserChange = (path: string) => {\n touched.add(path);\n serverErrors.delete(path);\n schedule(path, 'change');\n for (const [other, runtime] of fields) {\n if (other !== path && runtime.registration.deps?.includes(path) && isTouched(other))\n void runField(other, true);\n }\n notify();\n };\n\n const validate = async (paths?: string[]): Promise<ValidateResult> => {\n const values = getValues();\n const formResult = (formValidator ? await formValidator(values) : undefined) ?? {};\n const targets = paths ?? [...new Set([...fields.keys(), ...Object.keys(formResult)])];\n await Promise.all(targets.map((path) => runField(path, false)));\n for (const path of targets) setOrDelete(formErrors, path, formResult[path]);\n notify();\n const errors: Record<string, string> = {};\n for (const path of targets) {\n const error = errorOf(path);\n if (error !== undefined) errors[path] = error;\n }\n return { errors, valid: Object.keys(errors).length === 0 };\n };\n\n const clearRuntimeState = () => {\n for (const runtime of fields.values()) {\n clearTimeout(runtime.timer);\n runtime.token += 1;\n }\n fieldErrors.clear();\n formErrors.clear();\n serverErrors.clear();\n validating.clear();\n touched.clear();\n };\n\n const runSubmit = async (onSubmit?: (values: Record<string, unknown>) => unknown) => {\n submitCount += 1;\n serverErrors.clear();\n for (const path of fields.keys()) touched.add(path);\n let values: Record<string, unknown>;\n let result: ValidateResult;\n do {\n values = getValues();\n result = await validate();\n // Async validators must finish against unchanged values before submission.\n } while (values !== getValues());\n if (!result.valid) {\n notify();\n return result;\n }\n submitting = true;\n notify();\n try {\n await onSubmit?.(values);\n if (serverErrors.size > 0) return { errors: Object.fromEntries(serverErrors), valid: false };\n resetTo(getValues());\n } finally {\n submitting = false;\n notify();\n }\n return result;\n };\n\n const engine: FormEngine = {\n blurField: (path) => {\n touched.add(path);\n schedule(path, 'blur');\n notify();\n },\n destroy: () => {\n for (const runtime of fields.values()) clearTimeout(runtime.timer);\n storeSubscription.unsubscribe();\n listeners.clear();\n valueListeners.clear();\n },\n getField,\n getStatus,\n getValue,\n getValues,\n isTouched,\n moveItem: (path, from, to) => {\n form.moveFieldValues(toEnginePath(path) as never, from, to);\n touched.add(path);\n clearErrorsUnder(path);\n emitValueChange(path, 'user');\n notify();\n },\n onValueChange: (listener) => {\n valueListeners.add(listener);\n return () => valueListeners.delete(listener);\n },\n pushItem: (path, item) => {\n form.pushFieldValue(toEnginePath(path) as never, item as never);\n touched.add(path);\n emitValueChange(path, 'user');\n notify();\n },\n registerField: (path, registration) => {\n const runtime: FieldRuntime = { registration, token: 0 };\n fields.set(path, runtime);\n return () => {\n if (fields.get(path) !== runtime) return;\n clearTimeout(runtime.timer);\n fields.delete(path);\n fieldErrors.delete(path);\n validating.delete(path);\n snapshots.delete(path);\n notify();\n };\n },\n removeItem: (path, index) => {\n void form.removeFieldValue(toEnginePath(path) as never, index);\n touched.add(path);\n clearErrorsUnder(path);\n emitValueChange(path, 'user');\n notify();\n },\n reset: (values) => resetTo(values ?? baseline),\n resetField: (path) => {\n for (const [key, runtime] of fields) {\n if (!isUnder(key, path)) continue;\n clearTimeout(runtime.timer);\n runtime.token += 1;\n validating.delete(key);\n }\n for (const entry of touched) if (isUnder(entry, path)) touched.delete(entry);\n clearErrorsUnder(path, true);\n form.setFieldValue(toEnginePath(path) as never, getBy(baseline, toEnginePath(path)), {\n dontUpdateMeta: true,\n });\n emitValueChange(path, 'api');\n notify();\n },\n setErrors: (errors) => {\n for (const [path, error] of Object.entries(errors)) setOrDelete(serverErrors, path, error);\n notify();\n },\n setFormValidator: (validator) => {\n formValidator = validator;\n },\n setValue: (path, value, source) => {\n form.setFieldValue(toEnginePath(path) as never, value as never, { dontUpdateMeta: true });\n if (source === 'api') baseline = setBy(baseline, toEnginePath(path), () => value);\n emitValueChange(path, source);\n if (source === 'user') afterUserChange(path);\n },\n submit: (onSubmit) => {\n inflightSubmit ??= runSubmit(onSubmit).finally(() => {\n inflightSubmit = undefined;\n });\n return inflightSubmit;\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n validate,\n };\n\n return engine;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tanstack.mjs","names":[],"sources":["../../../src/FormKit/engine/tanstack.ts"],"sourcesContent":["import { FormApi, getBy, setBy } from '@tanstack/form-core';\nimport { isEqual } from 'es-toolkit/compat';\n\nimport type {\n ChangeSource,\n CreateEngine,\n FieldRegistration,\n FieldSnapshot,\n FormEngine,\n FormStatus,\n FormValidator,\n ValidateResult,\n ValidateTrigger,\n ValueChange,\n} from './types';\n\nconst toEnginePath = (path: string) => path.replaceAll(/\\.(\\d+)(?=\\.|$)/g, '[$1]');\n\nconst isSameOrNested = (a: string, b: string) =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`);\n\nconst isUnder = (path: string, root: string) => path === root || path.startsWith(`${root}.`);\n\ninterface FieldRuntime {\n registration: FieldRegistration;\n timer?: ReturnType<typeof setTimeout>;\n token: number;\n}\n\nexport const createTanstackEngine: CreateEngine = (initialValues) => {\n const form = new FormApi({ defaultValues: initialValues });\n\n const fields = new Map<string, FieldRuntime>();\n const fieldErrors = new Map<string, string>();\n const formErrors = new Map<string, string>();\n const serverErrors = new Map<string, string>();\n const validating = new Set<string>();\n const touched = new Set<string>();\n const listeners = new Set<() => void>();\n const valueListeners = new Set<(change: ValueChange) => void>();\n const snapshots = new Map<string, FieldSnapshot>();\n\n let formValidator: FormValidator | undefined;\n let baseline = initialValues;\n let dirtyCache: { baseline: unknown; dirty: boolean; values: unknown } | undefined;\n let inflightSubmit: Promise<ValidateResult> | undefined;\n let submitting = false;\n let submitCount = 0;\n let status: FormStatus = { dirty: false, submitCount: 0, submitting: false };\n\n const notify = () => {\n for (const listener of listeners) listener();\n };\n const storeSubscription = form.store.subscribe(notify);\n\n const getValue = (path: string) => form.getFieldValue(toEnginePath(path) as never) as unknown;\n const getValues = () => form.state.values as Record<string, unknown>;\n const isTouched = (path: string) => {\n for (const entry of touched) if (isSameOrNested(entry, path)) return true;\n return false;\n };\n const errorOf = (path: string) =>\n serverErrors.get(path) ?? fieldErrors.get(path) ?? formErrors.get(path);\n\n const getField = (path: string): FieldSnapshot => {\n const next: FieldSnapshot = {\n error: errorOf(path),\n touched: isTouched(path),\n validating: validating.has(path),\n value: getValue(path),\n };\n const prev = snapshots.get(path);\n if (\n prev &&\n prev.error === next.error &&\n prev.touched === next.touched &&\n prev.validating === next.validating &&\n Object.is(prev.value, next.value)\n )\n return prev;\n snapshots.set(path, next);\n return next;\n };\n\n const getStatus = (): FormStatus => {\n const values = getValues();\n if (dirtyCache?.values !== values || dirtyCache.baseline !== baseline)\n dirtyCache = { baseline, dirty: !isEqual(values, baseline), values };\n const { dirty } = dirtyCache;\n if (\n status.dirty !== dirty ||\n status.submitting !== submitting ||\n status.submitCount !== submitCount\n )\n status = { dirty, submitCount, submitting };\n return status;\n };\n\n const shouldValidate = (path: string, trigger: ValidateTrigger) => {\n const mode = fields.get(path)?.registration.validateOn ?? 'auto';\n if (trigger === 'submit') return true;\n if (mode === 'change') return true;\n if (mode === 'blur') return trigger === 'blur';\n if (mode === 'submit') return false;\n return trigger === 'blur' || errorOf(path) !== undefined;\n };\n\n const setOrDelete = (map: Map<string, string>, path: string, error: string | undefined) => {\n if (error === undefined) map.delete(path);\n else map.set(path, error);\n };\n\n const runField = (path: string, withFormValidator: boolean): Promise<void> => {\n const runtime = fields.get(path);\n if (!runtime) return Promise.resolve();\n runtime.token += 1;\n const token = runtime.token;\n const { validate } = runtime.registration;\n if (!validate && !(withFormValidator && formValidator)) return Promise.resolve();\n\n validating.add(path);\n notify();\n return (async () => {\n const values = getValues();\n const [fieldError, formResult] = await Promise.all([\n validate?.(getValue(path), values),\n withFormValidator ? formValidator?.(values) : undefined,\n ]);\n if (runtime.token !== token || fields.get(path) !== runtime) return;\n setOrDelete(fieldErrors, path, fieldError);\n if (withFormValidator) setOrDelete(formErrors, path, formResult?.[path]);\n validating.delete(path);\n notify();\n })();\n };\n\n const schedule = (path: string, trigger: ValidateTrigger) => {\n const runtime = fields.get(path);\n if (!runtime || !shouldValidate(path, trigger)) return;\n clearTimeout(runtime.timer);\n const debounceMs = runtime.registration.debounceMs ?? 0;\n if (trigger === 'change' && debounceMs > 0) {\n runtime.token += 1;\n runtime.timer = setTimeout(() => void runField(path, true), debounceMs);\n return;\n }\n void runField(path, true);\n };\n\n const clearErrorsUnder = (path: string, inclusive = false) => {\n for (const map of [fieldErrors, formErrors, serverErrors])\n for (const key of map.keys())\n if (inclusive ? isUnder(key, path) : key.startsWith(`${path}.`)) map.delete(key);\n };\n\n const resetTo = (values: Record<string, unknown>) => {\n clearRuntimeState();\n baseline = values;\n form.reset(values);\n notify();\n };\n\n const emitValueChange = (path: string, source: ChangeSource) => {\n for (const listener of valueListeners) listener({ path, source });\n };\n\n const afterUserChange = (path: string) => {\n touched.add(path);\n serverErrors.delete(path);\n schedule(path, 'change');\n for (const [other, runtime] of fields) {\n if (other !== path && runtime.registration.deps?.includes(path) && isTouched(other))\n void runField(other, true);\n }\n notify();\n };\n\n const validate = async (paths?: string[]): Promise<ValidateResult> => {\n const values = getValues();\n const formResult = (formValidator ? await formValidator(values) : undefined) ?? {};\n const targets = paths ?? [...new Set([...fields.keys(), ...Object.keys(formResult)])];\n await Promise.all(targets.map((path) => runField(path, false)));\n for (const path of targets) setOrDelete(formErrors, path, formResult[path]);\n notify();\n const errors: Record<string, string> = {};\n for (const path of targets) {\n const error = errorOf(path);\n if (error !== undefined) errors[path] = error;\n }\n return { errors, valid: Object.keys(errors).length === 0 };\n };\n\n const clearRuntimeState = () => {\n for (const runtime of fields.values()) {\n clearTimeout(runtime.timer);\n runtime.token += 1;\n }\n fieldErrors.clear();\n formErrors.clear();\n serverErrors.clear();\n validating.clear();\n touched.clear();\n };\n\n const runSubmit = async (onSubmit?: (values: Record<string, unknown>) => unknown) => {\n submitCount += 1;\n serverErrors.clear();\n for (const path of fields.keys()) touched.add(path);\n let values: Record<string, unknown>;\n let result: ValidateResult;\n do {\n values = getValues();\n result = await validate();\n // Async validators must finish against unchanged values before submission.\n } while (values !== getValues());\n if (!result.valid) {\n notify();\n return result;\n }\n submitting = true;\n notify();\n try {\n await onSubmit?.(values);\n if (serverErrors.size > 0) return { errors: Object.fromEntries(serverErrors), valid: false };\n resetTo(getValues());\n } finally {\n submitting = false;\n notify();\n }\n return result;\n };\n\n const engine: FormEngine = {\n blurField: (path) => {\n touched.add(path);\n schedule(path, 'blur');\n notify();\n },\n destroy: () => {\n for (const runtime of fields.values()) clearTimeout(runtime.timer);\n storeSubscription.unsubscribe();\n listeners.clear();\n valueListeners.clear();\n },\n getField,\n getStatus,\n getValue,\n getValues,\n isTouched,\n moveItem: (path, from, to) => {\n form.moveFieldValues(toEnginePath(path) as never, from, to);\n touched.add(path);\n clearErrorsUnder(path);\n emitValueChange(path, 'user');\n notify();\n },\n onValueChange: (listener) => {\n valueListeners.add(listener);\n return () => valueListeners.delete(listener);\n },\n pushItem: (path, item) => {\n form.pushFieldValue(toEnginePath(path) as never, item as never);\n touched.add(path);\n emitValueChange(path, 'user');\n notify();\n },\n registerField: (path, registration) => {\n const runtime: FieldRuntime = { registration, token: 0 };\n fields.set(path, runtime);\n return () => {\n if (fields.get(path) !== runtime) return;\n clearTimeout(runtime.timer);\n fields.delete(path);\n fieldErrors.delete(path);\n validating.delete(path);\n snapshots.delete(path);\n notify();\n };\n },\n removeItem: (path, index) => {\n void form.removeFieldValue(toEnginePath(path) as never, index);\n touched.add(path);\n clearErrorsUnder(path);\n emitValueChange(path, 'user');\n notify();\n },\n reset: (values) => resetTo(values ?? baseline),\n resetField: (path) => {\n for (const [key, runtime] of fields) {\n if (!isUnder(key, path)) continue;\n clearTimeout(runtime.timer);\n runtime.token += 1;\n validating.delete(key);\n }\n for (const entry of touched) if (isUnder(entry, path)) touched.delete(entry);\n clearErrorsUnder(path, true);\n form.setFieldValue(toEnginePath(path) as never, getBy(baseline, toEnginePath(path)), {\n dontUpdateMeta: true,\n });\n emitValueChange(path, 'api');\n notify();\n },\n setErrors: (errors) => {\n for (const [path, error] of Object.entries(errors)) setOrDelete(serverErrors, path, error);\n notify();\n },\n setFormValidator: (validator) => {\n formValidator = validator;\n },\n setValue: (path, value, source) => {\n form.setFieldValue(toEnginePath(path) as never, value as never, { dontUpdateMeta: true });\n if (source === 'api') baseline = setBy(baseline, toEnginePath(path), () => value);\n emitValueChange(path, source);\n if (source === 'user') afterUserChange(path);\n },\n submit: (onSubmit) => {\n inflightSubmit ??= runSubmit(onSubmit).finally(() => {\n inflightSubmit = undefined;\n });\n return inflightSubmit;\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n validate,\n };\n\n return engine;\n};\n"],"mappings":"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"}
|
package/es/Grid/style.mjs
CHANGED
package/es/Grid/style.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.mjs","names":[],"sources":["../../src/Grid/style.ts"],"sourcesContent":["import * as stylex from '@stylexjs/stylex';\n\nexport const styles = stylex.create({\n root: {\n '--gap': 'var(--grid-gap, 1em)',\n '--max-item-width': 'var(--grid-max-item-width, 240px)',\n '--rows': 'var(--grid-rows, 3)',\n 'display': 'grid',\n
|
|
1
|
+
{"version":3,"file":"style.mjs","names":[],"sources":["../../src/Grid/style.ts"],"sourcesContent":["import * as stylex from '@stylexjs/stylex';\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 = stylex.create({\n root: {\n '--gap': 'var(--grid-gap, 1em)',\n '--max-item-width': 'var(--grid-max-item-width, 240px)',\n '--rows': 'var(--grid-rows, 3)',\n 'display': 'grid',\n // eslint-disable-next-line @stylexjs/valid-styles -- the rule cannot resolve a module constant; the compiler inlines it\n 'gridTemplateColumns': gridTemplateColumns,\n },\n});\n"],"mappings":";AAEA,MAAa,SAAS,EACpB,MAAM;CACJ,SAAS;CACT,oBAAoB;CACpB,UAAU;CACV,QAAQ;CACR,QAAQ;CACR,OAAO;AACT,EACF"}
|
|
@@ -4,6 +4,7 @@ import { useThemeMode } from "../styles/theme/scope.mjs";
|
|
|
4
4
|
import { NORMATIVE_MODIFIER, checkIsAppleDevice, splitKeysByPlus } from "../Hotkey/utils.mjs";
|
|
5
5
|
import Hotkey from "../Hotkey/Hotkey.mjs";
|
|
6
6
|
import ActionIcon from "../ActionIcon/ActionIcon.mjs";
|
|
7
|
+
import { useControlledState } from "../hooks/useControlledState.mjs";
|
|
7
8
|
import { useTranslation } from "../i18n/useTranslation.mjs";
|
|
8
9
|
import hotkey_default from "../i18n/resources/en/hotkey.mjs";
|
|
9
10
|
import { styles } from "./style.mjs";
|
|
@@ -12,7 +13,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
12
13
|
import * as stylex from "@stylexjs/stylex";
|
|
13
14
|
import { Undo2Icon, XIcon } from "lucide-react";
|
|
14
15
|
import { isEqual } from "es-toolkit/compat";
|
|
15
|
-
import useControlledState from "use-merge-value";
|
|
16
16
|
import { useHotkeys, useRecordHotkeys } from "react-hotkeys-hook";
|
|
17
17
|
//#region src/HotkeyInput/HotkeyInput.tsx
|
|
18
18
|
const HotkeyInput = memo(({ value = "", defaultValue = "", resetValue = "", onChange, onClear, onConflict, placeholder, disabled, shadow, allowClear, allowReset = true, style, className, hotkeyConflicts = [], variant, texts, isApple, onBlur, onReset, onFocus }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HotkeyInput.mjs","names":["hotkeyMessages","Flexbox"],"sources":["../../src/HotkeyInput/HotkeyInput.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\nimport { isEqual } from 'es-toolkit/compat';\nimport { Undo2Icon, XIcon } from 'lucide-react';\nimport {\n type FocusEvent,\n memo,\n type MouseEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useHotkeys, useRecordHotkeys } from 'react-hotkeys-hook';\nimport useControlledState from 'use-merge-value';\n\nimport ActionIcon from '@/ActionIcon';\nimport { Flexbox } from '@/Flex';\nimport Hotkey from '@/Hotkey';\nimport { checkIsAppleDevice, NORMATIVE_MODIFIER, splitKeysByPlus } from '@/Hotkey/utils';\nimport hotkeyMessages from '@/i18n/resources/en/hotkey';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport { useThemeMode } from '@/styles/theme/scope';\n\nimport { styles } from './style';\nimport { type HotkeyInputProps } from './type';\n\nconst HotkeyInput = memo<HotkeyInputProps>(\n ({\n value = '',\n defaultValue = '',\n resetValue = '',\n onChange,\n onClear,\n onConflict,\n placeholder,\n disabled,\n shadow,\n allowClear,\n allowReset = true,\n style,\n className,\n hotkeyConflicts = [],\n variant,\n texts,\n isApple,\n onBlur,\n onReset,\n onFocus,\n }) => {\n const [isFocused, setIsFocused] = useState(false);\n const [hasConflict, setHasConflict] = useState(false);\n const [hasInvalidCombination, setHasInvalidCombination] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n const { isDarkMode } = useThemeMode();\n const { t } = useTranslation(hotkeyMessages);\n const isAppleDevice = useMemo(() => checkIsAppleDevice(isApple), [isApple]);\n const [hotkeyValue, setHotkeyValue] = useControlledState(defaultValue, {\n defaultValue,\n onChange,\n value,\n });\n\n // 使用 useRecordHotkeys 处理快捷键录入\n const [recordedKeys, { start, stop, isRecording, resetKeys }] = useRecordHotkeys();\n\n useHotkeys(\n '*',\n () => {\n inputRef.current?.blur();\n },\n {\n enableOnContentEditable: true,\n enableOnFormTags: true,\n enabled: isRecording && !disabled,\n keydown: false,\n keyup: true,\n preventDefault: true,\n },\n );\n\n // 处理按键,保证格式正确:修饰键在前,最多一个非修饰键在后\n const formatKeys = useCallback((keysSet: Set<string>) => {\n const modifiers: string[] = [];\n const normalKeys: string[] = [];\n\n for (const key of keysSet) {\n // 处理不同表示的修饰键\n const normalizedKey: any = key.toLowerCase();\n if (NORMATIVE_MODIFIER.includes(normalizedKey)) {\n // 统一修饰键表示\n if (\n (!isAppleDevice && normalizedKey === 'ctrl') ||\n (isAppleDevice && normalizedKey === 'meta')\n ) {\n if (!modifiers.includes('mod')) modifiers.push('mod');\n } else if (!modifiers.includes(normalizedKey)) {\n modifiers.push(normalizedKey);\n }\n } else {\n normalKeys.push(key);\n }\n }\n\n // 至少需要一个修饰键\n if (modifiers.length === 0 && normalKeys.length > 0) {\n return { isValid: false, keys: [] };\n }\n\n // 只允许一个非修饰键,如果有多个,只保留最后一个\n const finalKey = normalKeys.length > 0 ? [normalKeys.at(-1)] : [];\n const shortcuts = [modifiers, finalKey];\n\n return {\n // 组合必须包含至少一个按键\n isValid: shortcuts.every((k) => k.length > 0),\n keys: shortcuts.flat(),\n };\n }, []);\n\n // 获取格式化后的按键字符串\n const { isValid, keys } = formatKeys(recordedKeys);\n const keysString = keys.join('+');\n\n // 检查快捷键冲突\n const checkHotkeyConflict = useCallback(\n (newHotkey: string): boolean => {\n return hotkeyConflicts\n .filter((conflictKey) => conflictKey !== resetValue)\n .some((conflictKey) => {\n const newKeys = splitKeysByPlus(newHotkey);\n const conflictKeys = splitKeysByPlus(conflictKey);\n return isEqual(newKeys, conflictKeys);\n });\n },\n [hotkeyConflicts],\n );\n\n // 当按键组合完成时处理结果\n useEffect(() => {\n if (recordedKeys.size > 0 && !isRecording) {\n if (!isValid) {\n setHasInvalidCombination(true);\n setHasConflict(false);\n return;\n }\n\n setHasInvalidCombination(false);\n const newKeysString = keysString;\n\n // 检查冲突\n const conflict = checkHotkeyConflict(newKeysString);\n if (conflict) {\n setHasConflict(true);\n onConflict?.(newKeysString);\n } else {\n setHasConflict(false);\n setHotkeyValue?.(newKeysString);\n }\n }\n }, [\n recordedKeys,\n isRecording,\n isValid,\n keysString,\n checkHotkeyConflict,\n setHotkeyValue,\n onConflict,\n ]);\n\n // 处理输入框焦点\n const handleFocus = (e: FocusEvent<HTMLInputElement>) => {\n if (disabled) return;\n setIsFocused(true);\n setHasConflict(false);\n setHasInvalidCombination(false);\n start(); // 开始记录\n onFocus?.(e);\n };\n\n const handleBlur = (e: FocusEvent<HTMLInputElement>) => {\n setIsFocused(false);\n stop(); // 停止记录\n onBlur?.(e);\n };\n\n const handleClear = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setHotkeyValue?.('');\n resetKeys();\n setHasConflict(false);\n setHasInvalidCombination(false);\n setIsFocused(false);\n stop();\n onClear?.(hotkeyValue);\n };\n\n // 重置功能\n const handleReset = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setHotkeyValue?.(resetValue);\n resetKeys();\n setHasConflict(false);\n setHasInvalidCombination(false);\n setIsFocused(false);\n stop(); // 停止记录\n onReset?.(hotkeyValue, resetValue);\n };\n\n const handleClick = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (disabled || isFocused) return;\n inputRef.current?.focus();\n };\n\n const resolvedVariant = variant || (isDarkMode ? 'filled' : 'outlined');\n const hasError = hasConflict || hasInvalidCombination;\n\n const placeholderText = placeholder ?? t('hotkey.placeholder');\n const resetTitle = texts?.reset ?? t('hotkey.reset');\n const clearTitle = texts?.clear ?? t('hotkey.clear');\n const conflictText = texts?.conflicts ?? t('hotkey.conflict');\n const invalidText = texts?.invalidCombination ?? t('hotkey.invalidCombination');\n\n return (\n <Flexbox\n className={className}\n gap={8}\n style={{\n position: 'relative',\n ...style,\n }}\n >\n <Flexbox\n horizontal\n align={'center'}\n justify={'space-between'}\n {...stylex.props(\n styles.root,\n styles[resolvedVariant],\n shadow && styles.shadow,\n isFocused && styles.focused,\n hasError && styles.error,\n hasError && resolvedVariant === 'outlined' && styles.errorOutlined,\n disabled && styles.disabled,\n )}\n onClick={handleClick}\n >\n <div style={{ pointerEvents: 'none' }}>\n {isRecording ? (\n <span {...stylex.props(styles.placeholder)}>\n {keys.length > 0 ? <Hotkey keys={keysString} /> : placeholderText}\n </span>\n ) : hotkeyValue ? (\n <Hotkey keys={hotkeyValue} />\n ) : (\n <span {...stylex.props(styles.placeholder)}>{placeholderText}</span>\n )}\n </div>\n\n {/* 隐藏的输入框,用于接收焦点 */}\n <input\n readOnly\n {...stylex.props(styles.hiddenInput)}\n disabled={disabled}\n ref={inputRef as any}\n style={{ pointerEvents: 'none' }}\n onBlur={handleBlur}\n onFocus={handleFocus}\n />\n\n {!isFocused && hotkeyValue && !disabled && (allowReset || allowClear) && (\n <Flexbox horizontal gap={4}>\n {allowReset && hotkeyValue !== resetValue && (\n <ActionIcon\n icon={Undo2Icon}\n size={'small'}\n title={resetTitle}\n variant={'filled'}\n onClick={handleReset}\n />\n )}\n {allowClear && (\n <ActionIcon\n icon={XIcon}\n size={'small'}\n title={clearTitle}\n variant={'filled'}\n onClick={handleClear}\n />\n )}\n </Flexbox>\n )}\n </Flexbox>\n {hasConflict && <div {...stylex.props(styles.errorText)}>{conflictText}</div>}\n {hasInvalidCombination && <div {...stylex.props(styles.errorText)}>{invalidText}</div>}\n </Flexbox>\n );\n },\n);\n\nHotkeyInput.displayName = 'HotkeyInput';\n\nexport default HotkeyInput;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HotkeyInput.mjs","names":["hotkeyMessages","Flexbox"],"sources":["../../src/HotkeyInput/HotkeyInput.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\nimport { isEqual } from 'es-toolkit/compat';\nimport { Undo2Icon, XIcon } from 'lucide-react';\nimport {\n type FocusEvent,\n memo,\n type MouseEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useHotkeys, useRecordHotkeys } from 'react-hotkeys-hook';\n\nimport ActionIcon from '@/ActionIcon';\nimport { Flexbox } from '@/Flex';\nimport { useControlledState } from '@/hooks/useControlledState';\nimport Hotkey from '@/Hotkey';\nimport { checkIsAppleDevice, NORMATIVE_MODIFIER, splitKeysByPlus } from '@/Hotkey/utils';\nimport hotkeyMessages from '@/i18n/resources/en/hotkey';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport { useThemeMode } from '@/styles/theme/scope';\n\nimport { styles } from './style';\nimport { type HotkeyInputProps } from './type';\n\nconst HotkeyInput = memo<HotkeyInputProps>(\n ({\n value = '',\n defaultValue = '',\n resetValue = '',\n onChange,\n onClear,\n onConflict,\n placeholder,\n disabled,\n shadow,\n allowClear,\n allowReset = true,\n style,\n className,\n hotkeyConflicts = [],\n variant,\n texts,\n isApple,\n onBlur,\n onReset,\n onFocus,\n }) => {\n const [isFocused, setIsFocused] = useState(false);\n const [hasConflict, setHasConflict] = useState(false);\n const [hasInvalidCombination, setHasInvalidCombination] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n const { isDarkMode } = useThemeMode();\n const { t } = useTranslation(hotkeyMessages);\n const isAppleDevice = useMemo(() => checkIsAppleDevice(isApple), [isApple]);\n const [hotkeyValue, setHotkeyValue] = useControlledState(defaultValue, {\n defaultValue,\n onChange,\n value,\n });\n\n // 使用 useRecordHotkeys 处理快捷键录入\n const [recordedKeys, { start, stop, isRecording, resetKeys }] = useRecordHotkeys();\n\n useHotkeys(\n '*',\n () => {\n inputRef.current?.blur();\n },\n {\n enableOnContentEditable: true,\n enableOnFormTags: true,\n enabled: isRecording && !disabled,\n keydown: false,\n keyup: true,\n preventDefault: true,\n },\n );\n\n // 处理按键,保证格式正确:修饰键在前,最多一个非修饰键在后\n const formatKeys = useCallback((keysSet: Set<string>) => {\n const modifiers: string[] = [];\n const normalKeys: string[] = [];\n\n for (const key of keysSet) {\n // 处理不同表示的修饰键\n const normalizedKey: any = key.toLowerCase();\n if (NORMATIVE_MODIFIER.includes(normalizedKey)) {\n // 统一修饰键表示\n if (\n (!isAppleDevice && normalizedKey === 'ctrl') ||\n (isAppleDevice && normalizedKey === 'meta')\n ) {\n if (!modifiers.includes('mod')) modifiers.push('mod');\n } else if (!modifiers.includes(normalizedKey)) {\n modifiers.push(normalizedKey);\n }\n } else {\n normalKeys.push(key);\n }\n }\n\n // 至少需要一个修饰键\n if (modifiers.length === 0 && normalKeys.length > 0) {\n return { isValid: false, keys: [] };\n }\n\n // 只允许一个非修饰键,如果有多个,只保留最后一个\n const finalKey = normalKeys.length > 0 ? [normalKeys.at(-1)] : [];\n const shortcuts = [modifiers, finalKey];\n\n return {\n // 组合必须包含至少一个按键\n isValid: shortcuts.every((k) => k.length > 0),\n keys: shortcuts.flat(),\n };\n }, []);\n\n // 获取格式化后的按键字符串\n const { isValid, keys } = formatKeys(recordedKeys);\n const keysString = keys.join('+');\n\n // 检查快捷键冲突\n const checkHotkeyConflict = useCallback(\n (newHotkey: string): boolean => {\n return hotkeyConflicts\n .filter((conflictKey) => conflictKey !== resetValue)\n .some((conflictKey) => {\n const newKeys = splitKeysByPlus(newHotkey);\n const conflictKeys = splitKeysByPlus(conflictKey);\n return isEqual(newKeys, conflictKeys);\n });\n },\n [hotkeyConflicts],\n );\n\n // 当按键组合完成时处理结果\n useEffect(() => {\n if (recordedKeys.size > 0 && !isRecording) {\n if (!isValid) {\n setHasInvalidCombination(true);\n setHasConflict(false);\n return;\n }\n\n setHasInvalidCombination(false);\n const newKeysString = keysString;\n\n // 检查冲突\n const conflict = checkHotkeyConflict(newKeysString);\n if (conflict) {\n setHasConflict(true);\n onConflict?.(newKeysString);\n } else {\n setHasConflict(false);\n setHotkeyValue?.(newKeysString);\n }\n }\n }, [\n recordedKeys,\n isRecording,\n isValid,\n keysString,\n checkHotkeyConflict,\n setHotkeyValue,\n onConflict,\n ]);\n\n // 处理输入框焦点\n const handleFocus = (e: FocusEvent<HTMLInputElement>) => {\n if (disabled) return;\n setIsFocused(true);\n setHasConflict(false);\n setHasInvalidCombination(false);\n start(); // 开始记录\n onFocus?.(e);\n };\n\n const handleBlur = (e: FocusEvent<HTMLInputElement>) => {\n setIsFocused(false);\n stop(); // 停止记录\n onBlur?.(e);\n };\n\n const handleClear = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setHotkeyValue?.('');\n resetKeys();\n setHasConflict(false);\n setHasInvalidCombination(false);\n setIsFocused(false);\n stop();\n onClear?.(hotkeyValue);\n };\n\n // 重置功能\n const handleReset = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setHotkeyValue?.(resetValue);\n resetKeys();\n setHasConflict(false);\n setHasInvalidCombination(false);\n setIsFocused(false);\n stop(); // 停止记录\n onReset?.(hotkeyValue, resetValue);\n };\n\n const handleClick = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (disabled || isFocused) return;\n inputRef.current?.focus();\n };\n\n const resolvedVariant = variant || (isDarkMode ? 'filled' : 'outlined');\n const hasError = hasConflict || hasInvalidCombination;\n\n const placeholderText = placeholder ?? t('hotkey.placeholder');\n const resetTitle = texts?.reset ?? t('hotkey.reset');\n const clearTitle = texts?.clear ?? t('hotkey.clear');\n const conflictText = texts?.conflicts ?? t('hotkey.conflict');\n const invalidText = texts?.invalidCombination ?? t('hotkey.invalidCombination');\n\n return (\n <Flexbox\n className={className}\n gap={8}\n style={{\n position: 'relative',\n ...style,\n }}\n >\n <Flexbox\n horizontal\n align={'center'}\n justify={'space-between'}\n {...stylex.props(\n styles.root,\n styles[resolvedVariant],\n shadow && styles.shadow,\n isFocused && styles.focused,\n hasError && styles.error,\n hasError && resolvedVariant === 'outlined' && styles.errorOutlined,\n disabled && styles.disabled,\n )}\n onClick={handleClick}\n >\n <div style={{ pointerEvents: 'none' }}>\n {isRecording ? (\n <span {...stylex.props(styles.placeholder)}>\n {keys.length > 0 ? <Hotkey keys={keysString} /> : placeholderText}\n </span>\n ) : hotkeyValue ? (\n <Hotkey keys={hotkeyValue} />\n ) : (\n <span {...stylex.props(styles.placeholder)}>{placeholderText}</span>\n )}\n </div>\n\n {/* 隐藏的输入框,用于接收焦点 */}\n <input\n readOnly\n {...stylex.props(styles.hiddenInput)}\n disabled={disabled}\n ref={inputRef as any}\n style={{ pointerEvents: 'none' }}\n onBlur={handleBlur}\n onFocus={handleFocus}\n />\n\n {!isFocused && hotkeyValue && !disabled && (allowReset || allowClear) && (\n <Flexbox horizontal gap={4}>\n {allowReset && hotkeyValue !== resetValue && (\n <ActionIcon\n icon={Undo2Icon}\n size={'small'}\n title={resetTitle}\n variant={'filled'}\n onClick={handleReset}\n />\n )}\n {allowClear && (\n <ActionIcon\n icon={XIcon}\n size={'small'}\n title={clearTitle}\n variant={'filled'}\n onClick={handleClear}\n />\n )}\n </Flexbox>\n )}\n </Flexbox>\n {hasConflict && <div {...stylex.props(styles.errorText)}>{conflictText}</div>}\n {hasInvalidCombination && <div {...stylex.props(styles.errorText)}>{invalidText}</div>}\n </Flexbox>\n );\n },\n);\n\nHotkeyInput.displayName = 'HotkeyInput';\n\nexport default HotkeyInput;\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,MAAM,cAAc,MAAwB,EAC1C,QAAQ,IACR,eAAe,IACf,aAAa,IACb,UACA,SACA,YACA,aACA,UACA,QACA,YACA,aAAa,MACb,OACA,WACA,kBAAkB,CAAC,GACnB,SACA,OACA,SACA,QACA,SACA,cACI;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,aAAa,kBAAkB,SAAS,KAAK;CACpD,MAAM,CAAC,uBAAuB,4BAA4B,SAAS,KAAK;CACxE,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,EACJ,eACE,aAAa;CACjB,MAAM,EACJ,MACE,eAAeA,cAAc;CACjC,MAAM,gBAAgB,cAAc,mBAAmB,OAAO,GAAG,CAAC,OAAO,CAAC;CAC1E,MAAM,CAAC,aAAa,kBAAkB,mBAAmB,cAAc;EACrE;EACA;EACA;CACF,CAAC;CAGD,MAAM,CAAC,cAAc,EACnB,OACA,MACA,aACA,eACG,iBAAiB;CACtB,WAAW,WAAW;EACpB,SAAS,SAAS,KAAK;CACzB,GAAG;EACD,yBAAyB;EACzB,kBAAkB;EAClB,SAAS,eAAe,CAAC;EACzB,SAAS;EACT,OAAO;EACP,gBAAgB;CAClB,CAAC;CAwCD,MAAM,EACJ,SACA,SAvCiB,aAAa,YAAyB;EACvD,MAAM,YAAsB,CAAC;EAC7B,MAAM,aAAuB,CAAC;EAC9B,KAAK,MAAM,OAAO,SAAS;GAEzB,MAAM,gBAAqB,IAAI,YAAY;GAC3C,IAAI,mBAAmB,SAAS,aAAa,GAAG;IAE9C,IAAI,CAAC,iBAAiB,kBAAkB,UAAU,iBAAiB,kBAAkB,QAC/E;SAAA,CAAC,UAAU,SAAS,KAAK,GAAG,UAAU,KAAK,KAAK;IAAA,OAC/C,IAAI,CAAC,UAAU,SAAS,aAAa,GAC1C,UAAU,KAAK,aAAa;GAEhC,OACE,WAAW,KAAK,GAAG;EAEvB;EAGA,IAAI,UAAU,WAAW,KAAK,WAAW,SAAS,GAChD,OAAO;GACL,SAAS;GACT,MAAM,CAAC;EACT;EAKF,MAAM,YAAY,CAAC,WADF,WAAW,SAAS,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,CAC1B;EACtC,OAAO;GAEL,SAAS,UAAU,OAAM,MAAK,EAAE,SAAS,CAAC;GAC1C,MAAM,UAAU,KAAK;EACvB;CACF,GAAG,CAAC,CAMS,CAAC,CAAC,YAAY;CAC3B,MAAM,aAAa,KAAK,KAAK,GAAG;CAGhC,MAAM,sBAAsB,aAAa,cAA+B;EACtE,OAAO,gBAAgB,QAAO,gBAAe,gBAAgB,UAAU,CAAC,CAAC,MAAK,gBAAe;GAC3F,MAAM,UAAU,gBAAgB,SAAS;GACzC,MAAM,eAAe,gBAAgB,WAAW;GAChD,OAAO,QAAQ,SAAS,YAAY;EACtC,CAAC;CACH,GAAG,CAAC,eAAe,CAAC;CAGpB,gBAAgB;EACd,IAAI,aAAa,OAAO,KAAK,CAAC,aAAa;GACzC,IAAI,CAAC,SAAS;IACZ,yBAAyB,IAAI;IAC7B,eAAe,KAAK;IACpB;GACF;GACA,yBAAyB,KAAK;GAC9B,MAAM,gBAAgB;GAItB,IADiB,oBAAoB,aAC1B,GAAG;IACZ,eAAe,IAAI;IACnB,aAAa,aAAa;GAC5B,OAAO;IACL,eAAe,KAAK;IACpB,iBAAiB,aAAa;GAChC;EACF;CACF,GAAG;EAAC;EAAc;EAAa;EAAS;EAAY;EAAqB;EAAgB;CAAU,CAAC;CAGpG,MAAM,eAAe,MAAoC;EACvD,IAAI,UAAU;EACd,aAAa,IAAI;EACjB,eAAe,KAAK;EACpB,yBAAyB,KAAK;EAC9B,MAAM;EACN,UAAU,CAAC;CACb;CACA,MAAM,cAAc,MAAoC;EACtD,aAAa,KAAK;EAClB,KAAK;EACL,SAAS,CAAC;CACZ;CACA,MAAM,eAAe,MAAkB;EACrC,EAAE,eAAe;EACjB,EAAE,gBAAgB;EAClB,iBAAiB,EAAE;EACnB,UAAU;EACV,eAAe,KAAK;EACpB,yBAAyB,KAAK;EAC9B,aAAa,KAAK;EAClB,KAAK;EACL,UAAU,WAAW;CACvB;CAGA,MAAM,eAAe,MAAkB;EACrC,EAAE,eAAe;EACjB,EAAE,gBAAgB;EAClB,iBAAiB,UAAU;EAC3B,UAAU;EACV,eAAe,KAAK;EACpB,yBAAyB,KAAK;EAC9B,aAAa,KAAK;EAClB,KAAK;EACL,UAAU,aAAa,UAAU;CACnC;CACA,MAAM,eAAe,MAAkB;EACrC,EAAE,eAAe;EACjB,EAAE,gBAAgB;EAClB,IAAI,YAAY,WAAW;EAC3B,SAAS,SAAS,MAAM;CAC1B;CACA,MAAM,kBAAkB,YAAY,aAAa,WAAW;CAC5D,MAAM,WAAW,eAAe;CAChC,MAAM,kBAAkB,eAAe,EAAE,oBAAoB;CAC7D,MAAM,aAAa,OAAO,SAAS,EAAE,cAAc;CACnD,MAAM,aAAa,OAAO,SAAS,EAAE,cAAc;CACnD,MAAM,eAAe,OAAO,aAAa,EAAE,iBAAiB;CAC5D,MAAM,cAAc,OAAO,sBAAsB,EAAE,2BAA2B;CAC9E,OAAO,qBAACC,mBAAD;EAAoB;EAAW,KAAK;EAAG,OAAO;GACnD,UAAU;GACV,GAAG;EACL;EAHO,UAAA;GAID,qBAACA,mBAAD;IAAS,YAAA;IAAW,OAAO;IAAU,SAAS;IAAiB,GAAI,OAAO,MAAM,OAAO,MAAM,OAAO,kBAAkB,UAAU,OAAO,QAAQ,aAAa,OAAO,SAAS,YAAY,OAAO,OAAO,YAAY,oBAAoB,cAAc,OAAO,eAAe,YAAY,OAAO,QAAQ;IAAG,SAAS;IAAjT,UAAA;KACE,oBAAC,OAAD;MAAK,OAAO,EACd,eAAe,OACjB;MACO,UAAA,cAAc,oBAAC,QAAD;OAAM,GAAI,OAAO,MAAM,OAAO,WAAW;OACnD,UAAA,KAAK,SAAS,IAAI,oBAAC,QAAD,EAAQ,MAAM,WAAa,CAAA,IAAI;MAC9C,CAAA,IAAI,cAAc,oBAAC,QAAD,EAAQ,MAAM,YAAc,CAAA,IAAI,oBAAC,QAAD;OAAM,GAAI,OAAO,MAAM,OAAO,WAAW;OAAI,UAAA;MAAsB,CAAA;KAC1H,CAAA;KAGL,oBAAC,SAAD;MAAO,UAAA;MAAS,GAAI,OAAO,MAAM,OAAO,WAAW;MAAa;MAAU,KAAK;MAAiB,OAAO,EACzG,eAAe,OACjB;MAAG,QAAQ;MAAY,SAAS;KAAc,CAAA;KAEzC,CAAC,aAAa,eAAe,CAAC,aAAa,cAAc,eAAe,qBAACA,mBAAD;MAAS,YAAA;MAAW,KAAK;MAAzB,UAAA,CACpE,cAAc,gBAAgB,cAAc,oBAAC,YAAD;OAAY,MAAM;OAAW,MAAM;OAAS,OAAO;OAAY,SAAS;OAAU,SAAS;MAAc,CAAA,GACrJ,cAAc,oBAAC,YAAD;OAAY,MAAM;OAAO,MAAM;OAAS,OAAO;OAAY,SAAS;OAAU,SAAS;MAAc,CAAA,CAC7G;;IACJ;;GACR,eAAe,oBAAC,OAAD;IAAK,GAAI,OAAO,MAAM,OAAO,SAAS;IAAI,UAAA;GAAkB,CAAA;GAC3E,yBAAyB,oBAAC,OAAD;IAAK,GAAI,OAAO,MAAM,OAAO,SAAS;IAAI,UAAA;GAAiB,CAAA;EAC9E;;AACf,CAAC;AACD,YAAY,cAAc"}
|
package/es/Image/type.d.mts
CHANGED
|
@@ -4,6 +4,7 @@ import { CSSProperties, ComponentProps, ReactNode, Ref } from "react";
|
|
|
4
4
|
export 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,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useAppElement } from "../../ConfigProvider/AppElementContext.mjs";
|
|
3
3
|
import { styleProps } from "../../styles/stylex/props.mjs";
|
|
4
|
+
import { useMergeRefs } from "../../utils/mergeRefs.mjs";
|
|
4
5
|
import { useLayerZIndex } from "../../internal/zIndex/useLayerZIndex.mjs";
|
|
5
6
|
import { ToastHost } from "../../Toast/imperative.mjs";
|
|
6
7
|
import { styles } from "../style.mjs";
|
|
@@ -15,7 +16,6 @@ import { useZoomPan } from "./useZoomPan.mjs";
|
|
|
15
16
|
import ViewerChrome from "./ViewerChrome.mjs";
|
|
16
17
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
17
18
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
18
|
-
import { useMergeRefs } from "react-merge-refs";
|
|
19
19
|
import { Dialog } from "@base-ui/react/dialog";
|
|
20
20
|
//#region src/Image/viewer/ImageViewer.tsx
|
|
21
21
|
const readViewport = () => ({
|
|
@@ -250,6 +250,7 @@ const ImageViewer = memo(({ entries, index, openerFocusElement, token }) => {
|
|
|
250
250
|
canZoomOut,
|
|
251
251
|
chromeRef,
|
|
252
252
|
current: currentIndex,
|
|
253
|
+
fetchImage: currentEntry.options.fetchImage,
|
|
253
254
|
fitRect,
|
|
254
255
|
flipHorizontal,
|
|
255
256
|
flipVertical,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageViewer.mjs","names":[],"sources":["../../../src/Image/viewer/ImageViewer.tsx"],"sourcesContent":["'use client';\n\nimport '../style.css';\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 { useAppElement } from '@/ConfigProvider/AppElementContext';\nimport { useLayerZIndex } from '@/internal/zIndex';\nimport { styleProps } from '@/styles/stylex/props';\nimport { ToastHost } from '@/Toast';\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 {...styleProps(styles.viewerBackdrop, 'lobe-image-viewer-backdrop')}\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 {...styleProps(styles.viewerPopup, 'lobe-image-viewer-popup')}\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 {...styleProps(styles.viewerImage, 'lobe-image-viewer-image')}\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 '../style.css';\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';\n\nimport { useAppElement } from '@/ConfigProvider/AppElementContext';\nimport { useLayerZIndex } from '@/internal/zIndex';\nimport { styleProps } from '@/styles/stylex/props';\nimport { ToastHost } from '@/Toast';\nimport { useMergeRefs } from '@/utils/mergeRefs';\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 {...styleProps(styles.viewerBackdrop, 'lobe-image-viewer-backdrop')}\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 {...styleProps(styles.viewerPopup, 'lobe-image-viewer-popup')}\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 {...styleProps(styles.viewerImage, 'lobe-image-viewer-image')}\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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"}
|
|
@@ -42,7 +42,10 @@ const usePercentage = (scale, matchScale) => {
|
|
|
42
42
|
const getSnapshot = useCallback(() => Math.round(scale.get() / matchScale * 100), [matchScale, scale]);
|
|
43
43
|
return useSyncExternalStore(subscribe, getSnapshot);
|
|
44
44
|
};
|
|
45
|
-
const
|
|
45
|
+
const defaultFetchImage = async (src) => {
|
|
46
|
+
return (await fetch(src, { mode: "cors" })).blob();
|
|
47
|
+
};
|
|
48
|
+
const Toolbar = memo(({ canZoomIn, canZoomOut, fetchImage, fitRect, flipHorizontal, flipVertical, natural, onDownload, onMoreOpenChange, rotateLeft, rotateRight, rotation, scale, source, toggleActualSize, toolbarAddon, zoomIn, zoomOut }) => {
|
|
46
49
|
const { t } = useTranslation(image_default);
|
|
47
50
|
const [containerEl, setContainerEl] = useState(null);
|
|
48
51
|
const [copyLoading, setCopyLoading] = useState(false);
|
|
@@ -51,9 +54,10 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
51
54
|
const percentage = usePercentage(scale, matchScale);
|
|
52
55
|
const canToggleActualSize = Math.abs(matchScale - 1) > .01;
|
|
53
56
|
const showFitAffordance = canToggleActualSize && percentage === 100;
|
|
57
|
+
const loadImage = fetchImage ?? defaultFetchImage;
|
|
54
58
|
const defaultDownload = useCallback(async (src) => {
|
|
55
59
|
try {
|
|
56
|
-
const blob = await (
|
|
60
|
+
const blob = await loadImage(src);
|
|
57
61
|
const blobUrl = URL.createObjectURL(blob);
|
|
58
62
|
let fileName = getFileNameFromUrl(src);
|
|
59
63
|
const ext = getExtensionFromMimeType(blob.type);
|
|
@@ -62,10 +66,15 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
62
66
|
await downloadBlob(blobUrl, fileName);
|
|
63
67
|
URL.revokeObjectURL(blobUrl);
|
|
64
68
|
toast.success(t("image.downloadSuccess"));
|
|
65
|
-
} catch {
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (fetchImage) throw error;
|
|
66
71
|
window.open(src, "_blank", "noopener,noreferrer");
|
|
67
72
|
}
|
|
68
|
-
}, [
|
|
73
|
+
}, [
|
|
74
|
+
fetchImage,
|
|
75
|
+
loadImage,
|
|
76
|
+
t
|
|
77
|
+
]);
|
|
69
78
|
const handleDownload = useCallback(async () => {
|
|
70
79
|
setDownloadLoading(true);
|
|
71
80
|
try {
|
|
@@ -84,7 +93,7 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
84
93
|
const handleCopy = useCallback(async () => {
|
|
85
94
|
setCopyLoading(true);
|
|
86
95
|
try {
|
|
87
|
-
const blob = await (
|
|
96
|
+
const blob = await loadImage(source);
|
|
88
97
|
const clipboardBlob = await getClipboardBlob(blob);
|
|
89
98
|
await navigator.clipboard.write([new ClipboardItem(clipboardBlob)]);
|
|
90
99
|
toast.success(t("image.copySuccess"));
|
|
@@ -93,7 +102,11 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
93
102
|
} finally {
|
|
94
103
|
setCopyLoading(false);
|
|
95
104
|
}
|
|
96
|
-
}, [
|
|
105
|
+
}, [
|
|
106
|
+
loadImage,
|
|
107
|
+
source,
|
|
108
|
+
t
|
|
109
|
+
]);
|
|
97
110
|
const moreItems = useMemo(() => [
|
|
98
111
|
{
|
|
99
112
|
icon: FlipHorizontal,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toolbar.mjs","names":["imageMessages","Flexbox","ActionIcon"],"sources":["../../../src/Image/viewer/Toolbar.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\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 { ActionIconImpl as ActionIcon } from '@/ActionIcon/ActionIcon';\nimport DropdownMenu, { type DropdownItem } from '@/DropdownMenu';\nimport { Center, Flexbox } from '@/Flex';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport { styleProps } from '@/styles/stylex/props';\nimport { toast } from '@/Toast';\nimport { TooltipGroup } from '@/Tooltip';\nimport { getClipboardBlob } from '@/utils/blobToPng';\nimport { downloadBlob } from '@/utils/downloadBlob';\n\nimport { actualSizeMarker } from '../marker.stylex';\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 {...styleProps(styles.toolbar, 'lobe-image-viewer-toolbar')} ref={setContainerEl}>\n <Flexbox horizontal align=\"center\" gap={8} {...stylex.props(styles.toolbarRow)}>\n <ActionIcon\n disabled={!canZoomOut}\n icon={ZoomOut}\n style={{ borderRadius: 999 }}\n title={t('image.zoomOut')}\n onClick={zoomOut}\n />\n <Center horizontal {...stylex.props(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 xstyle={actualSizeMarker}\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","ActionIcon"],"sources":["../../../src/Image/viewer/Toolbar.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\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 { ActionIconImpl as ActionIcon } from '@/ActionIcon/ActionIcon';\nimport DropdownMenu, { type DropdownItem } from '@/DropdownMenu';\nimport { Center, Flexbox } from '@/Flex';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport { styleProps } from '@/styles/stylex/props';\nimport { toast } from '@/Toast';\nimport { TooltipGroup } from '@/Tooltip';\nimport { getClipboardBlob } from '@/utils/blobToPng';\nimport { downloadBlob } from '@/utils/downloadBlob';\n\nimport { actualSizeMarker } from '../marker.stylex';\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 {...styleProps(styles.toolbar, 'lobe-image-viewer-toolbar')} ref={setContainerEl}>\n <Flexbox horizontal align=\"center\" gap={8} {...stylex.props(styles.toolbarRow)}>\n <ActionIcon\n disabled={!canZoomOut}\n icon={ZoomOut}\n style={{ borderRadius: 999 }}\n title={t('image.zoomOut')}\n onClick={zoomOut}\n />\n <Center horizontal {...stylex.props(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 xstyle={actualSizeMarker}\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","ActionIcon"],"sources":["../../../src/Image/viewer/ViewerChrome.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\nimport { X } from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type MouseEvent, type ReactNode, useCallback } from 'react';\n\nimport { ActionIconImpl as ActionIcon } from '@/ActionIcon/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 {...stylex.props(styles.viewerChrome)} ref={chromeRef} onClick={handleChromeClick}>\n <div\n {...stylex.props(styles.viewerChromeIdle)}\n data-idle-hidden={idle.hidden ? '' : undefined}\n ref={idle.ref}\n >\n <ActionIcon\n className=\"lobe-image-viewer-close\"\n icon={X}\n title={t('image.close')}\n xstyle={styles.viewerClose}\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","ActionIcon"],"sources":["../../../src/Image/viewer/ViewerChrome.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\nimport { X } from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type MouseEvent, type ReactNode, useCallback } from 'react';\n\nimport { ActionIconImpl as ActionIcon } from '@/ActionIcon/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 {...stylex.props(styles.viewerChrome)} ref={chromeRef} onClick={handleChromeClick}>\n <div\n {...stylex.props(styles.viewerChromeIdle)}\n data-idle-hidden={idle.hidden ? '' : undefined}\n ref={idle.ref}\n >\n <ActionIcon\n className=\"lobe-image-viewer-close\"\n icon={X}\n title={t('image.close')}\n xstyle={styles.viewerClose}\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":";;;;;;;;;;;;;AAyCA,MAAM,eAAe,MAAyB,EAC5C,WACA,SACA,SACA,SACA,MACA,SACA,MACA,OACA,GAAG,mBACC;CACJ,MAAM,EACJ,MACE,eAAeA,aAAa;CAChC,MAAM,OAAO,cAAc;CAC3B,MAAM,oBAAoB,aAAa,UAAsC;EAC3E,MAAM,gBAAgB;CACxB,GAAG,CAAC,CAAC;CACL,OAAO,oBAAC,OAAD;EAAK,GAAI,OAAO,MAAM,OAAO,YAAY;EAAG,KAAK;EAAW,SAAS;EACtE,UAAA,qBAAC,OAAD;GAAK,GAAI,OAAO,MAAM,OAAO,gBAAgB;GAAG,oBAAkB,KAAK,SAAS,KAAK,KAAA;GAAW,KAAK,KAAK;GAA1G,UAAA;IACE,oBAACC,gBAAD;KAAY,WAAU;KAA0B,MAAM;KAAG,OAAO,EAAE,aAAa;KAAG,QAAQ,OAAO;KAAa,SAAS;IAAU,CAAA;IAChI,QAAQ,KAAK,oBAAC,YAAD;KAAqB;KAAkB;KAAkB;KAAe;KAAY;KAAa;IAAQ,CAAA;IACvH,oBAAC,SAAD;KAAS,GAAI;KAAc,kBAAkB,KAAK;IAAU,CAAA;GACzD;;CACF,CAAA;AACX,CAAC;AACD,aAAa,cAAc"}
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import { styleProps } from "../styles/stylex/props.mjs";
|
|
3
3
|
import Icon from "../Icon/Icon.mjs";
|
|
4
4
|
import FlexBasic_default from "../Flex/FlexBasic.mjs";
|
|
5
|
+
import { useControlledState } from "../hooks/useControlledState.mjs";
|
|
5
6
|
import Img from "../Img/index.mjs";
|
|
6
7
|
import { styles } from "./styles.mjs";
|
|
7
8
|
import { createElement, memo } from "react";
|
|
8
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
10
|
import * as stylex from "@stylexjs/stylex";
|
|
10
|
-
import useControlledState from "use-merge-value";
|
|
11
11
|
//#region src/ImageSelect/ImageSelect.tsx
|
|
12
12
|
const ImageSelect = memo(({ ref, className, style, value, defaultValue, onChange, options, width = 144, height = 86, styles: customStyles, classNames, unoptimized, ...rest }) => {
|
|
13
13
|
const [currentValue, setCurrentValue] = useControlledState(defaultValue, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageSelect.mjs","names":["Flexbox"],"sources":["../../src/ImageSelect/ImageSelect.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\nimport { memo } from 'react';\nimport
|
|
1
|
+
{"version":3,"file":"ImageSelect.mjs","names":["Flexbox"],"sources":["../../src/ImageSelect/ImageSelect.tsx"],"sourcesContent":["'use client';\n\nimport * as stylex from '@stylexjs/stylex';\nimport { memo } from 'react';\n\nimport { Flexbox } from '@/Flex';\nimport { useControlledState } from '@/hooks/useControlledState';\nimport Icon from '@/Icon';\nimport Img from '@/Img';\nimport { styleProps } from '@/styles/stylex/props';\n\nimport { styles } from './styles';\nimport type { ImageSelectProps } from './type';\n\nconst ImageSelect = memo<ImageSelectProps>(\n ({\n ref,\n className,\n style,\n value,\n defaultValue,\n onChange,\n options,\n width = 144,\n height = 86,\n styles: customStyles,\n classNames,\n unoptimized,\n ...rest\n }) => {\n const [currentValue, setCurrentValue] = useControlledState<string>(defaultValue, {\n defaultValue,\n onChange,\n value,\n });\n\n return (\n <Flexbox horizontal className={className} gap={16} ref={ref} style={style} {...rest}>\n {options?.map((item) => {\n const isActive = item.value === currentValue;\n return (\n <Flexbox\n align={'center'}\n gap={4}\n {...stylex.props(styles.container, isActive && styles.active)}\n key={item.value}\n onClick={() => setCurrentValue(item.value)}\n >\n <Img\n alt={item.alt || item.value}\n height={height}\n src={item.img}\n unoptimized={unoptimized}\n width={width}\n {...styleProps([styles.img, isActive && styles.imgActive], classNames?.img, {\n ...customStyles?.img,\n height,\n width,\n })}\n />\n\n <Flexbox horizontal align={'center'} gap={4}>\n {item.icon && <Icon icon={item.icon} />}\n {item.label}\n </Flexbox>\n </Flexbox>\n );\n })}\n </Flexbox>\n );\n },\n);\n\nImageSelect.displayName = 'ImageSelect';\n\nexport default ImageSelect;\n"],"mappings":";;;;;;;;;;;AAWA,MAAM,cAAc,MAAwB,EAC1C,KACA,WACA,OACA,OACA,cACA,UACA,SACA,QAAQ,KACR,SAAS,IACT,QAAQ,cACR,YACA,aACA,GAAG,WACC;CACJ,MAAM,CAAC,cAAc,mBAAmB,mBAA2B,cAAc;EAC/E;EACA;EACA;CACF,CAAC;CACD,OAAO,oBAACA,mBAAD;EAAS,YAAA;EAAsB;EAAW,KAAK;EAAS;EAAY;EAAO,GAAI;EAC/E,UAAA,SAAS,KAAI,SAAQ;GACxB,MAAM,WAAW,KAAK,UAAU;GAChC,OAAO,8BAACA,mBAAD;IAAS,OAAO;IAAU,KAAK;IAAG,GAAI,OAAO,MAAM,OAAO,WAAW,YAAY,OAAO,MAAM;IAAG,KAAK,KAAK;IAAO,eAAe,gBAAgB,KAAK,KAAK;GAWnJ,GAVP,oBAAC,KAAD;IAAK,KAAK,KAAK,OAAO,KAAK;IAAe;IAAQ,KAAK,KAAK;IAAkB;IAAoB;IAAO,GAAI,WAAW,CAAC,OAAO,KAAK,YAAY,OAAO,SAAS,GAAG,YAAY,KAAK;KACzL,GAAG,cAAc;KACjB;KACA;IACF,CAAC;GAAI,CAAA,GAEC,qBAACA,mBAAD;IAAS,YAAA;IAAW,OAAO;IAAU,KAAK;IAA1C,UAAA,CACG,KAAK,QAAQ,oBAAC,MAAD,EAAM,MAAM,KAAK,KAAO,CAAA,GACrC,KAAK,KACC;GACF,CAAA,CAAA;EACjB,CAAC;CACU,CAAA;AACf,CAAC;AACD,YAAY,cAAc"}
|
package/es/Input/Input.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { styleProps } from "../styles/stylex/props.mjs";
|
|
3
|
+
import { useMergeRefs } from "../utils/mergeRefs.mjs";
|
|
3
4
|
import { useThemeMode } from "../styles/theme/scope.mjs";
|
|
5
|
+
import { useEventCallback } from "../hooks/useEventCallback.mjs";
|
|
4
6
|
import { inputRootStyles, styles } from "./style.mjs";
|
|
5
7
|
import ClearButton from "./ClearButton.mjs";
|
|
6
8
|
import { clearNativeValue } from "./clearNativeValue.mjs";
|
|
7
9
|
import { isPressEnter } from "./pressEnter.mjs";
|
|
8
10
|
import { memo, useRef, useState } from "react";
|
|
9
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
import { useMergeRefs } from "react-merge-refs";
|
|
11
12
|
import { Input } from "@base-ui/react/input";
|
|
12
13
|
//#region src/Input/Input.tsx
|
|
13
14
|
const hasText = (value) => value !== void 0 && value !== null && String(value).length > 0;
|
|
@@ -19,6 +20,10 @@ const Input$1 = memo(({ ref, className, classNames, styles: customStyles, style,
|
|
|
19
20
|
const [filled, setFilled] = useState(() => hasText(value ?? defaultValue));
|
|
20
21
|
const isFilled = value === void 0 ? filled : hasText(value);
|
|
21
22
|
const showClear = allowClear && isFilled && !disabled && !readOnly;
|
|
23
|
+
const handleClear = useEventCallback(() => {
|
|
24
|
+
clearNativeValue(inputRef.current);
|
|
25
|
+
onClear?.();
|
|
26
|
+
});
|
|
22
27
|
return /* @__PURE__ */ jsxs("div", {
|
|
23
28
|
"data-disabled": disabled ? "" : void 0,
|
|
24
29
|
"data-variant": mergedVariant,
|
|
@@ -49,10 +54,7 @@ const Input$1 = memo(({ ref, className, classNames, styles: customStyles, style,
|
|
|
49
54
|
},
|
|
50
55
|
...rest
|
|
51
56
|
}),
|
|
52
|
-
showClear && /* @__PURE__ */ jsx(ClearButton, { onClear:
|
|
53
|
-
clearNativeValue(inputRef.current);
|
|
54
|
-
onClear?.();
|
|
55
|
-
} }),
|
|
57
|
+
showClear && /* @__PURE__ */ jsx(ClearButton, { onClear: handleClear }),
|
|
56
58
|
suffix && /* @__PURE__ */ jsx("span", {
|
|
57
59
|
...styleProps(styles.slot, classNames?.suffix, customStyles?.suffix),
|
|
58
60
|
children: suffix
|
package/es/Input/Input.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.mjs","names":["Input","BaseInput"],"sources":["../../src/Input/Input.tsx"],"sourcesContent":["'use client';\n\nimport { Input as BaseInput } from '@base-ui/react/input';\nimport { memo, useRef, useState } from 'react';\nimport {
|
|
1
|
+
{"version":3,"file":"Input.mjs","names":["Input","BaseInput"],"sources":["../../src/Input/Input.tsx"],"sourcesContent":["'use client';\n\nimport { Input as BaseInput } from '@base-ui/react/input';\nimport { memo, useRef, useState } from 'react';\n\nimport { useEventCallback } from '@/hooks/useEventCallback';\nimport { styleProps } from '@/styles/stylex/props';\nimport { useThemeMode } from '@/styles/theme/scope';\nimport { useMergeRefs } from '@/utils/mergeRefs';\n\nimport ClearButton from './ClearButton';\nimport { clearNativeValue } from './clearNativeValue';\nimport { isPressEnter } from './pressEnter';\nimport { inputRootStyles, styles } from './style';\nimport type { InputProps } from './type';\n\nconst hasText = (value: unknown) =>\n value !== undefined && value !== null && String(value).length > 0;\n\nconst Input = memo<InputProps>(\n ({\n ref,\n className,\n classNames,\n styles: customStyles,\n style,\n variant,\n shadow,\n size = 'middle',\n prefix,\n suffix,\n disabled,\n readOnly,\n allowClear,\n onClear,\n onPressEnter,\n onKeyDown,\n onChange,\n value,\n defaultValue,\n ...rest\n }) => {\n const { isDarkMode } = useThemeMode();\n const mergedVariant = variant || (isDarkMode ? 'filled' : 'outlined');\n const inputRef = useRef<HTMLInputElement>(null);\n const mergedRef = useMergeRefs([ref, inputRef]);\n const [filled, setFilled] = useState(() => hasText(value ?? defaultValue));\n const isFilled = value === undefined ? filled : hasText(value);\n const showClear = allowClear && isFilled && !disabled && !readOnly;\n\n const handleClear = useEventCallback(() => {\n clearNativeValue(inputRef.current);\n onClear?.();\n });\n\n return (\n <div\n data-disabled={disabled ? '' : undefined}\n data-variant={mergedVariant}\n {...styleProps(inputRootStyles({ shadow, size, variant: mergedVariant }), className, style)}\n >\n {prefix && (\n <span {...styleProps(styles.slot, classNames?.prefix, customStyles?.prefix)}>\n {prefix}\n </span>\n )}\n <BaseInput\n defaultValue={defaultValue}\n disabled={disabled}\n readOnly={readOnly}\n ref={mergedRef}\n value={value}\n {...styleProps(styles.input, classNames?.input, customStyles?.input)}\n onChange={(event) => {\n setFilled(event.currentTarget.value.length > 0);\n onChange?.(event);\n }}\n onKeyDown={(event) => {\n if (isPressEnter(event)) onPressEnter?.(event);\n onKeyDown?.(event);\n }}\n {...rest}\n />\n {showClear && <ClearButton onClear={handleClear} />}\n {suffix && (\n <span {...styleProps(styles.slot, classNames?.suffix, customStyles?.suffix)}>\n {suffix}\n </span>\n )}\n </div>\n );\n },\n);\n\nInput.displayName = 'Input';\n\nexport default Object.assign(Input, { formBinding: { emptyValue: '' } as const });\n"],"mappings":";;;;;;;;;;;;;AAgBA,MAAM,WAAW,UACf,UAAU,KAAA,KAAa,UAAU,QAAQ,OAAO,KAAK,CAAC,CAAC,SAAS;AAElE,MAAMA,UAAQ,MACX,EACC,KACA,WACA,YACA,QAAQ,cACR,OACA,SACA,QACA,OAAO,UACP,QACA,QACA,UACA,UACA,YACA,SACA,cACA,WACA,UACA,OACA,cACA,GAAG,WACC;CACJ,MAAM,EAAE,eAAe,aAAa;CACpC,MAAM,gBAAgB,YAAY,aAAa,WAAW;CAC1D,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,YAAY,aAAa,CAAC,KAAK,QAAQ,CAAC;CAC9C,MAAM,CAAC,QAAQ,aAAa,eAAe,QAAQ,SAAS,YAAY,CAAC;CACzE,MAAM,WAAW,UAAU,KAAA,IAAY,SAAS,QAAQ,KAAK;CAC7D,MAAM,YAAY,cAAc,YAAY,CAAC,YAAY,CAAC;CAE1D,MAAM,cAAc,uBAAuB;EACzC,iBAAiB,SAAS,OAAO;EACjC,UAAU;CACZ,CAAC;CAED,OACE,qBAAC,OAAD;EACE,iBAAe,WAAW,KAAK,KAAA;EAC/B,gBAAc;EACd,GAAI,WAAW,gBAAgB;GAAE;GAAQ;GAAM,SAAS;EAAc,CAAC,GAAG,WAAW,KAAK;EAH5F,UAAA;GAKG,UACC,oBAAC,QAAD;IAAM,GAAI,WAAW,OAAO,MAAM,YAAY,QAAQ,cAAc,MAAM;IACvE,UAAA;GACG,CAAA;GAER,oBAACC,OAAD;IACgB;IACJ;IACA;IACV,KAAK;IACE;IACP,GAAI,WAAW,OAAO,OAAO,YAAY,OAAO,cAAc,KAAK;IACnE,WAAW,UAAU;KACnB,UAAU,MAAM,cAAc,MAAM,SAAS,CAAC;KAC9C,WAAW,KAAK;IAClB;IACA,YAAY,UAAU;KACpB,IAAI,aAAa,KAAK,GAAG,eAAe,KAAK;KAC7C,YAAY,KAAK;IACnB;IACA,GAAI;GACL,CAAA;GACA,aAAa,oBAAC,aAAD,EAAa,SAAS,YAAc,CAAA;GACjD,UACC,oBAAC,QAAD;IAAM,GAAI,WAAW,OAAO,MAAM,YAAY,QAAQ,cAAc,MAAM;IACvE,UAAA;GACG,CAAA;EAEL;;AAET,CACF;AAEA,QAAM,cAAc;AAEpB,IAAA,gBAAe,OAAO,OAAOD,SAAO,EAAE,aAAa,EAAE,YAAY,GAAG,EAAW,CAAC"}
|
package/es/Input/TextArea.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { styleProps } from "../styles/stylex/props.mjs";
|
|
3
|
+
import { useMergeRefs } from "../utils/mergeRefs.mjs";
|
|
3
4
|
import { useThemeMode } from "../styles/theme/scope.mjs";
|
|
4
5
|
import { inputRootStyles, styles } from "./style.mjs";
|
|
5
6
|
import ClearButton from "./ClearButton.mjs";
|
|
@@ -8,7 +9,6 @@ import { isPressEnter } from "./pressEnter.mjs";
|
|
|
8
9
|
import { memo, useMemo, useRef, useState } from "react";
|
|
9
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
11
|
import * as stylex from "@stylexjs/stylex";
|
|
11
|
-
import { useMergeRefs } from "react-merge-refs";
|
|
12
12
|
import { Field } from "@base-ui/react/field";
|
|
13
13
|
//#region src/Input/TextArea.tsx
|
|
14
14
|
const textLength = (value) => value === void 0 || value === null ? 0 : Array.from(String(value)).length;
|