@autumnsgrove/groveengine 0.8.0 → 0.8.6

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 (76) hide show
  1. package/dist/components/OnboardingChecklist.svelte +2 -2
  2. package/dist/components/WispButton.svelte +83 -0
  3. package/dist/components/WispButton.svelte.d.ts +49 -0
  4. package/dist/components/WispPanel.svelte +1092 -0
  5. package/dist/components/WispPanel.svelte.d.ts +49 -0
  6. package/dist/components/custom/ContentWithGutter.svelte +7 -13
  7. package/dist/components/custom/TableOfContents.svelte +12 -1
  8. package/dist/components/quota/UpgradePrompt.svelte +1 -0
  9. package/dist/config/wisp.d.ts +145 -0
  10. package/dist/config/wisp.js +175 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +3 -0
  13. package/dist/server/inference-client.d.ts +139 -0
  14. package/dist/server/inference-client.js +294 -0
  15. package/dist/ui/components/forms/SearchInput.svelte +0 -1
  16. package/dist/ui/components/gallery/ImageGallery.svelte +14 -3
  17. package/dist/ui/components/gallery/Lightbox.svelte +8 -3
  18. package/dist/ui/components/gallery/ZoomableImage.svelte +12 -2
  19. package/dist/ui/components/nature/Logo.svelte +55 -19
  20. package/dist/ui/components/nature/botanical/LeafFalling.svelte +2 -2
  21. package/dist/ui/components/nature/botanical/PetalFalling.svelte +7 -7
  22. package/dist/ui/components/nature/ground/Crocus.svelte +3 -3
  23. package/dist/ui/components/nature/ground/Daffodil.svelte +3 -3
  24. package/dist/ui/components/nature/ground/Tulip.svelte +5 -5
  25. package/dist/ui/components/nature/palette.d.ts +187 -76
  26. package/dist/ui/components/nature/palette.js +169 -81
  27. package/dist/ui/components/nature/trees/TreeCherry.svelte +3 -3
  28. package/dist/ui/components/nature/trees/TreeCherry.svelte.d.ts +1 -1
  29. package/dist/ui/components/nature/trees/TreePine.svelte +2 -2
  30. package/dist/ui/components/nature/trees/TreePine.svelte.d.ts +1 -1
  31. package/dist/ui/components/primitives/textarea/textarea.svelte +1 -1
  32. package/dist/ui/components/typography/Alagard.svelte +17 -0
  33. package/dist/ui/components/typography/Alagard.svelte.d.ts +10 -0
  34. package/dist/ui/components/typography/Atkinson.svelte +17 -0
  35. package/dist/ui/components/typography/Atkinson.svelte.d.ts +10 -0
  36. package/dist/ui/components/typography/Calistoga.svelte +17 -0
  37. package/dist/ui/components/typography/Calistoga.svelte.d.ts +10 -0
  38. package/dist/ui/components/typography/Caveat.svelte +17 -0
  39. package/dist/ui/components/typography/Caveat.svelte.d.ts +10 -0
  40. package/dist/ui/components/typography/Cozette.svelte +17 -0
  41. package/dist/ui/components/typography/Cozette.svelte.d.ts +10 -0
  42. package/dist/ui/components/typography/FontProvider.svelte +98 -0
  43. package/dist/ui/components/typography/FontProvider.svelte.d.ts +17 -0
  44. package/dist/ui/components/typography/IBMPlexMono.svelte +17 -0
  45. package/dist/ui/components/typography/IBMPlexMono.svelte.d.ts +10 -0
  46. package/dist/ui/components/typography/Lexend.svelte +17 -0
  47. package/dist/ui/components/typography/Lexend.svelte.d.ts +10 -0
  48. package/dist/ui/components/typography/OpenDyslexic.svelte +17 -0
  49. package/dist/ui/components/typography/OpenDyslexic.svelte.d.ts +10 -0
  50. package/dist/ui/components/typography/PlusJakartaSans.svelte +17 -0
  51. package/dist/ui/components/typography/PlusJakartaSans.svelte.d.ts +10 -0
  52. package/dist/ui/components/typography/Quicksand.svelte +17 -0
  53. package/dist/ui/components/typography/Quicksand.svelte.d.ts +10 -0
  54. package/dist/ui/components/typography/README.md +153 -0
  55. package/dist/ui/components/typography/index.d.ts +13 -0
  56. package/dist/ui/components/typography/index.js +31 -0
  57. package/dist/ui/components/ui/CollapsibleSection.svelte +10 -0
  58. package/dist/ui/components/ui/GlassCarousel.svelte +446 -0
  59. package/dist/ui/components/ui/GlassCarousel.svelte.d.ts +57 -0
  60. package/dist/ui/components/ui/GlassConfirmDialog.svelte +2 -1
  61. package/dist/ui/components/ui/GlassLogo.svelte +2 -1
  62. package/dist/ui/components/ui/GlassOverlay.svelte +1 -1
  63. package/dist/ui/components/ui/index.d.ts +1 -0
  64. package/dist/ui/components/ui/index.js +1 -0
  65. package/dist/ui/index.d.ts +1 -0
  66. package/dist/ui/index.js +2 -0
  67. package/dist/ui/tokens/fonts.d.ts +1 -1
  68. package/dist/ui/tokens/fonts.js +0 -126
  69. package/dist/ui/vineyard/index.d.ts +9 -0
  70. package/dist/ui/vineyard/index.js +8 -0
  71. package/dist/utils/csrf.js +5 -2
  72. package/dist/utils/readability.d.ts +89 -0
  73. package/dist/utils/readability.js +204 -0
  74. package/package.json +38 -21
  75. package/static/fonts/alagard.ttf +0 -0
  76. package/LICENSE +0 -378
