pdf-smith 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,19 @@
1
1
  import { getDocuments } from 'virtual:pdf-smith-documents';
2
- import { useState } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { Document } from '../document';
4
4
  import { Page } from '../page';
5
+ import { colors, fonts, radii, SIDEBAR_WIDTH, TOOLBAR_HEIGHT } from './design-tokens';
6
+ import { MinusIcon, PlusSmallIcon } from './icons';
5
7
  import { Navigation } from './navigation';
6
8
 
7
- const containerStyle: React.CSSProperties = {
8
- marginLeft: '240px',
9
- minHeight: '100vh',
10
- background: '#e8e8e8',
11
- padding: '32px',
12
- boxSizing: 'border-box',
13
- };
14
-
15
- const pageWrapperStyle: React.CSSProperties = {
16
- marginBottom: '24px',
17
- };
18
-
19
- const pageLabelStyle: React.CSSProperties = {
20
- fontFamily: 'system-ui, -apple-system, sans-serif',
21
- fontSize: '12px',
22
- color: '#666',
23
- marginBottom: '8px',
24
- marginLeft: 'auto',
25
- marginRight: 'auto',
26
- width: 'fit-content',
27
- };
9
+ const ZOOM_STEPS = [50, 75, 100, 125, 150, 200];
10
+
11
+ function formatSlug(slug: string): string {
12
+ return slug
13
+ .split('-')
14
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
15
+ .join(' ');
16
+ }
28
17
 
