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,95 +1,276 @@
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 { 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
+ {/* Main */}
38
+ <main style={{ maxWidth: '960px', margin: '0 auto', padding: '32px' }}>
39
+ {/* Search */}
40
+ <div style={{ position: 'relative', maxWidth: '480px', margin: '0 auto 48px' }}>
41
+ <SearchIcon
42
+ size={16}
43
+ style={{
44
+ position: 'absolute',
45
+ left: '14px',
46
+ top: '50%',
47
+ transform: 'translateY(-50%)',
48
+ color: colors.stone500,
49
+ }}
50
+ />
51
+ <input
52
+ ref={searchRef}
53
+ type="text"
54
+ className="pdf-smith-search"
55
+ placeholder="Search documents..."
56
+ value={search}
57
+ onChange={(e) => setSearch(e.target.value)}
58
+ onFocus={() => setSearchFocused(true)}
59
+ onBlur={() => setSearchFocused(false)}
60
+ style={{
61
+ width: '100%',
62
+ padding: '12px 14px 12px 40px',
63
+ fontFamily: fonts.body,
64
+ fontSize: '14px',
65
+ background: colors.stone800,
66
+ color: colors.stone100,
67
+ border: `1px solid ${searchFocused ? colors.forge : colors.stone700}`,
68
+ borderRadius: radii.lg,
69
+ outline: 'none',
70
+ transition: 'all 0.15s',
71
+ boxShadow: searchFocused ? '0 0 0 3px rgba(194, 65, 12, 0.15)' : 'none',
72
+ }}
73
+ />
74
+ {!search && (
75
+ <span
76
+ style={{
77
+ position: 'absolute',
78
+ right: '14px',
79
+ top: '50%',
80
+ transform: 'translateY(-50%)',
81
+ fontFamily: fonts.mono,
82
+ fontSize: '11px',
83
+ color: colors.stone500,
84
+ background: colors.stone700,
85
+ padding: '2px 6px',
86
+ borderRadius: radii.sm,
87
+ }}
88
+ >
89
+ /
90
+ </span>
91
+ )}
92
+ </div>
46
93
 
47
- const cardTitleStyle: React.CSSProperties = {
48
- fontSize: '18px',
49
- fontWeight: 600,
50
- color: '#ffffff',
51
- marginBottom: '8px',
52
- };
94
+ {/* Section Header */}
95
+ <div
96
+ style={{
97
+ display: 'flex',
98
+ alignItems: 'center',
99
+ justifyContent: 'space-between',
100
+ marginBottom: '20px',
101
+ }}
102
+ >
103
+ <h2
104
+ style={{
105
+ fontFamily: fonts.heading,
106
+ fontSize: '14px',
107
+ fontWeight: 600,
108
+ color: colors.stone400,
109
+ textTransform: 'uppercase',
110
+ letterSpacing: '0.06em',
111
+ }}
112
+ >
113
+ Documents
114
+ </h2>
115
+ <span style={{ fontFamily: fonts.mono, fontSize: '12px', color: colors.stone500 }}>
116
+ {filtered.length} document{filtered.length !== 1 ? 's' : ''}
117
+ </span>
118
+ </div>
53
119
 
54
- const cardMetaStyle: React.CSSProperties = {
55
- fontSize: '13px',
56
- color: '#888',
57
- };
120
+ {/* Document List or Empty State */}
121
+ {filtered.length > 0 ? (
122
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
123
+ {filtered.map((slug, i) => {
124
+ const doc = documents[slug];
125
+ const pageCount = Object.keys(doc.pages).length;
126
+ const iconColors = [
127
+ { bg: 'rgba(194, 65, 12, 0.12)', fg: colors.ember },
128
+ { bg: 'rgba(217, 119, 6, 0.12)', fg: colors.gold },
129
+ { bg: 'rgba(168, 162, 158, 0.12)', fg: colors.stone400 },
130
+ ];
131
+ const ic = iconColors[i % iconColors.length];
58
132
 
59
- export function HomePage() {
60
- const documents = getDocuments();
61
- const slugs = Object.keys(documents);
133
+ return (
134
+ <a
135
+ key={slug}
136
+ href={`/${slug}`}
137
+ style={{
138
+ display: 'flex',
139
+ alignItems: 'center',
140
+ gap: '12px',
141
+ background: colors.stone800,
142
+ border: `1px solid ${colors.stone700}`,
143
+ borderRadius: radii.lg,
144
+ padding: '12px 16px',
145
+ textDecoration: 'none',
146
+ color: 'inherit',
147
+ transition: 'all 0.15s',
148
+ cursor: 'pointer',
149
+ }}
150
+ onMouseEnter={(e) => {
151
+ const el = e.currentTarget;
152
+ el.style.borderColor = colors.stone600;
153
+ el.style.background = '#2C2826';
154
+ }}
155
+ onMouseLeave={(e) => {
156
+ const el = e.currentTarget;
157
+ el.style.borderColor = colors.stone700;
158
+ el.style.background = colors.stone800;
159
+ }}
160
+ >
161
+ <div
162
+ style={{
163
+ width: '28px',
164
+ height: '28px',
165
+ display: 'flex',
166
+ alignItems: 'center',
167
+ justifyContent: 'center',
168
+ borderRadius: radii.md,
169
+ background: ic.bg,
170
+ color: ic.fg,
171
+ flexShrink: 0,
172
+ }}
173
+ >
174
+ <DocumentIcon size={14} />
175
+ </div>
176
+ <span
177
+ style={{
178
+ fontFamily: fonts.heading,
179
+ fontSize: '14px',
180
+ fontWeight: 600,
181
+ color: colors.stone100,
182
+ }}
183
+ >
184
+ {formatSlug(slug)}
185
+ </span>
186
+ <span
187
+ style={{
188
+ marginLeft: 'auto',
189
+ fontSize: '12px',
190
+ fontFamily: fonts.mono,
191
+ color: colors.stone500,
192
+ }}
193
+ >
194
+ {pageCount} page{pageCount !== 1 ? 's' : ''}
195
+ </span>
196
+ </a>
197
+ );
198
+ })}
199
+ </div>
200
+ ) : slugs.length === 0 ? (
201
+ <div
202
+ style={{
203
+ textAlign: 'center',
204
+ padding: '64px 32px',
205
+ border: `2px dashed ${colors.stone700}`,
206
+ borderRadius: radii.xl,
207
+ marginTop: '16px',
208
+ }}
209
+ >
210
+ <p style={{ color: colors.stone500, marginBottom: '16px' }}>
211
+ No documents yet. Create one to get started:
212
+ </p>
213
+ <code
214
+ style={{
215
+ fontFamily: fonts.mono,
216
+ fontSize: '13px',
217
+ color: colors.ember,
218
+ background: colors.stone800,
219
+ padding: '4px 12px',
220
+ borderRadius: radii.md,
221
+ }}
222
+ >
223
+ pdf-smith add my-doc
224
+ </code>
225
+ </div>
226
+ ) : (
227
+ <div
228
+ style={{
229
+ textAlign: 'center',
230
+ padding: '64px 32px',
231
+ border: `2px dashed ${colors.stone700}`,
232
+ borderRadius: radii.xl,
233
+ marginTop: '16px',
234
+ }}
235
+ >
236
+ <p style={{ color: colors.stone500 }}>No documents matching &ldquo;{search}&rdquo;</p>
237
+ </div>
238
+ )}
62
239
 
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 (
240
+ {/* Footer */}
241
+ <footer
242
+ style={{
243
+ marginTop: '48px',
244
+ paddingTop: '24px',
245
+ borderTop: `1px solid ${colors.stone800}`,
246
+ display: 'flex',
247
+ alignItems: 'center',
248
+ justifyContent: 'space-between',
249
+ fontSize: '12px',
250
+ color: colors.stone600,
251
+ }}
252
+ >
253
+ <span>pdf-smith v{VERSION} &middot; Preview Server</span>
254
+ <div style={{ display: 'flex', gap: '16px' }}>
74
255
  <a
75
- key={slug}
76
- href={`/${slug}`}
77
- style={cardStyle}
78
- onMouseEnter={(e) => {
79
- e.currentTarget.style.background = '#1e2d50';
80
- }}
81
- onMouseLeave={(e) => {
82
- e.currentTarget.style.background = '#16213e';
83
- }}
256
+ href="https://github.com/kareemaly/pdf-smith"
257
+ style={{ color: colors.stone500, textDecoration: 'none' }}
258
+ target="_blank"
259
+ rel="noopener noreferrer"
260
+ >
261
+ GitHub
262
+ </a>
263
+ <a
264
+ href="https://www.npmjs.com/package/pdf-smith"
265
+ style={{ color: colors.stone500, textDecoration: 'none' }}
266
+ target="_blank"
267
+ rel="noopener noreferrer"
84
268
  >
85
- <div style={cardTitleStyle}>{slug}</div>
86
- <div style={cardMetaStyle}>
87
- {pageCount} page{pageCount !== 1 ? 's' : ''}
88
- </div>
269
+ npm
89
270
  </a>
90
- );
91
- })}
92
- </div>
271
+ </div>
272
+ </footer>
273
+ </main>
93
274
  </div>
