Skip to content

Commit c2ae62e

Browse files
committed
Add responsive schema sidebar with mobile drawer support
Introduces a new SchemaSidebar component that adapts between a traditional sidebar and a draggable bottom drawer for mobile/small containers. Adds use-ui-container hooks and context for React, Svelte, and Vue to provide access to the UI root container, enabling responsive behavior and portaling. Updates imports and usage throughout the snippet app, removes the old SchemaPanel, and tweaks sidebar/tab UI for improved usability.
1 parent dd169a9 commit c2ae62e

File tree

14 files changed

+663
-149
lines changed

14 files changed

+663
-149
lines changed

packages/plugin-ui/src/shared/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './use-ui';
2+
export * from './use-ui-container';
23
export * from './use-register-anchor';
34
export * from './use-item-renderer';
45
export * from './use-schema-renderer';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createContext, useContext, RefObject } from '@framework';
2+
3+
export interface UIContainerContextValue {
4+
/** Reference to the UIRoot container element */
5+
containerRef: RefObject<HTMLDivElement>;
6+
/** Get the container element (may be null if not mounted) */
7+
getContainer: () => HTMLDivElement | null;
8+
}
9+
10+
export const UIContainerContext = createContext<UIContainerContextValue | null>(null);
11+
12+
/**
13+
* Hook to access the UI container element.
14+
*
15+
* This provides access to the UIRoot container for:
16+
* - Container query based responsiveness
17+
* - Portaling elements to the root
18+
* - Measuring container dimensions
19+
*
20+
* @example
21+
* ```tsx
22+
* function MyComponent() {
23+
* const { containerRef, getContainer } = useUIContainer();
24+
*
25+
* // Use containerRef for ResizeObserver
26+
* useEffect(() => {
27+
* const container = getContainer();
28+
* if (!container) return;
29+
*
30+
* const observer = new ResizeObserver(() => {
31+
* console.log('Container width:', container.clientWidth);
32+
* });
33+
* observer.observe(container);
34+
* return () => observer.disconnect();
35+
* }, [getContainer]);
36+
*
37+
* // Or portal to the container
38+
* return createPortal(<Modal />, getContainer()!);
39+
* }
40+
* ```
41+
*/
42+
export function useUIContainer(): UIContainerContextValue {
43+
const context = useContext(UIContainerContext);
44+
if (!context) {
45+
throw new Error('useUIContainer must be used within a UIProvider');
46+
}
47+
return context;
48+
}

