diff --git a/apps/nextjs-app/src/features/app/blocks/share/view/component/gallery/GalleryView.tsx b/apps/nextjs-app/src/features/app/blocks/share/view/component/gallery/GalleryView.tsx
index 9a1112aadd..b7c5c5bafd 100644
--- a/apps/nextjs-app/src/features/app/blocks/share/view/component/gallery/GalleryView.tsx
+++ b/apps/nextjs-app/src/features/app/blocks/share/view/component/gallery/GalleryView.tsx
@@ -1,17 +1,18 @@
import { RecordProvider, RowCountProvider, ShareViewContext } from '@teable/sdk/context';
import { SearchProvider } from '@teable/sdk/context/query';
-import { useIsHydrated } from '@teable/sdk/hooks';
import { cn } from '@teable/ui-lib/shadcn';
import { useRouter } from 'next/router';
import { useContext } from 'react';
-import { GalleryProvider } from '@/features/app/blocks/view/gallery/context';
-import { GalleryViewBase } from '@/features/app/blocks/view/gallery/GalleryViewBase';
+import {
+ GalleryProvider,
+ PresentationModeProvider,
+} from '@/features/app/blocks/view/gallery/context';
+import { GalleryViewContent } from '@/features/app/blocks/view/gallery/GalleryViewContent';
import { ShareViewHeader } from '../../ShareSignInButton';
import { GalleryToolbar } from './toolbar';
export const GalleryView = () => {
const { view } = useContext(ShareViewContext);
- const isHydrated = useIsHydrated();
const {
query: { hideToolBar, embed },
} = useRouter();
@@ -19,18 +20,18 @@ export const GalleryView = () => {
{!embed &&
}
-
-
-
- {!hideToolBar && }
-
-
- {isHydrated && }
-
-
-
-
-
+
+
+
+
+ {!hideToolBar && }
+
+
+
+
+
+
+
);
diff --git a/apps/nextjs-app/src/features/app/blocks/share/view/component/gallery/toolbar/Toolbar.tsx b/apps/nextjs-app/src/features/app/blocks/share/view/component/gallery/toolbar/Toolbar.tsx
index fa1486ad34..380a377523 100644
--- a/apps/nextjs-app/src/features/app/blocks/share/view/component/gallery/toolbar/Toolbar.tsx
+++ b/apps/nextjs-app/src/features/app/blocks/share/view/component/gallery/toolbar/Toolbar.tsx
@@ -4,6 +4,7 @@ import { useView } from '@teable/sdk/hooks/use-view';
import { cn } from '@teable/ui-lib/shadcn';
import { useToolbarChange } from '@/features/app/blocks/view/hooks/useToolbarChange';
import { SearchButton } from '@/features/app/blocks/view/search/SearchButton';
+import { PresentToggle } from '@/features/app/blocks/view/tool-bar/components/PresentToggle';
import { ToolBarButton } from '@/features/app/blocks/view/tool-bar/ToolBarButton';
import { Sort } from '../../grid/toolbar/Sort';
import { ShareViewFilter } from '../../share-view-filter';
@@ -50,6 +51,7 @@ export const GalleryToolbar: React.FC<{ disabled?: boolean }> = (props) => {
)}
+
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/GalleryView.tsx b/apps/nextjs-app/src/features/app/blocks/view/gallery/GalleryView.tsx
index cef79c5732..03aa0731c1 100644
--- a/apps/nextjs-app/src/features/app/blocks/view/gallery/GalleryView.tsx
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/GalleryView.tsx
@@ -1,24 +1,25 @@
import { RecordProvider, RowCountProvider } from '@teable/sdk/context';
import { SearchProvider } from '@teable/sdk/context/query';
-import { useIsHydrated, usePersonalView } from '@teable/sdk/hooks';
+import { usePersonalView } from '@teable/sdk/hooks';
import { GalleryToolBar } from '../tool-bar/GalleryToolBar';
-import { GalleryProvider } from './context';
-import { GalleryViewBase } from './GalleryViewBase';
+import { GalleryProvider, PresentationModeProvider } from './context';
+import { GalleryViewContent } from './GalleryViewContent';
export const GalleryView = () => {
- const isHydrated = useIsHydrated();
const { personalViewCommonQuery } = usePersonalView();
return (
-
-
-
-
-
- {isHydrated && }
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/GalleryViewContent.tsx b/apps/nextjs-app/src/features/app/blocks/view/gallery/GalleryViewContent.tsx
new file mode 100644
index 0000000000..975bd27798
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/GalleryViewContent.tsx
@@ -0,0 +1,15 @@
+import { useIsHydrated } from '@teable/sdk/hooks';
+import { GalleryPresentationView } from './components/GalleryPresentationView';
+import { usePresentationMode } from './context/PresentationModeContext';
+import { GalleryViewBase } from './GalleryViewBase';
+
+export const GalleryViewContent = () => {
+ const isHydrated = useIsHydrated();
+ const { isPresenting } = usePresentationMode();
+
+ return (
+
+ {isHydrated && (isPresenting ? : )}
+
+ );
+};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/components/GalleryPresentationView.tsx b/apps/nextjs-app/src/features/app/blocks/view/gallery/components/GalleryPresentationView.tsx
new file mode 100644
index 0000000000..fb34f5c665
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/components/GalleryPresentationView.tsx
@@ -0,0 +1,180 @@
+import { ChevronLeft, ChevronRight, X } from '@teable/icons';
+import { useRecords, useRowCount } from '@teable/sdk/hooks';
+import { Button, cn } from '@teable/ui-lib/shadcn';
+import { useTranslation } from 'next-i18next';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
+import { tableConfig } from '@/features/i18n/table.config';
+import { usePresentationMode } from '../context/PresentationModeContext';
+import { useGallery } from '../hooks';
+import {
+ clampIndex,
+ getRecordWindow,
+ resolvePresentationKey,
+ shouldIgnorePresentationHotkey,
+ stepIndex,
+} from '../utils/presentation';
+import { PresentationSlide } from './PresentationSlide';
+
+export const GalleryPresentationView = () => {
+ const { t } = useTranslation(tableConfig.i18nNamespaces);
+ const { setPresenting } = usePresentationMode();
+ const { recordQuery, displayFields, coverField, isCoverFit, isFieldNameHidden, primaryField } =
+ useGallery();
+ const rowCount = useRowCount() ?? 0;
+ const [index, setIndex] = useState(0);
+ const rootRef = useRef(null);
+
+ const { skip, take, offset } = getRecordWindow(index);
+ const windowQuery = useMemo(
+ () => ({
+ ...recordQuery,
+ skip,
+ take,
+ }),
+ [recordQuery, skip, take]
+ );
+ const { records } = useRecords(windowQuery);
+ const record = records[offset];
+
+ useEffect(() => {
+ setIndex((current) => clampIndex(current, rowCount));
+ }, [rowCount]);
+
+ const goTo = useCallback(
+ (delta: number) => {
+ setIndex((current) => stepIndex(current, rowCount, delta));
+ },
+ [rowCount]
+ );
+
+ const exit = useCallback(() => {
+ setPresenting(false);
+ }, [setPresenting]);
+
+ useEffect(() => {
+ const previousOverflow = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+ rootRef.current?.focus();
+ return () => {
+ document.body.style.overflow = previousOverflow;
+ };
+ }, []);
+
+ useEffect(() => {
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (shouldIgnorePresentationHotkey(event.target)) return;
+ const action = resolvePresentationKey(event);
+ if (!action) return;
+ event.preventDefault();
+ if (action === 'exit') {
+ exit();
+ return;
+ }
+ goTo(action === 'next' ? 1 : -1);
+ };
+ window.addEventListener('keydown', onKeyDown);
+ return () => window.removeEventListener('keydown', onKeyDown);
+ }, [exit, goTo]);
+
+ const atStart = index <= 0;
+ const atEnd = rowCount === 0 || index >= rowCount - 1;
+ const counter =
+ rowCount === 0
+ ? t('table:gallery.presentation.empty')
+ : t('table:gallery.presentation.counter', { current: index + 1, total: rowCount });
+
+ return createPortal(
+
+
+
+
+
+
+
+ {rowCount === 0 ? (
+
{t('table:gallery.presentation.empty')}
+ ) : (
+
+ )}
+
+
+
+
+
+
+
,
+ document.body
+ );
+};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/components/PresentationSlide.tsx b/apps/nextjs-app/src/features/app/blocks/view/gallery/components/PresentationSlide.tsx
new file mode 100644
index 0000000000..4a86ecd94a
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/components/PresentationSlide.tsx
@@ -0,0 +1,114 @@
+import type { IAttachmentCellValue } from '@teable/core';
+import { Image } from '@teable/icons';
+import { useTheme } from '@teable/next-themes';
+import { CellValue, getFileCover, isSystemFileIcon } from '@teable/sdk/components';
+import { useFieldStaticGetter } from '@teable/sdk/hooks';
+import type { IFieldInstance, Record } from '@teable/sdk/model';
+import { cn } from '@teable/ui-lib/shadcn';
+import { useTranslation } from 'next-i18next';
+import { useMemo } from 'react';
+import { tableConfig } from '@/features/i18n/table.config';
+
+interface IPresentationSlideProps {
+ card?: Record;
+ primaryField: IFieldInstance | null;
+ displayFields: IFieldInstance[];
+ coverField?: IFieldInstance;
+ isCoverFit?: boolean;
+ isFieldNameHidden?: boolean;
+}
+
+const PresentationCover = ({
+ value,
+ isCoverFit,
+}: {
+ value?: IAttachmentCellValue;
+ isCoverFit?: boolean;
+}) => {
+ const { resolvedTheme } = useTheme();
+ const first = value?.[0];
+
+ if (!first) {
+ return (
+
+
+
+ );
+ }
+
+ const isSystemFile = isSystemFileIcon(first.mimetype);
+ const url =
+ first.lgThumbnailUrl ??
+ getFileCover(first.mimetype, first.presignedUrl, resolvedTheme as 'light' | 'dark');
+
+ return (
+
+

+
+ );
+};
+
+export const PresentationSlide = (props: IPresentationSlideProps) => {
+ const { card, primaryField, displayFields, coverField, isCoverFit, isFieldNameHidden } = props;
+ const getFieldStatic = useFieldStaticGetter();
+ const { t } = useTranslation(tableConfig.i18nNamespaces);
+
+ const titleComponent = useMemo(() => {
+ if (primaryField == null || card == null) {
+ return {t('untitled')};
+ }
+ const value = card.getCellValue(primaryField.id);
+ if (value == null) {
+ return {t('untitled')};
+ }
+ return ;
+ }, [card, primaryField, t]);
+
+ const coverCellValue = coverField
+ ? (card?.getCellValue(coverField.id) as IAttachmentCellValue | undefined)
+ : undefined;
+
+ return (
+
+ {coverField && }
+
+
+ {titleComponent}
+
+
+ {displayFields.map((field) => {
+ const { id: fieldId, name, type, isLookup, isConditionalLookup, aiConfig } = field;
+ const { Icon } = getFieldStatic(type, {
+ isLookup,
+ isConditionalLookup,
+ hasAiConfig: Boolean(aiConfig),
+ deniedReadRecord: !field.canReadFieldRecord,
+ });
+ const cellValue = card?.getCellValue(fieldId);
+
+ return (
+
+ {!isFieldNameHidden && (
+
+
+ {name}
+
+ )}
+ {cellValue != null ? (
+
+ ) : (
+
-
+ )}
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/components/index.ts b/apps/nextjs-app/src/features/app/blocks/view/gallery/components/index.ts
index ece438c45f..910fd63036 100644
--- a/apps/nextjs-app/src/features/app/blocks/view/gallery/components/index.ts
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/components/index.ts
@@ -1,2 +1,4 @@
export * from './Card';
export * from './CardCarousel';
+export * from './GalleryPresentationView';
+export * from './PresentationSlide';
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/context/PresentationModeContext.ts b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/PresentationModeContext.ts
new file mode 100644
index 0000000000..3874ed3a9a
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/PresentationModeContext.ts
@@ -0,0 +1,16 @@
+import { createContext, useContext } from 'react';
+
+export interface IPresentationModeContext {
+ isPresenting: boolean;
+ setPresenting: (value: boolean) => void;
+}
+
+export const PresentationModeContext = createContext(null);
+
+export const usePresentationMode = () => {
+ const context = useContext(PresentationModeContext);
+ if (!context) {
+ throw new Error('usePresentationMode must be used within PresentationModeProvider');
+ }
+ return context;
+};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/context/PresentationModeProvider.tsx b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/PresentationModeProvider.tsx
new file mode 100644
index 0000000000..c9d66abb81
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/PresentationModeProvider.tsx
@@ -0,0 +1,11 @@
+import { useMemo, useState, type ReactNode } from 'react';
+import { PresentationModeContext } from './PresentationModeContext';
+
+export const PresentationModeProvider = ({ children }: { children: ReactNode }) => {
+ const [isPresenting, setPresenting] = useState(false);
+ const value = useMemo(() => ({ isPresenting, setPresenting }), [isPresenting]);
+
+ return (
+ {children}
+ );
+};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/context/index.ts b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/index.ts
index 803ceb3b56..b9fc345224 100644
--- a/apps/nextjs-app/src/features/app/blocks/view/gallery/context/index.ts
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/index.ts
@@ -1,2 +1,4 @@
export * from './GalleryContext';
export * from './GalleryProvider';
+export * from './PresentationModeContext';
+export * from './PresentationModeProvider';
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/index.ts b/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/index.ts
index 61e89e45ec..b4d8a05ac9 100644
--- a/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/index.ts
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/index.ts
@@ -1,2 +1,3 @@
export * from './card';
export * from './columns';
+export * from './presentation';
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/presentation.spec.ts b/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/presentation.spec.ts
new file mode 100644
index 0000000000..4f5669eac0
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/presentation.spec.ts
@@ -0,0 +1,90 @@
+import { describe, expect, it } from 'vitest';
+import {
+ clampIndex,
+ getRecordWindow,
+ resolvePresentationKey,
+ shouldIgnorePresentationHotkey,
+ stepIndex,
+} from './presentation';
+
+describe('clampIndex', () => {
+ it('returns 0 when the set is empty', () => {
+ expect(clampIndex(3, 0)).toBe(0);
+ expect(clampIndex(-1, 0)).toBe(0);
+ });
+
+ it('clamps to the last record in the filtered set', () => {
+ expect(clampIndex(9, 4)).toBe(3);
+ });
+
+ it('clamps negative indexes to 0', () => {
+ expect(clampIndex(-2, 4)).toBe(0);
+ });
+
+ it('keeps an in-range index', () => {
+ expect(clampIndex(2, 4)).toBe(2);
+ });
+});
+
+describe('stepIndex', () => {
+ it('moves forward and backward inside the current filtered set', () => {
+ expect(stepIndex(1, 5, 1)).toBe(2);
+ expect(stepIndex(1, 5, -1)).toBe(0);
+ });
+
+ it('does not wrap at either end', () => {
+ expect(stepIndex(0, 5, -1)).toBe(0);
+ expect(stepIndex(4, 5, 1)).toBe(4);
+ });
+});
+
+describe('getRecordWindow', () => {
+ it('loads the first three records when presenting the first slide', () => {
+ expect(getRecordWindow(0)).toEqual({ skip: 0, take: 3, offset: 0 });
+ });
+
+ it('centers the window on the current slide so prev and next stay available', () => {
+ expect(getRecordWindow(4)).toEqual({ skip: 3, take: 3, offset: 1 });
+ });
+});
+
+describe('resolvePresentationKey', () => {
+ it('maps arrow and vim keys to next and previous', () => {
+ expect(resolvePresentationKey({ key: 'ArrowRight' })).toBe('next');
+ expect(resolvePresentationKey({ key: 'j' })).toBe('next');
+ expect(resolvePresentationKey({ key: 'J' })).toBe('next');
+ expect(resolvePresentationKey({ key: 'ArrowLeft' })).toBe('prev');
+ expect(resolvePresentationKey({ key: 'k' })).toBe('prev');
+ expect(resolvePresentationKey({ key: 'K' })).toBe('prev');
+ });
+
+ it('maps Escape to exit', () => {
+ expect(resolvePresentationKey({ key: 'Escape' })).toBe('exit');
+ });
+
+ it('ignores unrelated keys', () => {
+ expect(resolvePresentationKey({ key: 'Enter' })).toBeNull();
+ expect(resolvePresentationKey({ key: ' ' })).toBeNull();
+ });
+});
+
+describe('shouldIgnorePresentationHotkey', () => {
+ it('ignores typing in form fields and contenteditable', () => {
+ const input = document.createElement('input');
+ const textarea = document.createElement('textarea');
+ const select = document.createElement('select');
+ const editable = document.createElement('div');
+ editable.contentEditable = 'true';
+
+ expect(shouldIgnorePresentationHotkey(input)).toBe(true);
+ expect(shouldIgnorePresentationHotkey(textarea)).toBe(true);
+ expect(shouldIgnorePresentationHotkey(select)).toBe(true);
+ expect(shouldIgnorePresentationHotkey(editable)).toBe(true);
+ });
+
+ it('handles keys from the slide chrome', () => {
+ const button = document.createElement('button');
+ expect(shouldIgnorePresentationHotkey(button)).toBe(false);
+ expect(shouldIgnorePresentationHotkey(null)).toBe(false);
+ });
+});
diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/presentation.ts b/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/presentation.ts
new file mode 100644
index 0000000000..b9e7028567
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/utils/presentation.ts
@@ -0,0 +1,53 @@
+export type PresentationAction = 'next' | 'prev' | 'exit';
+
+export interface IPresentationRecordWindow {
+ skip: number;
+ take: number;
+ offset: number;
+}
+
+export const clampIndex = (index: number, count: number): number => {
+ if (count <= 0) return 0;
+ return Math.min(Math.max(index, 0), count - 1);
+};
+
+export const stepIndex = (index: number, count: number, delta: number): number => {
+ return clampIndex(index + delta, count);
+};
+
+export const getRecordWindow = (index: number): IPresentationRecordWindow => {
+ const skip = Math.max(0, index - 1);
+ return {
+ skip,
+ take: 3,
+ offset: index === 0 ? 0 : 1,
+ };
+};
+
+export const resolvePresentationKey = (event: { key: string }): PresentationAction | null => {
+ switch (event.key) {
+ case 'ArrowRight':
+ case 'j':
+ case 'J':
+ return 'next';
+ case 'ArrowLeft':
+ case 'k':
+ case 'K':
+ return 'prev';
+ case 'Escape':
+ return 'exit';
+ default:
+ return null;
+ }
+};
+
+export const shouldIgnorePresentationHotkey = (target: EventTarget | null): boolean => {
+ if (!(target instanceof HTMLElement)) return false;
+ const tag = target.tagName;
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
+ return (
+ target.isContentEditable ||
+ target.contentEditable === 'true' ||
+ target.getAttribute('contenteditable')?.toLowerCase() === 'true'
+ );
+};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/GalleryViewOperators.tsx b/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/GalleryViewOperators.tsx
index 1878191dcf..596a309098 100644
--- a/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/GalleryViewOperators.tsx
+++ b/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/GalleryViewOperators.tsx
@@ -9,6 +9,7 @@ import { tableConfig } from '@/features/i18n/table.config';
import { useToolbarChange } from '../../hooks/useToolbarChange';
import { ToolBarButton } from '../ToolBarButton';
import { CoverFieldSelect } from './CoverFieldSelect';
+import { PresentToggle } from './PresentToggle';
import { ScrollableToolbarGroup } from './ScrollableToolbarGroup';
export const GalleryViewOperators: React.FC<{ disabled?: boolean }> = (props) => {
@@ -135,6 +136,7 @@ export const GalleryViewOperators: React.FC<{ disabled?: boolean }> = (props) =>
)}
+
);
};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/PresentToggle.tsx b/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/PresentToggle.tsx
new file mode 100644
index 0000000000..bb72d92d80
--- /dev/null
+++ b/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/PresentToggle.tsx
@@ -0,0 +1,27 @@
+import { Maximize2, Minimize2 } from '@teable/icons';
+import { useTranslation } from 'next-i18next';
+import { tableConfig } from '@/features/i18n/table.config';
+import { usePresentationMode } from '../../gallery/context/PresentationModeContext';
+import { ToolBarButton } from '../ToolBarButton';
+
+export const PresentToggle = () => {
+ const { isPresenting, setPresenting } = usePresentationMode();
+ const { t } = useTranslation(tableConfig.i18nNamespaces);
+
+ return (
+ setPresenting(!isPresenting)}
+ textClassName="@2xl/toolbar:inline"
+ >
+ {isPresenting ? (
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/index.ts b/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/index.ts
index 1b3a9d2e35..5a5df58c40 100644
--- a/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/index.ts
+++ b/apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/index.ts
@@ -3,4 +3,5 @@ export * from './KanbanViewOperators';
export * from './GalleryViewOperators';
export * from './CalendarViewOperators';
export * from './PersonalViewSwitch';
+export * from './PresentToggle';
export * from './ToolBarAddRecordButton';
diff --git a/packages/common-i18n/src/locales/ar/table.json b/packages/common-i18n/src/locales/ar/table.json
index 727fac5867..9132b87a81 100644
--- a/packages/common-i18n/src/locales/ar/table.json
+++ b/packages/common-i18n/src/locales/ar/table.json
@@ -1082,6 +1082,20 @@
"duplicateCard": "تكرار البطاقة"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "عرض تقديمي",
+ "exitPresent": "إنهاء العرض"
+ },
+ "presentation": {
+ "empty": "لا توجد سجلات للعرض",
+ "counter": "{{current}} من {{total}}",
+ "previous": "السابق",
+ "next": "التالي",
+ "exit": "إنهاء العرض",
+ "keyboardHint": "← → أو J K للتنقل · Esc للخروج"
+ }
+ },
"calendar": {
"toolbar": {
"config": "إعداد التقويم",
diff --git a/packages/common-i18n/src/locales/de/table.json b/packages/common-i18n/src/locales/de/table.json
index 6497fc6bd2..c186e489dd 100644
--- a/packages/common-i18n/src/locales/de/table.json
+++ b/packages/common-i18n/src/locales/de/table.json
@@ -1062,6 +1062,20 @@
"duplicateCard": "Karte duplizieren"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "Präsentieren",
+ "exitPresent": "Präsentation beenden"
+ },
+ "presentation": {
+ "empty": "Keine Datensätze zum Präsentieren",
+ "counter": "{{current}} von {{total}}",
+ "previous": "Zurück",
+ "next": "Weiter",
+ "exit": "Präsentation beenden",
+ "keyboardHint": "← → oder J K zum Navigieren · Esc zum Beenden"
+ }
+ },
"calendar": {
"toolbar": {
"config": "Kalendar Konfiguration",
diff --git a/packages/common-i18n/src/locales/en/table.json b/packages/common-i18n/src/locales/en/table.json
index f35f6fdbbb..19cbf4539f 100644
--- a/packages/common-i18n/src/locales/en/table.json
+++ b/packages/common-i18n/src/locales/en/table.json
@@ -1062,6 +1062,20 @@
"duplicateCard": "Duplicate card"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "Present",
+ "exitPresent": "Exit Present"
+ },
+ "presentation": {
+ "empty": "No records to present",
+ "counter": "{{current}} of {{total}}",
+ "previous": "Previous",
+ "next": "Next",
+ "exit": "Exit Present",
+ "keyboardHint": "← → or J K to navigate · Esc to exit"
+ }
+ },
"calendar": {
"toolbar": {
"config": "Calendar config",
diff --git a/packages/common-i18n/src/locales/es/table.json b/packages/common-i18n/src/locales/es/table.json
index 68aaf07571..56f816597f 100644
--- a/packages/common-i18n/src/locales/es/table.json
+++ b/packages/common-i18n/src/locales/es/table.json
@@ -1067,6 +1067,20 @@
"duplicateCard": "Duplicar tarjeta"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "Presentar",
+ "exitPresent": "Salir de la presentación"
+ },
+ "presentation": {
+ "empty": "No hay registros para presentar",
+ "counter": "{{current}} de {{total}}",
+ "previous": "Anterior",
+ "next": "Siguiente",
+ "exit": "Salir de la presentación",
+ "keyboardHint": "← → o J K para navegar · Esc para salir"
+ }
+ },
"calendar": {
"toolbar": {
"config": "Configuración de Calendario",
diff --git a/packages/common-i18n/src/locales/fr/table.json b/packages/common-i18n/src/locales/fr/table.json
index 73ce4d60e4..e9fdc6ba74 100644
--- a/packages/common-i18n/src/locales/fr/table.json
+++ b/packages/common-i18n/src/locales/fr/table.json
@@ -1067,6 +1067,20 @@
"duplicateCard": "Dupliquer la carte"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "Présenter",
+ "exitPresent": "Quitter la présentation"
+ },
+ "presentation": {
+ "empty": "Aucun enregistrement à présenter",
+ "counter": "{{current}} sur {{total}}",
+ "previous": "Précédent",
+ "next": "Suivant",
+ "exit": "Quitter la présentation",
+ "keyboardHint": "← → ou J K pour naviguer · Échap pour quitter"
+ }
+ },
"calendar": {
"toolbar": {
"config": "Configuration du calendrier",
diff --git a/packages/common-i18n/src/locales/he/table.json b/packages/common-i18n/src/locales/he/table.json
index dcd52437d8..07bb972a18 100644
--- a/packages/common-i18n/src/locales/he/table.json
+++ b/packages/common-i18n/src/locales/he/table.json
@@ -1067,6 +1067,20 @@
"duplicateCard": "שכפול הכרטיס"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "הצגה",
+ "exitPresent": "יציאה מההצגה"
+ },
+ "presentation": {
+ "empty": "אין רשומות להצגה",
+ "counter": "{{current}} מתוך {{total}}",
+ "previous": "הקודם",
+ "next": "הבא",
+ "exit": "יציאה מההצגה",
+ "keyboardHint": "← → או J K לניווט · Esc ליציאה"
+ }
+ },
"calendar": {
"toolbar": {
"config": "הגדרות לוח השנה",
diff --git a/packages/common-i18n/src/locales/it/table.json b/packages/common-i18n/src/locales/it/table.json
index 8b9a93a95c..82b4371843 100644
--- a/packages/common-i18n/src/locales/it/table.json
+++ b/packages/common-i18n/src/locales/it/table.json
@@ -1067,6 +1067,20 @@
"duplicateCard": "Duplica carta"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "Presenta",
+ "exitPresent": "Esci dalla presentazione"
+ },
+ "presentation": {
+ "empty": "Nessun record da presentare",
+ "counter": "{{current}} di {{total}}",
+ "previous": "Precedente",
+ "next": "Successivo",
+ "exit": "Esci dalla presentazione",
+ "keyboardHint": "← → o J K per navigare · Esc per uscire"
+ }
+ },
"calendar": {
"toolbar": {
"config": "Configurazione Calendario",
diff --git a/packages/common-i18n/src/locales/ja/table.json b/packages/common-i18n/src/locales/ja/table.json
index e4b573484e..8916ef9e6b 100644
--- a/packages/common-i18n/src/locales/ja/table.json
+++ b/packages/common-i18n/src/locales/ja/table.json
@@ -1057,6 +1057,20 @@
"duplicateCard": "カードを複製"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "プレゼン",
+ "exitPresent": "プレゼンを終了"
+ },
+ "presentation": {
+ "empty": "表示するレコードがありません",
+ "counter": "{{current}} / {{total}}",
+ "previous": "前へ",
+ "next": "次へ",
+ "exit": "プレゼンを終了",
+ "keyboardHint": "← → または J K で移動 · Esc で終了"
+ }
+ },
"calendar": {
"toolbar": {
"config": "カレンダー設定",
diff --git a/packages/common-i18n/src/locales/ru/table.json b/packages/common-i18n/src/locales/ru/table.json
index 59ce483f70..44bf015851 100644
--- a/packages/common-i18n/src/locales/ru/table.json
+++ b/packages/common-i18n/src/locales/ru/table.json
@@ -1072,6 +1072,20 @@
"duplicateCard": "Дублировать карточку"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "Презентация",
+ "exitPresent": "Выйти из презентации"
+ },
+ "presentation": {
+ "empty": "Нет записей для презентации",
+ "counter": "{{current}} из {{total}}",
+ "previous": "Назад",
+ "next": "Далее",
+ "exit": "Выйти из презентации",
+ "keyboardHint": "← → или J K для навигации · Esc для выхода"
+ }
+ },
"calendar": {
"toolbar": {
"config": "Настройка календаря",
diff --git a/packages/common-i18n/src/locales/tr/table.json b/packages/common-i18n/src/locales/tr/table.json
index dab277566b..4a40c064fe 100644
--- a/packages/common-i18n/src/locales/tr/table.json
+++ b/packages/common-i18n/src/locales/tr/table.json
@@ -1062,6 +1062,20 @@
"duplicateCard": "Kartı çoğalt"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "Sunum",
+ "exitPresent": "Sunumdan çık"
+ },
+ "presentation": {
+ "empty": "Sunulacak kayıt yok",
+ "counter": "{{current}} / {{total}}",
+ "previous": "Önceki",
+ "next": "Sonraki",
+ "exit": "Sunumdan çık",
+ "keyboardHint": "← → veya J K ile gezinin · Esc ile çıkın"
+ }
+ },
"calendar": {
"toolbar": {
"config": "Takvim Yapılandırması",
diff --git a/packages/common-i18n/src/locales/uk/table.json b/packages/common-i18n/src/locales/uk/table.json
index 7ab63c28a1..c163461f6e 100644
--- a/packages/common-i18n/src/locales/uk/table.json
+++ b/packages/common-i18n/src/locales/uk/table.json
@@ -1072,6 +1072,20 @@
"duplicateCard": "Дублірувати картку"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "Презентація",
+ "exitPresent": "Вийти з презентації"
+ },
+ "presentation": {
+ "empty": "Немає записів для презентації",
+ "counter": "{{current}} з {{total}}",
+ "previous": "Назад",
+ "next": "Далі",
+ "exit": "Вийти з презентації",
+ "keyboardHint": "← → або J K для навігації · Esc для виходу"
+ }
+ },
"calendar": {
"toolbar": {
"config": "Налаштування календаря",
diff --git a/packages/common-i18n/src/locales/zh/table.json b/packages/common-i18n/src/locales/zh/table.json
index 58c124d2a6..ab7fb0ee56 100644
--- a/packages/common-i18n/src/locales/zh/table.json
+++ b/packages/common-i18n/src/locales/zh/table.json
@@ -1057,6 +1057,20 @@
"duplicateCard": "复制卡片"
}
},
+ "gallery": {
+ "toolbar": {
+ "present": "演示",
+ "exitPresent": "退出演示"
+ },
+ "presentation": {
+ "empty": "没有可演示的记录",
+ "counter": "{{current}} / {{total}}",
+ "previous": "上一条",
+ "next": "下一条",
+ "exit": "退出演示",
+ "keyboardHint": "← → 或 J K 切换 · Esc 退出"
+ }
+ },
"calendar": {
"toolbar": {
"config": "日历配置",