29
18
  interface PreviewAppProps {
30
19
  slug: string;
@@ -35,8 +24,47 @@ export function PreviewApp({ slug }: PreviewAppProps) {
35
24
  const doc = documents[slug];
36
25
  const pages = doc?.pages ?? {};
37
26
  const [activePage, setActivePage] = useState<string | null>(null);
38
-
27
+ const [zoom, setZoom] = useState(100);
28
+ const pageNames = Object.keys(pages);
39
29
  const visiblePages = activePage ? { [activePage]: pages[activePage] } : pages;
30
+ const visibleNames = Object.keys(visiblePages);
31
+
32
+ // Minimap: track which page is in view
33
+ const pageRefs = useRef<Record<string, HTMLDivElement | null>>({});
34
+ const [activeMinimapIdx, setActiveMinimapIdx] = useState(0);
35
+
36
+ const observerCallback = useCallback(
37
+ (entries: IntersectionObserverEntry[]) => {
38
+ for (const entry of entries) {
39
+ if (entry.isIntersecting) {
40
+ const idx = visibleNames.indexOf(entry.target.getAttribute('data-page-name') ?? '');
41
+ if (idx !== -1) setActiveMinimapIdx(idx);
42
+ }
43
+ }
44
+ },
45
+ [visibleNames],
46
+ );
47
+
48
+ useEffect(() => {
49
+ const observer = new IntersectionObserver(observerCallback, {
50
+ threshold: 0.5,
51
+ });
52
+ for (const name of visibleNames) {
53
+ const el = pageRefs.current[name];
54
+ if (el) observer.observe(el);
55
+ }
56
+ return () => observer.disconnect();
57
+ }, [visibleNames, observerCallback]);
58
+
59
+ function zoomIn() {
60
+ const next = ZOOM_STEPS.find((s) => s > zoom);
61
+ if (next) setZoom(next);
62
+ }
63
+
64
+ function zoomOut() {
65
+ const prev = [...ZOOM_STEPS].reverse().find((s) => s < zoom);
66
+ if (prev) setZoom(prev);
67
+ }
40
68
 
41
69
  return (
42
70
  <>
@@ -46,18 +74,218 @@ export function PreviewApp({ slug }: PreviewAppProps) {
46
74
  onSelectPage={setActivePage}
47
75
  documentSlug={slug}
48
76
  />
49
- <div data-pdf-smith-container="" style={containerStyle}>
50
- <Document>
51
- {Object.entries(visiblePages).map(([name, PageComponent]) => (
52
- <div key={name} style={pageWrapperStyle}>
53
- <div style={pageLabelStyle}>{name}</div>
54
- <Page>
55
- <PageComponent />
56
- </Page>
57
- </div>
58
- ))}
59
- </Document>
77
+
78
+ {/* Toolbar */}
79
+ <div
80
+ data-pdf-smith-toolbar=""
81
+ style={{
82
+ position: 'fixed',
83
+ top: 0,
84
+ left: `${SIDEBAR_WIDTH}px`,
85
+ right: 0,
86
+ height: `${TOOLBAR_HEIGHT}px`,
87
+ background: colors.stone50,
88
+ borderBottom: `1px solid ${colors.stone200}`,
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ justifyContent: 'space-between',
92
+ padding: '0 24px',
93
+ zIndex: 50,
94
+ fontFamily: fonts.body,
95
+ }}
96
+ >
97
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
98
+ <span style={{ fontSize: '13px', color: colors.stone500 }}>
99
+ <strong style={{ color: colors.stone700, fontWeight: 600 }}>{formatSlug(slug)}</strong>
100
+ {' \u00B7 '}
101
+ {activePage ?? 'All Pages'}
102
+ </span>
103
+ </div>
104
+ <div
105
+ style={{
106
+ display: 'flex',
107
+ alignItems: 'center',
108
+ gap: '4px',
109
+ fontFamily: fonts.mono,
110
+ fontSize: '12px',
111
+ color: colors.stone500,
112
+ }}
113
+ >
114
+ <button
115
+ type="button"
116
+ onClick={zoomOut}
117
+ disabled={zoom <= ZOOM_STEPS[0]}
118
+ style={{
119
+ width: '28px',
120
+ height: '28px',
121
+ display: 'flex',
122
+ alignItems: 'center',
123
+ justifyContent: 'center',
124
+ background: 'none',
125
+ border: `1px solid ${colors.stone200}`,
126
+ borderRadius: radii.sm,
127
+ color: zoom <= ZOOM_STEPS[0] ? colors.stone300 : colors.stone500,
128
+ cursor: zoom <= ZOOM_STEPS[0] ? 'default' : 'pointer',
129
+ padding: 0,
130
+ }}
131
+ >
132
+ <MinusIcon size={14} />
133
+ </button>
134
+ <button
135
+ type="button"
136
+ onClick={() => setZoom(100)}
137
+ style={{
138
+ background: 'none',
139
+ border: 'none',
140
+ fontFamily: fonts.mono,
141
+ fontSize: '12px',
142
+ color: colors.stone500,
143
+ cursor: 'pointer',
144
+ padding: '4px 6px',
145
+ borderRadius: radii.sm,
146
+ minWidth: '42px',
147
+ textAlign: 'center',
148
+ }}
149
+ title="Reset zoom to 100%"
150
+ >
151
+ {zoom}%
152
+ </button>
153
+ <button
154
+ type="button"
155
+ onClick={zoomIn}
156
+ disabled={zoom >= ZOOM_STEPS[ZOOM_STEPS.length - 1]}
157
+ style={{
158
+ width: '28px',
159
+ height: '28px',
160
+ display: 'flex',
161
+ alignItems: 'center',
162
+ justifyContent: 'center',
163
+ background: 'none',
164
+ border: `1px solid ${colors.stone200}`,
165
+ borderRadius: radii.sm,
166
+ color: zoom >= ZOOM_STEPS[ZOOM_STEPS.length - 1] ? colors.stone300 : colors.stone500,
167
+ cursor: zoom >= ZOOM_STEPS[ZOOM_STEPS.length - 1] ? 'default' : 'pointer',
168
+ padding: 0,
169
+ }}
170
+ >
171
+ <PlusSmallIcon size={14} />
172
+ </button>
173
+ </div>
60
174
  </div>
175
+
176
+ {/* Content */}
177
+ <div
178
+ data-pdf-smith-container=""
179
+ style={{
180
+ marginLeft: `${SIDEBAR_WIDTH}px`,
181
+ paddingTop: `${TOOLBAR_HEIGHT}px`,
182
+ minHeight: '100vh',
183
+ background: colors.stone200,
184
+ boxSizing: 'border-box',
185
+ }}
186
+ >
187
+ <div
188
+ style={{
189
+ padding: '40px',
190
+ display: 'flex',
191
+ flexDirection: 'column',
192
+ alignItems: 'center',
193
+ gap: '32px',
194
+ }}
195
+ >
196
+ <Document>
197
+ {Object.entries(visiblePages).map(([name, PageComponent]) => (
198
+ <div
199
+ key={name}
200
+ ref={(el) => {
201
+ pageRefs.current[name] = el;
202
+ }}
203
+ data-page-name={name}
204
+ >
205
+ <div
206
+ style={{
207
+ fontFamily: fonts.mono,
208
+ fontSize: '11px',
209
+ color: colors.stone400,
210
+ marginTop: '10px',
211
+ marginBottom: '8px',
212
+ display: 'flex',
213
+ alignItems: 'center',
214
+ justifyContent: 'space-between',
215
+ }}
216
+ >
217
+ <span>
218
+ Page {pageNames.indexOf(name) + 1} &mdash; {name}
219
+ </span>
220
+ </div>
221
+ <div
222
+ style={{
223
+ transform: `scale(${zoom / 100})`,
224
+ transformOrigin: 'top center',
225
+ transition: 'transform 0.15s ease',
226
+ }}
227
+ >
228
+ <Page>
229
+ <PageComponent />
230
+ </Page>
231
+ </div>
232
+ </div>
233
+ ))}
234
+ </Document>
235
+ </div>
236
+ </div>
237
+
238
+ {/* Minimap */}
239
+ {visibleNames.length > 1 && (
240
+ <div
241
+ data-pdf-smith-minimap=""
242
+ style={{
243
+ position: 'fixed',
244
+ right: '20px',
245
+ top: '50%',
246
+ transform: 'translateY(-50%)',
247
+ display: 'flex',
248
+ flexDirection: 'column',
249
+ gap: '8px',
250
+ zIndex: 50,
251
+ }}
252
+ >
253
+ {visibleNames.map((name, idx) => (
254
+ <button
255
+ type="button"
256
+ key={name}
257
+ title={`Page ${pageNames.indexOf(name) + 1} — ${name}`}
258
+ aria-label={`Go to page ${pageNames.indexOf(name) + 1}: ${name}`}
259
+ style={{
260
+ width: '8px',
261
+ height: '8px',
262
+ borderRadius: '50%',
263
+ background: idx === activeMinimapIdx ? colors.forge : colors.stone300,
264
+ cursor: 'pointer',
265
+ transition: 'all 0.15s',
266
+ transform: idx === activeMinimapIdx ? 'scale(1.3)' : 'none',
267
+ border: 'none',
268
+ padding: 0,
269
+ }}
270
+ onClick={() => {
271
+ pageRefs.current[name]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
272
+ }}
273
+ onMouseEnter={(e) => {
274
+ if (idx !== activeMinimapIdx) {
275
+ e.currentTarget.style.background = colors.stone500;
276
+ e.currentTarget.style.transform = 'scale(1.3)';
277
+ }
278
+ }}
279
+ onMouseLeave={(e) => {
280
+ if (idx !== activeMinimapIdx) {
281
+ e.currentTarget.style.background = colors.stone300;
282
+ e.currentTarget.style.transform = 'none';
283
+ }
284
+ }}
285
+ />
286
+ ))}
287
+ </div>
288
+ )}
61
289
  </>
62
290
  );
63
291
  }
@@ -0,0 +1,42 @@
1
+ export const colors = {
2
+ forge: '#C2410C',
3
+ ember: '#EA580C',
4
+ deepForge: '#9A3412',
5
+ iron: '#292524',
6
+ amber: '#D97706',
7
+ gold: '#F59E0B',
8
+
9
+ stone50: '#FAF9F7',
10
+ stone100: '#F5F3F0',
11
+ stone200: '#E7E4DF',
12
+ stone300: '#D6D0C8',
13
+ stone400: '#A8A29E',
14
+ stone500: '#78716C',
15
+ stone600: '#57534E',
16
+ stone700: '#44403C',
17
+ stone800: '#292524',
18
+ stone900: '#1C1917',
19
+ stone950: '#0C0A09',
20
+
21
+ success: '#16A34A',
22
+ } as const;
23
+
24
+ export const fonts = {
25
+ display: "'Fraunces', Georgia, serif",
26
+ heading: "'Inter', system-ui, sans-serif",
27
+ body: "'Inter', system-ui, sans-serif",
28
+ mono: "'JetBrains Mono', monospace",
29
+ } as const;
30
+
31
+ export const radii = {
32
+ sm: '4px',
33
+ md: '6px',
34
+ lg: '8px',
35
+ xl: '12px',
36
+ '2xl': '16px',
37
+ full: '100px',
38
+ } as const;
39
+
40
+ export const SIDEBAR_WIDTH = 260;
41
+ export const TOOLBAR_HEIGHT = 48;
42
+ export const VERSION = '0.2.0';