pdf-smith 0.2.0 → 0.3.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,95 +1,550 @@
1
1
  import { getDocuments } from 'virtual:pdf-smith-documents';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { colors, fonts, radii, VERSION } from './design-tokens';
4
+ import { AnvilIcon, AnvilIconWhite, DocumentIcon, SearchIcon } from './icons';
2
5
 
3
- const containerStyle: React.CSSProperties = {
4
- minHeight: '100vh',
5
- background: '#1a1a2e',
6
- color: '#e0e0e0',
7
- fontFamily: 'system-ui, -apple-system, sans-serif',
8
- display: 'flex',
9
- flexDirection: 'column',
10
- alignItems: 'center',
11
- padding: '48px 24px',
12
- };
6
+ function formatSlug(slug: string): string {
7
+ return slug
8
+ .split('-')
9
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
10
+ .join(' ');
11
+ }
13
12
 
14
- const titleStyle: React.CSSProperties = {
15
- fontSize: '28px',
16
- fontWeight: 700,
17
- color: '#ffffff',
18
- marginBottom: '8px',
19
- letterSpacing: '0.5px',
20
- };
13
+ export function HomePage() {
14
+ const documents = getDocuments();
15
+ const slugs = Object.keys(documents);
16
+ const [search, setSearch] = useState('');
17
+ const [searchFocused, setSearchFocused] = useState(false);
18
+ const searchRef = useRef<HTMLInputElement>(null);
21
19
 
22
- const subtitleStyle: React.CSSProperties = {
23
- fontSize: '14px',
24
- color: '#888',
25
- marginBottom: '40px',
26
- };
20
+ const filtered = search
21
+ ? slugs.filter((s) => s.toLowerCase().includes(search.toLowerCase()))
22
+ : slugs;
27
23
 
28
- const gridStyle: React.CSSProperties = {
29
- display: 'grid',
30
- gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
31
- gap: '16px',
32
- width: '100%',
33
- maxWidth: '800px',
34
- };
24
+ useEffect(() => {
25
+ function handleKeyDown(e: KeyboardEvent) {
26
+ if (e.key === '/' && document.activeElement !== searchRef.current) {
27
+ e.preventDefault();
28
+ searchRef.current?.focus();
29
+ }
30
+ }
31
+ document.addEventListener('keydown', handleKeyDown);
32
+ return () => document.removeEventListener('keydown', handleKeyDown);
33
+ }, []);
35
34
 
36
- const cardStyle: React.CSSProperties = {
37
- display: 'block',
38
- background: '#16213e',
39
- borderRadius: '8px',
40
- padding: '24px',
41
- textDecoration: 'none',
42
- color: '#e0e0e0',
43
- transition: 'background 0.15s',
44
- border: '1px solid #2a2a4a',
45
- };
35
+ return (
36
+ <div style={{ minHeight: '100vh', background: colors.stone900, color: colors.stone100 }}>
37
+ {/* Top Bar */}
38
+ <header
39
+ style={{
40
+ display: 'flex',
41
+ alignItems: 'center',
42
+ justifyContent: 'space-between',
43
+ padding: '12px 24px',
44
+ background: colors.stone950,
45
+ borderBottom: `1px solid ${colors.stone800}`,
46
+ }}
47
+ >
48
+ <a
49
+ href="/"
50
+ style={{
51
+ display: 'flex',
52
+ alignItems: 'center',
53
+ gap: '10px',
54
+ textDecoration: 'none',
55
+ color: colors.stone100,
56
+ }}
57
+ >
58
+ <AnvilIcon size={24} />
59
+ <span
60
+ style={{
61
+ fontFamily: fonts.display,
62
+ fontSize: '18px',
63
+ fontWeight: 700,
64
+ fontVariationSettings: "'WONK' 1, 'SOFT' 50",
65
+ }}
66
+ >
67
+ pdf-smith
68
+ </span>
69
+ </a>
70
+ <span
71
+ style={{
72
+ fontFamily: fonts.mono,
73
+ fontSize: '11px',
74
+ color: colors.stone500,
75
+ background: colors.stone800,
76
+ padding: '3px 10px',
77
+ borderRadius: radii.full,
78
+ border: `1px solid ${colors.stone700}`,
79
+ }}
80
+ >
81
+ v{VERSION}
82
+ </span>
83
+ </header>
46
84
 
47
- const cardTitleStyle: React.CSSProperties = {
48
- fontSize: '18px',
49
- fontWeight: 600,
50
- color: '#ffffff',
51
- marginBottom: '8px',
52
- };
85
+ {/* Main */}
86
+ <main style={{ maxWidth: '960px', margin: '0 auto', padding: '64px 32px' }}>
87
+ {/* Hero */}
88
+ <div style={{ textAlign: 'center', marginBottom: '64px' }}>
89
+ <div
90
+ style={{
91
+ width: '56px',
92
+ height: '56px',
93
+ margin: '0 auto 24px',
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ justifyContent: 'center',
97
+ background: `linear-gradient(135deg, ${colors.forge}, ${colors.amber})`,
98
+ borderRadius: '14px',
99
+ }}
100
+ >
101
+ <AnvilIconWhite size={28} />
102
+ </div>
103
+ <h1
104
+ style={{
105
+ fontFamily: fonts.display,
106
+ fontSize: '40px',
107
+ fontWeight: 800,
108
+ fontVariationSettings: "'WONK' 1, 'SOFT' 50",
109
+ color: colors.stone50,
110
+ letterSpacing: '-0.02em',
111
+ marginBottom: '8px',
112
+ }}
113
+ >
114
+ Your Documents
115
+ </h1>
116
+ <p
117
+ style={{
118
+ fontSize: '16px',
119
+ color: colors.stone400,
120
+ maxWidth: '420px',
121
+ margin: '0 auto',
122
+ }}
123
+ >
124
+ {slugs.length} document{slugs.length !== 1 ? 's' : ''} &middot; Preview and export your
125
+ React-powered PDFs
126
+ </p>
127
+ </div>
53
128
 
54
- const cardMetaStyle: React.CSSProperties = {
55
- fontSize: '13px',
56
- color: '#888',
57
- };
129
+ {/* Search */}
130
+ <div style={{ position: 'relative', maxWidth: '480px', margin: '0 auto 48px' }}>
131
+ <SearchIcon
132
+ size={16}
133
+ style={{
134
+ position: 'absolute',
135
+ left: '14px',
136
+ top: '50%',
137
+ transform: 'translateY(-50%)',
138
+ color: colors.stone500,
139
+ }}
140
+ />
141
+ <input
142
+ ref={searchRef}
143
+ type="text"
144
+ className="pdf-smith-search"
145
+ placeholder="Search documents..."
146
+ value={search}
147
+ onChange={(e) => setSearch(e.target.value)}
148
+ onFocus={() => setSearchFocused(true)}
149
+ onBlur={() => setSearchFocused(false)}
150
+ style={{
151
+ width: '100%',
152
+ padding: '12px 14px 12px 40px',
153
+ fontFamily: fonts.body,
154
+ fontSize: '14px',
155
+ background: colors.stone800,
156
+ color: colors.stone100,
157
+ border: `1px solid ${searchFocused ? colors.forge : colors.stone700}`,
158
+ borderRadius: radii.lg,
159
+ outline: 'none',
160
+ transition: 'all 0.15s',
161
+ boxShadow: searchFocused ? '0 0 0 3px rgba(194, 65, 12, 0.15)' : 'none',
162
+ }}
163
+ />
164
+ {!search && (
165
+ <span
166
+ style={{
167
+ position: 'absolute',
168
+ right: '14px',
169
+ top: '50%',
170
+ transform: 'translateY(-50%)',
171
+ fontFamily: fonts.mono,
172
+ fontSize: '11px',
173
+ color: colors.stone500,
174
+ background: colors.stone700,
175
+ padding: '2px 6px',
176
+ borderRadius: radii.sm,
177
+ }}
178
+ >
179
+ /
180
+ </span>
181
+ )}
182
+ </div>
58
183
 
59
- export function HomePage() {
60
- const documents = getDocuments();
61
- const slugs = Object.keys(documents);
184
+ {/* Section Header */}
185
+ <div
186
+ style={{
187
+ display: 'flex',
188
+ alignItems: 'center',
189
+ justifyContent: 'space-between',
190
+ marginBottom: '20px',
191
+ }}
192
+ >
193
+ <h2
194
+ style={{
195
+ fontFamily: fonts.heading,
196
+ fontSize: '14px',
197
+ fontWeight: 600,
198
+ color: colors.stone400,
199
+ textTransform: 'uppercase',
200
+ letterSpacing: '0.06em',
201
+ }}
202
+ >
203
+ Documents
204
+ </h2>
205
+ <span style={{ fontFamily: fonts.mono, fontSize: '12px', color: colors.stone500 }}>
206
+ {filtered.length} document{filtered.length !== 1 ? 's' : ''}
207
+ </span>
208
+ </div>
62
209
 
63
- return (
64
- <div style={containerStyle}>
65
- <h1 style={titleStyle}>pdf-smith</h1>
66
- <p style={subtitleStyle}>
67
- {slugs.length} document{slugs.length !== 1 ? 's' : ''}
68
- </p>
69
- <div style={gridStyle}>
70
- {slugs.map((slug) => {
71
- const doc = documents[slug];
72
- const pageCount = Object.keys(doc.pages).length;
73
- return (
74
- <a
75
- key={slug}
76
- href={`/${slug}`}
77
- style={cardStyle}
78
- onMouseEnter={(e) => {
79
- e.currentTarget.style.background = '#1e2d50';
210
+ {/* Document Grid or Empty State */}
211
+ {filtered.length > 0 ? (
212
+ <div
213
+ style={{
214
+ display: 'grid',
215
+ gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
216
+ gap: '16px',
217
+ }}
218
+ >
219
+ {filtered.map((slug, i) => {
220
+ const doc = documents[slug];
221
+ const pageCount = Object.keys(doc.pages).length;
222
+ const iconColors = [
223
+ { bg: 'rgba(194, 65, 12, 0.12)', fg: colors.ember },
224
+ { bg: 'rgba(217, 119, 6, 0.12)', fg: colors.gold },
225
+ { bg: 'rgba(168, 162, 158, 0.12)', fg: colors.stone400 },
226
+ ];
227
+ const ic = iconColors[i % iconColors.length];
228
+
229
+ return (
230
+ <a
231
+ key={slug}
232
+ href={`/${slug}`}
233
+ style={{
234
+ display: 'block',
235
+ background: colors.stone800,
236
+ border: `1px solid ${colors.stone700}`,
237
+ borderRadius: '10px',
238
+ padding: '20px',
239
+ textDecoration: 'none',
240
+ color: 'inherit',
241
+ transition: 'all 0.15s',
242
+ cursor: 'pointer',
243
+ }}
244
+ onMouseEnter={(e) => {
245
+ const el = e.currentTarget;
246
+ el.style.borderColor = colors.stone600;
247
+ el.style.background = '#2C2826';
248
+ el.style.transform = 'translateY(-1px)';
249
+ el.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3)';
250
+ }}
251
+ onMouseLeave={(e) => {
252
+ const el = e.currentTarget;
253
+ el.style.borderColor = colors.stone700;
254
+ el.style.background = colors.stone800;
255
+ el.style.transform = 'none';
256
+ el.style.boxShadow = 'none';
257
+ }}
258
+ >
259
+ {/* Card Header */}
260
+ <div
261
+ style={{
262
+ display: 'flex',
263
+ alignItems: 'flex-start',
264
+ justifyContent: 'space-between',
265
+ marginBottom: '12px',
266
+ }}
267
+ >
268
+ <div
269
+ style={{
270
+ width: '36px',
271
+ height: '36px',
272
+ display: 'flex',
273
+ alignItems: 'center',
274
+ justifyContent: 'center',
275
+ borderRadius: radii.lg,
276
+ background: ic.bg,
277
+ color: ic.fg,
278
+ flexShrink: 0,
279
+ }}
280
+ >
281
+ <DocumentIcon size={18} />
282
+ </div>
283
+ <div
284
+ style={{
285
+ display: 'flex',
286
+ alignItems: 'center',
287
+ gap: '4px',
288
+ fontSize: '11px',
289
+ fontFamily: fonts.mono,
290
+ color: colors.stone500,
291
+ }}
292
+ >
293
+ <span
294
+ style={{
295
+ width: '6px',
296
+ height: '6px',
297
+ borderRadius: '50%',
298
+ background: colors.success,
299
+ display: 'inline-block',
300
+ }}
301
+ />
302
+ Ready
303
+ </div>
304
+ </div>
305
+
306
+ {/* Title */}
307
+ <div
308
+ style={{
309
+ fontFamily: fonts.heading,
310
+ fontSize: '16px',
311
+ fontWeight: 600,
312
+ color: colors.stone100,
313
+ marginBottom: '4px',
314
+ }}
315
+ >
316
+ {formatSlug(slug)}
317
+ </div>
318
+ <div
319
+ style={{
320
+ fontFamily: fonts.mono,
321
+ fontSize: '12px',
322
+ color: colors.stone500,
323
+ marginBottom: '12px',
324
+ }}
325
+ >
326
+ pdfs/{slug}
327
+ </div>
328
+
329
+ {/* Footer */}
330
+ <div
331
+ style={{
332
+ display: 'flex',
333
+ alignItems: 'center',
334
+ justifyContent: 'space-between',
335
+ paddingTop: '12px',
336
+ borderTop: `1px solid ${colors.stone700}`,
337
+ }}
338
+ >
339
+ <span
340
+ style={{ fontSize: '12px', fontFamily: fonts.mono, color: colors.stone500 }}
341
+ >
342
+ <strong style={{ color: colors.stone300, fontWeight: 600 }}>
343
+ {pageCount}
344
+ </strong>{' '}
345
+ page{pageCount !== 1 ? 's' : ''}
346
+ </span>
347
+ </div>
348
+ </a>
349
+ );
350
+ })}
351
+ </div>
352
+ ) : slugs.length === 0 ? (
353
+ <div
354
+ style={{
355
+ textAlign: 'center',
356
+ padding: '64px 32px',
357
+ border: `2px dashed ${colors.stone700}`,
358
+ borderRadius: radii.xl,
359
+ marginTop: '16px',
360
+ }}
361
+ >
362
+ <p style={{ color: colors.stone500, marginBottom: '16px' }}>
363
+ No documents yet. Create one to get started:
364
+ </p>
365
+ <code
366
+ style={{
367
+ fontFamily: fonts.mono,
368
+ fontSize: '13px',
369
+ color: colors.ember,
370
+ background: colors.stone800,
371
+ padding: '4px 12px',
372
+ borderRadius: radii.md,
80
373
  }}
81
- onMouseLeave={(e) => {
82
- e.currentTarget.style.background = '#16213e';
374
+ >
375
+ pdf-smith add my-doc
376
+ </code>
377
+ </div>
378
+ ) : (
379
+ <div
380
+ style={{
381
+ textAlign: 'center',
382
+ padding: '64px 32px',
383
+ border: `2px dashed ${colors.stone700}`,
384
+ borderRadius: radii.xl,
385
+ marginTop: '16px',
386
+ }}
387
+ >
388
+ <p style={{ color: colors.stone500 }}>No documents matching &ldquo;{search}&rdquo;</p>
389
+ </div>
390
+ )}
391
+
392
+ {/* Quick Start */}
393
+ {slugs.length > 0 && (
394
+ <div
395
+ style={{
396
+ marginTop: '64px',
397
+ padding: '32px',
398
+ background: colors.stone800,
399
+ border: `1px solid ${colors.stone700}`,
400
+ borderRadius: radii.xl,
401
+ }}
402
+ >
403
+ <h3
404
+ style={{
405
+ fontFamily: fonts.heading,
406
+ fontSize: '16px',
407
+ fontWeight: 600,
408
+ color: colors.stone200,
409
+ marginBottom: '16px',
83
410
  }}
84
411
  >
85
- <div style={cardTitleStyle}>{slug}</div>
86
- <div style={cardMetaStyle}>
87
- {pageCount} page{pageCount !== 1 ? 's' : ''}
88
- </div>
412
+ Quick Start
413
+ </h3>
414
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
415
+ {[
416
+ {
417
+ title: 'Add a document',
418
+ desc: (
419
+ <>
420
+ Run{' '}
421
+ <code
422
+ style={{
423
+ fontFamily: fonts.mono,
424
+ fontSize: '11px',
425
+ background: colors.stone900,
426
+ padding: '1px 6px',
427
+ borderRadius: '3px',
428
+ color: colors.ember,
429
+ }}
430
+ >
431
+ pdf-smith add my-doc
432
+ </code>{' '}
433
+ to scaffold a new document with sample pages.
434
+ </>
435
+ ),
436
+ },
437
+ {
438
+ title: 'Edit pages',
439
+ desc: 'Write React components with Tailwind. Changes hot-reload instantly.',
440
+ },
441
+ {
442
+ title: 'Export PDF',
443
+ desc: (
444
+ <>
445
+ Run{' '}
446
+ <code
447
+ style={{
448
+ fontFamily: fonts.mono,
449
+ fontSize: '11px',
450
+ background: colors.stone900,
451
+ padding: '1px 6px',
452
+ borderRadius: '3px',
453
+ color: colors.ember,
454
+ }}
455
+ >
456
+ npm run export
457
+ </code>{' '}
458
+ to generate pixel-perfect PDFs.
459
+ </>
460
+ ),
461
+ },
462
+ ].map((step, idx) => (
463
+ <div
464
+ key={step.title}
465
+ style={{
466
+ display: 'flex',
467
+ alignItems: 'flex-start',
468
+ gap: '12px',
469
+ padding: '16px',
470
+ background: colors.stone900,
471
+ borderRadius: radii.lg,
472
+ border: '1px solid transparent',
473
+ }}
474
+ >
475
+ <span
476
+ style={{
477
+ width: '24px',
478
+ height: '24px',
479
+ display: 'flex',
480
+ alignItems: 'center',
481
+ justifyContent: 'center',
482
+ background: colors.forge,
483
+ color: 'white',
484
+ fontFamily: fonts.mono,
485
+ fontSize: '12px',
486
+ fontWeight: 600,
487
+ borderRadius: radii.md,
488
+ flexShrink: 0,
489
+ }}
490
+ >
491
+ {idx + 1}
492
+ </span>
493
+ <div>
494
+ <h4
495
+ style={{
496
+ fontSize: '13px',
497
+ fontWeight: 600,
498
+ color: colors.stone200,
499
+ marginBottom: '2px',
500
+ }}
501
+ >
502
+ {step.title}
503
+ </h4>
504
+ <p style={{ fontSize: '12px', color: colors.stone500, lineHeight: 1.4 }}>
505
+ {step.desc}
506
+ </p>
507
+ </div>
508
+ </div>
509
+ ))}
510
+ </div>
511
+ </div>
512
+ )}
513
+
514
+ {/* Footer */}
515
+ <footer
516
+ style={{
517
+ marginTop: '48px',
518
+ paddingTop: '24px',
519
+ borderTop: `1px solid ${colors.stone800}`,
520
+ display: 'flex',
521
+ alignItems: 'center',
522
+ justifyContent: 'space-between',
523
+ fontSize: '12px',
524
+ color: colors.stone600,
525
+ }}
526
+ >
527
+ <span>pdf-smith v{VERSION} &middot; Preview Server</span>
528
+ <div style={{ display: 'flex', gap: '16px' }}>
529
+ <a
530
+ href="https://github.com/kareemaly/pdf-smith"
531
+ style={{ color: colors.stone500, textDecoration: 'none' }}
532
+ target="_blank"
533
+ rel="noopener noreferrer"
534
+ >
535
+ GitHub
536
+ </a>
537
+ <a
538
+ href="https://www.npmjs.com/package/pdf-smith"
539
+ style={{ color: colors.stone500, textDecoration: 'none' }}
540
+ target="_blank"
541
+ rel="noopener noreferrer"
542
+ >
543
+ npm
89
544
  </a>
90
- );
91
- })}
92
- </div>
545
+ </div>
546
+ </footer>
547
+ </main>
93
548
  </div>
94
549
  );
95
550
  }