highlite 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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/publish.md +45 -0
  3. data/README.md +176 -0
  4. data/Rakefile +12 -0
  5. data/app/assets/stylesheets/highlite/viewer.css +607 -0
  6. data/app/helpers/highlite/application_helper.rb +20 -0
  7. data/app/javascript/highlite/controllers/.keep +0 -0
  8. data/app/javascript/highlite/controllers/highlight_controller.js +829 -0
  9. data/app/javascript/highlite/controllers/highlights_panel_controller.js +313 -0
  10. data/app/javascript/highlite/controllers/sidebar_controller.js +465 -0
  11. data/app/javascript/highlite/controllers/viewer_controller.js +373 -0
  12. data/app/javascript/highlite/index.js +30 -0
  13. data/app/javascript/highlite/lib/.keep +0 -0
  14. data/app/javascript/highlite/lib/highlight_store.js +235 -0
  15. data/app/javascript/highlite/lib/pdf_renderer.js +212 -0
  16. data/app/views/highlite/_floating_toolbar.html.erb +79 -0
  17. data/app/views/highlite/_left_sidebar.html.erb +43 -0
  18. data/app/views/highlite/_right_sidebar.html.erb +56 -0
  19. data/app/views/highlite/_toolbar.html.erb +63 -0
  20. data/app/views/highlite/_viewer.html.erb +63 -0
  21. data/config/importmap.rb +17 -0
  22. data/lib/generators/highlite/install/install_generator.rb +44 -0
  23. data/lib/generators/highlite/install/templates/_right_sidebar.html.erb.tt +16 -0
  24. data/lib/generators/highlite/install/templates/initializer.rb.tt +20 -0
  25. data/lib/highlite/configuration.rb +27 -0
  26. data/lib/highlite/engine.rb +26 -0
  27. data/lib/highlite/version.rb +5 -0
  28. data/lib/highlite.rb +17 -0
  29. data/sig/highlite.rbs +4 -0
  30. data/tasks/todo.md +129 -0
  31. data/test/dummy/Rakefile +3 -0
  32. data/test/dummy/app/controllers/application_controller.rb +2 -0
  33. data/test/dummy/app/controllers/documents_controller.rb +6 -0
  34. data/test/dummy/app/javascript/application.js +1 -0
  35. data/test/dummy/app/views/documents/show.html.erb +1 -0
  36. data/test/dummy/app/views/layouts/application.html.erb +13 -0
  37. data/test/dummy/bin/rails +4 -0
  38. data/test/dummy/config/application.rb +15 -0
  39. data/test/dummy/config/boot.rb +2 -0
  40. data/test/dummy/config/environment.rb +2 -0
  41. data/test/dummy/config/importmap.rb +3 -0
  42. data/test/dummy/config/routes.rb +3 -0
  43. data/test/dummy/config.ru +2 -0
  44. data/test/dummy/public/convention-over-configuration.pdf +0 -0
  45. metadata +117 -0
