@farming-labs/svelte-theme 0.0.53 → 0.0.55

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/svelte-theme",
3
- "version": "0.0.53",
3
+ "version": "0.0.55",
4
4
  "description": "Svelte UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
5
5
  "keywords": [
6
6
  "docs",
@@ -82,8 +82,8 @@
82
82
  "dependencies": {
83
83
  "gray-matter": "^4.0.3",
84
84
  "sugar-high": "^0.9.5",
85
- "@farming-labs/docs": "0.0.53",
86
- "@farming-labs/svelte": "0.0.53"
85
+ "@farming-labs/docs": "0.0.55",
86
+ "@farming-labs/svelte": "0.0.55"
87
87
  },
88
88
  "peerDependencies": {
89
89
  "svelte": ">=5.0.0"
@@ -13,6 +13,8 @@
13
13
  let copyLabel = $state("Copy page");
14
14
  let copied = $state(false);
15
15
  let selectedFeedback = $state(null);
16
+ let feedbackComment = $state("");
17
+ let feedbackStatus = $state("idle");
16
18
 
17
19
  let titleSuffix = $derived(
18
20
  config?.metadata?.titleTemplate
@@ -122,12 +124,17 @@
122
124
  );
123
125
  let showActionsAbove = $derived(pageActionsPosition === "above-title" && showPageActions);
124
126
  let showActionsBelow = $derived(pageActionsPosition === "below-title" && showPageActions);
127
+ let feedbackPathKey = $derived(
128
+ `${data.locale ?? ""}:${data.entry ?? config?.entry ?? "docs"}:${data.slug ?? ""}`
129
+ );
125
130
  let feedbackConfig = $derived.by(() => {
126
131
  const defaults = {
127
132
  enabled: false,
128
133
  question: "How is this guide?",
134
+ placeholder: "Leave your feedback...",
129
135
  positiveLabel: "Good",
130
136
  negativeLabel: "Bad",
137
+ submitLabel: "Submit",
131
138
  onFeedback: undefined,
132
139
  };
133
140
 
@@ -137,12 +144,21 @@
137
144
  return {
138
145
  enabled: feedback.enabled !== false,
139
146
  question: feedback.question ?? defaults.question,
147
+ placeholder: feedback.placeholder ?? defaults.placeholder,
140
148
  positiveLabel: feedback.positiveLabel ?? defaults.positiveLabel,
141
149
  negativeLabel: feedback.negativeLabel ?? defaults.negativeLabel,
150
+ submitLabel: feedback.submitLabel ?? defaults.submitLabel,
142
151
  onFeedback: typeof feedback.onFeedback === "function" ? feedback.onFeedback : undefined,
143
152
  };
144
153
  });
145
154
 
155
+ $effect(() => {
156
+ feedbackPathKey;
157
+ selectedFeedback = null;
158
+ feedbackComment = "";
159
+ feedbackStatus = "idle";
160
+ });
161
+
146
162
  function handleCopyPage() {
147
163
  let text = "";
148
164
  if (data.rawMarkdown && typeof data.rawMarkdown === "string" && data.rawMarkdown.length > 0) {
@@ -191,9 +207,7 @@
191
207
  closeDropdown();
192
208
  }
193
209
 
194
- function handleFeedback(value) {
195
- selectedFeedback = value;
196
-
210
+ function buildFeedbackPayload() {
197
211
  const pathname =
198
212
  typeof window !== "undefined"
199
213
  ? window.location.pathname.replace(/\/$/, "") || "/"
@@ -201,8 +215,9 @@
201
215
  ? `/${data.entry ?? config?.entry ?? "docs"}/${data.slug}`
202
216
  : `/${data.entry ?? config?.entry ?? "docs"}`;
203
217
 
204
- const payload = {
205
- value,
218
+ return {
219
+ value: selectedFeedback,
220
+ comment: feedbackComment.trim() ? feedbackComment.trim() : undefined,
206
221
  title: data.title,
207
222
  description: data.description,
208
223
  url: typeof window !== "undefined" ? window.location.href : pathname,
@@ -212,21 +227,50 @@
212
227
  slug: data.slug ?? "",
213
228
  locale: data.locale,
214
229
  };
230
+ }
231
+
232
+ async function emitFeedback(payload) {
233
+ let firstError = null;
215
234
 
216
235
  try {
217
- feedbackConfig.onFeedback?.(payload);
218
- } catch {}
236
+ await feedbackConfig.onFeedback?.(payload);
237
+ } catch (error) {
238
+ firstError ??= error;
239
+ }
219
240
 
220
241
  try {
221
242
  const docsWindow = typeof window !== "undefined" ? window : undefined;
222
243
  if (docsWindow && typeof docsWindow.__fdOnFeedback__ === "function") {
223
- docsWindow.__fdOnFeedback__(payload);
244
+ await docsWindow.__fdOnFeedback__(payload);
224
245
  }
225
- } catch {}
246
+ } catch (error) {
247
+ firstError ??= error;
248
+ }
226
249
 
227
250
  if (typeof window !== "undefined") {
228
251
  window.dispatchEvent(new CustomEvent("fd:feedback", { detail: payload }));
229
252
  }
253
+
254
+ if (firstError) throw firstError;
255
+ }
256
+
257
+ function handleFeedback(value) {
258
+ selectedFeedback = value;
259
+ if (feedbackStatus !== "idle") feedbackStatus = "idle";
260
+ }
261
+
262
+ async function submitFeedback() {
263
+ if (!selectedFeedback || feedbackStatus === "submitting" || feedbackStatus === "submitted") {
264
+ return;
265
+ }
266
+
267
+ try {
268
+ feedbackStatus = "submitting";
269
+ await emitFeedback(buildFeedbackPayload());
270
+ feedbackStatus = "submitted";
271
+ } catch {
272
+ feedbackStatus = "error";
273
+ }
230
274
  }
231
275
 
232
276
  function handleClickOutside(e) {
@@ -411,9 +455,10 @@
411
455
  <div class="fd-feedback-actions" role="group" aria-label={feedbackConfig.question}>
412
456
  <button
413
457
  type="button"
414
- class="fd-page-action-btn"
458
+ class="fd-page-action-btn fd-feedback-choice"
415
459
  aria-pressed={selectedFeedback === "positive"}
416
460
  data-selected={selectedFeedback === "positive" ? "true" : undefined}
461
+ disabled={feedbackStatus === "submitting"}
417
462
  onclick={() => handleFeedback("positive")}
418
463
  >
419
464
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
@@ -429,9 +474,10 @@
429
474
  </button>
430
475
  <button
431
476
  type="button"
432
- class="fd-page-action-btn"
477
+ class="fd-page-action-btn fd-feedback-choice"
433
478
  aria-pressed={selectedFeedback === "negative"}
434
479
  data-selected={selectedFeedback === "negative" ? "true" : undefined}
480
+ disabled={feedbackStatus === "submitting"}
435
481
  onclick={() => handleFeedback("negative")}
436
482
  >
437
483
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
@@ -447,6 +493,57 @@
447
493
  </button>
448
494
  </div>
449
495
  </div>
496
+ {#if selectedFeedback}
497
+ <div class="fd-feedback-form">
498
+ <textarea
499
+ class="fd-feedback-input"
500
+ aria-label="Additional feedback"
501
+ placeholder={feedbackConfig.placeholder}
502
+ bind:value={feedbackComment}
503
+ disabled={feedbackStatus === "submitting"}
504
+ oninput={() => {
505
+ if (feedbackStatus !== "idle") feedbackStatus = "idle";
506
+ }}
507
+ ></textarea>
508
+ <div class="fd-feedback-submit-row">
509
+ <button
510
+ type="button"
511
+ class="fd-page-action-btn fd-feedback-submit"
512
+ disabled={feedbackStatus === "submitting" || feedbackStatus === "submitted"}
513
+ onclick={() => void submitFeedback()}
514
+ >
515
+ {#if feedbackStatus === "submitting"}
516
+ <span class="fd-feedback-spinner" aria-hidden="true"></span>
517
+ {:else if feedbackStatus === "submitted"}
518
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
519
+ <path
520
+ d="M20 6 9 17l-5-5"
521
+ stroke="currentColor"
522
+ stroke-width="2"
523
+ stroke-linecap="round"
524
+ stroke-linejoin="round"
525
+ />
526
+ </svg>
527
+ {/if}
528
+ <span>
529
+ {feedbackStatus === "submitted"
530
+ ? "Submitted"
531
+ : feedbackConfig.submitLabel}
532
+ </span>
533
+ </button>
534
+ {#if feedbackStatus === "submitted"}
535
+ <p class="fd-feedback-status" data-status="success" role="status" aria-live="polite">
536
+ Thanks for the feedback.
537
+ </p>
538
+ {/if}
539
+ </div>
540
+ {#if feedbackStatus === "error"}
541
+ <p class="fd-feedback-status" data-status="error" role="status" aria-live="polite">
542
+ Could not send feedback. Please try again.
543
+ </p>
544
+ {/if}
545
+ </div>
546
+ {/if}
450
547
  </section>
451
548
  {/if}
452
549
  </div>
@@ -328,3 +328,18 @@
328
328
  .fd-ai-floating-trigger .ask-ai-trigger:hover {
329
329
  box-shadow: 0 10px 40px rgba(180, 140, 20, 0.4);
330
330
  }
331
+
332
+ /* ─── Feedback (colorful theme) ──────────────────────────────────── */
333
+
334
+ .fd-feedback-input,
335
+ .fd-feedback-submit {
336
+ border-radius: 0.5rem;
337
+ }
338
+
339
+ .fd-feedback-choice[data-selected="true"] {
340
+ background: color-mix(in srgb, var(--color-fd-primary) 12%, var(--color-fd-secondary));
341
+ }
342
+
343
+ .fd-feedback-status[data-status="success"] {
344
+ color: var(--color-fd-primary);
345
+ }
@@ -275,3 +275,22 @@ code:not(pre code) {
275
275
  background: var(--color-fd-accent) !important;
276
276
  color: var(--color-fd-accent-foreground) !important;
277
277
  }
278
+
279
+ /* ─── Feedback (darksharp theme) ─────────────────────────────────── */
280
+
281
+ .fd-feedback-input,
282
+ .fd-feedback-submit {
283
+ border-radius: 0.2rem !important;
284
+ }
285
+
286
+ .dark .fd-feedback-input {
287
+ background: hsl(0 0% 4%);
288
+ }
289
+
290
+ .dark .fd-feedback-choice[data-selected="true"] {
291
+ background: hsl(0 0% 8%);
292
+ }
293
+
294
+ .dark .fd-feedback-status[data-status="success"] {
295
+ color: hsl(0 0% 90%);
296
+ }
package/styles/docs.css CHANGED
@@ -726,6 +726,7 @@ samp {
726
726
 
727
727
  .fd-feedback {
728
728
  margin-top: 2rem;
729
+ margin-bottom: 1.25rem;
729
730
  padding-top: 1.25rem;
730
731
  border-top: 1px solid var(--color-fd-border);
731
732
  }
@@ -753,6 +754,122 @@ samp {
753
754
  flex-wrap: wrap;
754
755
  }
755
756
 
757
+ .fd-feedback-form {
758
+ display: flex;
759
+ flex-direction: column;
760
+ gap: 0.75rem;
761
+ margin-top: 0.875rem;
762
+ }
763
+
764
+ .fd-feedback-form[hidden],
765
+ .fd-feedback-spinner[hidden],
766
+ .fd-feedback-status[hidden],
767
+ .fd-feedback-submit svg[hidden] {
768
+ display: none !important;
769
+ }
770
+
771
+ .fd-feedback-choice[data-selected="true"] {
772
+ background: var(--color-fd-accent);
773
+ color: var(--color-fd-accent-foreground);
774
+ border-color: color-mix(in srgb, var(--color-fd-primary, currentColor) 65%, transparent);
775
+ }
776
+
777
+ .fd-feedback-input {
778
+ width: 100%;
779
+ min-height: 4.75rem;
780
+ resize: vertical;
781
+ padding: 0.875rem 1rem;
782
+ border: 1px solid
783
+ color-mix(in srgb, var(--color-fd-border) 78%, var(--color-fd-foreground) 22%);
784
+ border-radius: 0.5rem;
785
+ background: var(--color-fd-card, transparent);
786
+ color: var(--color-fd-foreground);
787
+ font: inherit;
788
+ line-height: 1.55;
789
+ outline: none;
790
+ transition:
791
+ border-color 150ms ease,
792
+ box-shadow 150ms ease,
793
+ background-color 150ms ease;
794
+ }
795
+
796
+ .fd-feedback-input::placeholder {
797
+ color: var(--color-fd-muted-foreground);
798
+ }
799
+
800
+ .fd-feedback-input:focus {
801
+ border-color: var(--color-fd-ring, var(--color-fd-primary, currentColor));
802
+ box-shadow: 0 0 0 1px var(--color-fd-ring, var(--color-fd-primary, currentColor));
803
+ }
804
+
805
+ .fd-feedback-submit-row {
806
+ display: flex;
807
+ align-items: center;
808
+ gap: 0.75rem;
809
+ flex-wrap: nowrap;
810
+ min-height: 2rem;
811
+ }
812
+
813
+ .fd-feedback-submit {
814
+ min-width: 7rem;
815
+ padding-inline: 0.875rem;
816
+ justify-content: center;
817
+ }
818
+
819
+ .fd-feedback-submit:disabled {
820
+ cursor: not-allowed;
821
+ opacity: 0.65;
822
+ }
823
+
824
+ .fd-feedback-spinner {
825
+ width: 0.875rem;
826
+ height: 0.875rem;
827
+ border-radius: 9999px;
828
+ border: 2px solid currentColor;
829
+ border-right-color: transparent;
830
+ animation: fd-feedback-spin 0.8s linear infinite;
831
+ }
832
+
833
+ .fd-feedback-status {
834
+ margin: 0 !important;
835
+ font-size: 0.75rem;
836
+ line-height: 1.3;
837
+ color: var(--color-fd-muted-foreground);
838
+ }
839
+
840
+ .fd-feedback-status[data-status="success"] {
841
+ display: inline-flex;
842
+ align-self: center;
843
+ align-items: center;
844
+ gap: 0.4rem;
845
+ min-height: 2rem;
846
+ }
847
+
848
+ .fd-feedback-status[data-status="success"] {
849
+ color: color-mix(in srgb, var(--color-fd-primary) 85%, var(--color-fd-foreground));
850
+ }
851
+
852
+ .fd-feedback-status[data-status="error"] {
853
+ color: var(--color-fd-foreground);
854
+ }
855
+
856
+ @keyframes fd-feedback-spin {
857
+ to {
858
+ transform: rotate(360deg);
859
+ }
860
+ }
861
+
862
+ @media (max-width: 640px) {
863
+ .fd-feedback-content {
864
+ flex-direction: column;
865
+ align-items: flex-start;
866
+ }
867
+
868
+ .fd-feedback-submit-row {
869
+ flex-wrap: wrap;
870
+ }
871
+ }
872
+
756
873
  /* ─── Breadcrumb ─────────────────────────────────────────────────────── */
757
874
 
758
875
  .fd-breadcrumb {
@@ -892,3 +892,18 @@ details > :not(summary) {
892
892
  .fd-sidebar::-webkit-scrollbar-track {
893
893
  background: transparent;
894
894
  }
895
+
896
+ /* ─── Feedback (greentree theme) ─────────────────────────────────── */
897
+
898
+ .fd-feedback-input,
899
+ .fd-feedback-submit {
900
+ border-radius: 0.75rem;
901
+ }
902
+
903
+ .fd-feedback-choice[data-selected="true"] {
904
+ background: color-mix(in srgb, var(--color-fd-primary) 12%, var(--color-fd-secondary));
905
+ }
906
+
907
+ .fd-feedback-status[data-status="success"] {
908
+ color: var(--color-fd-primary);
909
+ }
@@ -848,3 +848,20 @@ code:not(pre code) {
848
848
  .fd-page-action-menu-item:first-child {
849
849
  border-top: none !important;
850
850
  }
851
+
852
+ /* ─── Feedback (pixel-border theme) ──────────────────────────────── */
853
+
854
+ .fd-feedback-input,
855
+ .fd-feedback-submit {
856
+ border-radius: 0 !important;
857
+ box-shadow: 3px 3px 0 0 var(--color-fd-border);
858
+ }
859
+
860
+ .fd-feedback-choice[data-selected="true"] {
861
+ background: var(--color-fd-secondary);
862
+ }
863
+
864
+ .fd-feedback-status[data-status="success"] {
865
+ font-family: var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace));
866
+ text-transform: uppercase;
867
+ }