@@ -0,0 +1,1092 @@
1
+ <script>
2
+ import { slide, fade } from "svelte/transition";
3
+ import { Button } from "../ui/components/primitives/button";
4
+ import { MAX_CONTENT_LENGTH } from "../config/wisp.js";
5
+
6
+ /**
7
+ * @typedef {Object} Props
8
+ * @property {string} content - Content to analyze
9
+ * @property {boolean} enabled - Whether Wisp is enabled
10
+ * @property {string} postTitle - Optional post title for context
11
+ * @property {string} postSlug - Optional post slug for logging
12
+ * @property {(original: string, suggestion: string) => void} onApplyFix - Callback when user applies a fix
13
+ */
14
+
15
+ /** @type {Props} */
16
+ let {
17
+ content = "",
18
+ enabled = false,
19
+ postTitle = "",
20
+ postSlug = "",
21
+ onApplyFix = (original, suggestion) => {},
22
+ } = $props();
23
+
24
+ // Panel state
25
+ let isOpen = $state(false);
26
+ let isMinimized = $state(true);
27
+
28
+ // Analysis state
29
+ let isAnalyzing = $state(false);
30
+ let analysisError = $state(null);
31
+ let results = $state(null);
32
+ let activeTab = $state("grammar");
33
+ let selectedMode = $state("quick"); // quick or thorough
34
+
35
+ // ASCII art vibes - text landscapes that create atmosphere
36
+ const vibes = {
37
+ idle: `
38
+ . * . . *
39
+ . _ . .
40
+ . / \\ * .
41
+ / ~ ~ \\ . .
42
+ / \\______
43
+ ~~~~~~~~~~~~~~~~~~~`,
44
+
45
+ analyzing: `
46
+ . * . analyzing . *
47
+ \\ | /
48
+ -- (o.o) -- thinking
49
+ / | \\
50
+ ~~~~~~~~~~~~~~~~~
51
+ words flowing...`,
52
+
53
+ success: `
54
+ *
55
+ . * /|\\ .
56
+ * . / | \\ *
57
+ /__|__\\
58
+ ~~~~~/ \\~~~~
59
+ all clear `,
60
+
61
+ grammarGood: `
62
+ .-~~~-.
63
+ .' '.
64
+ / ^ ^ \\
65
+ | (o) (o) | nice!
66
+ \\ <=> /
67
+ '-.___.-'`,
68
+
69
+ toneWarm: `
70
+ __/\\__
71
+ \\ /
72
+ <( ~~~~ )>
73
+ / \\
74
+ / ^^ \\
75
+ warm & cozy`,
76
+
77
+ error: `
78
+ . x .
79
+ /|\\
80
+ / | \\ oops
81
+ / | \\
82
+ _____|_____
83
+ try again?`,
84
+ };
85
+
86
+ // Get current vibe based on state
87
+ let currentVibe = $derived(() => {
88
+ if (isAnalyzing) return vibes.analyzing;
89
+ if (analysisError) return vibes.error;
90
+ if (results?.grammar?.overallScore >= 90) return vibes.grammarGood;
91
+ if (results?.tone) return vibes.toneWarm;
92
+ if (results) return vibes.success;
93
+ return vibes.idle;
94
+ });
95
+
96
+ // Seasonal vibe rotation for idle state
97
+ const seasonalVibes = [
98
+ // Forest morning
99
+ `
100
+ . * . . *
101
+ . _ . .
102
+ / \\ * .
103
+ / ~ ~ \\ . .
104
+ / \\______
105
+ ~~~~~~~~~~~~~~~~~~~`,
106
+ // Starry grove
107
+ `
108
+ * . * . * . *
109
+ . * * .
110
+ _/\\_ *
111
+ . / \\ .
112
+ ___/ \\___
113
+ ~~~~~~ ~~~~~~~~~`,
114
+ // Mountain vista
115
+ `
116
+ /\\
117
+ . / \\ . *
118
+ / \\ .
119
+ * / /\\ \\ .
120
+ __/ / \\ \\__
121
+ ~~~~~~~~~~~~~~~~`,
122
+ // Meadow
123
+ `
124
+ . * . * . * . * .
125
+ ~ ~ ~ ~ ~
126
+ , , , , , ,
127
+ v v v v v v v v v
128
+ | | | | | | | | |
129
+ ==================`,
130
+ // Night grove
131
+ `
132
+ * . . * . . * . *
133
+ . * .
134
+ \\ | /
135
+ --- (._.) ---
136
+ / | \\
137
+ ~~~quiet night~~~`,
138
+ ];
139
+
140
+ let vibeIndex = $state(0);
141
+ let panelRef = $state(null);
142
+
143
+ // Content length status
144
+ let contentLengthStatus = $derived(() => {
145
+ const len = content.length;
146
+ const pct = Math.round((len / MAX_CONTENT_LENGTH) * 100);
147
+ if (len > MAX_CONTENT_LENGTH) return { status: "over", pct: 100, len };
148
+ if (pct > 80) return { status: "warn", pct, len };
149
+ return { status: "ok", pct, len };
150
+ });
151
+
152
+ // Rotate through vibes when idle
153
+ $effect(() => {
154
+ if (!isOpen || isAnalyzing || results) return;
155
+
156
+ const interval = setInterval(() => {
157
+ vibeIndex = (vibeIndex + 1) % seasonalVibes.length;
158
+ }, 8000);
159
+
160
+ return () => clearInterval(interval);
161
+ });
162
+
163
+ // Handle keyboard navigation
164
+ function handleKeydown(e) {
165
+ if (e.key === "Escape" && isOpen) {
166
+ e.preventDefault();
167
+ minimize();
168
+ }
169
+ }
170
+
171
+ // Get the display vibe
172
+ let displayVibe = $derived(() => {
173
+ if (isAnalyzing) return vibes.analyzing;
174
+ if (analysisError) return vibes.error;
175
+ if (results) return currentVibe();
176
+ return seasonalVibes[vibeIndex];
177
+ });
178
+
179
+ // Run analysis
180
+ async function runAnalysis(action = "all") {
181
+ if (!content.trim()) {
182
+ analysisError = "Write something first!";
183
+ return;
184
+ }
185
+
186
+ if (content.length > MAX_CONTENT_LENGTH) {
187
+ analysisError = `Content too long (${content.length.toLocaleString()} chars). Max ${MAX_CONTENT_LENGTH.toLocaleString()}.`;
188
+ return;
189
+ }
190
+
191
+ isAnalyzing = true;
192
+ analysisError = null;
193
+
194
+ try {
195
+ const res = await fetch("/api/grove/wisp", {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/json" },
198
+ body: JSON.stringify({
199
+ content,
200
+ action,
201
+ mode: selectedMode,
202
+ context: { title: postTitle, slug: postSlug }
203
+ })
204
+ });
205
+
206
+ if (res.ok) {
207
+ results = await res.json();
208
+ if (action === "grammar") activeTab = "grammar";
209
+ else if (action === "tone") activeTab = "tone";
210
+ else if (action === "readability") activeTab = "readability";
211
+ } else {
212
+ const error = await res.json();
213
+ analysisError = error.error || "Analysis failed";
214
+ }
215
+ } catch (err) {
216
+ analysisError = "Could not connect to Wisp";
217
+ } finally {
218
+ isAnalyzing = false;
219
+ }
220
+ }
221
+
222
+ // Apply a grammar fix
223
+ function applyFix(suggestion) {
224
+ onApplyFix(suggestion.original, suggestion.suggestion);
225
+ // Remove from list
226
+ if (results?.grammar?.suggestions) {
227
+ results.grammar.suggestions = results.grammar.suggestions.filter(
228
+ s => s.original !== suggestion.original
229
+ );
230
+ }
231
+ }
232
+
233
+ // Clear results
234
+ function clearResults() {
235
+ results = null;
236
+ analysisError = null;
237
+ }
238
+
239
+ // Toggle panel
240
+ function togglePanel() {
241
+ if (isMinimized) {
242
+ isMinimized = false;
243
+ isOpen = true;
244
+ } else {
245
+ isOpen = !isOpen;
246
+ }
247
+ }
248
+
249
+ // Minimize to tab
250
+ function minimize() {
251
+ isMinimized = true;
252
+ isOpen = false;
253
+ }
254
+
255
+ // Severity colors
256
+ function getSeverityClass(severity) {
257
+ switch (severity) {
258
+ case "error": return "severity-error";
259
+ case "warning": return "severity-warning";
260
+ default: return "severity-style";
261
+ }
262
+ }
263
+
264
+ // Format score as visual bar
265
+ function formatScore(score) {
266
+ if (score === null || score === undefined) return "░░░░░░░░░░";
267
+ const filled = Math.round(score / 10);
268
+ const empty = 10 - filled;
269
+ return "█".repeat(filled) + "░".repeat(empty);
270
+ }
271
+ </script>
272
+
273
+ <svelte:window onkeydown={handleKeydown} />
274
+
275
+ {#if enabled}
276
+ <!-- Minimized tab on the side -->
277
+ {#if isMinimized}
278
+ <button
279
+ class="wisp-tab"
280
+ onclick={togglePanel}
281
+ title="Open Wisp"
282
+ aria-label="Open Wisp writing assistant"
283
+ transition:fade={{ duration: 150 }}
284
+ >
285
+ <span class="tab-icon" aria-hidden="true">~</span>
286
+ <span class="tab-text">wisp</span>
287
+ </button>
288
+ {/if}
289
+
290
+ <!-- Main panel -->
291
+ {#if isOpen && !isMinimized}
292
+ <aside
293
+ class="wisp-panel"
294
+ aria-label="Wisp writing assistant"
295
+ bind:this={panelRef}
296
+ transition:slide={{ axis: "x", duration: 200 }}
297
+ >
298
+ <!-- Header -->
299
+ <header class="panel-header">
300
+ <h3>wisp</h3>
301
+ <div class="header-actions">
302
+ <button class="icon-btn" onclick={minimize} title="Minimize" aria-label="Minimize panel">
303
+ <span aria-hidden="true">−</span>
304
+ </button>
305
+ <button class="icon-btn" onclick={() => isOpen = false} title="Close (Esc)" aria-label="Close panel">
306
+ <span aria-hidden="true">×</span>
307
+ </button>
308
+ </div>
309
+ </header>
310
+
311
+ <!-- Content length indicator -->
312
+ <div
313
+ class="content-length"
314
+ class:warn={contentLengthStatus().status === "warn"}
315
+ class:over={contentLengthStatus().status === "over"}
316
+ aria-live="polite"
317
+ >
318
+ <span class="length-text">
319
+ {contentLengthStatus().len.toLocaleString()} / {MAX_CONTENT_LENGTH.toLocaleString()}
320
+ </span>
321
+ <div class="length-bar">
322
+ <div class="length-fill" style="width: {contentLengthStatus().pct}%"></div>
323
+ </div>
324
+ </div>
325
+
326
+ <!-- Vibes section - the ASCII art atmosphere -->
327
+ <div class="vibes-section">
328
+ <pre class="ascii-vibe" aria-hidden="true">{displayVibe()}</pre>
329
+ </div>
330
+
331
+ <!-- Mode selector -->
332
+ <div class="mode-selector">
333
+ <label>
334
+ <input type="radio" bind:group={selectedMode} value="quick" />
335
+ <span>quick</span>
336
+ </label>
337
+ <label>
338
+ <input type="radio" bind:group={selectedMode} value="thorough" />
339
+ <span>thorough</span>
340
+ </label>
341
+ </div>
342
+
343
+ <!-- Action buttons -->
344
+ <div class="actions" role="group" aria-label="Analysis actions">
345
+ <button
346
+ class="action-btn"
347
+ onclick={() => runAnalysis("grammar")}
348
+ disabled={isAnalyzing || contentLengthStatus().status === "over"}
349
+ aria-busy={isAnalyzing}
350
+ >
351
+ grammar
352
+ </button>
353
+ <button
354
+ class="action-btn"
355
+ onclick={() => runAnalysis("tone")}
356
+ disabled={isAnalyzing || contentLengthStatus().status === "over"}
357
+ aria-busy={isAnalyzing}
358
+ >
359
+ tone
360
+ </button>
361
+ <button
362
+ class="action-btn"
363
+ onclick={() => runAnalysis("readability")}
364
+ disabled={isAnalyzing}
365
+ aria-busy={isAnalyzing}
366
+ >
367
+ reading
368
+ </button>
369
+ <button
370
+ class="action-btn action-full"
371
+ onclick={() => runAnalysis("all")}
372
+ disabled={isAnalyzing || contentLengthStatus().status === "over"}
373
+ aria-busy={isAnalyzing}
374
+ >
375
+ {isAnalyzing ? "thinking..." : "full check"}
376
+ </button>
377
+ </div>
378
+
379
+ <!-- Error message -->
380
+ {#if analysisError}
381
+ <div class="error-message" transition:slide>
382
+ <p>{analysisError}</p>
383
+ <button onclick={() => analysisError = null}>dismiss</button>
384
+ </div>
385
+ {/if}
386
+
387
+ <!-- Results -->
388
+ {#if results}
389
+ <div class="results" transition:slide>
390
+ <!-- Tabs -->
391
+ <div class="tabs">
392
+ {#if results.grammar}
393
+ <button
394
+ class="tab"
395
+ class:active={activeTab === "grammar"}
396
+ onclick={() => activeTab = "grammar"}
397
+ >
398
+ grammar
399
+ </button>
400
+ {/if}
401
+ {#if results.tone}
402
+ <button
403
+ class="tab"
404
+ class:active={activeTab === "tone"}
405
+ onclick={() => activeTab = "tone"}
406
+ >
407
+ tone
408
+ </button>
409
+ {/if}
410
+ {#if results.readability}
411
+ <button
412
+ class="tab"
413
+ class:active={activeTab === "readability"}
414
+ onclick={() => activeTab = "readability"}
415
+ >
416
+ reading
417
+ </button>
418
+ {/if}
419
+ </div>
420
+
421
+ <!-- Grammar Results -->
422
+ {#if activeTab === "grammar" && results.grammar}
423
+ <div class="tab-content">
424
+ <div class="score-display">
425
+ <span class="score-label">clarity</span>
426
+ <span class="score-bar">{formatScore(results.grammar.overallScore)}</span>
427
+ <span class="score-num">{results.grammar.overallScore ?? "—"}</span>
428
+ </div>
429
+
430
+ {#if results.grammar.suggestions?.length > 0}
431
+ <div class="suggestions">
432
+ {#each results.grammar.suggestions as suggestion}
433
+ <div class="suggestion {getSeverityClass(suggestion.severity)}">
434
+ <div class="suggestion-original">
435
+ <span class="strike">{suggestion.original}</span>
436
+ </div>
437
+ <div class="suggestion-fix">
438
+ <span class="arrow">→</span>
439
+ <span class="fix-text">{suggestion.suggestion}</span>
440
+ </div>
441
+ <div class="suggestion-reason">{suggestion.reason}</div>
442
+ <button class="apply-btn" onclick={() => applyFix(suggestion)}>
443
+ apply
444
+ </button>
445
+ </div>
446
+ {/each}
447
+ </div>
448
+ {:else}
449
+ <p class="no-issues">looking good!</p>
450
+ {/if}
451
+ </div>
452
+ {/if}
453
+
454
+ <!-- Tone Results -->
455
+ {#if activeTab === "tone" && results.tone}
456
+ <div class="tab-content">
457
+ <p class="tone-analysis">{results.tone.analysis}</p>
458
+
459
+ {#if results.tone.traits?.length > 0}
460
+ <div class="traits">
461
+ {#each results.tone.traits as trait}
462
+ <div class="trait">
463
+ <span class="trait-name">{trait.trait}</span>
464
+ <div class="trait-bar-container">
465
+ <div class="trait-bar" style="width: {trait.score}%"></div>
466
+ </div>
467
+ <span class="trait-score">{trait.score}</span>
468
+ </div>
469
+ {/each}
470
+ </div>
471
+ {/if}
472
+
473
+ {#if results.tone.suggestions?.length > 0}
474
+ <div class="tone-suggestions">
475
+ {#each results.tone.suggestions as sug}
476
+ <p class="tone-sug">• {sug}</p>
477
+ {/each}
478
+ </div>
479
+ {/if}
480
+ </div>
481
+ {/if}
482
+
483
+ <!-- Readability Results -->
484
+ {#if activeTab === "readability" && results.readability}
485
+ <div class="tab-content">
486
+ <div class="readability-stats">
487
+ <div class="stat">
488
+ <span class="stat-label">grade level</span>
489
+ <span class="stat-value">{results.readability.fleschKincaid}</span>
490
+ </div>
491
+ <div class="stat">
492
+ <span class="stat-label">reading time</span>
493
+ <span class="stat-value">{results.readability.readingTime}</span>
494
+ </div>
495
+ <div class="stat">
496
+ <span class="stat-label">words</span>
497
+ <span class="stat-value">{results.readability.wordCount}</span>
498
+ </div>
499
+ <div class="stat">
500
+ <span class="stat-label">sentences</span>
501
+ <span class="stat-value">{results.readability.sentenceCount}</span>
502
+ </div>
503
+ <div class="stat">
504
+ <span class="stat-label">avg sentence</span>
505
+ <span class="stat-value">{results.readability.sentenceStats.average} words</span>
506
+ </div>
507
+ <div class="stat">
508
+ <span class="stat-label">longest</span>
509
+ <span class="stat-value">{results.readability.sentenceStats.longest} words</span>
510
+ </div>
511
+ </div>
512
+
513
+ {#if results.readability.suggestions?.length > 0}
514
+ <div class="readability-suggestions">
515
+ {#each results.readability.suggestions as sug}
516
+ <p class="read-sug">• {sug}</p>
517
+ {/each}
518
+ </div>
519
+ {/if}
520
+ </div>
521
+ {/if}
522
+
523
+ <!-- Usage info -->
524
+ <div class="usage-info">
525
+ {#if results.meta?.tokensUsed}
526
+ <span>tokens: {results.meta.tokensUsed}</span>
527
+ <span>cost: ${results.meta.cost?.toFixed(4) || "0.0000"}</span>
528
+ {/if}
529
+ <button class="clear-btn" onclick={clearResults}>clear</button>
530
+ </div>
531
+ </div>
532
+ {/if}
533
+
534
+ <!-- Footer note -->
535
+ <footer class="panel-footer" aria-label="Wisp philosophy: analyzes your writing but never generates content">
536
+ <p>a helper, not a writer</p>
537
+ </footer>
538
+ </aside>
539
+ {/if}
540
+ {/if}
541
+
542
+ <style>
543
+ /* Minimized tab */
544
+ .wisp-tab {
545
+ position: fixed;
546
+ right: 0;
547
+ top: 50%;
548
+ transform: translateY(-50%);
549
+ background: var(--color-surface, #2a2a2a);
550
+ border: 1px solid var(--color-border, #3a3a3a);
551
+ border-right: none;
552
+ border-radius: 8px 0 0 8px;
553
+ padding: 0.75rem 0.5rem;
554
+ cursor: pointer;
555
+ display: flex;
556
+ flex-direction: column;
557
+ align-items: center;
558
+ gap: 0.25rem;
559
+ z-index: 100;
560
+ transition: background-color 0.2s, transform 0.2s;
561
+ }
562
+
563
+ .wisp-tab:hover {
564
+ background: var(--color-primary, #2d5a2d);
565
+ transform: translateY(-50%) translateX(-2px);
566
+ }
567
+
568
+ .tab-icon {
569
+ font-family: monospace;
570
+ font-size: 1.2rem;
571
+ color: var(--color-accent, #8bc48b);
572
+ }
573
+
574
+ .tab-text {
575
+ font-size: 0.6rem;
576
+ text-transform: lowercase;
577
+ letter-spacing: 0.1em;
578
+ color: var(--color-muted-foreground, #888);
579
+ writing-mode: vertical-rl;
580
+ text-orientation: mixed;
581
+ }
582
+
583
+ /* Main panel */
584
+ .wisp-panel {
585
+ position: fixed;
586
+ right: 0;
587
+ top: 0;
588
+ bottom: 0;
589
+ width: 280px;
590
+ background: var(--color-background, #1e1e1e);
591
+ border-left: 1px solid var(--color-border, #3a3a3a);
592
+ display: flex;
593
+ flex-direction: column;
594
+ z-index: 100;
595
+ font-size: 0.85rem;
596
+ overflow: hidden;
597
+ }
598
+
599
+ /* Header */
600
+ .panel-header {
601
+ display: flex;
602
+ justify-content: space-between;
603
+ align-items: center;
604
+ padding: 0.75rem 1rem;
605
+ border-bottom: 1px solid var(--color-border, #3a3a3a);
606
+ }
607
+
608
+ .panel-header h3 {
609
+ margin: 0;
610
+ font-size: 0.9rem;
611
+ font-weight: 500;
612
+ color: var(--color-accent, #8bc48b);
613
+ letter-spacing: 0.05em;
614
+ }
615
+
616
+ .header-actions {
617
+ display: flex;
618
+ gap: 0.25rem;
619
+ }
620
+
621
+ .icon-btn {
622
+ background: none;
623
+ border: none;
624
+ color: var(--color-muted-foreground, #888);
625
+ cursor: pointer;
626
+ padding: 0.25rem 0.5rem;
627
+ font-size: 1rem;
628
+ line-height: 1;
629
+ border-radius: 4px;
630
+ transition: background-color 0.15s, color 0.15s;
631
+ }
632
+
633
+ .icon-btn:hover {
634
+ background: var(--color-surface, #2a2a2a);
635
+ color: var(--color-foreground, #d4d4d4);
636
+ }
637
+
638
+ /* Content length indicator */
639
+ .content-length {
640
+ padding: 0.25rem 0.75rem;
641
+ border-bottom: 1px solid var(--color-border, #3a3a3a);
642
+ font-size: 0.65rem;
643
+ color: var(--color-muted-foreground, #888);
644
+ }
645
+
646
+ .content-length.warn {
647
+ background: rgba(255, 193, 7, 0.1);
648
+ }
649
+
650
+ .content-length.warn .length-text {
651
+ color: #ffc107;
652
+ }
653
+
654
+ .content-length.over {
655
+ background: rgba(220, 53, 69, 0.1);
656
+ }
657
+
658
+ .content-length.over .length-text {
659
+ color: #dc3545;
660
+ }
661
+
662
+ .length-text {
663
+ display: block;
664
+ margin-bottom: 0.25rem;
665
+ }
666
+
667
+ .length-bar {
668
+ height: 2px;
669
+ background: var(--color-border, #3a3a3a);
670
+ border-radius: 1px;
671
+ overflow: hidden;
672
+ }
673
+
674
+ .length-fill {
675
+ height: 100%;
676
+ background: var(--color-accent, #8bc48b);
677
+ transition: width 0.2s ease;
678
+ }
679
+
680
+ .content-length.warn .length-fill {
681
+ background: #ffc107;
682
+ }
683
+
684
+ .content-length.over .length-fill {
685
+ background: #dc3545;
686
+ }
687
+
688
+ /* Vibes section */
689
+ .vibes-section {
690
+ padding: 0.5rem;
691
+ text-align: center;
692
+ border-bottom: 1px solid var(--color-border, #3a3a3a);
693
+ background: var(--color-surface, #2a2a2a);
694
+ }
695
+
696
+ .ascii-vibe {
697
+ margin: 0;
698
+ font-family: monospace;
699
+ font-size: 0.6rem;
700
+ line-height: 1.2;
701
+ color: var(--color-accent, #8bc48b);
702
+ opacity: 0.8;
703
+ white-space: pre;
704
+ -webkit-user-select: none;
705
+ -moz-user-select: none;
706
+ user-select: none;
707
+ }
708
+
709
+ /* Mode selector */
710
+ .mode-selector {
711
+ display: flex;
712
+ gap: 1rem;
713
+ padding: 0.5rem 1rem;
714
+ border-bottom: 1px solid var(--color-border, #3a3a3a);
715
+ font-size: 0.75rem;
716
+ }
717
+
718
+ .mode-selector label {
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 0.25rem;
722
+ cursor: pointer;
723
+ color: var(--color-muted-foreground, #888);
724
+ }
725
+
726
+ .mode-selector input[type="radio"] {
727
+ accent-color: var(--color-accent, #8bc48b);
728
+ }
729
+
730
+ /* Actions */
731
+ .actions {
732
+ display: grid;
733
+ grid-template-columns: 1fr 1fr;
734
+ gap: 0.5rem;
735
+ padding: 0.75rem;
736
+ }
737
+
738
+ .action-btn {
739
+ background: var(--color-surface, #2a2a2a);
740
+ border: 1px solid var(--color-border, #3a3a3a);
741
+ border-radius: 4px;
742
+ padding: 0.5rem;
743
+ color: var(--color-foreground, #d4d4d4);
744
+ cursor: pointer;
745
+ font-size: 0.75rem;
746
+ transition: background-color 0.15s, border-color 0.15s;
747
+ }
748
+
749
+ .action-btn:hover:not(:disabled) {
750
+ background: var(--color-primary, #2d5a2d);
751
+ border-color: var(--color-accent, #8bc48b);
752
+ }
753
+
754
+ .action-btn:disabled {
755
+ opacity: 0.5;
756
+ cursor: not-allowed;
757
+ }
758
+
759
+ .action-full {
760
+ grid-column: 1 / -1;
761
+ background: var(--color-primary, #2d5a2d);
762
+ border-color: var(--color-accent, #8bc48b);
763
+ }
764
+
765
+ /* Error message */
766
+ .error-message {
767
+ margin: 0.5rem;
768
+ padding: 0.5rem;
769
+ background: rgba(220, 53, 69, 0.1);
770
+ border: 1px solid rgba(220, 53, 69, 0.3);
771
+ border-radius: 4px;
772
+ color: #ff6b6b;
773
+ font-size: 0.75rem;
774
+ }
775
+
776
+ .error-message button {
777
+ background: none;
778
+ border: none;
779
+ color: inherit;
780
+ text-decoration: underline;
781
+ cursor: pointer;
782
+ padding: 0;
783
+ margin-top: 0.25rem;
784
+ }
785
+
786
+ /* Results */
787
+ .results {
788
+ flex: 1;
789
+ overflow-y: auto;
790
+ display: flex;
791
+ flex-direction: column;
792
+ }
793
+
794
+ /* Tabs */
795
+ .tabs {
796
+ display: flex;
797
+ border-bottom: 1px solid var(--color-border, #3a3a3a);
798
+ }
799
+
800
+ .tab {
801
+ flex: 1;
802
+ background: none;
803
+ border: none;
804
+ padding: 0.5rem;
805
+ color: var(--color-muted-foreground, #888);
806
+ cursor: pointer;
807
+ font-size: 0.7rem;
808
+ text-transform: lowercase;
809
+ border-bottom: 2px solid transparent;
810
+ transition: color 0.15s, border-color 0.15s;
811
+ }
812
+
813
+ .tab:hover {
814
+ color: var(--color-foreground, #d4d4d4);
815
+ }
816
+
817
+ .tab.active {
818
+ color: var(--color-accent, #8bc48b);
819
+ border-bottom-color: var(--color-accent, #8bc48b);
820
+ }
821
+
822
+ /* Tab content */
823
+ .tab-content {
824
+ flex: 1;
825
+ overflow-y: auto;
826
+ padding: 0.75rem;
827
+ }
828
+
829
+ /* Score display */
830
+ .score-display {
831
+ display: flex;
832
+ align-items: center;
833
+ gap: 0.5rem;
834
+ margin-bottom: 0.75rem;
835
+ font-size: 0.75rem;
836
+ }
837
+
838
+ .score-label {
839
+ color: var(--color-muted-foreground, #888);
840
+ }
841
+
842
+ .score-bar {
843
+ font-family: monospace;
844
+ color: var(--color-accent, #8bc48b);
845
+ letter-spacing: -0.05em;
846
+ }
847
+
848
+ .score-num {
849
+ color: var(--color-foreground, #d4d4d4);
850
+ font-weight: 600;
851
+ }
852
+
853
+ /* Suggestions */
854
+ .suggestions {
855
+ display: flex;
856
+ flex-direction: column;
857
+ gap: 0.75rem;
858
+ }
859
+
860
+ .suggestion {
861
+ background: var(--color-surface, #2a2a2a);
862
+ border-radius: 4px;
863
+ padding: 0.5rem;
864
+ border-left: 3px solid var(--color-border, #3a3a3a);
865
+ }
866
+
867
+ .suggestion.severity-error {
868
+ border-left-color: #dc3545;
869
+ }
870
+
871
+ .suggestion.severity-warning {
872
+ border-left-color: #ffc107;
873
+ }
874
+
875
+ .suggestion.severity-style {
876
+ border-left-color: var(--color-accent, #8bc48b);
877
+ }
878
+
879
+ .suggestion-original {
880
+ margin-bottom: 0.25rem;
881
+ }
882
+
883
+ .strike {
884
+ text-decoration: line-through;
885
+ color: var(--color-muted-foreground, #888);
886
+ font-style: italic;
887
+ }
888
+
889
+ .suggestion-fix {
890
+ display: flex;
891
+ align-items: center;
892
+ gap: 0.25rem;
893
+ margin-bottom: 0.25rem;
894
+ }
895
+
896
+ .arrow {
897
+ color: var(--color-accent, #8bc48b);
898
+ }
899
+
900
+ .fix-text {
901
+ color: var(--color-accent, #8bc48b);
902
+ }
903
+
904
+ .suggestion-reason {
905
+ font-size: 0.7rem;
906
+ color: var(--color-muted-foreground, #888);
907
+ margin-bottom: 0.5rem;
908
+ }
909
+
910
+ .apply-btn {
911
+ background: var(--color-primary, #2d5a2d);
912
+ border: none;
913
+ border-radius: 3px;
914
+ padding: 0.25rem 0.5rem;
915
+ color: white;
916
+ cursor: pointer;
917
+ font-size: 0.65rem;
918
+ transition: background-color 0.15s;
919
+ }
920
+
921
+ .apply-btn:hover {
922
+ background: var(--color-accent, #8bc48b);
923
+ }
924
+
925
+ .no-issues {
926
+ color: var(--color-accent, #8bc48b);
927
+ font-style: italic;
928
+ text-align: center;
929
+ padding: 1rem;
930
+ }
931
+
932
+ /* Tone results */
933
+ .tone-analysis {
934
+ color: var(--color-foreground, #d4d4d4);
935
+ margin-bottom: 0.75rem;
936
+ line-height: 1.4;
937
+ }
938
+
939
+ .traits {
940
+ display: flex;
941
+ flex-direction: column;
942
+ gap: 0.5rem;
943
+ margin-bottom: 0.75rem;
944
+ }
945
+
946
+ .trait {
947
+ display: grid;
948
+ grid-template-columns: 80px 1fr 30px;
949
+ align-items: center;
950
+ gap: 0.5rem;
951
+ font-size: 0.7rem;
952
+ }
953
+
954
+ .trait-name {
955
+ color: var(--color-muted-foreground, #888);
956
+ text-transform: lowercase;
957
+ }
958
+
959
+ .trait-bar-container {
960
+ background: var(--color-surface, #2a2a2a);
961
+ height: 6px;
962
+ border-radius: 3px;
963
+ overflow: hidden;
964
+ }
965
+
966
+ .trait-bar {
967
+ height: 100%;
968
+ background: var(--color-accent, #8bc48b);
969
+ border-radius: 3px;
970
+ transition: width 0.3s ease;
971
+ }
972
+
973
+ .trait-score {
974
+ text-align: right;
975
+ color: var(--color-muted-foreground, #888);
976
+ }
977
+
978
+ .tone-suggestions {
979
+ border-top: 1px solid var(--color-border, #3a3a3a);
980
+ padding-top: 0.5rem;
981
+ }
982
+
983
+ .tone-sug {
984
+ color: var(--color-muted-foreground, #888);
985
+ font-size: 0.7rem;
986
+ margin: 0.25rem 0;
987
+ }
988
+
989
+ /* Readability results */
990
+ .readability-stats {
991
+ display: grid;
992
+ grid-template-columns: 1fr 1fr;
993
+ gap: 0.5rem;
994
+ margin-bottom: 0.75rem;
995
+ }
996
+
997
+ .stat {
998
+ background: var(--color-surface, #2a2a2a);
999
+ padding: 0.5rem;
1000
+ border-radius: 4px;
1001
+ }
1002
+
1003
+ .stat-label {
1004
+ display: block;
1005
+ font-size: 0.65rem;
1006
+ color: var(--color-muted-foreground, #888);
1007
+ text-transform: lowercase;
1008
+ margin-bottom: 0.25rem;
1009
+ }
1010
+
1011
+ .stat-value {
1012
+ font-size: 0.9rem;
1013
+ color: var(--color-foreground, #d4d4d4);
1014
+ font-weight: 500;
1015
+ }
1016
+
1017
+ .readability-suggestions {
1018
+ border-top: 1px solid var(--color-border, #3a3a3a);
1019
+ padding-top: 0.5rem;
1020
+ }
1021
+
1022
+ .read-sug {
1023
+ color: var(--color-muted-foreground, #888);
1024
+ font-size: 0.7rem;
1025
+ margin: 0.25rem 0;
1026
+ }
1027
+
1028
+ /* Usage info */
1029
+ .usage-info {
1030
+ display: flex;
1031
+ justify-content: space-between;
1032
+ align-items: center;
1033
+ padding: 0.5rem 0.75rem;
1034
+ border-top: 1px solid var(--color-border, #3a3a3a);
1035
+ font-size: 0.65rem;
1036
+ color: var(--color-muted-foreground, #888);
1037
+ }
1038
+
1039
+ .clear-btn {
1040
+ background: none;
1041
+ border: none;
1042
+ color: var(--color-muted-foreground, #888);
1043
+ cursor: pointer;
1044
+ text-decoration: underline;
1045
+ font-size: inherit;
1046
+ }
1047
+
1048
+ .clear-btn:hover {
1049
+ color: var(--color-foreground, #d4d4d4);
1050
+ }
1051
+
1052
+ /* Footer */
1053
+ .panel-footer {
1054
+ padding: 0.5rem;
1055
+ text-align: center;
1056
+ border-top: 1px solid var(--color-border, #3a3a3a);
1057
+ background: var(--color-surface, #2a2a2a);
1058
+ }
1059
+
1060
+ .panel-footer p {
1061
+ margin: 0;
1062
+ font-size: 0.6rem;
1063
+ color: var(--color-muted-foreground, #888);
1064
+ font-style: italic;
1065
+ letter-spacing: 0.05em;
1066
+ }
1067
+
1068
+ /* Scrollbar styling */
1069
+ .results::-webkit-scrollbar,
1070
+ .tab-content::-webkit-scrollbar {
1071
+ width: 4px;
1072
+ }
1073
+
1074
+ .results::-webkit-scrollbar-track,
1075
+ .tab-content::-webkit-scrollbar-track {
1076
+ background: transparent;
1077
+ }
1078
+
1079
+ .results::-webkit-scrollbar-thumb,
1080
+ .tab-content::-webkit-scrollbar-thumb {
1081
+ background: var(--color-border, #3a3a3a);
1082
+ border-radius: 2px;
1083
+ }
1084
+
1085
+ /* Responsive */
1086
+ @media (max-width: 768px) {
1087
+ .wisp-panel {
1088
+ width: 100%;
1089
+ max-width: 320px;
1090
+ }
1091
+ }
1092
+ </style>