@farming-labs/nuxt-theme 0.0.50 → 0.0.52

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.50",
3
+ "version": "0.0.52",
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.50"
63
+ "@farming-labs/docs": "0.0.52"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "nuxt": ">=3.0.0",
@@ -20,6 +20,7 @@ const props = defineProps<{
20
20
  editOnGithub?: string;
21
21
  lastModified?: string;
22
22
  entry?: string;
23
+ slug?: string;
23
24
  locale?: string;
24
25
  };
25
26
  config?: Record<string, unknown> | null;
@@ -29,6 +30,7 @@ const route = useRoute();
29
30
  const openDropdownMenu = ref(false);
30
31
  const copyLabel = ref("Copy page");
31
32
  const copied = ref(false);
33
+ const selectedFeedback = ref<"positive" | "negative" | null>(null);
32
34
 
33
35
  const titleSuffix = computed(() =>
34
36
  props.config?.metadata?.titleTemplate
@@ -145,6 +147,35 @@ const showPageActions = computed(
145
147
  );
146
148
  const showActionsAbove = computed(() => pageActionsPosition.value === "above-title" && showPageActions.value);
147
149
  const showActionsBelow = computed(() => pageActionsPosition.value === "below-title" && showPageActions.value);
150
+ const feedbackConfig = computed(() => {
151
+ const defaults = {
152
+ enabled: false,
153
+ question: "How is this guide?",
154
+ positiveLabel: "Good",
155
+ negativeLabel: "Bad",
156
+ onFeedback: undefined as ((payload: Record<string, unknown>) => void) | undefined,
157
+ };
158
+
159
+ const feedback = props.config?.feedback as Record<string, unknown> | boolean | null | undefined;
160
+ if (feedback === undefined || feedback === false) return defaults;
161
+ if (feedback === true) return { ...defaults, enabled: true };
162
+ if (typeof feedback !== "object" || feedback === null) return defaults;
163
+
164
+ return {
165
+ enabled: feedback.enabled !== false,
166
+ question: String((feedback as { question?: string }).question ?? defaults.question),
167
+ positiveLabel: String(
168
+ feedback.positiveLabel ?? defaults.positiveLabel,
169
+ ),
170
+ negativeLabel: String(
171
+ feedback.negativeLabel ?? defaults.negativeLabel,
172
+ ),
173
+ onFeedback:
174
+ typeof feedback.onFeedback === "function"
175
+ ? (feedback.onFeedback as (payload: Record<string, unknown>) => void)
176
+ : undefined,
177
+ };
178
+ });
148
179
 
149
180
  useHead({
150
181
  title: () => `${props.data.title}${titleSuffix.value}`,
@@ -201,6 +232,43 @@ function openInProvider(provider: { name: string; urlTemplate: string }) {
201
232
  closeDropdown();
202
233
  }
203
234
 
235
+ function handleFeedback(value: "positive" | "negative") {
236
+ selectedFeedback.value = value;
237
+
238
+ const pathname =
239
+ typeof window !== "undefined"
240
+ ? window.location.pathname.replace(/\/$/, "") || "/"
241
+ : props.data.slug
242
+ ? `/${entry.value}/${props.data.slug}`
243
+ : `/${entry.value}`;
244
+
245
+ const payload = {
246
+ value,
247
+ title: props.data.title,
248
+ description: props.data.description,
249
+ url: typeof window !== "undefined" ? window.location.href : pathname,
250
+ pathname,
251
+ path: pathname,
252
+ entry: entry.value,
253
+ slug: props.data.slug ?? "",
254
+ locale: props.data.locale,
255
+ };
256
+
257
+ try {
258
+ feedbackConfig.value.onFeedback?.(payload);
259
+ } catch {}
260
+
261
+ try {
262
+ if (typeof window !== "undefined" && (window as any).__fdOnFeedback__) {
263
+ (window as any).__fdOnFeedback__(payload);
264
+ }
265
+ } catch {}
266
+
267
+ if (typeof window !== "undefined") {
268
+ window.dispatchEvent(new CustomEvent("fd:feedback", { detail: payload }));
269
+ }
270
+ }
271
+
204
272
  function handleClickOutside(e: MouseEvent) {
205
273
  const target = e.target as Node;
206
274
  if (openDropdownMenu.value && !(target as Element).closest?.(".fd-page-action-dropdown")) {
@@ -354,5 +422,49 @@ onUnmounted(() => {
354
422
  </template>
355
423
 
356
424
  <div v-html="htmlWithoutFirstH1" />
425
+
426
+ <section v-if="feedbackConfig.enabled" class="fd-feedback" aria-label="Page feedback">
427
+ <div class="fd-feedback-content">
428
+ <p class="fd-feedback-question">{{ feedbackConfig.question }}</p>
429
+ <div class="fd-feedback-actions" role="group" :aria-label="feedbackConfig.question">
430
+ <button
431
+ type="button"
432
+ class="fd-page-action-btn"
433
+ :aria-pressed="selectedFeedback === 'positive'"
434
+ :data-selected="selectedFeedback === 'positive' ? 'true' : undefined"
435
+ @click="handleFeedback('positive')"
436
+ >
437
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
438
+ <path
439
+ d="M7 21H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h2m0 11V10m0 11h9.28a2 2 0 0 0 1.97-1.66l1.2-7A2 2 0 0 0 17.48 10H13V6.5a2.5 2.5 0 0 0-2.5-2.5L7 10"
440
+ stroke="currentColor"
441
+ stroke-width="2"
442
+ stroke-linecap="round"
443
+ stroke-linejoin="round"
444
+ />
445
+ </svg>
446
+ <span>{{ feedbackConfig.positiveLabel }}</span>
447
+ </button>
448
+ <button
449
+ type="button"
450
+ class="fd-page-action-btn"
451
+ :aria-pressed="selectedFeedback === 'negative'"
452
+ :data-selected="selectedFeedback === 'negative' ? 'true' : undefined"
453
+ @click="handleFeedback('negative')"
454
+ >
455
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
456
+ <path
457
+ d="M17 3h2a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-2M17 3v11m0-11H7.72a2 2 0 0 0-1.97 1.66l-1.2 7A2 2 0 0 0 6.52 14H11v3.5a2.5 2.5 0 0 0 2.5 2.5L17 14"
458
+ stroke="currentColor"
459
+ stroke-width="2"
460
+ stroke-linecap="round"
461
+ stroke-linejoin="round"
462
+ />
463
+ </svg>
464
+ <span>{{ feedbackConfig.negativeLabel }}</span>
465
+ </button>
466
+ </div>
467
+ </div>
468
+ </section>
357
469
  </DocsPage>
358
470
  </template>
@@ -70,6 +70,24 @@ function scanHeadings() {
70
70
  });
71
71
  }
72
72
 
73
+ function setHoverLinkOpen(root: HTMLElement, open: boolean) {
74
+ const trigger = root.querySelector(".fd-hover-link-trigger");
75
+ const popover = root.querySelector(".fd-hover-link-popover");
76
+ if (!(trigger instanceof HTMLElement) || !(popover instanceof HTMLElement)) return;
77
+
78
+ root.classList.toggle("fd-hover-link-open", open);
79
+ trigger.setAttribute("aria-expanded", String(open));
80
+ popover.setAttribute("aria-hidden", String(!open));
81
+ }
82
+
83
+ function closeOpenHoverLinks(event: Event) {
84
+ document.querySelectorAll("[data-hover-link].fd-hover-link-open").forEach((root) => {
85
+ if (!(root instanceof HTMLElement)) return;
86
+ if (event.target instanceof Node && root.contains(event.target)) return;
87
+ setHoverLinkOpen(root, false);
88
+ });
89
+ }
90
+
73
91
  function wireInteractive() {
74
92
  requestAnimationFrame(() => {
75
93
  document.querySelectorAll(".fd-copy-btn").forEach((btn) => {
@@ -122,6 +140,82 @@ function wireInteractive() {
122
140
  const localized = withLang(href);
123
141
  if (localized) link.setAttribute("href", localized);
124
142
  });
143
+
144
+ document.querySelectorAll("[data-hover-link]").forEach((root) => {
145
+ if (!(root instanceof HTMLElement)) return;
146
+ if (root.dataset.fdHoverLinkBound === "true") return;
147
+ root.dataset.fdHoverLinkBound = "true";
148
+
149
+ const trigger = root.querySelector(".fd-hover-link-trigger");
150
+ const popover = root.querySelector(".fd-hover-link-popover");
151
+ if (!(trigger instanceof HTMLElement) || !(popover instanceof HTMLElement)) return;
152
+
153
+ let closeTimer = 0;
154
+ const containsTarget = (target: EventTarget | null) => target instanceof Node && root.contains(target);
155
+ const clearCloseTimer = () => {
156
+ if (closeTimer !== 0) {
157
+ window.clearTimeout(closeTimer);
158
+ closeTimer = 0;
159
+ }
160
+ };
161
+
162
+ const openPopover = () => {
163
+ clearCloseTimer();
164
+ setHoverLinkOpen(root, true);
165
+ };
166
+ const closePopover = () => {
167
+ clearCloseTimer();
168
+ setHoverLinkOpen(root, false);
169
+ };
170
+ const closePopoverSoon = () => {
171
+ clearCloseTimer();
172
+ closeTimer = window.setTimeout(closePopover, 90);
173
+ };
174
+
175
+ trigger.addEventListener("pointerenter", openPopover);
176
+ trigger.addEventListener("pointerleave", (event) => {
177
+ if (containsTarget(event.relatedTarget)) return;
178
+ closePopoverSoon();
179
+ });
180
+ trigger.addEventListener("focus", (event) => {
181
+ if (!(event.currentTarget instanceof HTMLElement)) return;
182
+ if (typeof event.currentTarget.matches === "function" && !event.currentTarget.matches(":focus-visible")) {
183
+ return;
184
+ }
185
+ openPopover();
186
+ });
187
+ trigger.addEventListener("blur", (event) => {
188
+ if (containsTarget(event.relatedTarget)) return;
189
+ closePopoverSoon();
190
+ });
191
+ trigger.addEventListener("click", (event) => {
192
+ event.preventDefault();
193
+ openPopover();
194
+ });
195
+
196
+ popover.addEventListener("pointerenter", openPopover);
197
+ popover.addEventListener("pointerleave", (event) => {
198
+ if (containsTarget(event.relatedTarget)) return;
199
+ closePopoverSoon();
200
+ });
201
+ popover.addEventListener("focusin", openPopover);
202
+ popover.addEventListener("focusout", (event) => {
203
+ if (containsTarget(event.relatedTarget)) return;
204
+ closePopoverSoon();
205
+ });
206
+
207
+ root.addEventListener("keydown", (event) => {
208
+ if (event.key !== "Escape") return;
209
+ closePopover();
210
+ });
211
+ });
212
+
213
+ if (document.documentElement.dataset.fdHoverLinkGlobalBound !== "true") {
214
+ document.documentElement.dataset.fdHoverLinkGlobalBound = "true";
215
+
216
+ document.addEventListener("pointerdown", closeOpenHoverLinks);
217
+ document.addEventListener("focusin", closeOpenHoverLinks);
218
+ }
125
219
  });
126
220
  }
127
221
 
@@ -30,6 +30,7 @@ const ColorfulUIDefaults = {
30
30
  components: {
31
31
  Callout: { variant: "soft", icon: true },
32
32
  CodeBlock: { showCopyButton: true },
33
+ HoverLink: { linkLabel: "Open page", showIndicator: false },
33
34
  Tabs: { style: "default" },
34
35
  },
35
36
  };
@@ -30,6 +30,7 @@ const DarksharpUIDefaults = {
30
30
  components: {
31
31
  Callout: { variant: "soft", icon: true },
32
32
  CodeBlock: { showCopyButton: true },
33
+ HoverLink: { linkLabel: "Open page", showIndicator: false },
33
34
  Tabs: { style: "default" },
34
35
  },
35
36
  };
@@ -30,6 +30,7 @@ const DefaultUIDefaults = {
30
30
  components: {
31
31
  Callout: { variant: "soft", icon: true },
32
32
  CodeBlock: { showCopyButton: true },
33
+ HoverLink: { linkLabel: "Open page", showIndicator: false },
33
34
  Tabs: { style: "default" },
34
35
  },
35
36
  };
@@ -30,6 +30,7 @@ const GreenTreeUIDefaults = {
30
30
  components: {
31
31
  Callout: { variant: "soft", icon: true },
32
32
  CodeBlock: { showCopyButton: true },
33
+ HoverLink: { linkLabel: "Open page", showIndicator: false },
33
34
  Tabs: { style: "default" },
34
35
  },
35
36
  };
@@ -27,7 +27,9 @@ const PixelBorderUIDefaults = {
27
27
  toc: { enabled: true, depth: 3 },
28
28
  header: { height: 56, sticky: true },
29
29
  },
30
- components: {},
30
+ components: {
31
+ HoverLink: { linkLabel: "Open page", showIndicator: false },
32
+ },
31
33
  };
32
34
 
33
35
  export const pixelBorder = createTheme({
package/styles/docs.css CHANGED
@@ -649,10 +649,19 @@ samp {
649
649
  color: var(--color-fd-foreground);
650
650
  }
651
651
 
652
+ .fd-page-action-btn[data-selected="true"] {
653
+ color: var(--color-fd-accent-foreground);
654
+ background: var(--color-fd-accent);
655
+ }
656
+
652
657
  .fd-page-action-btn svg {
653
658
  flex-shrink: 0;
654
659
  }
655
660
 
661
+ .fd-page-action-btn[data-selected="true"] svg {
662
+ color: currentColor;
663
+ }
664
+
656
665
  .fd-page-action-dropdown {
657
666
  position: relative;
658
667
  }
@@ -720,6 +729,35 @@ samp {
720
729
  margin-bottom: 1rem;
721
730
  }
722
731
 
732
+ .fd-feedback {
733
+ margin-top: 2rem;
734
+ padding-top: 1.25rem;
735
+ border-top: 1px solid var(--color-fd-border);
736
+ }
737
+
738
+ .fd-feedback-content {
739
+ display: flex;
740
+ align-items: center;
741
+ justify-content: space-between;
742
+ gap: 1rem;
743
+ flex-wrap: wrap;
744
+ }
745
+
746
+ .fd-feedback-question {
747
+ margin: 0;
748
+ font-size: 0.9375rem;
749
+ font-weight: 600;
750
+ line-height: 1.5;
751
+ color: var(--color-fd-foreground);
752
+ }
753
+
754
+ .fd-feedback-actions {
755
+ display: inline-flex;
756
+ align-items: center;
757
+ gap: 0.5rem;
758
+ flex-wrap: wrap;
759
+ }
760
+
723
761
  /* ─── Breadcrumb ─────────────────────────────────────────────────────── */
724
762
 
725
763
  .fd-breadcrumb {
@@ -919,6 +957,206 @@ samp {
919
957
  opacity: 0.8;
920
958
  }
921
959
 
960
+ .fd-hover-link {
961
+ position: relative;
962
+ display: inline-block;
963
+ vertical-align: baseline;
964
+ max-width: 100%;
965
+ }
966
+
967
+ .fd-hover-link-trigger {
968
+ display: inline !important;
969
+ border: none;
970
+ background: transparent;
971
+ padding: 0;
972
+ margin: 0;
973
+ color: var(--color-fd-foreground);
974
+ cursor: pointer;
975
+ text-decoration: underline;
976
+ text-decoration-style: dashed;
977
+ text-decoration-thickness: 0.08em;
978
+ text-underline-offset: 0.22em;
979
+ text-decoration-color: color-mix(in srgb, var(--color-fd-foreground) 46%, transparent);
980
+ font: inherit;
981
+ line-height: inherit;
982
+ vertical-align: baseline;
983
+ appearance: none;
984
+ transition: text-decoration-color 180ms ease, opacity 180ms ease;
985
+ }
986
+
987
+ .fd-hover-link-trigger:hover,
988
+ .fd-hover-link-trigger:focus-visible,
989
+ .fd-hover-link-open > .fd-hover-link-trigger {
990
+ text-decoration-color: color-mix(in srgb, var(--color-fd-foreground) 68%, transparent);
991
+ }
992
+
993
+ .fd-hover-link-trigger:focus-visible {
994
+ outline: none;
995
+ }
996
+
997
+ .fd-hover-link-popover {
998
+ --fd-hover-link-rest-x: -50%;
999
+ --fd-hover-link-rest-y: 8px;
1000
+ --fd-hover-link-open-x: -50%;
1001
+ --fd-hover-link-open-y: 0;
1002
+ position: absolute;
1003
+ top: calc(100% + var(--fd-hover-link-side-offset, 12px));
1004
+ left: 50%;
1005
+ z-index: 40;
1006
+ width: min(22rem, calc(100vw - 2rem));
1007
+ max-width: min(22rem, calc(100vw - 2rem));
1008
+ opacity: 0;
1009
+ visibility: hidden;
1010
+ pointer-events: none;
1011
+ transform: translate3d(var(--fd-hover-link-rest-x), var(--fd-hover-link-rest-y), 0);
1012
+ transition: opacity 180ms ease, transform 180ms ease, visibility 180ms linear;
1013
+ }
1014
+
1015
+ .fd-hover-link[data-align="start"] > .fd-hover-link-popover {
1016
+ left: 0;
1017
+ --fd-hover-link-rest-x: 0;
1018
+ --fd-hover-link-open-x: 0;
1019
+ }
1020
+
1021
+ .fd-hover-link[data-align="end"] > .fd-hover-link-popover {
1022
+ left: auto;
1023
+ right: 0;
1024
+ --fd-hover-link-rest-x: 0;
1025
+ --fd-hover-link-open-x: 0;
1026
+ }
1027
+
1028
+ .fd-hover-link[data-side="top"] > .fd-hover-link-popover {
1029
+ top: auto;
1030
+ bottom: calc(100% + var(--fd-hover-link-side-offset, 12px));
1031
+ --fd-hover-link-rest-y: -8px;
1032
+ --fd-hover-link-open-y: 0;
1033
+ }
1034
+
1035
+ .fd-hover-link[data-side="right"] > .fd-hover-link-popover {
1036
+ top: 50%;
1037
+ left: calc(100% + var(--fd-hover-link-side-offset, 12px));
1038
+ right: auto;
1039
+ --fd-hover-link-rest-x: 8px;
1040
+ --fd-hover-link-rest-y: -50%;
1041
+ --fd-hover-link-open-x: 0;
1042
+ --fd-hover-link-open-y: -50%;
1043
+ }
1044
+
1045
+ .fd-hover-link[data-side="right"][data-align="start"] > .fd-hover-link-popover {
1046
+ top: 0;
1047
+ --fd-hover-link-rest-y: 0;
1048
+ --fd-hover-link-open-y: 0;
1049
+ }
1050
+
1051
+ .fd-hover-link[data-side="right"][data-align="end"] > .fd-hover-link-popover {
1052
+ top: auto;
1053
+ bottom: 0;
1054
+ --fd-hover-link-rest-y: 0;
1055
+ --fd-hover-link-open-y: 0;
1056
+ }
1057
+
1058
+ .fd-hover-link[data-side="left"] > .fd-hover-link-popover {
1059
+ top: 50%;
1060
+ left: auto;
1061
+ right: calc(100% + var(--fd-hover-link-side-offset, 12px));
1062
+ --fd-hover-link-rest-x: -8px;
1063
+ --fd-hover-link-rest-y: -50%;
1064
+ --fd-hover-link-open-x: 0;
1065
+ --fd-hover-link-open-y: -50%;
1066
+ }
1067
+
1068
+ .fd-hover-link[data-side="left"][data-align="start"] > .fd-hover-link-popover {
1069
+ top: 0;
1070
+ --fd-hover-link-rest-y: 0;
1071
+ --fd-hover-link-open-y: 0;
1072
+ }
1073
+
1074
+ .fd-hover-link[data-side="left"][data-align="end"] > .fd-hover-link-popover {
1075
+ top: auto;
1076
+ bottom: 0;
1077
+ --fd-hover-link-rest-y: 0;
1078
+ --fd-hover-link-open-y: 0;
1079
+ }
1080
+
1081
+ .fd-hover-link-open > .fd-hover-link-popover {
1082
+ opacity: 1;
1083
+ visibility: visible;
1084
+ pointer-events: auto;
1085
+ transform: translate3d(var(--fd-hover-link-open-x), var(--fd-hover-link-open-y), 0);
1086
+ }
1087
+
1088
+ .fd-hover-link-card {
1089
+ display: flex;
1090
+ flex-direction: column;
1091
+ gap: 0.75rem;
1092
+ width: 100%;
1093
+ border-radius: calc(var(--radius, 0.75rem) + 2px);
1094
+ border: 1px solid color-mix(in srgb, var(--color-fd-border) 88%, transparent);
1095
+ background: var(--color-fd-popover, var(--color-fd-background));
1096
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground));
1097
+ padding: 0.95rem 1rem;
1098
+ box-shadow: 0 20px 45px color-mix(in srgb, var(--color-fd-background) 78%, transparent);
1099
+ backdrop-filter: blur(14px);
1100
+ }
1101
+
1102
+ .fd-hover-link-body {
1103
+ display: flex;
1104
+ flex-direction: column;
1105
+ gap: 0.35rem;
1106
+ }
1107
+
1108
+ .fd-hover-link-preview-label {
1109
+ font-size: 0.68rem;
1110
+ text-transform: uppercase;
1111
+ letter-spacing: 0.1em;
1112
+ font-family: var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace));
1113
+ color: color-mix(in srgb, var(--color-fd-popover-foreground, var(--color-fd-foreground)) 55%, transparent);
1114
+ }
1115
+
1116
+ .fd-hover-link-title,
1117
+ .fd-hover-link-cta {
1118
+ text-decoration: none !important;
1119
+ }
1120
+
1121
+ .fd-hover-link-title {
1122
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground));
1123
+ font-size: 1rem;
1124
+ font-weight: 600 !important;
1125
+ line-height: 1.3;
1126
+ }
1127
+
1128
+ .fd-hover-link-title:hover,
1129
+ .fd-hover-link-cta:hover {
1130
+ opacity: 1;
1131
+ }
1132
+
1133
+ .fd-hover-link-description {
1134
+ font-size: 0.92rem;
1135
+ line-height: 1.6;
1136
+ color: color-mix(in srgb, var(--color-fd-popover-foreground, var(--color-fd-foreground)) 74%, transparent);
1137
+ }
1138
+
1139
+ .fd-hover-link-footer {
1140
+ display: flex;
1141
+ align-items: center;
1142
+ justify-content: space-between;
1143
+ gap: 0.75rem;
1144
+ padding-top: 0.25rem;
1145
+ border-top: 1px solid color-mix(in srgb, var(--color-fd-border) 72%, transparent);
1146
+ }
1147
+
1148
+ .fd-hover-link-cta {
1149
+ display: inline-flex !important;
1150
+ align-items: center;
1151
+ gap: 0.4rem;
1152
+ color: var(--color-fd-primary);
1153
+ font-size: 0.8rem;
1154
+ font-weight: 600 !important;
1155
+ text-transform: uppercase;
1156
+ letter-spacing: 0.08em;
1157
+ font-family: var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace));
1158
+ }
1159
+
922
1160
  .fd-page-body h1 {
923
1161
  font-size: var(--fd-h1-size, 2.25rem);
924
1162
  font-weight: var(--fd-h1-weight, 700);