mddir 0.1.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.
data/public/style.css ADDED
@@ -0,0 +1,435 @@
1
+ /* Reset */
2
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
3
+
4
+ body {
5
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
6
+ line-height: 1.6;
7
+ color: #24292e;
8
+ background: #fff;
9
+ }
10
+
11
+ /* Layout */
12
+ header {
13
+ border-bottom: 1px solid #e1e4e8;
14
+ padding: 12px 24px;
15
+ }
16
+
17
+ header nav {
18
+ max-width: 900px;
19
+ margin: 0 auto;
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 24px;
23
+ }
24
+
25
+ .logo {
26
+ font-size: 18px;
27
+ font-weight: 700;
28
+ color: #24292e;
29
+ text-decoration: none;
30
+ }
31
+
32
+ .search-form {
33
+ flex: 1;
34
+ max-width: 400px;
35
+ display: flex;
36
+ gap: 8px;
37
+ }
38
+
39
+ .search-form input {
40
+ flex: 1;
41
+ padding: 6px 12px;
42
+ border: 1px solid #d1d5da;
43
+ border-radius: 6px;
44
+ font-size: 14px;
45
+ background: #f6f8fa;
46
+ }
47
+
48
+ .search-form input:focus {
49
+ outline: none;
50
+ border-color: #0366d6;
51
+ background: #fff;
52
+ }
53
+
54
+ .search-filter {
55
+ padding: 6px 8px;
56
+ border: 1px solid #d1d5da;
57
+ border-radius: 6px;
58
+ font-size: 13px;
59
+ background: #f6f8fa;
60
+ color: #24292e;
61
+ }
62
+
63
+ main {
64
+ max-width: 900px;
65
+ margin: 0 auto;
66
+ padding: 32px 24px;
67
+ }
68
+
69
+ h1 {
70
+ font-size: 24px;
71
+ margin-bottom: 8px;
72
+ }
73
+
74
+ .subtitle {
75
+ color: #586069;
76
+ margin-bottom: 24px;
77
+ }
78
+
79
+ .empty {
80
+ color: #586069;
81
+ font-style: italic;
82
+ }
83
+
84
+ .empty code {
85
+ font-style: normal;
86
+ background: #f6f8fa;
87
+ padding: 2px 6px;
88
+ border-radius: 3px;
89
+ font-size: 13px;
90
+ }
91
+
92
+ /* Breadcrumb */
93
+ .breadcrumb {
94
+ font-size: 14px;
95
+ color: #586069;
96
+ margin-bottom: 16px;
97
+ }
98
+
99
+ .breadcrumb a {
100
+ color: #0366d6;
101
+ text-decoration: none;
102
+ }
103
+
104
+ .breadcrumb a:hover { text-decoration: underline; }
105
+
106
+ /* Collections */
107
+ .collection-list {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 8px;
111
+ margin-top: 16px;
112
+ }
113
+
114
+ .collection-card {
115
+ display: flex;
116
+ justify-content: space-between;
117
+ align-items: center;
118
+ padding: 16px;
119
+ border: 1px solid #e1e4e8;
120
+ border-radius: 6px;
121
+ text-decoration: none;
122
+ color: inherit;
123
+ transition: border-color 0.15s;
124
+ }
125
+
126
+ .collection-card:hover { border-color: #0366d6; }
127
+
128
+ .collection-name {
129
+ font-weight: 600;
130
+ font-size: 16px;
131
+ }
132
+
133
+ .collection-meta {
134
+ display: flex;
135
+ gap: 16px;
136
+ align-items: center;
137
+ }
138
+
139
+ .collection-count {
140
+ color: #586069;
141
+ font-size: 14px;
142
+ }
143
+
144
+ .collection-date {
145
+ color: #959da5;
146
+ font-size: 13px;
147
+ }
148
+
149
+ /* Entries */
150
+ .entry-list {
151
+ display: flex;
152
+ flex-direction: column;
153
+ gap: 16px;
154
+ }
155
+
156
+ .entry-card {
157
+ padding: 16px;
158
+ border: 1px solid #e1e4e8;
159
+ border-radius: 6px;
160
+ }
161
+
162
+ .entry-title {
163
+ font-weight: 600;
164
+ font-size: 16px;
165
+ color: #0366d6;
166
+ text-decoration: none;
167
+ }
168
+
169
+ .entry-title:hover { text-decoration: underline; }
170
+
171
+ .entry-description {
172
+ color: #586069;
173
+ font-size: 14px;
174
+ margin-top: 4px;
175
+ }
176
+
177
+ .entry-meta {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 16px;
181
+ margin-top: 8px;
182
+ font-size: 13px;
183
+ color: #586069;
184
+ }
185
+
186
+ .entry-tokens {
187
+ font-size: 12px;
188
+ color: #586069;
189
+ background: #f6f8fa;
190
+ padding: 1px 6px;
191
+ border-radius: 3px;
192
+ }
193
+
194
+ /* Buttons */
195
+ .inline-form { display: inline; }
196
+
197
+ .btn-delete-small {
198
+ background: none;
199
+ border: none;
200
+ color: #cb2431;
201
+ cursor: pointer;
202
+ font-size: 13px;
203
+ padding: 0;
204
+ }
205
+
206
+ .btn-delete-small:hover { text-decoration: underline; }
207
+
208
+ .btn-delete {
209
+ background: #cb2431;
210
+ color: #fff;
211
+ border: none;
212
+ padding: 8px 16px;
213
+ border-radius: 6px;
214
+ cursor: pointer;
215
+ font-size: 14px;
216
+ }
217
+
218
+ .btn-delete:hover { background: #a82230; }
219
+
220
+ .danger-zone {
221
+ margin-top: 48px;
222
+ padding-top: 24px;
223
+ border-top: 1px solid #e1e4e8;
224
+ }
225
+
226
+ /* Reader */
227
+ .reader-header {
228
+ margin-bottom: 32px;
229
+ padding-bottom: 16px;
230
+ border-bottom: 1px solid #e1e4e8;
231
+ }
232
+
233
+ .reader-header h1 {
234
+ font-size: 28px;
235
+ line-height: 1.3;
236
+ }
237
+
238
+ .reader-description {
239
+ color: #586069;
240
+ margin-top: 8px;
241
+ font-size: 15px;
242
+ }
243
+
244
+ .reader-meta {
245
+ margin-top: 12px;
246
+ font-size: 13px;
247
+ color: #586069;
248
+ display: flex;
249
+ flex-wrap: wrap;
250
+ gap: 16px;
251
+ }
252
+
253
+ .reader-meta a {
254
+ color: #0366d6;
255
+ text-decoration: none;
256
+ word-break: break-all;
257
+ }
258
+
259
+ .reader-meta a:hover { text-decoration: underline; }
260
+
261
+ .reader-content {
262
+ max-width: 700px;
263
+ font-size: 16px;
264
+ line-height: 1.8;
265
+ }
266
+
267
+ .reader-content h1,
268
+ .reader-content h2,
269
+ .reader-content h3,
270
+ .reader-content h4 {
271
+ margin-top: 24px;
272
+ margin-bottom: 12px;
273
+ }
274
+
275
+ .reader-content p { margin-bottom: 16px; }
276
+
277
+ .reader-content pre {
278
+ background: #f6f8fa;
279
+ padding: 16px;
280
+ border-radius: 6px;
281
+ overflow-x: auto;
282
+ margin-bottom: 16px;
283
+ font-size: 14px;
284
+ }
285
+
286
+ .reader-content code {
287
+ background: #f6f8fa;
288
+ padding: 2px 6px;
289
+ border-radius: 3px;
290
+ font-size: 14px;
291
+ }
292
+
293
+ .reader-content pre code {
294
+ background: none;
295
+ padding: 0;
296
+ }
297
+
298
+ .reader-content img {
299
+ max-width: 100%;
300
+ height: auto;
301
+ border-radius: 6px;
302
+ }
303
+
304
+ .reader-content blockquote {
305
+ border-left: 4px solid #dfe2e5;
306
+ padding-left: 16px;
307
+ color: #586069;
308
+ margin-bottom: 16px;
309
+ }
310
+
311
+ .reader-content ul, .reader-content ol {
312
+ margin-bottom: 16px;
313
+ padding-left: 24px;
314
+ }
315
+
316
+ .reader-content li { margin-bottom: 4px; }
317
+
318
+ .reader-content table {
319
+ border-collapse: collapse;
320
+ margin-bottom: 16px;
321
+ width: 100%;
322
+ }
323
+
324
+ .reader-content th, .reader-content td {
325
+ border: 1px solid #dfe2e5;
326
+ padding: 8px 12px;
327
+ text-align: left;
328
+ }
329
+
330
+ .reader-content th { background: #f6f8fa; font-weight: 600; }
331
+
332
+ .reader-content a { color: #0366d6; }
333
+ .reader-content a:hover { text-decoration: underline; }
334
+
335
+ /* Search */
336
+ .search-results {
337
+ display: flex;
338
+ flex-direction: column;
339
+ gap: 20px;
340
+ margin-top: 16px;
341
+ }
342
+
343
+ .search-result {
344
+ padding: 16px;
345
+ border: 1px solid #e1e4e8;
346
+ border-radius: 6px;
347
+ }
348
+
349
+ .search-result-header {
350
+ font-size: 16px;
351
+ }
352
+
353
+ .search-collection {
354
+ color: #586069;
355
+ font-size: 14px;
356
+ margin-right: 4px;
357
+ }
358
+
359
+ .search-result-header a {
360
+ color: #0366d6;
361
+ text-decoration: none;
362
+ font-weight: 600;
363
+ }
364
+
365
+ .search-result-header a:hover { text-decoration: underline; }
366
+
367
+ .search-result-file {
368
+ font-size: 13px;
369
+ color: #586069;
370
+ margin-top: 4px;
371
+ }
372
+
373
+ .search-result-matches { margin-top: 8px; }
374
+
375
+ .search-match {
376
+ font-size: 13px;
377
+ font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
378
+ color: #24292e;
379
+ padding: 4px 0;
380
+ }
381
+
382
+ mark {
383
+ background: #fff3bf;
384
+ padding: 1px 2px;
385
+ border-radius: 2px;
386
+ }
387
+
388
+ /* Rouge syntax highlighting (GitHub theme) */
389
+ .highlight table td { padding: 5px; }
390
+ .highlight table pre { margin: 0; }
391
+ .highlight, .highlight .w { color: #24292f; background-color: #f6f8fa; }
392
+ .highlight .k, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt, .highlight .kv { color: #cf222e; }
393
+ .highlight .gr { color: #f6f8fa; }
394
+ .highlight .gd { color: #82071e; background-color: #ffebe9; }
395
+ .highlight .nb { color: #953800; }
396
+ .highlight .nc { color: #953800; }
397
+ .highlight .no { color: #953800; }
398
+ .highlight .nn { color: #953800; }
399
+ .highlight .sr { color: #116329; }
400
+ .highlight .na { color: #116329; }
401
+ .highlight .nt { color: #116329; }
402
+ .highlight .gi { color: #116329; background-color: #dafbe1; }
403
+ .highlight .ges { font-weight: bold; font-style: italic; }
404
+ .highlight .kc { color: #0550ae; }
405
+ .highlight .l, .highlight .ld, .highlight .m, .highlight .mb, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .il, .highlight .mo, .highlight .mx { color: #0550ae; }
406
+ .highlight .sb { color: #0550ae; }
407
+ .highlight .bp { color: #0550ae; }
408
+ .highlight .ne { color: #0550ae; }
409
+ .highlight .nl { color: #0550ae; }
410
+ .highlight .py { color: #0550ae; }
411
+ .highlight .nv, .highlight .vc, .highlight .vg, .highlight .vi, .highlight .vm { color: #0550ae; }
412
+ .highlight .o, .highlight .ow { color: #0550ae; }
413
+ .highlight .gh { color: #0550ae; font-weight: bold; }
414
+ .highlight .gu { color: #0550ae; font-weight: bold; }
415
+ .highlight .s, .highlight .sa, .highlight .sc, .highlight .dl, .highlight .sd, .highlight .s2, .highlight .se, .highlight .sh, .highlight .sx, .highlight .s1, .highlight .ss { color: #0a3069; }
416
+ .highlight .nd { color: #8250df; }
417
+ .highlight .nf, .highlight .fm { color: #8250df; }
418
+ .highlight .err { color: #f6f8fa; background-color: #82071e; }
419
+ .highlight .c, .highlight .ch, .highlight .cd, .highlight .cm, .highlight .cp, .highlight .cpf, .highlight .c1, .highlight .cs { color: #6e7781; }
420
+ .highlight .gl { color: #6e7781; }
421
+ .highlight .gt { color: #6e7781; }
422
+ .highlight .ni { color: #24292f; }
423
+ .highlight .si { color: #24292f; }
424
+ .highlight .ge { color: #24292f; font-style: italic; }
425
+ .highlight .gs { color: #24292f; font-weight: bold; }
426
+
427
+ /* Responsive */
428
+ @media (max-width: 600px) {
429
+ header nav { flex-direction: column; gap: 8px; }
430
+ .search-form { max-width: 100%; flex-wrap: wrap; }
431
+ .search-filter { flex: 1; }
432
+ main { padding: 16px; }
433
+ .reader-content { font-size: 15px; }
434
+ .collection-card { flex-wrap: wrap; gap: 4px; }
435
+ }
@@ -0,0 +1,43 @@
1
+ <% @page_title = @collection.name %>
2
+
3
+ <div class="breadcrumb">
4
+ <a href="/">mddir</a> &rsaquo; <span><%= h(@collection.name) %></span>
5
+ </div>
6
+
7
+ <h1><%= h(@collection.name) %></h1>
8
+ <p class="subtitle"><%= @entries.length %> <%= @entries.length == 1 ? "entry" : "entries" %></p>
9
+
10
+ <% if @entries.empty? %>
11
+ <p class="empty">No entries in this collection.</p>
12
+ <% else %>
13
+ <div class="entry-list">
14
+ <% @entries.each do |entry| %>
15
+ <div class="entry-card">
16
+ <a href="/<%= h(@collection.name) %>/<%= h(entry['slug']) %>" class="entry-title"><%= h(entry["title"]) %></a>
17
+ <% unless entry["description"].to_s.empty? %>
18
+ <p class="entry-description"><%= h(truncate(entry["description"])) %></p>
19
+ <% end %>
20
+ <div class="entry-meta">
21
+ <span class="entry-domain"><%= h(domain_from_url(entry["url"])) %></span>
22
+ <span class="entry-date"><%= format_date(entry["saved_at"]) %></span>
23
+
24
+ <% if entry["token_count"] %>
25
+ <span class="entry-tokens"><%= format_tokens(entry["token_count"]) %></span>
26
+ <% end %>
27
+
28
+ <form method="post" action="/<%= h(@collection.name) %>/<%= h(entry['slug']) %>" class="inline-form" onsubmit="return confirm('Remove this entry?')">
29
+ <input type="hidden" name="_method" value="DELETE">
30
+ <button type="submit" class="btn-delete-small">remove</button>
31
+ </form>
32
+ </div>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ <% end %>
37
+
38
+ <div class="danger-zone">
39
+ <form method="post" action="/<%= h(@collection.name) %>" class="inline-form" onsubmit="return confirm('Delete collection \'<%= h(@collection.name) %>\' and all its entries?')">
40
+ <input type="hidden" name="_method" value="DELETE">
41
+ <button type="submit" class="btn-delete">Delete collection</button>
42
+ </form>
43
+ </div>
data/views/home.erb ADDED
@@ -0,0 +1,21 @@
1
+ <% @page_title = "mddir" %>
2
+
3
+ <h1>Collections</h1>
4
+
5
+ <% if @collections.empty? %>
6
+ <p class="empty">No collections yet. Use <code>mddir add &lt;collection&gt; &lt;url&gt;</code> to get started.</p>
7
+ <% else %>
8
+ <div class="collection-list">
9
+ <% @collections.each do |name, info| %>
10
+ <a href="/<%= h(name) %>" class="collection-card">
11
+ <span class="collection-name"><%= h(name) %></span>
12
+ <span class="collection-meta">
13
+ <span class="collection-count"><%= info["entry_count"] || 0 %> <%= (info["entry_count"] || 0) == 1 ? "entry" : "entries" %></span>
14
+ <% if info["last_added"] %>
15
+ <span class="collection-date"><%= format_date(info["last_added"]) %></span>
16
+ <% end %>
17
+ </span>
18
+ </a>
19
+ <% end %>
20
+ </div>
21
+ <% end %>
data/views/layout.erb ADDED
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title><%= @page_title || "mddir" %></title>
5
+
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+
9
+ <link rel="stylesheet" href="/style.css">
10
+ </head>
11
+ <body>
12
+ <header>
13
+ <nav>
14
+ <a href="/" class="logo">mddir</a>
15
+
16
+ <form action="/search" method="get" class="search-form">
17
+ <input type="text" name="q" id="search-input" placeholder="Search (press /)" value="<%= h(params['q'].to_s) %>" autocomplete="query">
18
+
19
+ <% if @collection_names && !@collection_names.empty? %>
20
+ <select name="collection" class="search-filter">
21
+ <option value="">All</option>
22
+ <% @collection_names.each do |name| %>
23
+ <option value="<%= h(name) %>" <%= 'selected' if (params['collection'] || @current_collection) == name %>><%= h(name) %></option>
24
+ <% end %>
25
+ </select>
26
+ <% end %>
27
+ </form>
28
+ </nav>
29
+ </header>
30
+
31
+ <main>
32
+ <%= yield %>
33
+ </main>
34
+
35
+ <script>
36
+ document.addEventListener("keydown", function(e) {
37
+ if (e.key === "/" && !e.ctrlKey && !e.metaKey && !["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName)) {
38
+ e.preventDefault();
39
+ document.getElementById("search-input").focus();
40
+ }
41
+ });
42
+ </script>
43
+ </body>
44
+ </html>
data/views/reader.erb ADDED
@@ -0,0 +1,24 @@
1
+ <% @page_title = @entry["title"] %>
2
+
3
+ <div class="breadcrumb">
4
+ <a href="/">mddir</a> &rsaquo; <a href="/<%= h(@collection.name) %>"><%= h(@collection.name) %></a> &rsaquo; <span><%= h(@entry["title"]) %></span>
5
+ </div>
6
+
7
+ <article class="reader">
8
+ <header class="reader-header">
9
+ <h1><%= h(@entry["title"]) %></h1>
10
+
11
+ <% unless @entry["description"].to_s.empty? %>
12
+ <p class="reader-description"><%= h(@entry["description"]) %></p>
13
+ <% end %>
14
+
15
+ <div class="reader-meta">
16
+ <a href="<%= h(@entry['url']) %>" target="_blank" rel="noopener"><%= h(@entry["url"]) %></a>
17
+ <span class="reader-date">Saved <%= format_date(@entry["saved_at"]) %></span>
18
+ </div>
19
+ </header>
20
+
21
+ <div class="reader-content">
22
+ <%= @html_content %>
23
+ </div>
24
+ </article>
data/views/search.erb ADDED
@@ -0,0 +1,30 @@
1
+ <% @page_title = "Search — mddir" %>
2
+
3
+ <h1>Search</h1>
4
+
5
+ <% if @query.empty? %>
6
+ <p class="empty">Enter a search query above.</p>
7
+ <% elsif @results.empty? %>
8
+ <p class="empty">No matches found for "<%= h(@query) %>".</p>
9
+ <% else %>
10
+ <p class="subtitle">Found <%= @results.sum { |r| r.matches.length } %> matches in <%= @results.length %> files</p>
11
+
12
+ <div class="search-results">
13
+ <% @results.each do |result| %>
14
+ <div class="search-result">
15
+ <div class="search-result-header">
16
+ <span class="search-collection">[<%= h(result.collection_name) %>]</span>
17
+ <a href="/<%= h(result.collection_name) %>/<%= h(result.entry['slug']) %>"><%= h(result.entry["title"]) %></a>
18
+ </div>
19
+
20
+ <div class="search-result-file"><%= h(result.entry["filename"]) %></div>
21
+
22
+ <div class="search-result-matches">
23
+ <% result.matches.each do |m| %>
24
+ <div class="search-match">Line <%= m.line_number %>: <%= highlight(m.snippet, @query) %></div>
25
+ <% end %>
26
+ </div>
27
+ </div>
28
+ <% end %>
29
+ </div>
30
+ <% end %>