94
275
  );
95
276
  }
@@ -0,0 +1,250 @@
1
+ interface IconProps {
2
+ size?: number;
3
+ style?: React.CSSProperties;
4
+ }
5
+
6
+ export function AnvilIcon({ size = 24, style }: IconProps) {
7
+ return (
8
+ <svg
9
+ width={size}
10
+ height={size}
11
+ viewBox="0 0 32 32"
12
+ fill="none"
13
+ style={style}
14
+ aria-hidden="true"
15
+ >
16
+ <rect x="4" y="6" width="24" height="5" rx="1.5" fill="#C2410C" />
17
+ <path d="M2 24h28v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-3z" fill="#FAF9F7" />
18
+ <path d="M8 11h16v2a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-2z" fill="#57534E" />
19
+ <rect x="10" y="14" width="12" height="4" rx="1" fill="#78716C" />
20
+ <rect x="6" y="18" width="20" height="3" rx="1" fill="#57534E" />
21
+ <rect x="4" y="21" width="24" height="3" rx="1" fill="#44403C" />
22
+ </svg>
23
+ );
24
+ }
25
+
26
+ export function AnvilIconWhite({ size = 28, style }: IconProps) {
27
+ return (
28
+ <svg
29
+ width={size}
30
+ height={size}
31
+ viewBox="0 0 32 32"
32
+ fill="none"
33
+ style={style}
34
+ aria-hidden="true"
35
+ >
36
+ <rect x="4" y="6" width="24" height="5" rx="1.5" fill="white" />
37
+ <path d="M2 24h28v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-3z" fill="white" />
38
+ <path d="M8 11h16v2a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-2z" fill="rgba(255,255,255,0.6)" />
39
+ <rect x="10" y="14" width="12" height="4" rx="1" fill="rgba(255,255,255,0.5)" />
40
+ <rect x="6" y="18" width="20" height="3" rx="1" fill="rgba(255,255,255,0.6)" />
41
+ <rect x="4" y="21" width="24" height="3" rx="1" fill="rgba(255,255,255,0.8)" />
42
+ </svg>
43
+ );
44
+ }
45
+
46
+ export function SearchIcon({ size = 16, style }: IconProps) {
47
+ return (
48
+ <svg
49
+ width={size}
50
+ height={size}
51
+ viewBox="0 0 16 16"
52
+ fill="none"
53
+ style={style}
54
+ aria-hidden="true"
55
+ >
56
+ <circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
57
+ <path d="M11 11l3.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
58
+ </svg>
59
+ );
60
+ }
61
+
62
+ export function ChevronLeftIcon({ size = 14, style }: IconProps) {
63
+ return (
64
+ <svg
65
+ width={size}
66
+ height={size}
67
+ viewBox="0 0 16 16"
68
+ fill="none"
69
+ style={style}
70
+ aria-hidden="true"
71
+ >
72
+ <path
73
+ d="M10 12L6 8l4-4"
74
+ stroke="currentColor"
75
+ strokeWidth="1.5"
76
+ strokeLinecap="round"
77
+ strokeLinejoin="round"
78
+ />
79
+ </svg>
80
+ );
81
+ }
82
+
83
+ export function GridViewIcon({ size = 14, style }: IconProps) {
84
+ return (
85
+ <svg
86
+ width={size}
87
+ height={size}
88
+ viewBox="0 0 16 16"
89
+ fill="none"
90
+ style={style}
91
+ aria-hidden="true"
92
+ >
93
+ <rect x="2" y="2" width="5" height="5" rx="1" stroke="currentColor" strokeWidth="1.2" />
94
+ <rect x="9" y="2" width="5" height="5" rx="1" stroke="currentColor" strokeWidth="1.2" />
95
+ <rect x="2" y="9" width="5" height="5" rx="1" stroke="currentColor" strokeWidth="1.2" />
96
+ <rect x="9" y="9" width="5" height="5" rx="1" stroke="currentColor" strokeWidth="1.2" />
97
+ </svg>
98
+ );
99
+ }
100
+
101
+ export function DownloadIcon({ size = 14, style }: IconProps) {
102
+ return (
103
+ <svg
104
+ width={size}
105
+ height={size}
106
+ viewBox="0 0 16 16"
107
+ fill="none"
108
+ style={style}
109
+ aria-hidden="true"
110
+ >
111
+ <path
112
+ d="M2 11v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-2"
113
+ stroke="currentColor"
114
+ strokeWidth="1.5"
115
+ strokeLinecap="round"
116
+ />
117
+ <path
118
+ d="M8 2v8M5 7l3 3 3-3"
119
+ stroke="currentColor"
120
+ strokeWidth="1.5"
121
+ strokeLinecap="round"
122
+ strokeLinejoin="round"
123
+ />
124
+ </svg>
125
+ );
126
+ }
127
+
128
+ export function DocumentIcon({ size = 18, style }: IconProps) {
129
+ return (
130
+ <svg
131
+ width={size}
132
+ height={size}
133
+ viewBox="0 0 24 24"
134
+ fill="none"
135
+ style={style}
136
+ aria-hidden="true"
137
+ >
138
+ <path
139
+ d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
140
+ stroke="currentColor"
141
+ strokeWidth="1.5"
142
+ />
143
+ <polyline points="14,2 14,8 20,8" stroke="currentColor" strokeWidth="1.5" />
144
+ <line x1="8" y1="13" x2="16" y2="13" stroke="currentColor" strokeWidth="1.5" />
145
+ <line x1="8" y1="17" x2="16" y2="17" stroke="currentColor" strokeWidth="1.5" />
146
+ </svg>
147
+ );
148
+ }
149
+
150
+ export function BookIcon({ size = 18, style }: IconProps) {
151
+ return (
152
+ <svg
153
+ width={size}
154
+ height={size}
155
+ viewBox="0 0 24 24"
156
+ fill="none"
157
+ style={style}
158
+ aria-hidden="true"
159
+ >
160
+ <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="currentColor" strokeWidth="1.5" />
161
+ <path
162
+ d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"
163
+ stroke="currentColor"
164
+ strokeWidth="1.5"
165
+ />
166
+ </svg>
167
+ );
168
+ }
169
+
170
+ export function GridIcon({ size = 18, style }: IconProps) {
171
+ return (
172
+ <svg
173
+ width={size}
174
+ height={size}
175
+ viewBox="0 0 24 24"
176
+ fill="none"
177
+ style={style}
178
+ aria-hidden="true"
179
+ >
180
+ <rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="1.5" />
181
+ <line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" strokeWidth="1.5" />
182
+ <line x1="9" y1="9" x2="9" y2="21" stroke="currentColor" strokeWidth="1.5" />
183
+ </svg>
184
+ );
185
+ }
186
+
187
+ export function MinusIcon({ size = 14, style }: IconProps) {
188
+ return (
189
+ <svg
190
+ width={size}
191
+ height={size}
192
+ viewBox="0 0 16 16"
193
+ fill="none"
194
+ style={style}
195
+ aria-hidden="true"
196
+ >
197
+ <path d="M3 8h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
198
+ </svg>
199
+ );
200
+ }
201
+
202
+ export function PlusSmallIcon({ size = 14, style }: IconProps) {
203
+ return (
204
+ <svg
205
+ width={size}
206
+ height={size}
207
+ viewBox="0 0 16 16"
208
+ fill="none"
209
+ style={style}
210
+ aria-hidden="true"
211
+ >
212
+ <path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
213
+ </svg>
214
+ );
215
+ }
216
+
217
+ export function PreviewIcon({ size = 12, style }: IconProps) {
218
+ return (
219
+ <svg
220
+ width={size}
221
+ height={size}
222
+ viewBox="0 0 16 16"
223
+ fill="none"
224
+ style={style}
225
+ aria-hidden="true"
226
+ >
227
+ <rect x="2" y="2" width="12" height="12" rx="1" stroke="currentColor" strokeWidth="1.5" />
228
+ </svg>
229
+ );
230
+ }
231
+
232
+ export function OutlineIcon({ size = 12, style }: IconProps) {
233
+ return (
234
+ <svg
235
+ width={size}
236
+ height={size}
237
+ viewBox="0 0 16 16"
238
+ fill="none"
239
+ style={style}
240
+ aria-hidden="true"
241
+ >
242
+ <path
243
+ d="M2 4h12M2 8h12M2 12h8"
244
+ stroke="currentColor"
245
+ strokeWidth="1.5"
246
+ strokeLinecap="round"
247
+ />
248
+ </svg>
249
+ );
250
+ }