@farming-labs/nuxt-theme 0.0.53 → 0.0.54

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/nuxt-theme",
3
- "version": "0.0.53",
3
+ "version": "0.0.54",
4
4
  "description": "Nuxt/Vue UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
5
5
  "keywords": [
6
6
  "docs",
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "sugar-high": "^0.9.5",
63
- "@farming-labs/docs": "0.0.53"
63
+ "@farming-labs/docs": "0.0.54"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "nuxt": ">=3.0.0",
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref, onMounted, onUnmounted } from "vue";
2
+ import { computed, ref, onMounted, onUnmounted, watch } from "vue";
3
3
  import { useRoute } from "vue-router";
4
4
  import { useHead } from "#app";
5
5
  import DocsPage from "./DocsPage.vue";
@@ -26,11 +26,32 @@ const props = defineProps<{
26
26
  config?: Record<string, unknown> | null;
27
27
  }>();
28
28
 
29
+ type FeedbackValue = "positive" | "negative";
30
+ type FeedbackStatus = "idle" | "submitting" | "submitted" | "error";
31
+ type FeedbackPayload = {
32
+ value: FeedbackValue;
33
+ comment?: string;
34
+ title?: string;
35
+ description?: string;
36
+ url: string;
37
+ pathname: string;
38
+ path: string;
39
+ entry: string;
40
+ slug: string;
41
+ locale?: string;
42
+ };
43
+
44
+ interface DocsWindowHooks extends Window {
45
+ __fdOnFeedback__?: (payload: FeedbackPayload) => void | Promise<void>;
46
+ }
47
+
29
48
  const route = useRoute();
30
49
  const openDropdownMenu = ref(false);
31
50
  const copyLabel = ref("Copy page");
32
51
  const copied = ref(false);
33
- const selectedFeedback = ref<"positive" | "negative" | null>(null);
52
+ const selectedFeedback = ref<FeedbackValue | null>(null);
53
+ const feedbackComment = ref("");
54
+ const feedbackStatus = ref<FeedbackStatus>("idle");
34
55
 