packages/plugin-ui/src/shared/root.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { UI_ATTRIBUTES, UI_SELECTORS } from '@embedpdf/plugin-ui';
22
import { useUICapability, useUIPlugin } from './hooks/use-ui';
3+
import { UIContainerContext, UIContainerContextValue } from './hooks/use-ui-container';
34
import {
45
useState,
56
useEffect,
@@ -39,15 +40,26 @@ export function UIRoot({ children, style, ...restProps }: UIRootProps) {
3940
const styleElRef = useRef<HTMLStyleElement | null>(null);
4041
const styleTargetRef = useRef<HTMLElement | ShadowRoot | null>(null);
4142
const previousElementRef = useRef<HTMLDivElement | null>(null);
43+
const containerRef = useRef<HTMLDivElement>(null);
44+
45+
// Create container context value (memoized to prevent unnecessary re-renders)
46+
const containerContextValue = useMemo<UIContainerContextValue>(
47+
() => ({
48+
containerRef,
49+
getContainer: () => containerRef.current,
50+
}),
51+
[],
52+
);
4253

4354
// Callback ref that handles style injection when element mounts
4455
// Handles React Strict Mode by tracking previous element
4556
const rootRefCallback = useCallback(
4657
(element: HTMLDivElement | null) => {
4758
const previousElement = previousElementRef.current;
4859

49-
// Update ref
60+
// Update refs
5061
previousElementRef.current = element;
62+
(containerRef as any).current = element;
5163

5264
// If element is null (unmount), don't do anything yet
5365
// React Strict Mode will remount, so we'll handle cleanup in useEffect
@@ -147,8 +159,10 @@ export function UIRoot({ children, style, ...restProps }: UIRootProps) {
147159
};
148160

149161
return (
150-
<div ref={rootRefCallback} {...rootProps} {...restProps} style={combinedStyle}>
151-
{children}
152-
</div>
162+
<UIContainerContext.Provider value={containerContextValue}>
163+
<div ref={rootRefCallback} {...rootProps} {...restProps} style={combinedStyle}>
164+
{children}
165+
</div>
166+
</UIContainerContext.Provider>
153167
);
154168
}

packages/plugin-ui/src/svelte/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './use-ui.svelte';
2+
export * from './use-ui-container.svelte';
23
export * from './use-register-anchor.svelte';
34
export * from './use-item-renderer.svelte';
45
export * from './use-schema-renderer.svelte';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getContext, setContext } from 'svelte';
2+
3+
export interface UIContainerContextValue {
4+
/** Get the container element (may be null if not mounted) */
5+
getContainer: () => HTMLDivElement | null;
6+
}
7+
8+
const UI_CONTAINER_KEY = Symbol('ui-container');
9+
10+
/**
11+
* Set up the container context (called by UIRoot)
12+
*/
13+
export function setUIContainerContext(value: UIContainerContextValue): void {
14+
setContext(UI_CONTAINER_KEY, value);
15+
}
16+
17+
/**
18+
* Hook to access the UI container element.
19+
*
20+
* This provides access to the UIRoot container for:
21+
* - Container query based responsiveness
22+
* - Portaling elements to the root
23+
* - Measuring container dimensions
24+
*
25+
* @example
26+
* ```svelte
27+
* <script>
28+
* import { useUIContainer } from '@embedpdf/plugin-ui/svelte';
29+
* import { onMount, onDestroy } from 'svelte';
30+
*
31+
* const { getContainer } = useUIContainer();
32+
*
33+
* let observer;
34+
*
35+
* onMount(() => {
36+
* const container = getContainer();
37+
* if (!container) return;
38+
*
39+
* observer = new ResizeObserver(() => {
40+
* console.log('Container width:', container.clientWidth);
41+
* });
42+
* observer.observe(container);
43+
* });
44+
*
45+
* onDestroy(() => observer?.disconnect());
46+
* </script>
47+
* ```
48+
*/
49+
export function useUIContainer(): UIContainerContextValue {
50+
const context = getContext<UIContainerContextValue>(UI_CONTAINER_KEY);
51+
if (!context) {
52+
throw new Error('useUIContainer must be used within a UIProvider');
53+
}
54+
return context;
55+
}

packages/plugin-ui/src/svelte/root.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { UI_ATTRIBUTES, UI_SELECTORS } from '@embedpdf/plugin-ui';
33
import { useUIPlugin, useUICapability } from './hooks/use-ui.svelte';
4+
import { setUIContainerContext } from './hooks/use-ui-container.svelte';
45
import type { Snippet } from 'svelte';
56
import type { HTMLAttributes } from 'svelte/elements';
67
@@ -18,6 +19,11 @@
1819
let styleEl: HTMLStyleElement | null = null;
1920
let styleTarget: HTMLElement | ShadowRoot | null = null;
2021
22+
// Provide container context for child components
23+
setUIContainerContext({
24+
getContainer: () => rootElement,
25+
});
26+
2127
function getStyleTarget(element: HTMLElement): HTMLElement | ShadowRoot {
2228
const root = element.getRootNode();
2329
if (root instanceof ShadowRoot) {

packages/plugin-ui/src/vue/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './use-ui';
2+
export * from './use-ui-container';
23
export * from './use-register-anchor';
34
export * from './use-item-renderer';
45
export * from './use-schema-renderer';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { inject, type Ref, type InjectionKey } from 'vue';
2+
3+
export interface UIContainerContextValue {
4+
/** Reference to the UIRoot container element */
5+
containerRef: Ref<HTMLDivElement | null>;
6+
/** Get the container element (may be null if not mounted) */
7+
getContainer: () => HTMLDivElement | null;
8+
}
9+
10+
export const UI_CONTAINER_KEY: InjectionKey<UIContainerContextValue> = Symbol('ui-container');
11+
12+
/**
13+
* Hook to access the UI container element.
14+
*
15+
* This provides access to the UIRoot container for:
16+
* - Container query based responsiveness
17+
* - Portaling elements to the root
18+
* - Measuring container dimensions
19+
*
20+
* @example
21+
* ```vue
22+
* <script setup>
23+
* import { useUIContainer } from '@embedpdf/plugin-ui/vue';
24+
* import { onMounted, onUnmounted } from 'vue';
25+
*
26+
* const { containerRef, getContainer } = useUIContainer();
27+
*
28+
* onMounted(() => {
29+
* const container = getContainer();
30+
* if (!container) return;
31+
*
32+
* const observer = new ResizeObserver(() => {
33+
* console.log('Container width:', container.clientWidth);
34+
* });
35+
* observer.observe(container);
36+
*
37+
* onUnmounted(() => observer.disconnect());
38+
* });
39+
* </script>
40+
* ```
41+
*/
42+
export function useUIContainer(): UIContainerContextValue {
43+
const context = inject(UI_CONTAINER_KEY);
44+
if (!context) {
45+
throw new Error('useUIContainer must be used within a UIProvider');
46+
}
47+
return context;
48+
}

packages/plugin-ui/src/vue/root.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
</template>
1010

1111
<script setup lang="ts">
12-
import { ref, computed, onMounted, onUnmounted, watch, useAttrs } from 'vue';
12+
import { ref, computed, onMounted, onUnmounted, watch, useAttrs, provide } from 'vue';
1313
import { UI_ATTRIBUTES, UI_SELECTORS } from '@embedpdf/plugin-ui';
1414
import { useUIPlugin, useUICapability } from './hooks/use-ui';
15+
import { UI_CONTAINER_KEY, type UIContainerContextValue } from './hooks/use-ui-container';
1516
1617
// Disable automatic attribute inheritance since we handle it manually
1718
defineOptions({
@@ -26,6 +27,13 @@ const { provides } = useUICapability();
2627
const disabledCategories = ref<string[]>([]);
2728
const rootRef = ref<HTMLDivElement | null>(null);
2829
30+
// Provide container context for child components
31+
const containerContext: UIContainerContextValue = {
32+
containerRef: rootRef,
33+
getContainer: () => rootRef.value,
34+
};
35+
provide(UI_CONTAINER_KEY, containerContext);
36+
2937
let styleEl: HTMLStyleElement | null = null;
3038
let styleTarget: HTMLElement | ShadowRoot | null = null;
3139

snippet/src/components/app.tsx

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,6 @@ import {
3535
} from '@embedpdf/plugin-document-manager/preact';
3636
import { CommandsPluginPackage } from '@embedpdf/plugin-commands/preact';
3737
import { I18nPluginPackage } from '@embedpdf/plugin-i18n/preact';
38-
import {
39-
commands,
40-
viewerUISchema,
41-
englishTranslations,
42-
paramResolvers,
43-
dutchTranslations,
44-
germanTranslations,
45-
frenchTranslations,
46-
} from '../config';
47-
import { SchemaToolbar } from '../ui/schema-toolbar';
48-
import { SchemaPanel } from '../ui/schema-panel';
49-
import { SchemaMenu } from '../ui/schema-menu';
50-
import { SchemaModal } from '../ui/schema-modal';
51-
// Custom components for schema-driven UI
52-
import { ThumbnailsSidebar } from './thumbnails-sidebar';
53-
import { SearchSidebar } from './search-sidebar';
54-
import { OutlineSidebar } from './outline-sidebar';
5538
import {
5639
MarqueeZoom,
5740
ZoomMode,
@@ -69,7 +52,6 @@ import {
6952
} from '@embedpdf/plugin-tiling/preact';
7053
import { ThumbnailPluginConfig, ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/preact';
7154
import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/preact';
72-
import { LoadingIndicator } from './ui/loading-indicator';
7355
import { PrintPluginPackage } from '@embedpdf/plugin-print/preact';
7456
import { FullscreenPluginPackage } from '@embedpdf/plugin-fullscreen/preact';
7557
import { BookmarkPluginPackage } from '@embedpdf/plugin-bookmark/preact';
@@ -84,14 +66,35 @@ import { MarqueeCapture, CapturePluginPackage } from '@embedpdf/plugin-capture/p
8466
import { HistoryPluginPackage } from '@embedpdf/plugin-history/preact';
8567
import { RedactionLayer, RedactionPluginPackage } from '@embedpdf/plugin-redaction/preact';
8668
import { AttachmentPluginPackage } from '@embedpdf/plugin-attachment/preact';
87-
import { HintLayer } from './hint-layer';
88-
import { CommentSidebar } from './comment-sidebar';
89-
import { CustomZoomToolbar } from './custom-zoom-toolbar';
90-
import { AnnotationSidebar } from './annotation-sidebar';
69+
70+
import { SchemaToolbar } from '@/ui/schema-toolbar';
71+
import { SchemaSidebar } from '@/ui/schema-sidebar';
72+
import { SchemaMenu } from '@/ui/schema-menu';
73+
import { SchemaModal } from '@/ui/schema-modal';
74+
// Custom components for schema-driven UI
75+
import { ThumbnailsSidebar } from '@/components/thumbnails-sidebar';
76+
import { SearchSidebar } from '@/components/search-sidebar';
77+
import { OutlineSidebar } from '@/components/outline-sidebar';
78+
79+
import { LoadingIndicator } from '@/components/ui/loading-indicator';
80+
import { HintLayer } from '@/components/hint-layer';
81+
import { CommentSidebar } from '@/components/comment-sidebar';
82+
import { CustomZoomToolbar } from '@/components/custom-zoom-toolbar';
83+
import { AnnotationSidebar } from '@/components/annotation-sidebar';
9184
import { SchemaSelectionMenu } from '@/ui/schema-selection-menu';
9285
import { SchemaOverlay } from '@/ui/schema-overlay';
93-
import { PrintModal } from './print-modal';
94-
import { PageControls } from './page-controls';
86+
import { PrintModal } from '@/components/print-modal';
87+
import { PageControls } from '@/components/page-controls';
88+
89+
import {
90+
commands,
91+
viewerUISchema,
92+
englishTranslations,
93+
paramResolvers,
94+
dutchTranslations,
95+
germanTranslations,
96+
frenchTranslations,
97+
} from '@/config';
9598

9699
export { ScrollStrategy, ZoomMode, SpreadMode, Rotation };
97100

@@ -303,9 +306,9 @@ export function PDFViewer({ config, onRegistryReady }: PDFViewerProps) {
303306
const uiRenderers = useMemo(
304307
() => ({
305308
toolbar: SchemaToolbar,
306-
sidebar: SchemaPanel, // SchemaPanel handles sidebars
309+
sidebar: SchemaSidebar,
307310
modal: SchemaModal,
308-
overlay: SchemaOverlay, // SchemaOverlay handles floating overlays
311+
overlay: SchemaOverlay,
309312
menu: SchemaMenu,
310313
selectionMenu: SchemaSelectionMenu,
311314
}),

0 commit comments

Comments
 (0)