@@ -0,0 +1,607 @@
1
+ /* Highlite — Required CSS
2
+ These styles can't be handled by Tailwind utilities alone.
3
+ They cover PDF.js text layer positioning, highlight overlays, and page layout. */
4
+
5
+ /* ==========================================================================
6
+ PDF.js Text Layer
7
+ Positioned spans overlaid on the canvas so text is selectable.
8
+ ========================================================================== */
9
+
10
+ .highlite-text-layer {
11
+ position: absolute;
12
+ text-align: initial;
13
+ inset: 0;
14
+ overflow: clip;
15
+ opacity: 1;
16
+ line-height: 1;
17
+ text-size-adjust: none;
18
+ forced-color-adjust: none;
19
+ transform-origin: 0 0;
20
+ z-index: 2;
21
+
22
+ /* PDF.js TextLayer CSS variable calculations.
23
+ --total-scale-factor is set from JS (scale * devicePixelRatio).
24
+ --min-font-size is set by PDF.js TextLayer constructor. */
25
+ --text-scale-factor: calc(var(--total-scale-factor, 1) * var(--min-font-size, 1));
26
+ --min-font-size-inv: calc(1 / var(--min-font-size, 1));
27
+ }
28
+
29
+ .highlite-text-layer :is(span, br) {
30
+ color: transparent;
31
+ position: absolute;
32
+ white-space: pre;
33
+ cursor: text;
34
+ transform-origin: 0% 0%;
35
+ }
36
+
37
+ /* PDF.js sets --font-height, --scale-x, --rotate per span via inline styles.
38
+ These rules compute the actual font-size and transform from those variables. */
39
+ .highlite-text-layer > :not(.markedContent),
40
+ .highlite-text-layer .markedContent span:not(.markedContent) {
41
+ z-index: 1;
42
+ --font-height: 0;
43
+ font-size: calc(var(--text-scale-factor) * var(--font-height));
44
+ --scale-x: 1;
45
+ --rotate: 0deg;
46
+ transform: rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv));
47
+ }
48
+
49
+ .highlite-text-layer ::selection {
50
+ background: rgba(0, 100, 200, 0.3);
51
+ }
52
+
53
+ /* ==========================================================================
54
+ Highlight Layer
55
+ Absolute-positioned container for highlight overlay divs.
56
+ ========================================================================== */
57
+
58
+ .highlite-highlight-layer {
59
+ position: absolute;
60
+ inset: 0;
61
+ z-index: 3;
62
+ pointer-events: none;
63
+ }
64
+
65
+ /* ==========================================================================
66
+ Highlight Overlays
67
+ Colored rectangles with mix-blend-mode for a natural highlight look.
68
+ ========================================================================== */
69
+
70
+ .highlite-highlight {
71
+ position: absolute;
72
+ mix-blend-mode: multiply;
73
+ border-radius: 2px;
74
+ cursor: pointer;
75
+ pointer-events: auto;
76
+ transition: opacity 0.15s ease;
77
+ }
78
+
79
+ .highlite-highlight:hover {
80
+ opacity: 0.6;
81
+ }
82
+
83
+ .highlite-highlight.active {
84
+ outline: 2px solid rgba(59, 130, 246, 0.8);
85
+ outline-offset: 1px;
86
+ }
87
+
88
+ /* ==========================================================================
89
+ Area Highlight (rectangle drag selection)
90
+ ========================================================================== */
91
+
92
+ .highlite-area-selection {
93
+ position: absolute;
94
+ border: 2px dashed rgba(59, 130, 246, 0.7);
95
+ background: rgba(59, 130, 246, 0.1);
96
+ z-index: 4;
97
+ pointer-events: none;
98
+ }
99
+
100
+ /* ==========================================================================
101
+ Pages Container
102
+ Scrollable area holding all PDF pages, with padding so first/last pages
103
+ can be scrolled to the vertical center of the viewport.
104
+ ========================================================================== */
105
+
106
+ .highlite-pages-container {
107
+ padding: 1.5rem 2rem calc(50vh - 100px);
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ min-height: 100%;
112
+ }
113
+
114
+ /* ==========================================================================
115
+ Page Wrapper
116
+ Each PDF page is rendered inside this container.
117
+ ========================================================================== */
118
+
119
+ .highlite-page {
120
+ position: relative;
121
+ margin: 0 auto 16px;
122
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
123
+ background: #1a1a1a;
124
+ }
125
+
126
+ .highlite-page-canvas {
127
+ display: block;
128
+ filter: invert(0.88) hue-rotate(180deg);
129
+ }
130
+
131
+ /* ==========================================================================
132
+ Error State
133
+ ========================================================================== */
134
+
135
+ .highlite-error {
136
+ display: flex;
137
+ flex-direction: column;
138
+ align-items: center;
139
+ justify-content: center;
140
+ height: 100%;
141
+ color: #6b7280;
142
+ gap: 0.5rem;
143
+ padding: 2rem;
144
+ text-align: center;
145
+ }
146
+
147
+ .highlite-error-detail {
148
+ font-size: 0.875rem;
149
+ color: #9ca3af;
150
+ max-width: 400px;
151
+ word-break: break-word;
152
+ }
153
+
154
+ /* ==========================================================================
155
+ Left Sidebar — Tab Active State
156
+ ========================================================================== */
157
+
158
+ .highlite-tab-active {
159
+ color: #ffffff;
160
+ border-bottom-color: #ffffff;
161
+ }
162
+
163
+ /* ==========================================================================
164
+ Left Sidebar — Outline Tree
165
+ ========================================================================== */
166
+
167
+ .highlite-outline-item {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 0.375rem;
171
+ padding: 0.375rem 0.5rem;
172
+ font-size: 0.875rem;
173
+ color: #d1d5db;
174
+ border-radius: 0.25rem;
175
+ cursor: pointer;
176
+ transition: background-color 0.15s;
177
+ }
178
+
179
+ .highlite-outline-item:hover {
180
+ background-color: rgba(255, 255, 255, 0.05);
181
+ color: #ffffff;
182
+ }
183
+
184
+ .highlite-outline-item.active {
185
+ background-color: rgba(59, 130, 246, 0.15);
186
+ color: #93c5fd;
187
+ }
188
+
189
+ .highlite-outline-children {
190
+ padding-left: 0.75rem;
191
+ }
192
+
193
+ /* Highlight indicator dot on outline items */
194
+ .highlite-outline-dot {
195
+ width: 6px;
196
+ height: 6px;
197
+ border-radius: 50%;
198
+ background: #3b82f6;
199
+ flex-shrink: 0;
200
+ }
201
+
202
+ /* ==========================================================================
203
+ Left Sidebar — Page Thumbnails
204
+ ========================================================================== */
205
+
206
+ .highlite-thumbnail {
207
+ position: relative;
208
+ display: block;
209
+ width: 100%;
210
+ border: 2px solid transparent;
211
+ border-radius: 0.25rem;
212
+ overflow: hidden;
213
+ cursor: pointer;
214
+ transition: border-color 0.15s;
215
+ background: none;
216
+ padding: 0;
217
+ text-align: center;
218
+ }
219
+
220
+ .highlite-thumbnail:hover {
221
+ border-color: rgba(255, 255, 255, 0.3);
222
+ }
223
+
224
+ .highlite-thumbnail-active,
225
+ .highlite-thumbnail.active {
226
+ border-color: #3b82f6;
227
+ }
228
+
229
+ .highlite-thumbnail canvas {
230
+ display: block;
231
+ width: 100%;
232
+ height: auto;
233
+ }
234
+
235
+ .highlite-thumbnail-label {
236
+ display: block;
237
+ text-align: center;
238
+ font-size: 0.75rem;
239
+ color: #9ca3af;
240
+ padding: 0.125rem 0;
241
+ }
242
+
243
+ /* ==========================================================================
244
+ Right Sidebar — Highlight Cards
245
+ ========================================================================== */
246
+
247
+ .highlite-highlight-card {
248
+ padding: 0.5rem;
249
+ background: rgba(255, 255, 255, 0.03);
250
+ border: 1px solid rgba(255, 255, 255, 0.06);
251
+ border-radius: 0.375rem;
252
+ cursor: pointer;
253
+ transition: background-color 0.15s;
254
+ }
255
+
256
+ .highlite-highlight-card:hover {
257
+ background: rgba(255, 255, 255, 0.06);
258
+ }
259
+
260
+ .highlite-highlight-card.active {
261
+ background: rgba(59, 130, 246, 0.1);
262
+ border-color: rgba(59, 130, 246, 0.3);
263
+ }
264
+
265
+ .highlite-type-badge {
266
+ display: inline-block;
267
+ font-size: 0.75rem;
268
+ font-weight: 500;
269
+ text-transform: uppercase;
270
+ letter-spacing: 0.05em;
271
+ padding: 0.0625rem 0.375rem;
272
+ border-radius: 0.25rem;
273
+ }
274
+
275
+ .highlite-type-badge--text {
276
+ background: rgba(255, 226, 143, 0.2);
277
+ color: #fde68a;
278
+ }
279
+
280
+ .highlite-type-badge--area {
281
+ background: rgba(137, 180, 250, 0.2);
282
+ color: #93c5fd;
283
+ }
284
+
285
+ .highlite-quote {
286
+ font-size: 0.875rem;
287
+ line-height: 1.4;
288
+ color: #d1d5db;
289
+ border-left: 2px solid;
290
+ padding-left: 0.5rem;
291
+ margin: 0.375rem 0;
292
+ display: -webkit-box;
293
+ -webkit-line-clamp: 3;
294
+ -webkit-box-orient: vertical;
295
+ overflow: hidden;
296
+ }
297
+
298
+ /* ==========================================================================
299
+ Page Group Headers (in highlights list)
300
+ ========================================================================== */
301
+
302
+ .highlite-page-group-header,
303
+ .highlite-panel-page-header {
304
+ font-size: 0.75rem;
305
+ font-weight: 600;
306
+ text-transform: uppercase;
307
+ letter-spacing: 0.05em;
308
+ color: #6b7280;
309
+ padding: 0.5rem 0 0.25rem;
310
+ }
311
+
312
+ /* ==========================================================================
313
+ Right Sidebar — Panel Components (generated by highlights_panel_controller)
314
+ ========================================================================== */
315
+
316
+ .highlite-panel-card {
317
+ display: flex;
318
+ flex-direction: column;
319
+ gap: 0.25rem;
320
+ width: 100%;
321
+ text-align: left;
322
+ padding: 0.5rem;
323
+ background: rgba(255, 255, 255, 0.03);
324
+ border: 1px solid rgba(255, 255, 255, 0.06);
325
+ border-radius: 0.375rem;
326
+ cursor: pointer;
327
+ transition: background-color 0.15s;
328
+ margin-bottom: 0.375rem;
329
+ }
330
+
331
+ .highlite-panel-card:hover {
332
+ background: rgba(255, 255, 255, 0.06);
333
+ }
334
+
335
+ .highlite-panel-card-header {
336
+ display: flex;
337
+ align-items: center;
338
+ gap: 0.375rem;
339
+ }
340
+
341
+ .highlite-panel-swatch {
342
+ width: 10px;
343
+ height: 10px;
344
+ border-radius: 2px;
345
+ flex-shrink: 0;
346
+ }
347
+
348
+ .highlite-panel-badge {
349
+ font-size: 0.75rem;
350
+ font-weight: 500;
351
+ text-transform: uppercase;
352
+ letter-spacing: 0.05em;
353
+ padding: 0.0625rem 0.375rem;
354
+ border-radius: 0.25rem;
355
+ background: rgba(255, 255, 255, 0.1);
356
+ color: #d1d5db;
357
+ }
358
+
359
+ .highlite-panel-time {
360
+ margin-left: auto;
361
+ font-size: 0.75rem;
362
+ color: #6b7280;
363
+ }
364
+
365
+ .highlite-panel-delete {
366
+ display: flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ flex-shrink: 0;
370
+ padding: 0.25rem;
371
+ margin-left: 0.25rem;
372
+ background: none;
373
+ border: none;
374
+ border-radius: 0.25rem;
375
+ color: #6b7280;
376
+ cursor: pointer;
377
+ opacity: 0;
378
+ transition: opacity 0.15s, color 0.15s, background-color 0.15s;
379
+ }
380
+
381
+ .highlite-panel-card:hover .highlite-panel-delete {
382
+ opacity: 1;
383
+ }
384
+
385
+ .highlite-panel-delete:hover {
386
+ color: #ef4444;
387
+ background: rgba(239, 68, 68, 0.1);
388
+ }
389
+
390
+ .highlite-panel-text {
391
+ font-size: 0.875rem;
392
+ line-height: 1.4;
393
+ color: #d1d5db;
394
+ }
395
+
396
+ .highlite-panel-empty {
397
+ font-size: 0.875rem;
398
+ color: #6b7280;
399
+ text-align: center;
400
+ padding: 2rem 1rem;
401
+ }
402
+
403
+ /* ==========================================================================
404
+ Left Sidebar — Outline Link Styling
405
+ ========================================================================== */
406
+
407
+ .highlite-outline-list {
408
+ list-style: none;
409
+ margin: 0;
410
+ padding: 0;
411
+ }
412
+
413
+ .highlite-outline-link {
414
+ display: flex;
415
+ align-items: center;
416
+ gap: 0.375rem;
417
+ width: 100%;
418
+ padding: 0.375rem 0.5rem;
419
+ font-size: 0.875rem;
420
+ color: #d1d5db;
421
+ border-radius: 0.25rem;
422
+ cursor: pointer;
423
+ transition: background-color 0.15s;
424
+ border: none;
425
+ background: none;
426
+ text-align: left;
427
+ }
428
+
429
+ .highlite-outline-link:hover {
430
+ background-color: rgba(255, 255, 255, 0.05);
431
+ color: #ffffff;
432
+ }
433
+
434
+ .highlite-outline-title {
435
+ flex: 1;
436
+ overflow: hidden;
437
+ text-overflow: ellipsis;
438
+ white-space: nowrap;
439
+ }
440
+
441
+ .highlite-outline-page {
442
+ flex-shrink: 0;
443
+ font-size: 0.75rem;
444
+ color: #6b7280;
445
+ }
446
+
447
+ .highlite-sidebar-empty {
448
+ font-size: 0.875rem;
449
+ color: #6b7280;
450
+ text-align: center;
451
+ padding: 2rem 1rem;
452
+ font-style: italic;
453
+ }
454
+
455
+ /* ==========================================================================
456
+ Selection Popup — "Add highlight" button near text selection
457
+ ========================================================================== */
458
+
459
+ .highlite-selection-popup {
460
+ position: fixed;
461
+ z-index: 10000;
462
+ padding: 0.375rem 0.75rem;
463
+ background: #1f2937;
464
+ color: #f9fafb;
465
+ font-size: 0.875rem;
466
+ font-weight: 500;
467
+ border: none;
468
+ border-radius: 0.375rem;
469
+ cursor: pointer;
470
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
471
+ white-space: nowrap;
472
+ transition: background-color 0.1s;
473
+ }
474
+
475
+ .highlite-selection-popup:hover {
476
+ background: #374151;
477
+ }
478
+
479
+ .highlite-selection-popup:active {
480
+ background: #4b5563;
481
+ }
482
+
483
+ /* ==========================================================================
484
+ Note Dialog — Modal for adding a note to a highlight
485
+ ========================================================================== */
486
+
487
+ .highlite-note-dialog-overlay {
488
+ position: fixed;
489
+ inset: 0;
490
+ z-index: 10001;
491
+ background: rgba(0, 0, 0, 0.5);
492
+ display: flex;
493
+ align-items: center;
494
+ justify-content: center;
495
+ }
496
+
497
+ .highlite-note-dialog {
498
+ background: #1f2937;
499
+ border: 1px solid rgba(255, 255, 255, 0.1);
500
+ border-radius: 0.5rem;
501
+ padding: 1.25rem;
502
+ width: 100%;
503
+ max-width: 400px;
504
+ margin: 1rem;
505
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
506
+ display: flex;
507
+ flex-direction: column;
508
+ gap: 0.75rem;
509
+ }
510
+
511
+ .highlite-note-dialog-title {
512
+ margin: 0;
513
+ font-size: 1rem;
514
+ font-weight: 600;
515
+ color: #f9fafb;
516
+ }
517
+
518
+ .highlite-note-dialog-preview {
519
+ margin: 0;
520
+ font-size: 0.875rem;
521
+ line-height: 1.4;
522
+ color: #9ca3af;
523
+ border-left: 2px solid #4b5563;
524
+ padding-left: 0.5rem;
525
+ }
526
+
527
+ .highlite-note-dialog-label {
528
+ font-size: 0.875rem;
529
+ font-weight: 500;
530
+ color: #d1d5db;
531
+ }
532
+
533
+ .highlite-note-dialog-textarea {
534
+ width: 100%;
535
+ padding: 0.5rem;
536
+ font-size: 0.875rem;
537
+ line-height: 1.5;
538
+ color: #f9fafb;
539
+ background: #111827;
540
+ border: 1px solid rgba(255, 255, 255, 0.15);
541
+ border-radius: 0.375rem;
542
+ resize: vertical;
543
+ font-family: inherit;
544
+ box-sizing: border-box;
545
+ }
546
+
547
+ .highlite-note-dialog-textarea:focus {
548
+ outline: none;
549
+ border-color: #3b82f6;
550
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
551
+ }
552
+
553
+ .highlite-note-dialog-textarea::placeholder {
554
+ color: #6b7280;
555
+ }
556
+
557
+ .highlite-note-dialog-actions {
558
+ display: flex;
559
+ justify-content: flex-end;
560
+ gap: 0.5rem;
561
+ }
562
+
563
+ .highlite-note-dialog-cancel,
564
+ .highlite-note-dialog-submit {
565
+ padding: 0.375rem 0.875rem;
566
+ font-size: 0.875rem;
567
+ font-weight: 500;
568
+ border-radius: 0.375rem;
569
+ border: none;
570
+ cursor: pointer;
571
+ transition: background-color 0.1s;
572
+ }
573
+
574
+ .highlite-note-dialog-cancel {
575
+ background: transparent;
576
+ color: #9ca3af;
577
+ }
578
+
579
+ .highlite-note-dialog-cancel:hover {
580
+ background: rgba(255, 255, 255, 0.05);
581
+ color: #d1d5db;
582
+ }
583
+
584
+ .highlite-note-dialog-submit {
585
+ background: #3b82f6;
586
+ color: #ffffff;
587
+ }
588
+
589
+ .highlite-note-dialog-submit:hover {
590
+ background: #2563eb;
591
+ }
592
+
593
+ .highlite-note-dialog-submit:active {
594
+ background: #1d4ed8;
595
+ }
596
+
597
+ /* ==========================================================================
598
+ Highlight Note — Displayed in highlight cards in the panel
599
+ ========================================================================== */
600
+
601
+ .highlite-panel-note {
602
+ font-size: 0.8125rem;
603
+ line-height: 1.4;
604
+ color: #9ca3af;
605
+ font-style: italic;
606
+ margin-top: 0.125rem;
607
+ }
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Highlite
4
+ module ApplicationHelper
5
+ def highlite_viewer(url:, document_id:, **options)
6
+ viewer_options = {
7
+ url: url,
8
+ document_id: document_id,
9
+ scale: options.fetch(:scale, 2.25),
10
+ show_toolbar: options.fetch(:show_toolbar, true),
11
+ show_left_sidebar: options.fetch(:show_left_sidebar, true),
12
+ show_right_sidebar: options.fetch(:show_right_sidebar, true),
13
+ right_sidebar_partial: options[:right_sidebar_partial] ||
14
+ Highlite.configuration.right_sidebar_partial
15
+ }
16
+
17
+ render partial: "highlite/viewer", locals: viewer_options
18
+ end
19
+ end
20
+ end
File without changes