35
56
  const titleSuffix = computed(() =>
36
57
  props.config?.metadata?.titleTemplate
@@ -151,9 +172,11 @@ const feedbackConfig = computed(() => {
151
172
  const defaults = {
152
173
  enabled: false,
153
174
  question: "How is this guide?",
175
+ placeholder: "Leave your feedback...",
154
176
  positiveLabel: "Good",
155
177
  negativeLabel: "Bad",
156
- onFeedback: undefined as ((payload: Record<string, unknown>) => void) | undefined,
178
+ submitLabel: "Submit",
179
+ onFeedback: undefined as ((payload: FeedbackPayload) => void | Promise<void>) | undefined,
157
180
  };
158
181
 
159
182
  const feedback = props.config?.feedback as Record<string, unknown> | boolean | null | undefined;
@@ -164,15 +187,17 @@ const feedbackConfig = computed(() => {
164
187
  return {
165
188
  enabled: feedback.enabled !== false,
166
189
  question: String((feedback as { question?: string }).question ?? defaults.question),
190
+ placeholder: String(feedback.placeholder ?? defaults.placeholder),
167
191
  positiveLabel: String(
168
192
  feedback.positiveLabel ?? defaults.positiveLabel,
169
193
  ),
170
194
  negativeLabel: String(
171
195
  feedback.negativeLabel ?? defaults.negativeLabel,
172
196
  ),
197
+ submitLabel: String(feedback.submitLabel ?? defaults.submitLabel),
173
198
  onFeedback:
174
199
  typeof feedback.onFeedback === "function"
175
- ? (feedback.onFeedback as (payload: Record<string, unknown>) => void)
200
+ ? (feedback.onFeedback as (payload: FeedbackPayload) => void | Promise<void>)
176
201
  : undefined,
177
202
  };
178
203
  });
@@ -232,9 +257,15 @@ function openInProvider(provider: { name: string; urlTemplate: string }) {
232
257
  closeDropdown();
233
258
  }
234
259
 
235
- function handleFeedback(value: "positive" | "negative") {
236
- selectedFeedback.value = value;
260
+ function resetFeedback() {
261
+ selectedFeedback.value = null;
262
+ feedbackComment.value = "";
263
+ feedbackStatus.value = "idle";
264
+ }
265
+
266
+ watch(() => route.path, resetFeedback, { immediate: true });
237
267
 
268
+ function buildFeedbackPayload(): FeedbackPayload {
238
269
  const pathname =
239
270
  typeof window !== "undefined"
240
271
  ? window.location.pathname.replace(/\/$/, "") || "/"
@@ -242,8 +273,9 @@ function handleFeedback(value: "positive" | "negative") {
242
273
  ? `/${entry.value}/${props.data.slug}`
243
274
  : `/${entry.value}`;
244
275
 
245
- const payload = {
246
- value,
276
+ return {
277
+ value: selectedFeedback.value as FeedbackValue,
278
+ comment: feedbackComment.value.trim() ? feedbackComment.value.trim() : undefined,
247
279
  title: props.data.title,
248
280
  description: props.data.description,
249
281
  url: typeof window !== "undefined" ? window.location.href : pathname,
@@ -253,20 +285,49 @@ function handleFeedback(value: "positive" | "negative") {
253
285
  slug: props.data.slug ?? "",
254
286
  locale: props.data.locale,
255
287
  };
288
+ }
289
+
290
+ async function emitFeedback(payload: FeedbackPayload) {
291
+ let firstError: unknown;
256
292
 
257
293
  try {
258
- feedbackConfig.value.onFeedback?.(payload);
259
- } catch {}
294
+ await feedbackConfig.value.onFeedback?.(payload);
295
+ } catch (error) {
296
+ firstError ??= error;
297
+ }
260
298
 
261
299
  try {
262
- if (typeof window !== "undefined" && (window as any).__fdOnFeedback__) {
263
- (window as any).__fdOnFeedback__(payload);
300
+ if (typeof window !== "undefined") {
301
+ await (window as DocsWindowHooks).__fdOnFeedback__?.(payload);
264
302
  }
265
- } catch {}
303
+ } catch (error) {
304
+ firstError ??= error;
305
+ }
266
306
 
267
307
  if (typeof window !== "undefined") {
268
308
  window.dispatchEvent(new CustomEvent("fd:feedback", { detail: payload }));
269
309
  }
310
+
311
+ if (firstError) throw firstError;
312
+ }
313
+
314
+ function handleFeedback(value: FeedbackValue) {
315
+ selectedFeedback.value = value;
316
+ if (feedbackStatus.value !== "idle") feedbackStatus.value = "idle";
317
+ }
318
+
319
+ async function submitFeedback() {
320
+ if (!selectedFeedback.value || feedbackStatus.value === "submitting" || feedbackStatus.value === "submitted") {
321
+ return;
322
+ }
323
+
324
+ try {
325
+ feedbackStatus.value = "submitting";
326
+ await emitFeedback(buildFeedbackPayload());
327
+ feedbackStatus.value = "submitted";
328
+ } catch {
329
+ feedbackStatus.value = "error";
330
+ }
270
331
  }
271
332
 
272
333
  function handleClickOutside(e: MouseEvent) {
@@ -429,9 +490,10 @@ onUnmounted(() => {
429
490
  <div class="fd-feedback-actions" role="group" :aria-label="feedbackConfig.question">
430
491
  <button
431
492
  type="button"
432
- class="fd-page-action-btn"
493
+ class="fd-page-action-btn fd-feedback-choice"
433
494
  :aria-pressed="selectedFeedback === 'positive'"
434
495
  :data-selected="selectedFeedback === 'positive' ? 'true' : undefined"
496
+ :disabled="feedbackStatus === 'submitting'"
435
497
  @click="handleFeedback('positive')"
436
498
  >
437
499
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
@@ -447,9 +509,10 @@ onUnmounted(() => {
447
509
  </button>
448
510
  <button
449
511
  type="button"
450
- class="fd-page-action-btn"
512
+ class="fd-page-action-btn fd-feedback-choice"
451
513
  :aria-pressed="selectedFeedback === 'negative'"
452
514
  :data-selected="selectedFeedback === 'negative' ? 'true' : undefined"
515
+ :disabled="feedbackStatus === 'submitting'"
453
516
  @click="handleFeedback('negative')"
454
517
  >
455
518
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
@@ -465,6 +528,67 @@ onUnmounted(() => {
465
528
  </button>
466
529
  </div>
467
530
  </div>
531
+ <div v-if="selectedFeedback" class="fd-feedback-form">
532
+ <textarea
533
+ v-model="feedbackComment"
534
+ class="fd-feedback-input"
535
+ aria-label="Additional feedback"
536
+ :placeholder="feedbackConfig.placeholder"
537
+ :disabled="feedbackStatus === 'submitting'"
538
+ @input="feedbackStatus !== 'idle' && (feedbackStatus = 'idle')"
539
+ />
540
+ <div class="fd-feedback-submit-row">
541
+ <button
542
+ type="button"
543
+ class="fd-page-action-btn fd-feedback-submit"
544
+ :disabled="feedbackStatus === 'submitting' || feedbackStatus === 'submitted'"
545
+ @click="submitFeedback"
546
+ >
547
+ <span v-if="feedbackStatus === 'submitting'" class="fd-feedback-spinner" aria-hidden="true" />
548
+ <svg
549
+ v-else-if="feedbackStatus === 'submitted'"
550
+ width="14"
551
+ height="14"
552
+ viewBox="0 0 24 24"
553
+ fill="none"
554
+ aria-hidden="true"
555
+ >
556
+ <path
557
+ d="M20 6 9 17l-5-5"
558
+ stroke="currentColor"
559
+ stroke-width="2"
560
+ stroke-linecap="round"
561
+ stroke-linejoin="round"
562
+ />
563
+ </svg>
564
+ <span>
565
+ {{
566
+ feedbackStatus === "submitted"
567
+ ? "Submitted"
568
+ : feedbackConfig.submitLabel
569
+ }}
570
+ </span>
571
+ </button>
572
+ <p
573
+ v-if="feedbackStatus === 'submitted'"
574
+ class="fd-feedback-status"
575
+ data-status="success"
576
+ role="status"
577
+ aria-live="polite"
578
+ >
579
+ Thanks for the feedback.
580
+ </p>
581
+ </div>
582
+ <p
583
+ v-if="feedbackStatus === 'error'"
584
+ class="fd-feedback-status"
585
+ data-status="error"
586
+ role="status"
587
+ aria-live="polite"
588
+ >
589
+ Could not send feedback. Please try again.
590
+ </p>
591
+ </div>
468
592
  </section>
469
593
  </DocsPage>
470
594
  </template>
@@ -301,3 +301,18 @@
301
301
  .fd-ai-floating-trigger .ask-ai-trigger:hover {
302
302
  box-shadow: 0 2px 30px rgba(180, 140, 20, 0.4);
303
303
  }
304
+
305
+ /* ─── Feedback (colorful theme) ──────────────────────────────────── */
306
+
307
+ .fd-feedback-input,
308
+ .fd-feedback-submit {
309
+ border-radius: 0.5rem;
310
+ }
311
+
312
+ .fd-feedback-choice[data-selected="true"] {
313
+ background: color-mix(in srgb, var(--color-fd-primary) 12%, var(--color-fd-secondary));
314
+ }
315
+
316
+ .fd-feedback-status[data-status="success"] {
317
+ color: var(--color-fd-primary);
318
+ }
@@ -222,3 +222,22 @@ code:not(pre code) {
222
222
  .fd-ai-fm-input-bar .fd-ai-floating-trigger .ask-ai-trigger:hover {
223
223
  transform: none;
224
224
  }
225
+
226
+ /* ─── Feedback (darksharp theme) ─────────────────────────────────── */
227
+
228
+ .fd-feedback-input,
229
+ .fd-feedback-submit {
230
+ border-radius: 0.2rem !important;
231
+ }
232
+
233
+ .dark .fd-feedback-input {
234
+ background: hsl(0 0% 4%);
235
+ }
236
+
237
+ .dark .fd-feedback-choice[data-selected="true"] {
238
+ background: hsl(0 0% 8%);
239
+ }
240
+
241
+ .dark .fd-feedback-status[data-status="success"] {
242
+ color: hsl(0 0% 90%);
243
+ }
package/styles/docs.css CHANGED
@@ -731,6 +731,7 @@ samp {
731
731
 
732
732
  .fd-feedback {
733
733
  margin-top: 2rem;
734
+ margin-bottom: 1.25rem;
734
735
  padding-top: 1.25rem;
735
736
  border-top: 1px solid var(--color-fd-border);
736
737
  }
@@ -758,6 +759,122 @@ samp {
758
759
  flex-wrap: wrap;
759
760
  }
760
761
 
762
+ .fd-feedback-form {
763
+ display: flex;
764
+ flex-direction: column;
765
+ gap: 0.75rem;
766
+ margin-top: 0.875rem;
767
+ }
768
+
769
+ .fd-feedback-form[hidden],
770
+ .fd-feedback-spinner[hidden],
771
+ .fd-feedback-status[hidden],
772
+ .fd-feedback-submit svg[hidden] {
773
+ display: none !important;
774
+ }
775
+
776
+ .fd-feedback-choice[data-selected="true"] {
777
+ background: var(--color-fd-accent);
778
+ color: var(--color-fd-accent-foreground);
779
+ border-color: color-mix(in srgb, var(--color-fd-primary, currentColor) 65%, transparent);
780
+ }
781
+
782
+ .fd-feedback-input {
783
+ width: 100%;
784
+ min-height: 4.75rem;
785
+ resize: vertical;
786
+ padding: 0.875rem 1rem;
787
+ border: 1px solid
788
+ color-mix(in srgb, var(--color-fd-border) 78%, var(--color-fd-foreground) 22%);
789
+ border-radius: 0.5rem;
790
+ background: var(--color-fd-card, transparent);
791
+ color: var(--color-fd-foreground);
792
+ font: inherit;
793
+ line-height: 1.55;
794
+ outline: none;
795
+ transition:
796
+ border-color 150ms ease,
797
+ box-shadow 150ms ease,
798
+ background-color 150ms ease;
799
+ }
800
+
801
+ .fd-feedback-input::placeholder {
802
+ color: var(--color-fd-muted-foreground);
803
+ }
804
+
805
+ .fd-feedback-input:focus {
806
+ border-color: var(--color-fd-ring, var(--color-fd-primary, currentColor));
807
+ box-shadow: 0 0 0 1px var(--color-fd-ring, var(--color-fd-primary, currentColor));
808
+ }
809
+
810
+ .fd-feedback-submit-row {
811
+ display: flex;
812
+ align-items: center;
813
+ gap: 0.75rem;
814
+ flex-wrap: nowrap;
815
+ min-height: 2rem;
816
+ }
817
+
818
+ .fd-feedback-submit {
819
+ min-width: 7rem;
820
+ padding-inline: 0.875rem;
821
+ justify-content: center;
822
+ }
823
+
824
+ .fd-feedback-submit:disabled {
825
+ cursor: not-allowed;
826
+ opacity: 0.65;
827
+ }
828
+
829
+ .fd-feedback-spinner {
830
+ width: 0.875rem;
831
+ height: 0.875rem;
832
+ border-radius: 9999px;
833
+ border: 2px solid currentColor;
834
+ border-right-color: transparent;
835
+ animation: fd-feedback-spin 0.8s linear infinite;
836
+ }
837
+
838
+ .fd-feedback-status {
839
+ margin: 0 !important;
840
+ font-size: 0.8125rem;
841
+ line-height: 1.35;
842
+ color: var(--color-fd-muted-foreground);
843
+ }
844
+
845
+ .fd-feedback-status[data-status="success"] {
846
+ display: inline-flex;
847
+ align-self: center;
848
+ align-items: center;
849
+ gap: 0.4rem;
850
+ min-height: 2rem;
851
+ }
852
+
853
+ .fd-feedback-status[data-status="success"] {
854
+ color: color-mix(in srgb, var(--color-fd-primary) 85%, var(--color-fd-foreground));
855
+ }
856
+
857
+ .fd-feedback-status[data-status="error"] {
858
+ color: var(--color-fd-foreground);
859
+ }
860
+
861
+ @keyframes fd-feedback-spin {
862
+ to {
863
+ transform: rotate(360deg);
864
+ }
865
+ }
866
+
867
+ @media (max-width: 640px) {
868
+ .fd-feedback-content {
869
+ flex-direction: column;
870
+ align-items: flex-start;
871
+ }
872
+
873
+ .fd-feedback-submit-row {
874
+ flex-wrap: wrap;
875
+ }
876
+ }
877
+
761
878
  /* ─── Breadcrumb ─────────────────────────────────────────────────────── */
762
879
 
763
880
  .fd-breadcrumb {
@@ -810,3 +810,18 @@ details > :not(summary) {
810
810
  .fd-sidebar::-webkit-scrollbar-track {
811
811
  background: transparent;
812
812
  }
813
+
814
+ /* ─── Feedback (greentree theme) ─────────────────────────────────── */
815
+
816
+ .fd-feedback-input,
817
+ .fd-feedback-submit {
818
+ border-radius: 0.75rem;
819
+ }
820
+
821
+ .fd-feedback-choice[data-selected="true"] {
822
+ background: color-mix(in srgb, var(--color-fd-primary) 12%, var(--color-fd-secondary));
823
+ }
824
+
825
+ .fd-feedback-status[data-status="success"] {
826
+ color: var(--color-fd-primary);
827
+ }
@@ -766,3 +766,20 @@ code:not(pre code) {
766
766
  :root:not(.dark) .fd-ai-code-block code {
767
767
  color: #1f2937;
768
768
  }
769
+
770
+ /* ─── Feedback (pixel-border theme) ──────────────────────────────── */
771
+
772
+ .fd-feedback-input,
773
+ .fd-feedback-submit {
774
+ border-radius: 0 !important;
775
+ box-shadow: 3px 3px 0 0 var(--color-fd-border);
776
+ }
777
+
778
+ .fd-feedback-choice[data-selected="true"] {
779
+ background: var(--color-fd-secondary);
780
+ }
781
+
782
+ .fd-feedback-status[data-status="success"] {
783
+ font-family: var(--fd-font-mono, ui-monospace, monospace);
784
+ text-transform: uppercase;
785
+ }