@soubiran/ui 0.9.1 → 0.10.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.
package/dist/index.js CHANGED
@@ -1,4 +1,2730 @@
1
- /* empty css */
2
- import useTableOfContents_default from "./composables/useTableOfContents.js";
3
- import useUmami from "./composables/useUmami.js";
4
- export { useTableOfContents_default as useTableOfContents, useUmami };
1
+ import { prose } from "./wrapper-classes.js";
2
+ import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, markRaw, mergeModels, mergeProps, nextTick, normalizeClass, onBeforeUnmount, onMounted, onScopeDispose, openBlock, provide, readonly, ref, renderList, renderSlot, resolveComponent, toDisplayString, toRef, toValue, unref, useModel, useTemplateRef, watch, withCtx, withModifiers } from "vue";
3
+ import { buildLocaleContext } from "@nuxt/ui/utils/locale";
4
+ import { defineQueryOptions, useMutation, useQuery, useQueryCache } from "@pinia/colada";
5
+ import { AnimatePresence, Motion, motion, useReducedMotion } from "motion-v";
6
+ import { tv } from "tailwind-variants";
7
+ import { FetchError, ofetch } from "ofetch";
8
+ import { RadioGroupIndicator, RadioGroupItem, RadioGroupRoot, TabsContent, TabsList, TabsRoot, TabsTrigger } from "reka-ui";
9
+ import { createSharedComposable, useTimeAgoIntl } from "@vueuse/core";
10
+ import PartySocket from "partysocket";
11
+ //#region src/composables/comments/context.ts
12
+ const commentsContextInjectionKey = Symbol("soubiran-ui.comments-context");
13
+ function provideCommentsContext(context) {
14
+ provide(commentsContextInjectionKey, context);
15
+ }
16
+ function useCommentsContext() {
17
+ const context = inject(commentsContextInjectionKey);
18
+ if (!context) throw new Error("Comments components must be rendered inside <Comments>.");
19
+ return context;
20
+ }
21
+ //#endregion
22
+ //#region src/composables/comments/useCommentsBanner.ts
23
+ function useCommentsBanner() {
24
+ const banner = ref();
25
+ let timeout;
26
+ function dismiss() {
27
+ if (timeout !== void 0) {
28
+ clearTimeout(timeout);
29
+ timeout = void 0;
30
+ }
31
+ banner.value = void 0;
32
+ }
33
+ function show(nextBanner) {
34
+ dismiss();
35
+ banner.value = nextBanner;
36
+ timeout = setTimeout(() => {
37
+ banner.value = void 0;
38
+ timeout = void 0;
39
+ }, 4e3);
40
+ }
41
+ onScopeDispose(dismiss);
42
+ return {
43
+ banner: readonly(banner),
44
+ isActive: computed(() => banner.value !== void 0),
45
+ dismiss,
46
+ show
47
+ };
48
+ }
49
+ //#endregion
50
+ //#region src/locale/en.ts
51
+ var en_default = defineLocale({
52
+ name: "English",
53
+ code: "en",
54
+ dir: "ltr",
55
+ messages: {
56
+ Editor: {
57
+ placeholder: "Write something...",
58
+ write: { help: "You can use Markdown syntax to format the content." },
59
+ preview: {
60
+ loading: "Loading preview...",
61
+ placeholder: "No content to preview."
62
+ },
63
+ tabs: {
64
+ write: "Write",
65
+ preview: "Preview"
66
+ }
67
+ },
68
+ LoginRequired: {
69
+ text: "You need to be logged in to access this feature.",
70
+ action: "Log in",
71
+ errors: {
72
+ popupBlocked: "The login window was blocked. Allow popups and try again.",
73
+ popupClosed: "The login window was closed before authentication finished.",
74
+ refreshFailed: "You are logged in, but your session could not be refreshed. Please try again."
75
+ }
76
+ },
77
+ LoginCallback: {
78
+ success: "Login complete. You can close this window.",
79
+ error: "The login could not be completed. Close this window and try again."
80
+ },
81
+ ConfirmModal: { actions: {
82
+ cancel: "Cancel",
83
+ confirm: "Confirm"
84
+ } },
85
+ Feedback: { action: "Give feedback" },
86
+ FeedbackCard: {
87
+ placeholder: "Your feedback...",
88
+ action: "Send",
89
+ ratings: {
90
+ hate: "Hate it",
91
+ poor: "Not great",
92
+ okay: "It's ok",
93
+ love: "Love it"
94
+ },
95
+ success: {
96
+ received: "Your feedback has been received.",
97
+ thanks: "Thanks for your help!"
98
+ },
99
+ errors: {
100
+ notFound: "Page not found. Cannot send feedback. It's us, not you!",
101
+ unavailable: "Service is currently unavailable. Please try again later.",
102
+ unexpected: "An unexpected error occurred."
103
+ }
104
+ },
105
+ comments: {
106
+ Comment: {
107
+ actions: {
108
+ edit: "Edit",
109
+ delete: "Delete"
110
+ },
111
+ actionMenu: "Comment actions"
112
+ },
113
+ CommentDeleteConfirmModal: {
114
+ title: "Delete comment",
115
+ description: "Are you sure you want to delete this comment? This action cannot be undone."
116
+ },
117
+ CommentFormSection: { title: "Add a comment" },
118
+ CommentHeader: {
119
+ author: "Author",
120
+ publishedAt: "Published at"
121
+ },
122
+ CommentLike: { title: "Like this comment" },
123
+ CommentUnlike: { title: "Remove your like" },
124
+ CommentRepliesCount: {
125
+ replies: "{count} replies",
126
+ reply: "{count} reply"
127
+ },
128
+ CommentForm: { actions: {
129
+ add: "Comment",
130
+ reply: "Reply",
131
+ edit: "Edit",
132
+ cancel: "Cancel"
133
+ } },
134
+ errors: {
135
+ create: "An error occurred while adding the comment.",
136
+ update: "An error occurred while editing the comment.",
137
+ delete: "An error occurred while deleting the comment.",
138
+ like: "An error occurred while adding the like.",
139
+ unlike: "An error occurred while removing the like."
140
+ },
141
+ success: { delete: "The comment was deleted successfully." }
142
+ },
143
+ discussions: {
144
+ DiscussionReply: { reply: "Reply to this discussion..." },
145
+ DiscussionsSection: { empty: "No discussions yet." },
146
+ DiscussionsSectionTitle: { title: "Discussions" },
147
+ DiscussionsSectionSubtitle: {
148
+ comments: {
149
+ plural: "{count} comments",
150
+ singular: "{count} comment"
151
+ },
152
+ replies: {
153
+ plural: "{count} replies",
154
+ singular: "{count} reply"
155
+ }
156
+ }
157
+ },
158
+ state: {
159
+ empty: "No data to display.",
160
+ error: "An error occurred while loading the data.",
161
+ pending: "Loading data..."
162
+ }
163
+ }
164
+ });
165
+ //#endregion
166
+ //#region src/composables/useLocale.ts
167
+ const localeContextInjectionKey = Symbol.for("soubiran-ui.locale-context");
168
+ function useLocale(localeOverrides) {
169
+ const locale = localeOverrides || inject(localeContextInjectionKey);
170
+ return buildLocaleContext(computed(() => locale?.value || en_default));
171
+ }
172
+ //#endregion
173
+ //#region src/locale/fr.ts
174
+ var fr_default = defineLocale({
175
+ name: "Français",
176
+ code: "fr",
177
+ dir: "ltr",
178
+ messages: {
179
+ Editor: {
180
+ placeholder: "Écrivez quelque chose...",
181
+ write: { help: "Vous pouvez utiliser la syntaxe Markdown pour formater le contenu." },
182
+ preview: {
183
+ loading: "Chargement de l’aperçu...",
184
+ placeholder: "Aucun contenu à prévisualiser."
185
+ },
186
+ tabs: {
187
+ write: "Écrire",
188
+ preview: "Aperçu"
189
+ }
190
+ },
191
+ LoginRequired: {
192
+ text: "Vous devez être connecté pour accéder à cette fonctionnalité.",
193
+ action: "Se connecter",
194
+ errors: {
195
+ popupBlocked: "La fenêtre de connexion a été bloquée. Autorisez les fenêtres contextuelles et réessayez.",
196
+ popupClosed: "La fenêtre de connexion a été fermée avant la fin de l’authentification.",
197
+ refreshFailed: "Vous êtes connecté, mais votre session n’a pas pu être actualisée. Veuillez réessayer."
198
+ }
199
+ },
200
+ LoginCallback: {
201
+ success: "Connexion terminée. Vous pouvez fermer cette fenêtre.",
202
+ error: "La connexion n’a pas pu être terminée. Fermez cette fenêtre et réessayez."
203
+ },
204
+ ConfirmModal: { actions: {
205
+ cancel: "Annuler",
206
+ confirm: "Confirmer"
207
+ } },
208
+ Feedback: { action: "Donner mon avis" },
209
+ FeedbackCard: {
210
+ placeholder: "Votre avis...",
211
+ action: "Envoyer",
212
+ ratings: {
213
+ hate: "Je déteste",
214
+ poor: "Pas terrible",
215
+ okay: "Ça va",
216
+ love: "J’adore"
217
+ },
218
+ success: {
219
+ received: "Votre avis a bien été reçu.",
220
+ thanks: "Merci pour votre aide !"
221
+ },
222
+ errors: {
223
+ notFound: "Page introuvable. Impossible d’envoyer votre avis.",
224
+ unavailable: "Le service est indisponible. Veuillez réessayer plus tard.",
225
+ unexpected: "Une erreur inattendue est survenue."
226
+ }
227
+ },
228
+ comments: {
229
+ Comment: {
230
+ actions: {
231
+ edit: "Modifier",
232
+ delete: "Supprimer"
233
+ },
234
+ actionMenu: "Actions du commentaire"
235
+ },
236
+ CommentDeleteConfirmModal: {
237
+ title: "Supprimer le commentaire",
238
+ description: "Êtes-vous sûr de vouloir supprimer ce commentaire ? Cette action ne peut pas être annulée."
239
+ },
240
+ CommentFormSection: { title: "Ajouter un commentaire" },
241
+ CommentHeader: {
242
+ author: "Auteur",
243
+ publishedAt: "Publié le"
244
+ },
245
+ CommentLike: { title: "Aimer ce commentaire" },
246
+ CommentUnlike: { title: "Retirer votre like" },
247
+ CommentRepliesCount: {
248
+ replies: "{count} réponses",
249
+ reply: "{count} réponse"
250
+ },
251
+ CommentForm: { actions: {
252
+ add: "Commenter",
253
+ reply: "Répondre",
254
+ edit: "Modifier",
255
+ cancel: "Annuler"
256
+ } },
257
+ errors: {
258
+ create: "Une erreur est survenue lors de l’ajout du commentaire.",
259
+ update: "Une erreur est survenue lors de la modification du commentaire.",
260
+ delete: "Une erreur est survenue lors de la suppression du commentaire.",
261
+ like: "Une erreur est survenue lors de l’ajout du like.",
262
+ unlike: "Une erreur est survenue lors de la suppression du like."
263
+ },
264
+ success: { delete: "Le commentaire a bien été supprimé." }
265
+ },
266
+ discussions: {
267
+ DiscussionReply: { reply: "Répondre à cette discussion..." },
268
+ DiscussionsSection: { empty: "Aucune discussion pour le moment." },
269
+ DiscussionsSectionTitle: { title: "Discussions" },
270
+ DiscussionsSectionSubtitle: {
271
+ comments: {
272
+ plural: "{count} commentaires",
273
+ singular: "{count} commentaire"
274
+ },
275
+ replies: {
276
+ plural: "{count} réponses",
277
+ singular: "{count} réponse"
278
+ }
279
+ }
280
+ },
281
+ state: {
282
+ empty: "Aucune donnée à afficher.",
283
+ error: "Une erreur est survenue lors du chargement des données.",
284
+ pending: "Chargement des données..."
285
+ }
286
+ }
287
+ });
288
+ //#endregion
289
+ //#region src/utils/api.ts
290
+ const api = ofetch.create({
291
+ baseURL: import.meta.env.VITE_API_URL,
292
+ headers: { Accept: "application/json" },
293
+ credentials: "include",
294
+ onRequest: [async ({ options }) => {
295
+ const method = options.method?.toUpperCase() || "GET";
296
+ if ([
297
+ "GET",
298
+ "HEAD",
299
+ "OPTIONS"
300
+ ].includes(method)) return;
301
+ if (typeof document === "undefined") return;
302
+ if (!getCookie("XSRF-TOKEN")) await ofetch("/sanctum/csrf-cookie", {
303
+ baseURL: import.meta.env.VITE_API_URL,
304
+ credentials: "include"
305
+ });
306
+ options.headers.set("X-Xsrf-Token", decodeURIComponent(getCookie("XSRF-TOKEN") || ""));
307
+ }]
308
+ });
309
+ function getCookie(name) {
310
+ if (typeof document === "undefined") return null;
311
+ const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
312
+ return match ? match[2] : null;
313
+ }
314
+ function isUnprocessableEntityError(error) {
315
+ return error instanceof FetchError && error.response?.status === 422;
316
+ }
317
+ //#endregion
318
+ //#region src/api/comments.ts
319
+ const localizedHeaders = (locale) => ({ "Accept-Language": locale });
320
+ const getMarkdown = (content, locale) => api("/api/markdown", {
321
+ method: "POST",
322
+ body: { content },
323
+ headers: localizedHeaders(locale)
324
+ });
325
+ const getComments = (pageId, locale) => api(`/api/pages/${pageId}/comments`, { headers: localizedHeaders(locale) });
326
+ const postComment = (pageId, content, locale, parentCommentId) => api(`/api/pages/${pageId}/comments`, {
327
+ method: "POST",
328
+ body: {
329
+ content,
330
+ parent_id: parentCommentId
331
+ },
332
+ headers: localizedHeaders(locale)
333
+ });
334
+ const putComment = (commentId, content, locale) => api(`/api/comments/${commentId}`, {
335
+ method: "PUT",
336
+ body: { content },
337
+ headers: localizedHeaders(locale)
338
+ });
339
+ const deleteComment = (commentId, locale) => api(`/api/comments/${commentId}`, {
340
+ method: "DELETE",
341
+ headers: localizedHeaders(locale)
342
+ });
343
+ const postCommentLike = (commentId, locale) => api(`/api/comments/${commentId}/likes`, {
344
+ method: "POST",
345
+ headers: localizedHeaders(locale)
346
+ });
347
+ const deleteCommentLike = (commentId, locale) => api(`/api/comments/${commentId}/likes`, {
348
+ method: "DELETE",
349
+ headers: localizedHeaders(locale)
350
+ });
351
+ //#endregion
352
+ //#region src/keys/comments.ts
353
+ const COMMENT_QUERY_KEYS = {
354
+ root: ["comments"],
355
+ byPageId: (pageId, locale) => [
356
+ "pages",
357
+ pageId,
358
+ locale,
359
+ ...COMMENT_QUERY_KEYS.root
360
+ ]
361
+ };
362
+ //#endregion
363
+ //#region src/api/users.ts
364
+ const getCurrentUser = () => api("/api/user");
365
+ //#endregion
366
+ //#region src/keys/user.ts
367
+ const USER_QUERY_KEYS = { current: ["user"] };
368
+ //#endregion
369
+ //#region src/queries/users.ts
370
+ const currentUserQuery = defineQueryOptions(() => ({
371
+ key: USER_QUERY_KEYS.current,
372
+ query: () => getCurrentUser().then((response) => response.data),
373
+ enabled: typeof window !== "undefined"
374
+ }));
375
+ //#endregion
376
+ //#region src/utils/login.ts
377
+ const LOGIN_CALLBACK_MESSAGE_TYPE = "soubiran:login-complete";
378
+ const LOGIN_CALLBACK_NONCE_PARAM = "login_attempt";
379
+ function buildLoginCallbackUrl(currentUrl, callbackPath, nonce) {
380
+ const callback = new URL(callbackPath, currentUrl);
381
+ callback.searchParams.set(LOGIN_CALLBACK_NONCE_PARAM, nonce);
382
+ return callback;
383
+ }
384
+ function isLoginCallbackMessage(data) {
385
+ if (!data || typeof data !== "object") return false;
386
+ const message = data;
387
+ return message.type === "soubiran:login-complete" && typeof message.nonce === "string";
388
+ }
389
+ //#endregion
390
+ //#region src/composables/useLogin.ts
391
+ const LOGIN_ERROR_MESSAGE_KEYS = {
392
+ "popup-blocked": "LoginRequired.errors.popupBlocked",
393
+ "popup-closed": "LoginRequired.errors.popupClosed",
394
+ "refresh-failed": "LoginRequired.errors.refreshFailed"
395
+ };
396
+ const DEFAULT_CALLBACK_PATH = "/auth/callback";
397
+ const POPUP_WIDTH = 800;
398
+ const POPUP_HEIGHT = 600;
399
+ const LOGIN_POPUP_NAME = "soubiran-login";
400
+ function getLoginErrorMessageKey(error) {
401
+ return LOGIN_ERROR_MESSAGE_KEYS[error];
402
+ }
403
+ function buildLoginLink(apiUrl, currentUrl, fragment) {
404
+ const redirect = new URL(currentUrl);
405
+ redirect.hash = "";
406
+ const login = new URL("/login", apiUrl);
407
+ login.searchParams.set("redirect", redirect.toString());
408
+ login.hash = fragment ?? "";
409
+ return login.toString();
410
+ }
411
+ function createNonce() {
412
+ return crypto.randomUUID();
413
+ }
414
+ function getPopupFeatures() {
415
+ const left = Math.max(0, window.screenX + (window.outerWidth - POPUP_WIDTH) / 2);
416
+ const top = Math.max(0, window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2);
417
+ return [
418
+ "popup=yes",
419
+ `width=${POPUP_WIDTH}`,
420
+ `height=${POPUP_HEIGHT}`,
421
+ `left=${Math.round(left)}`,
422
+ `top=${Math.round(top)}`
423
+ ].join(",");
424
+ }
425
+ function useLogin(options = {}) {
426
+ const queryCache = useQueryCache();
427
+ const error = ref();
428
+ const isPending = ref(false);
429
+ let popup = null;
430
+ let closeTimer;
431
+ let removeMessageListener;
432
+ let resolveAttempt;
433
+ let activeAttempt;
434
+ function stopMonitoring() {
435
+ if (closeTimer) {
436
+ clearInterval(closeTimer);
437
+ closeTimer = void 0;
438
+ }
439
+ removeMessageListener?.();
440
+ removeMessageListener = void 0;
441
+ popup = null;
442
+ }
443
+ function cleanup(success) {
444
+ stopMonitoring();
445
+ isPending.value = false;
446
+ resolveAttempt?.(success);
447
+ resolveAttempt = void 0;
448
+ activeAttempt = void 0;
449
+ }
450
+ function openLoginWindow() {
451
+ if (typeof window === "undefined") return Promise.resolve(false);
452
+ if (popup && !popup.closed && activeAttempt) {
453
+ popup.focus();
454
+ return activeAttempt;
455
+ }
456
+ error.value = void 0;
457
+ isPending.value = true;
458
+ const nonce = createNonce();
459
+ const callback = buildLoginCallbackUrl(window.location.href, toValue(options.callbackPath) ?? DEFAULT_CALLBACK_PATH, nonce);
460
+ const loginUrl = buildLoginLink(import.meta.env.VITE_API_URL, callback.toString());
461
+ activeAttempt = new Promise((resolve) => {
462
+ resolveAttempt = resolve;
463
+ });
464
+ const onMessage = async (event) => {
465
+ if (event.origin !== callback.origin || event.source !== popup || !isLoginCallbackMessage(event.data) || event.data.type !== "soubiran:login-complete" || event.data.nonce !== nonce) return;
466
+ stopMonitoring();
467
+ try {
468
+ await queryCache.invalidateQueries({
469
+ key: USER_QUERY_KEYS.current,
470
+ exact: true
471
+ });
472
+ cleanup(true);
473
+ } catch {
474
+ error.value = "refresh-failed";
475
+ cleanup(false);
476
+ }
477
+ };
478
+ window.addEventListener("message", onMessage);
479
+ removeMessageListener = () => window.removeEventListener("message", onMessage);
480
+ popup = window.open(loginUrl, LOGIN_POPUP_NAME, getPopupFeatures());
481
+ if (!popup) {
482
+ error.value = "popup-blocked";
483
+ cleanup(false);
484
+ return Promise.resolve(false);
485
+ }
486
+ popup.focus();
487
+ closeTimer = setInterval(() => {
488
+ if (popup?.closed) {
489
+ error.value = "popup-closed";
490
+ cleanup(false);
491
+ }
492
+ }, 500);
493
+ return activeAttempt;
494
+ }
495
+ onBeforeUnmount(() => cleanup(false));
496
+ return {
497
+ error: computed(() => error.value),
498
+ isPending: computed(() => isPending.value),
499
+ openLoginWindow
500
+ };
501
+ }
502
+ //#endregion
503
+ //#region src/components/LoginRequired.vue
504
+ const loginRequired = tv({ slots: {
505
+ base: "flex flex-col gap-4 items-center",
506
+ prose: "text-sm",
507
+ actions: "flex flex-row items-center justify-center gap-2"
508
+ } });
509
+ const _sfc_main$35 = /* @__PURE__ */ defineComponent({
510
+ __name: "LoginRequired",
511
+ props: {
512
+ class: {},
513
+ ui: {}
514
+ },
515
+ setup(__props) {
516
+ const props = __props;
517
+ const { t } = useLocale();
518
+ const toast = useToast();
519
+ const { error, isPending, openLoginWindow } = useLogin();
520
+ async function login() {
521
+ if (!await openLoginWindow() && error.value) toast.add({
522
+ color: "error",
523
+ description: t(getLoginErrorMessageKey(error.value))
524
+ });
525
+ }
526
+ const ui = computed(() => loginRequired());
527
+ return (_ctx, _cache) => {
528
+ const _component_UButton = resolveComponent("UButton");
529
+ return openBlock(), createElementBlock("div", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [createElementVNode("p", { class: normalizeClass(ui.value.prose({ class: props.ui?.prose })) }, toDisplayString(unref(t)("LoginRequired.text")), 3), createElementVNode("div", { class: normalizeClass(ui.value.actions({ class: props.ui?.actions })) }, [createVNode(_component_UButton, {
530
+ color: "neutral",
531
+ variant: "solid",
532
+ label: unref(t)("LoginRequired.action"),
533
+ loading: unref(isPending),
534
+ disabled: unref(isPending),
535
+ onClick: login
536
+ }, null, 8, [
537
+ "label",
538
+ "loading",
539
+ "disabled"
540
+ ])], 2)], 2);
541
+ };
542
+ }
543
+ });
544
+ //#endregion
545
+ //#region ~icons/ph/info-duotone
546
+ const _hoisted_1$21 = {
547
+ viewBox: "0 0 256 256",
548
+ width: "1.2em",
549
+ height: "1.2em"
550
+ };
551
+ function render$12(_ctx, _cache) {
552
+ return openBlock(), createElementBlock("svg", _hoisted_1$21, [..._cache[0] || (_cache[0] = [createElementVNode("g", { fill: "currentColor" }, [createElementVNode("path", {
553
+ d: "M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96",
554
+ opacity: ".2"
555
+ }), createElementVNode("path", { d: "M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12" })], -1)])]);
556
+ }
557
+ var info_duotone_default = markRaw({
558
+ name: "ph-info-duotone",
559
+ render: render$12
560
+ });
561
+ //#endregion
562
+ //#region src/components/Editor.vue
563
+ const _hoisted_1$20 = ["innerHTML"];
564
+ const editor = tv({ slots: {
565
+ root: "overflow-hidden border border-muted rounded-md",
566
+ tabs: "mb-[-1px] ml-[-1px] mt-[-1px] bg-muted text-sm space-x-1",
567
+ tab: "relative border-x rounded-t-md px-4 py-2 text-sm text-dimmed data-[state=active]:border-t data-[state=active]:border-muted data-[state=inactive]:border-transparent data-[state=active]:bg-default data-[state=active]:text-default hover:text-default focus-visible:outline-2 focus-visible:outline-offset-[-6px] focus-visible:ring-inverted",
568
+ content: "data-[state=active]:p-2 flex flex-col gap-1 border-t border-muted bg-default",
569
+ preview: "",
570
+ prose: "w-full rounded-md p-2 text-sm"
571
+ } });
572
+ const _sfc_main$34 = /* @__PURE__ */ defineComponent({
573
+ __name: "Editor",
574
+ props: /* @__PURE__ */ mergeModels({
575
+ class: {},
576
+ textarea: {},
577
+ error: {},
578
+ ui: {}
579
+ }, {
580
+ "content": {
581
+ default: "",
582
+ required: true
583
+ },
584
+ "contentModifiers": {}
585
+ }),
586
+ emits: ["update:content"],
587
+ setup(__props, { expose: __expose }) {
588
+ const props = __props;
589
+ const content = useModel(__props, "content");
590
+ const { code, t } = useLocale();
591
+ const { data: preview, isLoading: isPreviewLoading, refresh: refreshPreview } = useQuery({
592
+ enabled: false,
593
+ staleTime: 1e3 * 60 * 5,
594
+ key: () => [
595
+ "preview",
596
+ code.value,
597
+ content.value
598
+ ],
599
+ query: async () => {
600
+ if (!content.value) return Promise.resolve(`<p>${t("Editor.preview.placeholder")}</p>`);
601
+ return getMarkdown(content.value, code.value === "fr" ? "fr" : "en").then((response) => response.data);
602
+ }
603
+ });
604
+ function onFetchPreview() {
605
+ refreshPreview();
606
+ }
607
+ function onUpdateModelValue(value) {
608
+ if (value === "write") setTimeout(() => {
609
+ focus();
610
+ }, 0);
611
+ }
612
+ const textarea = useTemplateRef("textarea");
613
+ function focus() {
614
+ nextTick(() => {
615
+ textarea.value?.textareaRef?.focus();
616
+ });
617
+ }
618
+ __expose({ focus });
619
+ const ui = computed(() => editor());
620
+ return (_ctx, _cache) => {
621
+ const _component_UTextarea = resolveComponent("UTextarea");
622
+ const _component_UIcon = resolveComponent("UIcon");
623
+ const _component_UFormField = resolveComponent("UFormField");
624
+ return openBlock(), createBlock(unref(TabsRoot), {
625
+ "default-value": "write",
626
+ class: normalizeClass(ui.value.root({ class: [props.ui?.root, props.class] })),
627
+ "onUpdate:modelValue": onUpdateModelValue
628
+ }, {
629
+ default: withCtx(() => [
630
+ createVNode(unref(TabsList), { class: normalizeClass(ui.value.tabs({ class: props.ui?.tabs })) }, {
631
+ default: withCtx(() => [createVNode(unref(TabsTrigger), {
632
+ value: "write",
633
+ class: normalizeClass(ui.value.tab({ class: props.ui?.tab }))
634
+ }, {
635
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("Editor.tabs.write")), 1)]),
636
+ _: 1
637
+ }, 8, ["class"]), createVNode(unref(TabsTrigger), {
638
+ value: "preview",
639
+ class: normalizeClass(ui.value.tab({ class: props.ui?.tab })),
640
+ onMouseenter: onFetchPreview,
641
+ onFocus: onFetchPreview
642
+ }, {
643
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("Editor.tabs.preview")), 1)]),
644
+ _: 1
645
+ }, 8, ["class"])]),
646
+ _: 1
647
+ }, 8, ["class"]),
648
+ createVNode(unref(TabsContent), {
649
+ value: "write",
650
+ class: normalizeClass(ui.value.content({ class: props.ui?.content })),
651
+ tabindex: "-1"
652
+ }, {
653
+ default: withCtx(() => [createVNode(_component_UFormField, {
654
+ ui: { help: "flex items-center gap-1" },
655
+ error: props.error,
656
+ help: unref(t)("Editor.write.help")
657
+ }, {
658
+ help: withCtx(({ help }) => [createVNode(_component_UIcon, {
659
+ name: unref(info_duotone_default),
660
+ class: "inline-block size-4"
661
+ }, null, 8, ["name"]), createElementVNode("span", null, toDisplayString(help), 1)]),
662
+ default: withCtx(() => [createVNode(_component_UTextarea, mergeProps({
663
+ ref_key: "textarea",
664
+ ref: textarea,
665
+ modelValue: content.value,
666
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => content.value = $event),
667
+ variant: "none",
668
+ ui: {
669
+ root: "flex",
670
+ base: "w-full resize-none"
671
+ },
672
+ rows: 4,
673
+ placeholder: unref(t)("Editor.placeholder")
674
+ }, props.textarea), null, 16, ["modelValue", "placeholder"])]),
675
+ _: 1
676
+ }, 8, ["error", "help"])]),
677
+ _: 1
678
+ }, 8, ["class"]),
679
+ createVNode(unref(TabsContent), {
680
+ value: "preview",
681
+ class: normalizeClass(ui.value.content({ class: props.ui?.content })),
682
+ tabindex: "-1"
683
+ }, {
684
+ default: withCtx(() => [unref(isPreviewLoading) ? (openBlock(), createElementBlock("div", {
685
+ key: 0,
686
+ class: normalizeClass(ui.value.prose({ class: [unref(prose), props.ui?.prose] }))
687
+ }, [createElementVNode("p", null, toDisplayString(unref(t)("Editor.preview.loading")), 1)], 2)) : (openBlock(), createElementBlock("div", {
688
+ key: 1,
689
+ class: normalizeClass(ui.value.prose({ class: [unref(prose), props.ui?.prose] })),
690
+ innerHTML: unref(preview)
691
+ }, null, 10, _hoisted_1$20))]),
692
+ _: 1
693
+ }, 8, ["class"])
694
+ ]),
695
+ _: 1
696
+ }, 8, ["class"]);
697
+ };
698
+ }
699
+ });
700
+ //#endregion
701
+ //#region src/components/Form.vue
702
+ const form = tv({ slots: {
703
+ base: "space-y-4",
704
+ wrapper: "space-y-4",
705
+ actions: "flex items-center justify-end gap-2"
706
+ } });
707
+ const _sfc_main$33 = /* @__PURE__ */ defineComponent({
708
+ __name: "Form",
709
+ props: {
710
+ class: {},
711
+ ui: {}
712
+ },
713
+ emits: ["submit"],
714
+ setup(__props, { emit: __emit }) {
715
+ const props = __props;
716
+ const emit = __emit;
717
+ function onSubmit(event) {
718
+ emit("submit", event);
719
+ }
720
+ const ui = computed(() => form());
721
+ return (_ctx, _cache) => {
722
+ return openBlock(), createElementBlock("form", {
723
+ class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })),
724
+ onSubmit: withModifiers(onSubmit, ["prevent"])
725
+ }, [createElementVNode("div", { class: normalizeClass(ui.value.wrapper({ class: props.ui?.wrapper })) }, [renderSlot(_ctx.$slots, "default")], 2), createElementVNode("div", { class: normalizeClass(ui.value.actions({ class: props.ui?.actions })) }, [renderSlot(_ctx.$slots, "actions")], 2)], 34);
726
+ };
727
+ }
728
+ });
729
+ //#endregion
730
+ //#region src/components/Comments/CommentForm.vue
731
+ const commentForm = tv({ base: "space-y-2" });
732
+ const _sfc_main$32 = /* @__PURE__ */ defineComponent({
733
+ __name: "CommentForm",
734
+ props: {
735
+ cancelable: { type: Boolean },
736
+ parentComment: {},
737
+ comment: {},
738
+ class: {}
739
+ },
740
+ emits: ["cancel", "success"],
741
+ setup(__props, { expose: __expose, emit: __emit }) {
742
+ const props = __props;
743
+ const emits = __emit;
744
+ const formData = ref({ content: props.comment?.content || "" });
745
+ const { t } = useLocale();
746
+ const { locale, pageId } = useCommentsContext();
747
+ const contentError = ref();
748
+ const queryCache = useQueryCache();
749
+ const { mutate: createComment, isLoading: isCreateCommentLoading } = useMutation({
750
+ mutation: ({ pageId, content, locale, parentCommentId }) => postComment(pageId, content, locale, parentCommentId),
751
+ onError: (error) => {
752
+ if (isUnprocessableEntityError(error)) contentError.value = error.data.message;
753
+ else contentError.value = t("comments.errors.create");
754
+ },
755
+ onSuccess: () => {
756
+ clearFormData();
757
+ emits("success");
758
+ },
759
+ onSettled: () => {
760
+ queryCache.invalidateQueries({ key: COMMENT_QUERY_KEYS.byPageId(pageId.value, locale.value) });
761
+ }
762
+ });
763
+ const { mutate: updateComment, isLoading: isUpdateCommentLoading } = useMutation({
764
+ mutation: ({ commentId, content, locale }) => putComment(commentId, content, locale),
765
+ onError: (error) => {
766
+ if (isUnprocessableEntityError(error)) contentError.value = error.data.message;
767
+ else contentError.value = t("comments.errors.update");
768
+ },
769
+ onSuccess: () => {
770
+ clearFormData();
771
+ emits("success");
772
+ },
773
+ onSettled: () => {
774
+ queryCache.invalidateQueries({ key: COMMENT_QUERY_KEYS.byPageId(pageId.value, locale.value) });
775
+ }
776
+ });
777
+ const isLoading = computed(() => isCreateCommentLoading.value || isUpdateCommentLoading.value);
778
+ async function onSubmit() {
779
+ if (isLoading.value || !formData.value.content) return;
780
+ contentError.value = void 0;
781
+ if (props.comment) updateComment({
782
+ locale: locale.value,
783
+ commentId: props.comment.id,
784
+ content: formData.value.content
785
+ });
786
+ else createComment({
787
+ pageId: pageId.value,
788
+ locale: locale.value,
789
+ content: formData.value.content,
790
+ parentCommentId: props.parentComment?.id
791
+ });
792
+ }
793
+ function onEditorKeydown(event) {
794
+ if (event.key !== "Enter" || !event.ctrlKey && !event.metaKey) return;
795
+ event.preventDefault();
796
+ onSubmit();
797
+ }
798
+ function clearFormData() {
799
+ formData.value = { content: "" };
800
+ }
801
+ function onCancel() {
802
+ if (!isLoading.value) emits("cancel");
803
+ }
804
+ const editor = useTemplateRef("editor");
805
+ function focus() {
806
+ editor.value?.focus();
807
+ }
808
+ __expose({ focus });
809
+ const ui = computed(() => commentForm({ class: props.class }));
810
+ return (_ctx, _cache) => {
811
+ const _component_UButton = resolveComponent("UButton");
812
+ return openBlock(), createBlock(_sfc_main$33, {
813
+ class: normalizeClass(ui.value),
814
+ onSubmit
815
+ }, {
816
+ actions: withCtx(() => [props.cancelable ? (openBlock(), createBlock(_component_UButton, {
817
+ key: 0,
818
+ variant: "link",
819
+ label: unref(t)("comments.CommentForm.actions.cancel"),
820
+ disabled: isLoading.value,
821
+ onClick: onCancel
822
+ }, null, 8, ["label", "disabled"])) : createCommentVNode("v-if", true), createVNode(_component_UButton, {
823
+ disabled: !formData.value.content,
824
+ loading: isLoading.value,
825
+ type: "submit",
826
+ label: props.comment ? unref(t)("comments.CommentForm.actions.edit") : props.parentComment ? unref(t)("comments.CommentForm.actions.reply") : unref(t)("comments.CommentForm.actions.add")
827
+ }, null, 8, [
828
+ "disabled",
829
+ "loading",
830
+ "label"
831
+ ])]),
832
+ default: withCtx(() => [createVNode(_sfc_main$34, {
833
+ ref_key: "editor",
834
+ ref: editor,
835
+ content: formData.value.content,
836
+ "onUpdate:content": _cache[0] || (_cache[0] = ($event) => formData.value.content = $event),
837
+ error: contentError.value,
838
+ onKeydown: onEditorKeydown
839
+ }, null, 8, ["content", "error"])]),
840
+ _: 1
841
+ }, 8, ["class"]);
842
+ };
843
+ }
844
+ });
845
+ //#endregion
846
+ //#region src/components/Comments/CommentFormSection.vue
847
+ const commentFormSection = tv({ slots: {
848
+ base: "space-y-2",
849
+ header: "flex items-center gap-2",
850
+ title: "font-medium text-default"
851
+ } });
852
+ const _sfc_main$31 = /* @__PURE__ */ defineComponent({
853
+ __name: "CommentFormSection",
854
+ props: {
855
+ class: {},
856
+ ui: {}
857
+ },
858
+ setup(__props) {
859
+ const props = __props;
860
+ const { t } = useLocale();
861
+ const { data: user } = useQuery(currentUserQuery);
862
+ const ui = computed(() => commentFormSection());
863
+ return (_ctx, _cache) => {
864
+ const _component_UAvatar = resolveComponent("UAvatar");
865
+ return openBlock(), createElementBlock("section", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [createElementVNode("div", { class: normalizeClass(ui.value.header({ class: props.ui?.header })) }, [unref(user) ? (openBlock(), createBlock(_component_UAvatar, {
866
+ key: 0,
867
+ src: unref(user).avatar,
868
+ title: unref(user).name
869
+ }, null, 8, ["src", "title"])) : createCommentVNode("v-if", true), createElementVNode("h3", { class: normalizeClass(ui.value.title({ class: props.ui?.title })) }, toDisplayString(unref(t)("comments.CommentFormSection.title")), 3)], 2), !unref(user) ? (openBlock(), createBlock(_sfc_main$35, { key: 0 })) : (openBlock(), createBlock(_sfc_main$32, { key: 1 }))], 2);
870
+ };
871
+ }
872
+ });
873
+ //#endregion
874
+ //#region src/components/State/StateEmpty.vue
875
+ const stateEmpty = tv({ slots: { base: "" } });
876
+ const _sfc_main$30 = /* @__PURE__ */ defineComponent({
877
+ __name: "StateEmpty",
878
+ props: {
879
+ class: {},
880
+ ui: {}
881
+ },
882
+ setup(__props) {
883
+ const props = __props;
884
+ const { t } = useLocale();
885
+ const ui = computed(() => stateEmpty());
886
+ return (_ctx, _cache) => {
887
+ return openBlock(), createElementBlock("p", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, toDisplayString(unref(t)("state.empty")), 3);
888
+ };
889
+ }
890
+ });
891
+ //#endregion
892
+ //#region src/components/State/StateError.vue
893
+ const stateError = tv({ slots: { base: "" } });
894
+ const _sfc_main$29 = /* @__PURE__ */ defineComponent({
895
+ __name: "StateError",
896
+ props: {
897
+ class: {},
898
+ ui: {}
899
+ },
900
+ setup(__props) {
901
+ const props = __props;
902
+ const { t } = useLocale();
903
+ const ui = computed(() => stateError());
904
+ return (_ctx, _cache) => {
905
+ return openBlock(), createElementBlock("p", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, toDisplayString(unref(t)("state.error")), 3);
906
+ };
907
+ }
908
+ });
909
+ //#endregion
910
+ //#region src/components/State/StatePending.vue
911
+ const statePending = tv({ slots: { base: "" } });
912
+ const _sfc_main$28 = /* @__PURE__ */ defineComponent({
913
+ __name: "StatePending",
914
+ props: {
915
+ class: {},
916
+ ui: {}
917
+ },
918
+ setup(__props) {
919
+ const props = __props;
920
+ const { t } = useLocale();
921
+ const ui = computed(() => statePending());
922
+ return (_ctx, _cache) => {
923
+ return openBlock(), createElementBlock("p", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, toDisplayString(unref(t)("state.pending")), 3);
924
+ };
925
+ }
926
+ });
927
+ //#endregion
928
+ //#region ~icons/ph/note-pencil-duotone
929
+ const _hoisted_1$19 = {
930
+ viewBox: "0 0 256 256",
931
+ width: "1.2em",
932
+ height: "1.2em"
933
+ };
934
+ function render$11(_ctx, _cache) {
935
+ return openBlock(), createElementBlock("svg", _hoisted_1$19, [..._cache[0] || (_cache[0] = [createElementVNode("g", { fill: "currentColor" }, [createElementVNode("path", {
936
+ d: "m200 88l-72 72H96v-32l72-72Z",
937
+ opacity: ".2"
938
+ }), createElementVNode("path", { d: "m229.66 58.34l-32-32a8 8 0 0 0-11.32 0l-96 96A8 8 0 0 0 88 128v32a8 8 0 0 0 8 8h32a8 8 0 0 0 5.66-2.34l96-96a8 8 0 0 0 0-11.32M124.69 152H104v-20.69l64-64L188.69 88ZM200 76.69L179.31 56L192 43.31L212.69 64ZM224 128v80a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h80a8 8 0 0 1 0 16H48v160h160v-80a8 8 0 0 1 16 0" })], -1)])]);
939
+ }
940
+ var note_pencil_duotone_default = markRaw({
941
+ name: "ph-note-pencil-duotone",
942
+ render: render$11
943
+ });
944
+ //#endregion
945
+ //#region ~icons/ph/trash-duotone
946
+ const _hoisted_1$18 = {
947
+ viewBox: "0 0 256 256",
948
+ width: "1.2em",
949
+ height: "1.2em"
950
+ };
951
+ function render$10(_ctx, _cache) {
952
+ return openBlock(), createElementBlock("svg", _hoisted_1$18, [..._cache[0] || (_cache[0] = [createElementVNode("g", { fill: "currentColor" }, [createElementVNode("path", {
953
+ d: "M200 56v152a8 8 0 0 1-8 8H64a8 8 0 0 1-8-8V56Z",
954
+ opacity: ".2"
955
+ }), createElementVNode("path", { d: "M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16M96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0m48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0" })], -1)])]);
956
+ }
957
+ var trash_duotone_default = markRaw({
958
+ name: "ph-trash-duotone",
959
+ render: render$10
960
+ });
961
+ //#endregion
962
+ //#region src/components/ConfirmModal.vue
963
+ const confirmModal = tv({ slots: { base: "" } });
964
+ const _sfc_main$27 = /* @__PURE__ */ defineComponent({
965
+ __name: "ConfirmModal",
966
+ props: {
967
+ title: {},
968
+ description: {},
969
+ cancelLabel: {},
970
+ confirmLabel: {},
971
+ loading: { type: Boolean },
972
+ class: {},
973
+ ui: {}
974
+ },
975
+ emits: ["confirm", "close"],
976
+ setup(__props, { emit: __emit }) {
977
+ const props = __props;
978
+ const emit = __emit;
979
+ const { t } = useLocale();
980
+ function onClose() {
981
+ emit("close");
982
+ }
983
+ function onConfirm() {
984
+ emit("confirm");
985
+ }
986
+ const ui = computed(() => confirmModal());
987
+ return (_ctx, _cache) => {
988
+ const _component_UButton = resolveComponent("UButton");
989
+ const _component_UModal = resolveComponent("UModal");
990
+ return openBlock(), createBlock(_component_UModal, {
991
+ title: __props.title,
992
+ description: __props.description,
993
+ ui: {
994
+ content: "space-y-4",
995
+ footer: "justify-end"
996
+ },
997
+ class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] }))
998
+ }, {
999
+ footer: withCtx(() => [createVNode(_component_UButton, {
1000
+ label: props.cancelLabel ?? unref(t)("ConfirmModal.actions.cancel"),
1001
+ variant: "ghost",
1002
+ color: "neutral",
1003
+ onClick: onClose
1004
+ }, null, 8, ["label"]), createVNode(_component_UButton, {
1005
+ label: props.confirmLabel ?? unref(t)("ConfirmModal.actions.confirm"),
1006
+ variant: "solid",
1007
+ color: "neutral",
1008
+ loading: props.loading,
1009
+ onClick: _cache[0] || (_cache[0] = ($event) => onConfirm())
1010
+ }, null, 8, ["label", "loading"])]),
1011
+ _: 1
1012
+ }, 8, [
1013
+ "title",
1014
+ "description",
1015
+ "class"
1016
+ ]);
1017
+ };
1018
+ }
1019
+ });
1020
+ //#endregion
1021
+ //#region src/components/Comments/CommentDeleteConfirmModal.vue
1022
+ const commentDeleteConfirmModal = tv({ slots: { base: "" } });
1023
+ const _sfc_main$26 = /* @__PURE__ */ defineComponent({
1024
+ __name: "CommentDeleteConfirmModal",
1025
+ props: {
1026
+ pageId: {},
1027
+ commentId: {},
1028
+ locale: {},
1029
+ show: { type: Function },
1030
+ class: {},
1031
+ ui: {}
1032
+ },
1033
+ emits: ["close"],
1034
+ setup(__props, { emit: __emit }) {
1035
+ const props = __props;
1036
+ const emit = __emit;
1037
+ const { t } = useLocale();
1038
+ const queryCache = useQueryCache();
1039
+ const { mutate, isLoading } = useMutation({
1040
+ mutation: ({ commentId, locale }) => deleteComment(commentId, locale),
1041
+ onSuccess: () => {
1042
+ props.show({
1043
+ kind: "success",
1044
+ message: t("comments.success.delete")
1045
+ });
1046
+ },
1047
+ onError: () => {
1048
+ props.show({
1049
+ kind: "error",
1050
+ message: t("comments.errors.delete")
1051
+ });
1052
+ },
1053
+ onSettled: async () => {
1054
+ await queryCache.invalidateQueries({ key: COMMENT_QUERY_KEYS.byPageId(props.pageId, props.locale) });
1055
+ emit("close");
1056
+ }
1057
+ });
1058
+ function onClose() {
1059
+ emit("close");
1060
+ }
1061
+ function onConfirm() {
1062
+ mutate({
1063
+ commentId: props.commentId,
1064
+ locale: props.locale
1065
+ });
1066
+ }
1067
+ const ui = computed(() => commentDeleteConfirmModal());
1068
+ return (_ctx, _cache) => {
1069
+ return openBlock(), createBlock(_sfc_main$27, {
1070
+ title: unref(t)("comments.CommentDeleteConfirmModal.title"),
1071
+ description: unref(t)("comments.CommentDeleteConfirmModal.description"),
1072
+ loading: unref(isLoading),
1073
+ class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })),
1074
+ onClose,
1075
+ onConfirm
1076
+ }, null, 8, [
1077
+ "title",
1078
+ "description",
1079
+ "loading",
1080
+ "class"
1081
+ ]);
1082
+ };
1083
+ }
1084
+ });
1085
+ //#endregion
1086
+ //#region src/composables/comments/useComments.ts
1087
+ function useComments() {
1088
+ const overlay = useOverlay();
1089
+ const { banner } = useCommentsContext();
1090
+ function confirmDeleteComment(pageId, commentId, locale) {
1091
+ overlay.create(_sfc_main$26, {
1092
+ destroyOnClose: true,
1093
+ props: {
1094
+ pageId,
1095
+ commentId,
1096
+ locale,
1097
+ show: banner.show
1098
+ }
1099
+ }).open();
1100
+ }
1101
+ return {
1102
+ confirmDeleteComment,
1103
+ show: banner.show
1104
+ };
1105
+ }
1106
+ //#endregion
1107
+ //#region src/components/Comments/CommentContent.vue
1108
+ const _hoisted_1$17 = ["innerHTML"];
1109
+ const commentContent = tv({ base: "" });
1110
+ const _sfc_main$25 = /* @__PURE__ */ defineComponent({
1111
+ __name: "CommentContent",
1112
+ props: /* @__PURE__ */ mergeModels({
1113
+ parentComment: {},
1114
+ comment: {},
1115
+ class: {}
1116
+ }, {
1117
+ "viewEditor": { type: Boolean },
1118
+ "viewEditorModifiers": {}
1119
+ }),
1120
+ emits: /* @__PURE__ */ mergeModels(["success"], ["update:viewEditor"]),
1121
+ setup(__props, { emit: __emit }) {
1122
+ const props = __props;
1123
+ const emit = __emit;
1124
+ const editor = ref(null);
1125
+ const viewEditor = useModel(__props, "viewEditor");
1126
+ watch(viewEditor, (value) => {
1127
+ if (value) nextTick(() => {
1128
+ editor.value?.focus();
1129
+ });
1130
+ });
1131
+ function onSuccess() {
1132
+ emit("success");
1133
+ viewEditor.value = false;
1134
+ }
1135
+ const ui = computed(() => commentContent({ class: props.class }));
1136
+ return (_ctx, _cache) => {
1137
+ return viewEditor.value ? (openBlock(), createBlock(_sfc_main$32, {
1138
+ key: 0,
1139
+ ref_key: "editor",
1140
+ ref: editor,
1141
+ cancelable: "",
1142
+ comment: __props.comment,
1143
+ "parent-comment": __props.parentComment,
1144
+ class: normalizeClass(ui.value),
1145
+ onSuccess,
1146
+ onCancel: _cache[0] || (_cache[0] = ($event) => viewEditor.value = false)
1147
+ }, null, 8, [
1148
+ "comment",
1149
+ "parent-comment",
1150
+ "class"
1151
+ ])) : (openBlock(), createElementBlock("div", {
1152
+ key: 1,
1153
+ class: normalizeClass(["text-sm", unref(prose)]),
1154
+ innerHTML: __props.comment.content_html
1155
+ }, null, 10, _hoisted_1$17));
1156
+ };
1157
+ }
1158
+ });
1159
+ //#endregion
1160
+ //#region src/components/Comments/CommentHeader.vue
1161
+ const _hoisted_1$16 = { class: "sr-only" };
1162
+ const _hoisted_2$1 = { class: "sr-only" };
1163
+ const _hoisted_3 = ["datetime"];
1164
+ const commentHeader = tv({ slots: {
1165
+ root: "flex flex-col items-start text-sm",
1166
+ wrapper: "flex items-center gap-2",
1167
+ username: "text-default font-semibold tracking-wide",
1168
+ time: "text-dimmed",
1169
+ link: "text-xs"
1170
+ } });
1171
+ const _sfc_main$24 = /* @__PURE__ */ defineComponent({
1172
+ __name: "CommentHeader",
1173
+ props: {
1174
+ comment: {},
1175
+ class: {},
1176
+ ui: {}
1177
+ },
1178
+ setup(__props) {
1179
+ const props = __props;
1180
+ const { code, t } = useLocale();
1181
+ const englishTimeAgo = useTimeAgoIntl(() => props.comment.created_at, { locale: "en" });
1182
+ const frenchTimeAgo = useTimeAgoIntl(() => props.comment.created_at, { locale: "fr" });
1183
+ const timeAgo = computed(() => code.value === "fr" ? frenchTimeAgo.value : englishTimeAgo.value);
1184
+ const ui = computed(() => commentHeader());
1185
+ return (_ctx, _cache) => {
1186
+ return openBlock(), createElementBlock("div", { class: normalizeClass(ui.value.root({ class: [props.ui?.root, props.class] })) }, [createElementVNode("dl", { class: normalizeClass(ui.value.wrapper({ class: props.ui?.wrapper })) }, [
1187
+ createElementVNode("dt", _hoisted_1$16, toDisplayString(unref(t)("comments.CommentHeader.author")), 1),
1188
+ createElementVNode("dd", { class: normalizeClass(ui.value.username({ class: props.ui?.username })) }, toDisplayString(props.comment.user.name), 3),
1189
+ createElementVNode("dt", _hoisted_2$1, toDisplayString(unref(t)("comments.CommentHeader.publishedAt")), 1),
1190
+ createElementVNode("dd", { class: normalizeClass(ui.value.time({ class: props.ui?.time })) }, [createElementVNode("time", { datetime: new Date(props.comment.created_at).toISOString() }, toDisplayString(timeAgo.value), 9, _hoisted_3)], 2)
1191
+ ], 2)], 2);
1192
+ };
1193
+ }
1194
+ });
1195
+ //#endregion
1196
+ //#region ~icons/ph/heart-duotone
1197
+ const _hoisted_1$15 = {
1198
+ viewBox: "0 0 256 256",
1199
+ width: "1.2em",
1200
+ height: "1.2em"
1201
+ };
1202
+ function render$9(_ctx, _cache) {
1203
+ return openBlock(), createElementBlock("svg", _hoisted_1$15, [..._cache[0] || (_cache[0] = [createElementVNode("g", { fill: "currentColor" }, [createElementVNode("path", {
1204
+ d: "M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54",
1205
+ opacity: ".2"
1206
+ }), createElementVNode("path", { d: "M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8" })], -1)])]);
1207
+ }
1208
+ var heart_duotone_default = markRaw({
1209
+ name: "ph-heart-duotone",
1210
+ render: render$9
1211
+ });
1212
+ //#endregion
1213
+ //#region ~icons/ph/heart-fill
1214
+ const _hoisted_1$14 = {
1215
+ viewBox: "0 0 256 256",
1216
+ width: "1.2em",
1217
+ height: "1.2em"
1218
+ };
1219
+ function render$8(_ctx, _cache) {
1220
+ return openBlock(), createElementBlock("svg", _hoisted_1$14, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
1221
+ fill: "currentColor",
1222
+ d: "M240 102c0 70-103.79 126.66-108.21 129a8 8 0 0 1-7.58 0C119.79 228.66 16 172 16 102a62.07 62.07 0 0 1 62-62c20.65 0 38.73 8.88 50 23.89C139.27 48.88 157.35 40 178 40a62.07 62.07 0 0 1 62 62"
1223
+ }, null, -1)])]);
1224
+ }
1225
+ var heart_fill_default = markRaw({
1226
+ name: "ph-heart-fill",
1227
+ render: render$8
1228
+ });
1229
+ //#endregion
1230
+ //#region src/components/Comments/CommentLike.vue
1231
+ const commentLike = tv({ slots: { base: "" } });
1232
+ function isToggleCommentLikeContext(context) {
1233
+ return typeof context === "object" && context !== null && "oldComments" in context && "newComments" in context;
1234
+ }
1235
+ function getCachedComment(comments, commentId, parentCommentId) {
1236
+ if (parentCommentId === void 0) return comments.find((comment) => comment.id === commentId);
1237
+ return comments.find((comment) => comment.id === parentCommentId)?.replies?.find((comment) => comment.id === commentId);
1238
+ }
1239
+ function replaceCachedComment(comments, commentId, nextComment, parentCommentId) {
1240
+ const replace = (comment) => {
1241
+ if (comment.id !== commentId) return comment;
1242
+ return nextComment;
1243
+ };
1244
+ if (parentCommentId === void 0) return comments.map(replace);
1245
+ return comments.map((comment) => {
1246
+ if (comment.id !== parentCommentId || !comment.replies) return comment;
1247
+ const replies = comment.replies.map(replace);
1248
+ return replies.every((reply, index) => reply === comment.replies?.[index]) ? comment : {
1249
+ ...comment,
1250
+ replies
1251
+ };
1252
+ });
1253
+ }
1254
+ const _sfc_main$23 = /* @__PURE__ */ defineComponent({
1255
+ __name: "CommentLike",
1256
+ props: {
1257
+ parentComment: {},
1258
+ comment: {},
1259
+ class: {},
1260
+ ui: {}
1261
+ },
1262
+ setup(__props) {
1263
+ const props = __props;
1264
+ const { t } = useLocale();
1265
+ const toast = useToast();
1266
+ const queryCache = useQueryCache();
1267
+ const { banner, locale, pageId } = useCommentsContext();
1268
+ const { error: loginError, openLoginWindow } = useLogin();
1269
+ const { data: user } = useQuery(currentUserQuery);
1270
+ const { mutate: toggleLike } = useMutation({
1271
+ onMutate(variables) {
1272
+ const key = COMMENT_QUERY_KEYS.byPageId(variables.pageId, variables.locale);
1273
+ const oldComments = queryCache.getQueryData(key);
1274
+ const oldComment = getCachedComment(oldComments, variables.commentId, variables.parentCommentId);
1275
+ const newComment = {
1276
+ ...oldComment,
1277
+ likes: Math.max(0, oldComment.likes + (variables.shouldLike ? 1 : -1)),
1278
+ can: {
1279
+ ...oldComment.can,
1280
+ like: !variables.shouldLike,
1281
+ unlike: variables.shouldLike
1282
+ }
1283
+ };
1284
+ const newComments = replaceCachedComment(oldComments, variables.commentId, newComment, variables.parentCommentId);
1285
+ queryCache.setQueryData(key, newComments);
1286
+ queryCache.cancelQueries({
1287
+ key,
1288
+ exact: true
1289
+ });
1290
+ return {
1291
+ oldComments,
1292
+ newComments
1293
+ };
1294
+ },
1295
+ mutation: (variables) => variables.shouldLike ? postCommentLike(variables.commentId, variables.locale) : deleteCommentLike(variables.commentId, variables.locale),
1296
+ onError(_error, variables, context) {
1297
+ const key = COMMENT_QUERY_KEYS.byPageId(variables.pageId, variables.locale);
1298
+ if (isToggleCommentLikeContext(context) && context.newComments === queryCache.getQueryData(key)) queryCache.setQueryData(key, context.oldComments);
1299
+ banner.show({
1300
+ kind: "error",
1301
+ message: t(variables.shouldLike ? "comments.errors.like" : "comments.errors.unlike")
1302
+ });
1303
+ },
1304
+ onSettled(_data, _error, variables) {
1305
+ queryCache.invalidateQueries({
1306
+ key: COMMENT_QUERY_KEYS.byPageId(variables.pageId, variables.locale),
1307
+ exact: true
1308
+ });
1309
+ }
1310
+ });
1311
+ async function onClick() {
1312
+ if (!user.value) {
1313
+ if (!await openLoginWindow() && loginError.value) toast.add({
1314
+ color: "error",
1315
+ description: t(getLoginErrorMessageKey(loginError.value))
1316
+ });
1317
+ return;
1318
+ }
1319
+ if (!props.comment.can.like && !props.comment.can.unlike) return;
1320
+ toggleLike({
1321
+ pageId: pageId.value,
1322
+ locale: locale.value,
1323
+ commentId: props.comment.id,
1324
+ parentCommentId: props.parentComment?.id,
1325
+ shouldLike: props.comment.can.like
1326
+ });
1327
+ }
1328
+ const ui = computed(() => commentLike());
1329
+ return (_ctx, _cache) => {
1330
+ const _component_UButton = resolveComponent("UButton");
1331
+ return openBlock(), createBlock(_component_UButton, {
1332
+ variant: "link",
1333
+ color: "neutral",
1334
+ title: unref(t)(props.comment.can.unlike ? "comments.CommentUnlike.title" : "comments.CommentLike.title"),
1335
+ label: props.comment.likes.toString(),
1336
+ icon: props.comment.can.unlike ? unref(heart_fill_default) : unref(heart_duotone_default),
1337
+ class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })),
1338
+ onClick
1339
+ }, null, 8, [
1340
+ "title",
1341
+ "label",
1342
+ "icon",
1343
+ "class"
1344
+ ]);
1345
+ };
1346
+ }
1347
+ });
1348
+ //#endregion
1349
+ //#region src/components/Comments/CommentRepliesCount.vue
1350
+ const commentRepliesCount = tv({ base: "text-xs text-dimmed" });
1351
+ const _sfc_main$22 = /* @__PURE__ */ defineComponent({
1352
+ __name: "CommentRepliesCount",
1353
+ props: {
1354
+ comment: {},
1355
+ class: {}
1356
+ },
1357
+ setup(__props) {
1358
+ const props = __props;
1359
+ const { t } = useLocale();
1360
+ const replies = computed(() => {
1361
+ if (props.comment.replies && props.comment.replies?.length > 1) return t("comments.CommentRepliesCount.replies", { count: props.comment.replies.length });
1362
+ return t("comments.CommentRepliesCount.reply", { count: props.comment.replies?.length ?? 0 });
1363
+ });
1364
+ const ui = computed(() => commentRepliesCount({ class: props.class }));
1365
+ return (_ctx, _cache) => {
1366
+ return props.comment.replies?.length ? (openBlock(), createElementBlock("span", {
1367
+ key: 0,
1368
+ class: normalizeClass(ui.value)
1369
+ }, toDisplayString(replies.value), 3)) : createCommentVNode("v-if", true);
1370
+ };
1371
+ }
1372
+ });
1373
+ //#endregion
1374
+ //#region src/components/Comments/Comment.vue
1375
+ const _hoisted_1$13 = ["id"];
1376
+ const comment = tv({
1377
+ slots: {
1378
+ base: "comment relative before:content-['']",
1379
+ wrapper: "flex items-start justify-between",
1380
+ header: "flex items-center gap-2",
1381
+ headerAvatar: "",
1382
+ contentWrapper: "",
1383
+ contentFooter: "flex items-center justify-between",
1384
+ like: "-mx-3 -my-1.5"
1385
+ },
1386
+ variants: { isReply: {
1387
+ true: {
1388
+ base: "space-y-2 px-4 pb-2 pt-4 before:absolute before:bottom-0 before:left-[calc((var(--avatar-size)/2)-1px+1rem)] before:top-0 before:w-[2px] before:bg-border",
1389
+ headerAvatar: "size-[var(--avatar-size)]",
1390
+ contentWrapper: "ml-[calc(var(--avatar-size)+0.5rem)] space-y-2"
1391
+ },
1392
+ false: {
1393
+ base: "p-4 space-y-4",
1394
+ contentWrapper: "space-y-4"
1395
+ }
1396
+ } }
1397
+ });
1398
+ const _sfc_main$21 = /* @__PURE__ */ defineComponent({
1399
+ __name: "Comment",
1400
+ props: {
1401
+ parentComment: {},
1402
+ comment: {},
1403
+ class: {},
1404
+ ui: {}
1405
+ },
1406
+ setup(__props) {
1407
+ const props = __props;
1408
+ const { t } = useLocale();
1409
+ const { pageId, locale } = useCommentsContext();
1410
+ const { confirmDeleteComment } = useComments();
1411
+ const { data: user } = useQuery(currentUserQuery);
1412
+ const viewEditCommentEditor = ref(false);
1413
+ function onCommentEdited() {
1414
+ viewEditCommentEditor.value = false;
1415
+ }
1416
+ const showActions = computed(() => user.value && (props.comment.can.update || props.comment.can.delete));
1417
+ const actions = computed(() => {
1418
+ const items = [];
1419
+ if (props.comment.can.update) items.push({
1420
+ icon: note_pencil_duotone_default,
1421
+ label: t("comments.Comment.actions.edit"),
1422
+ onSelect: () => {
1423
+ viewEditCommentEditor.value = true;
1424
+ }
1425
+ });
1426
+ if (props.comment.can.delete) items.push({
1427
+ icon: trash_duotone_default,
1428
+ label: t("comments.Comment.actions.delete"),
1429
+ onSelect: () => confirmDeleteComment(pageId.value, props.comment.id, locale.value)
1430
+ });
1431
+ return items;
1432
+ });
1433
+ const ui = computed(() => comment({ isReply: !!props.parentComment }));
1434
+ return (_ctx, _cache) => {
1435
+ const _component_UAvatar = resolveComponent("UAvatar");
1436
+ const _component_UButton = resolveComponent("UButton");
1437
+ const _component_UDropdownMenu = resolveComponent("UDropdownMenu");
1438
+ return openBlock(), createElementBlock("article", {
1439
+ id: props.comment.html_id,
1440
+ class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })),
1441
+ style: { "--avatar-size": "1.875rem" }
1442
+ }, [createElementVNode("div", { class: normalizeClass(ui.value.wrapper({ class: props.ui?.wrapper })) }, [createElementVNode("div", { class: normalizeClass(ui.value.header({ class: props.ui?.header })) }, [createVNode(_component_UAvatar, {
1443
+ src: props.comment.user.avatar,
1444
+ alt: props.comment.user.name,
1445
+ size: props.parentComment ? "sm" : "md",
1446
+ class: normalizeClass(["z-1", ui.value.headerAvatar({ class: props.ui?.headerAvatar })])
1447
+ }, null, 8, [
1448
+ "src",
1449
+ "alt",
1450
+ "size",
1451
+ "class"
1452
+ ]), createVNode(_sfc_main$24, { comment: props.comment }, null, 8, ["comment"])], 2), showActions.value ? (openBlock(), createBlock(_component_UDropdownMenu, {
1453
+ key: 0,
1454
+ items: actions.value,
1455
+ content: { align: "end" }
1456
+ }, {
1457
+ default: withCtx(() => [createVNode(_component_UButton, {
1458
+ icon: "i-ph-dots-three-bold",
1459
+ variant: "link",
1460
+ "aria-label": unref(t)("comments.Comment.actionMenu")
1461
+ }, null, 8, ["aria-label"])]),
1462
+ _: 1
1463
+ }, 8, ["items"])) : createCommentVNode("v-if", true)], 2), createElementVNode("div", { class: normalizeClass(ui.value.contentWrapper({ class: props.ui?.contentWrapper })) }, [createVNode(_sfc_main$25, {
1464
+ "view-editor": viewEditCommentEditor.value,
1465
+ "onUpdate:viewEditor": _cache[0] || (_cache[0] = ($event) => viewEditCommentEditor.value = $event),
1466
+ "parent-comment": props.parentComment,
1467
+ comment: props.comment,
1468
+ onSuccess: onCommentEdited
1469
+ }, null, 8, [
1470
+ "view-editor",
1471
+ "parent-comment",
1472
+ "comment"
1473
+ ]), createElementVNode("div", { class: normalizeClass(ui.value.contentFooter({ class: props.ui?.contentFooter })) }, [props.comment.can.like || props.comment.can.unlike ? (openBlock(), createBlock(_sfc_main$23, {
1474
+ key: 0,
1475
+ "parent-comment": props.parentComment,
1476
+ comment: props.comment,
1477
+ class: normalizeClass(ui.value.like({ class: props.ui?.like }))
1478
+ }, null, 8, [
1479
+ "parent-comment",
1480
+ "comment",
1481
+ "class"
1482
+ ])) : createCommentVNode("v-if", true), !props.parentComment && props.comment.replies?.length ? (openBlock(), createBlock(_sfc_main$22, {
1483
+ key: 1,
1484
+ comment: props.comment
1485
+ }, null, 8, ["comment"])) : createCommentVNode("v-if", true)], 2)], 2)], 10, _hoisted_1$13);
1486
+ };
1487
+ }
1488
+ });
1489
+ //#endregion
1490
+ //#region src/components/Discussions/DiscussionReply.vue
1491
+ const discussionReply = tv({ slots: { button: "justify-start text-muted" } });
1492
+ const _sfc_main$20 = /* @__PURE__ */ defineComponent({
1493
+ __name: "DiscussionReply",
1494
+ props: {
1495
+ parentComment: {},
1496
+ class: {},
1497
+ ui: {}
1498
+ },
1499
+ setup(__props) {
1500
+ const props = __props;
1501
+ const { t } = useLocale();
1502
+ const toast = useToast();
1503
+ const { data: user } = useQuery(currentUserQuery);
1504
+ const { error: loginError, openLoginWindow } = useLogin();
1505
+ const form = useTemplateRef("form");
1506
+ const isFormShown = ref(false);
1507
+ async function showForm() {
1508
+ if (!user.value) {
1509
+ if (!await openLoginWindow() && loginError.value) toast.add({
1510
+ color: "error",
1511
+ description: t(getLoginErrorMessageKey(loginError.value))
1512
+ });
1513
+ return;
1514
+ }
1515
+ isFormShown.value = true;
1516
+ nextTick(() => form.value?.focus());
1517
+ }
1518
+ function hideForm() {
1519
+ isFormShown.value = false;
1520
+ }
1521
+ function onSuccess() {
1522
+ isFormShown.value = false;
1523
+ }
1524
+ const ui = computed(() => discussionReply());
1525
+ return (_ctx, _cache) => {
1526
+ const _component_UButton = resolveComponent("UButton");
1527
+ return isFormShown.value ? (openBlock(), createBlock(_sfc_main$32, {
1528
+ key: 0,
1529
+ ref_key: "form",
1530
+ ref: form,
1531
+ cancelable: "",
1532
+ "parent-comment": props.parentComment,
1533
+ onSuccess,
1534
+ onCancel: hideForm
1535
+ }, null, 8, ["parent-comment"])) : (openBlock(), createBlock(_component_UButton, {
1536
+ key: 1,
1537
+ color: "neutral",
1538
+ variant: "link",
1539
+ block: "",
1540
+ class: normalizeClass(ui.value.button({ class: props.ui?.button })),
1541
+ onClick: showForm
1542
+ }, {
1543
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("discussions.DiscussionReply.reply")), 1)]),
1544
+ _: 1
1545
+ }, 8, ["class"]));
1546
+ };
1547
+ }
1548
+ });
1549
+ //#endregion
1550
+ //#region src/components/Discussions/Discussion.vue
1551
+ const discussion = tv({ slots: {
1552
+ base: "overflow-hidden border border-muted rounded-md",
1553
+ answers: "bg-muted",
1554
+ reply: "p-2"
1555
+ } });
1556
+ const _sfc_main$19 = /* @__PURE__ */ defineComponent({
1557
+ __name: "Discussion",
1558
+ props: {
1559
+ comment: {},
1560
+ class: {},
1561
+ ui: {}
1562
+ },
1563
+ setup(__props) {
1564
+ const props = __props;
1565
+ const ui = computed(() => discussion());
1566
+ return (_ctx, _cache) => {
1567
+ return openBlock(), createElementBlock("div", { class: normalizeClass(ui.value.base({ class: [props.class, props.ui?.base] })) }, [
1568
+ createVNode(_sfc_main$21, { comment: __props.comment }, null, 8, ["comment"]),
1569
+ __props.comment.replies?.length ? (openBlock(), createElementBlock("div", {
1570
+ key: 0,
1571
+ class: normalizeClass(ui.value.answers({ class: props.ui?.answers }))
1572
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.comment.replies, (reply) => {
1573
+ return openBlock(), createBlock(_sfc_main$21, {
1574
+ key: reply.id,
1575
+ "parent-comment": __props.comment,
1576
+ comment: reply
1577
+ }, null, 8, ["parent-comment", "comment"]);
1578
+ }), 128))], 2)) : createCommentVNode("v-if", true),
1579
+ createElementVNode("div", { class: normalizeClass(ui.value.reply({ class: props.ui?.reply })) }, [createVNode(_sfc_main$20, { "parent-comment": __props.comment }, null, 8, ["parent-comment"])], 2)
1580
+ ], 2);
1581
+ };
1582
+ }
1583
+ });
1584
+ //#endregion
1585
+ //#region src/components/Discussions/DiscussionsList.vue
1586
+ const discussionsList = tv({ slots: {
1587
+ base: "space-y-8",
1588
+ discussion: ""
1589
+ } });
1590
+ const _sfc_main$18 = /* @__PURE__ */ defineComponent({
1591
+ __name: "DiscussionsList",
1592
+ props: {
1593
+ comments: {},
1594
+ class: {},
1595
+ ui: {}
1596
+ },
1597
+ setup(__props) {
1598
+ const props = __props;
1599
+ const ui = computed(() => discussionsList());
1600
+ return (_ctx, _cache) => {
1601
+ return openBlock(), createElementBlock("div", { class: normalizeClass(ui.value.base({ class: [props.class, props.ui?.base] })) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(props.comments, (comment) => {
1602
+ return openBlock(), createBlock(_sfc_main$19, {
1603
+ key: comment.id,
1604
+ comment,
1605
+ class: normalizeClass(ui.value.discussion({ class: props.ui?.discussion }))
1606
+ }, null, 8, ["comment", "class"]);
1607
+ }), 128))], 2);
1608
+ };
1609
+ }
1610
+ });
1611
+ //#endregion
1612
+ //#region src/components/Subtitle.vue
1613
+ const subtitle = tv({ base: "text-sm text-dimmed" });
1614
+ const _sfc_main$17 = /* @__PURE__ */ defineComponent({
1615
+ __name: "Subtitle",
1616
+ props: {
1617
+ text: {},
1618
+ class: {}
1619
+ },
1620
+ setup(__props) {
1621
+ const props = __props;
1622
+ const ui = computed(() => subtitle({ class: props.class }));
1623
+ return (_ctx, _cache) => {
1624
+ return openBlock(), createElementBlock("p", { class: normalizeClass(ui.value) }, [renderSlot(_ctx.$slots, "default", {}, () => [createTextVNode(toDisplayString(props.text), 1)])], 2);
1625
+ };
1626
+ }
1627
+ });
1628
+ //#endregion
1629
+ //#region src/components/Discussions/DiscussionsSectionSubtitle.vue
1630
+ const discussionsSectionSubtitle = tv({ base: "" });
1631
+ const _sfc_main$16 = /* @__PURE__ */ defineComponent({
1632
+ __name: "DiscussionsSectionSubtitle",
1633
+ props: {
1634
+ comments: {},
1635
+ class: {}
1636
+ },
1637
+ setup(__props) {
1638
+ const props = __props;
1639
+ const { t } = useLocale();
1640
+ const commentsSubtitle = computed(() => {
1641
+ const count = props.comments.length;
1642
+ if (count !== 1) return t("discussions.DiscussionsSectionSubtitle.comments.plural", { count });
1643
+ return t("discussions.DiscussionsSectionSubtitle.comments.singular", { count });
1644
+ });
1645
+ const repliesSubtitle = computed(() => {
1646
+ const count = props.comments.reduce((acc, comment) => acc + (comment.replies?.length ?? 0), 0);
1647
+ if (count !== 1) return t("discussions.DiscussionsSectionSubtitle.replies.plural", { count });
1648
+ return t("discussions.DiscussionsSectionSubtitle.replies.singular", { count });
1649
+ });
1650
+ const ui = computed(() => discussionsSectionSubtitle({ class: props.class }));
1651
+ return (_ctx, _cache) => {
1652
+ return openBlock(), createBlock(_sfc_main$17, { class: normalizeClass(ui.value) }, {
1653
+ default: withCtx(() => [createTextVNode(toDisplayString(commentsSubtitle.value) + " · " + toDisplayString(repliesSubtitle.value), 1)]),
1654
+ _: 1
1655
+ }, 8, ["class"]);
1656
+ };
1657
+ }
1658
+ });
1659
+ //#endregion
1660
+ //#region src/components/Section/SectionTitle.vue
1661
+ const sectionTitle = tv({ slots: { base: "text-lg font-semibold text-default" } });
1662
+ const _sfc_main$15 = /* @__PURE__ */ defineComponent({
1663
+ __name: "SectionTitle",
1664
+ props: {
1665
+ class: {},
1666
+ ui: {}
1667
+ },
1668
+ setup(__props) {
1669
+ const props = __props;
1670
+ const ui = computed(() => sectionTitle());
1671
+ return (_ctx, _cache) => {
1672
+ return openBlock(), createElementBlock("h2", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [renderSlot(_ctx.$slots, "default")], 2);
1673
+ };
1674
+ }
1675
+ });
1676
+ //#endregion
1677
+ //#region src/components/Discussions/DiscussionsSectionTitle.vue
1678
+ const discussionsSectionTitle = tv({ slots: { base: "" } });
1679
+ const _sfc_main$14 = /* @__PURE__ */ defineComponent({
1680
+ __name: "DiscussionsSectionTitle",
1681
+ props: {
1682
+ class: {},
1683
+ ui: {}
1684
+ },
1685
+ setup(__props) {
1686
+ const props = __props;
1687
+ const { t } = useLocale();
1688
+ const ui = computed(() => discussionsSectionTitle());
1689
+ return (_ctx, _cache) => {
1690
+ return openBlock(), createBlock(_sfc_main$15, { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, {
1691
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("discussions.DiscussionsSectionTitle.title")), 1)]),
1692
+ _: 1
1693
+ }, 8, ["class"]);
1694
+ };
1695
+ }
1696
+ });
1697
+ //#endregion
1698
+ //#region src/components/Discussions/DiscussionsSection.vue
1699
+ const _hoisted_1$12 = { id: "comments" };
1700
+ const _hoisted_2 = {
1701
+ key: 0,
1702
+ class: "mt-6"
1703
+ };
1704
+ const discussionsSection = tv({ slots: {
1705
+ base: "",
1706
+ alert: "",
1707
+ header: "mb-6",
1708
+ content: "mt-6"
1709
+ } });
1710
+ const _sfc_main$13 = /* @__PURE__ */ defineComponent({
1711
+ __name: "DiscussionsSection",
1712
+ props: {
1713
+ class: {},
1714
+ ui: {}
1715
+ },
1716
+ setup(__props) {
1717
+ const props = __props;
1718
+ const { t } = useLocale();
1719
+ const reducedMotion = useReducedMotion();
1720
+ const { pageId, locale, banner: bannerController } = useCommentsContext();
1721
+ const { banner, isActive, dismiss } = bannerController;
1722
+ const { data: comments, error: commentsError, isPending: isCommentsPending } = useQuery({
1723
+ key: COMMENT_QUERY_KEYS.byPageId(pageId.value, locale.value),
1724
+ query: () => getComments(pageId.value, locale.value).then((response) => response.data),
1725
+ enabled: typeof window !== "undefined"
1726
+ });
1727
+ const ui = computed(() => discussionsSection());
1728
+ return (_ctx, _cache) => {
1729
+ const _component_UAlert = resolveComponent("UAlert");
1730
+ return openBlock(), createElementBlock("section", _hoisted_1$12, [createElementVNode("div", { class: normalizeClass(ui.value.base({ class: [props.class, props.ui?.base] })) }, [
1731
+ createElementVNode("div", { class: normalizeClass(ui.value.header({ class: props.ui?.header })) }, [createVNode(_sfc_main$14), createVNode(_sfc_main$16, { comments: unref(comments) ?? [] }, null, 8, ["comments"])], 2),
1732
+ createVNode(unref(AnimatePresence), null, {
1733
+ default: withCtx(() => [unref(banner) && unref(isActive) ? (openBlock(), createBlock(unref(Motion), {
1734
+ key: 0,
1735
+ initial: unref(reducedMotion) ? false : {
1736
+ height: 0,
1737
+ opacity: 0
1738
+ },
1739
+ animate: {
1740
+ height: "auto",
1741
+ opacity: 1,
1742
+ transition: {
1743
+ duration: unref(reducedMotion) ? 0 : .2,
1744
+ ease: "easeOut"
1745
+ }
1746
+ },
1747
+ exit: unref(reducedMotion) ? void 0 : {
1748
+ height: 0,
1749
+ opacity: 0
1750
+ }
1751
+ }, {
1752
+ default: withCtx(() => [createVNode(_component_UAlert, {
1753
+ title: unref(banner).message,
1754
+ class: normalizeClass(ui.value.alert({ class: props.ui?.alert })),
1755
+ color: unref(banner).kind === "error" ? "error" : "success",
1756
+ variant: "subtle",
1757
+ close: "",
1758
+ role: unref(banner).kind === "error" ? "alert" : "status",
1759
+ "onUpdate:open": unref(dismiss)
1760
+ }, null, 8, [
1761
+ "title",
1762
+ "class",
1763
+ "color",
1764
+ "role",
1765
+ "onUpdate:open"
1766
+ ])]),
1767
+ _: 1
1768
+ }, 8, [
1769
+ "initial",
1770
+ "animate",
1771
+ "exit"
1772
+ ])) : createCommentVNode("v-if", true)]),
1773
+ _: 1
1774
+ }),
1775
+ createCommentVNode("\n Spacer to preserve layout during the Alert height animation.\n Adding margin to the Alert breaks the smooth transition because\n the animated element collapses abruptly. We render an empty\n div with `mt-6` while the Alert is active to keep the animation smooth.\n "),
1776
+ unref(banner) && unref(isActive) ? (openBlock(), createElementBlock("div", _hoisted_2)) : createCommentVNode("v-if", true),
1777
+ unref(isCommentsPending) ? (openBlock(), createBlock(_sfc_main$28, { key: 1 })) : unref(commentsError) ? (openBlock(), createBlock(_sfc_main$29, { key: 2 })) : !(unref(comments) ?? []).length ? (openBlock(), createBlock(_sfc_main$30, {
1778
+ key: 3,
1779
+ text: unref(t)("discussions.DiscussionsSection.empty")
1780
+ }, null, 8, ["text"])) : (openBlock(), createBlock(_sfc_main$18, {
1781
+ key: 4,
1782
+ comments: unref(comments) ?? []
1783
+ }, null, 8, ["comments"])),
1784
+ createVNode(_sfc_main$31, { class: "mt-12" })
1785
+ ], 2)]);
1786
+ };
1787
+ }
1788
+ });
1789
+ //#endregion
1790
+ //#region src/components/Comments.vue
1791
+ const _sfc_main = /* @__PURE__ */ defineComponent({
1792
+ __name: "Comments",
1793
+ props: {
1794
+ pageId: {},
1795
+ locale: { default: "en" },
1796
+ class: {},
1797
+ ui: {}
1798
+ },
1799
+ setup(__props) {
1800
+ const props = __props;
1801
+ provide(localeContextInjectionKey, computed(() => extendLocale(props.locale === "fr" ? fr_default : en_default, { messages: {} })));
1802
+ const banner = useCommentsBanner();
1803
+ provideCommentsContext({
1804
+ pageId: toRef(() => props.pageId),
1805
+ locale: toRef(() => props.locale),
1806
+ banner
1807
+ });
1808
+ return (_ctx, _cache) => {
1809
+ return openBlock(), createBlock(_sfc_main$13, {
1810
+ class: normalizeClass(props.class),
1811
+ ui: props.ui
1812
+ }, null, 8, ["class", "ui"]);
1813
+ };
1814
+ }
1815
+ });
1816
+ //#endregion
1817
+ //#region src/components/Container.vue
1818
+ const container = tv({ slots: {
1819
+ base: "px-4 sm:px-6 lg:px-8",
1820
+ inner: "w-full max-w-7xl mx-auto"
1821
+ } });
1822
+ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
1823
+ __name: "Container",
1824
+ props: {
1825
+ class: {},
1826
+ ui: {}
1827
+ },
1828
+ setup(__props) {
1829
+ const props = __props;
1830
+ const ui = computed(() => container());
1831
+ return (_ctx, _cache) => {
1832
+ return openBlock(), createElementBlock("div", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [createElementVNode("div", { class: normalizeClass(ui.value.inner({ class: props.ui?.inner })) }, [renderSlot(_ctx.$slots, "default")], 2)], 2);
1833
+ };
1834
+ }
1835
+ });
1836
+ //#endregion
1837
+ //#region ~icons/ph/thumbs-up
1838
+ const _hoisted_1$11 = {
1839
+ viewBox: "0 0 256 256",
1840
+ width: "1.2em",
1841
+ height: "1.2em"
1842
+ };
1843
+ function render$7(_ctx, _cache) {
1844
+ return openBlock(), createElementBlock("svg", _hoisted_1$11, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
1845
+ fill: "currentColor",
1846
+ d: "M234 80.12A24 24 0 0 0 216 72h-56V56a40 40 0 0 0-40-40a8 8 0 0 0-7.16 4.42L75.06 96H32a16 16 0 0 0-16 16v88a16 16 0 0 0 16 16h172a24 24 0 0 0 23.82-21l12-96A24 24 0 0 0 234 80.12M32 112h40v88H32Zm191.94-15l-12 96a8 8 0 0 1-7.94 7H88v-94.11l36.71-73.43A24 24 0 0 1 144 56v24a8 8 0 0 0 8 8h64a8 8 0 0 1 7.94 9"
1847
+ }, null, -1)])]);
1848
+ }
1849
+ var thumbs_up_default = markRaw({
1850
+ name: "ph-thumbs-up",
1851
+ render: render$7
1852
+ });
1853
+ //#endregion
1854
+ //#region src/composables/useUmami.ts
1855
+ function useUmami() {
1856
+ function track(event, data) {
1857
+ if (typeof window === "undefined") return;
1858
+ window.umami?.track(event, {
1859
+ ...data,
1860
+ page_path: window.location.pathname
1861
+ });
1862
+ }
1863
+ return { track };
1864
+ }
1865
+ //#endregion
1866
+ //#region ~icons/ph/check-circle
1867
+ const _hoisted_1$10 = {
1868
+ viewBox: "0 0 256 256",
1869
+ width: "1.2em",
1870
+ height: "1.2em"
1871
+ };
1872
+ function render$6(_ctx, _cache) {
1873
+ return openBlock(), createElementBlock("svg", _hoisted_1$10, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
1874
+ fill: "currentColor",
1875
+ d: "M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88"
1876
+ }, null, -1)])]);
1877
+ }
1878
+ var check_circle_default = markRaw({
1879
+ name: "ph-check-circle",
1880
+ render: render$6
1881
+ });
1882
+ //#endregion
1883
+ //#region src/api/feedback.ts
1884
+ function postFeedback(pageId, rating, content) {
1885
+ return api(`/api/pages/${pageId}/feedback`, {
1886
+ method: "POST",
1887
+ body: {
1888
+ rating,
1889
+ content
1890
+ }
1891
+ });
1892
+ }
1893
+ //#endregion
1894
+ //#region src/components/Feedback/FeedbackCard.vue
1895
+ const _hoisted_1$9 = ["for"];
1896
+ const feedbackCard = tv({ slots: {
1897
+ base: "relative w-64",
1898
+ successfullySentOverlay: "absolute z-10 inset-0 bg-default",
1899
+ successfullySentContent: "absolute z-20 inset-0 flex flex-col justify-center items-center text-muted text-xs",
1900
+ successfullySentIcon: "size-5",
1901
+ input: "w-full",
1902
+ radioGroup: "flex flex-row gap-2 text-lg",
1903
+ radioGroupItem: "peer",
1904
+ radioGroupLabel: "grayscale-100 hover:grayscale-0 peer-data-[state=checked]:grayscale-0"
1905
+ } });
1906
+ const _sfc_main$3 = /* @__PURE__ */ defineComponent({
1907
+ __name: "FeedbackCard",
1908
+ props: /* @__PURE__ */ mergeModels({
1909
+ id: {},
1910
+ class: {},
1911
+ ui: {}
1912
+ }, {
1913
+ "content": { required: true },
1914
+ "contentModifiers": {},
1915
+ "rating": { required: true },
1916
+ "ratingModifiers": {}
1917
+ }),
1918
+ emits: /* @__PURE__ */ mergeModels(["success"], ["update:content", "update:rating"]),
1919
+ setup(__props, { emit: __emit }) {
1920
+ const props = __props;
1921
+ const emits = __emit;
1922
+ const content = useModel(__props, "content");
1923
+ const rating = useModel(__props, "rating");
1924
+ const ratings = [
1925
+ {
1926
+ label: "😭",
1927
+ value: "Hate it",
1928
+ title: "FeedbackCard.ratings.hate"
1929
+ },
1930
+ {
1931
+ label: "🙁",
1932
+ value: "Not great",
1933
+ title: "FeedbackCard.ratings.poor"
1934
+ },
1935
+ {
1936
+ label: "🙂",
1937
+ value: "It's ok",
1938
+ title: "FeedbackCard.ratings.okay"
1939
+ },
1940
+ {
1941
+ label: "🤩",
1942
+ value: "Love it",
1943
+ title: "FeedbackCard.ratings.love"
1944
+ }
1945
+ ];
1946
+ const successfullySent = ref(false);
1947
+ const { t } = useLocale();
1948
+ const { track } = useUmami();
1949
+ const { mutate, isLoading, error } = useMutation({
1950
+ mutation: ({ rating, content }) => postFeedback(props.id, rating, content),
1951
+ onSuccess: () => {
1952
+ successfullySent.value = true;
1953
+ track("feedback_submit");
1954
+ setTimeout(emits, 200, "success");
1955
+ }
1956
+ });
1957
+ function sendFeedback() {
1958
+ mutate({
1959
+ rating: rating.value,
1960
+ content: content.value
1961
+ });
1962
+ }
1963
+ const formattedError = computed(() => {
1964
+ if (!error.value) return void 0;
1965
+ if (error.value.data?.errors) return error.value.data.errors.content?.[0] || error.value.data.errors.rating?.[0];
1966
+ if (error.value.status === 404) return t("FeedbackCard.errors.notFound");
1967
+ if (error.value.status === 503) return t("FeedbackCard.errors.unavailable");
1968
+ return t("FeedbackCard.errors.unexpected");
1969
+ });
1970
+ const ui = computed(() => feedbackCard());
1971
+ return (_ctx, _cache) => {
1972
+ const _component_UIcon = resolveComponent("UIcon");
1973
+ const _component_UTextarea = resolveComponent("UTextarea");
1974
+ const _component_UFormField = resolveComponent("UFormField");
1975
+ const _component_UTooltip = resolveComponent("UTooltip");
1976
+ const _component_UButton = resolveComponent("UButton");
1977
+ const _component_UCard = resolveComponent("UCard");
1978
+ return openBlock(), createBlock(_component_UCard, {
1979
+ class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })),
1980
+ ui: {
1981
+ body: "p-2 sm:p-2",
1982
+ footer: "p-2 sm:p-2 flex items-center justify-between"
1983
+ }
1984
+ }, {
1985
+ footer: withCtx(() => [createVNode(unref(RadioGroupRoot), {
1986
+ modelValue: rating.value,
1987
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => rating.value = $event),
1988
+ class: normalizeClass(ui.value.radioGroup({ class: props.ui?.radioGroup }))
1989
+ }, {
1990
+ default: withCtx(() => [(openBlock(), createElementBlock(Fragment, null, renderList(ratings, (item) => {
1991
+ return createElementVNode("div", { key: item.value }, [createVNode(unref(RadioGroupItem), {
1992
+ id: item.value,
1993
+ value: item.value,
1994
+ class: normalizeClass(ui.value.radioGroupItem({ class: props.ui?.radioGroupItem }))
1995
+ }, {
1996
+ default: withCtx(() => [createVNode(unref(RadioGroupIndicator))]),
1997
+ _: 1
1998
+ }, 8, [
1999
+ "id",
2000
+ "value",
2001
+ "class"
2002
+ ]), createVNode(_component_UTooltip, { text: unref(t)(item.title) }, {
2003
+ default: withCtx(() => [createElementVNode("label", {
2004
+ for: item.value,
2005
+ class: normalizeClass(ui.value.radioGroupLabel({ class: props.ui?.radioGroupLabel }))
2006
+ }, toDisplayString(item.label), 11, _hoisted_1$9)]),
2007
+ _: 2
2008
+ }, 1032, ["text"])]);
2009
+ }), 64))]),
2010
+ _: 1
2011
+ }, 8, ["modelValue", "class"]), createVNode(_component_UButton, {
2012
+ size: "sm",
2013
+ label: unref(t)("FeedbackCard.action"),
2014
+ loading: unref(isLoading),
2015
+ onClick: sendFeedback
2016
+ }, null, 8, ["label", "loading"])]),
2017
+ default: withCtx(() => [successfullySent.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createVNode(unref(motion).div, {
2018
+ initial: { opacity: 0 },
2019
+ animate: {
2020
+ opacity: 1,
2021
+ transition: { duration: .2 }
2022
+ },
2023
+ class: normalizeClass(ui.value.successfullySentOverlay({ class: props.ui?.successfullySentOverlay }))
2024
+ }, null, 8, ["class"]), createElementVNode("div", { class: normalizeClass(ui.value.successfullySentContent({ class: props.ui?.successfullySentContent })) }, [
2025
+ createVNode(unref(motion).div, {
2026
+ initial: {
2027
+ opacity: 0,
2028
+ transform: "translateY(4px)"
2029
+ },
2030
+ animate: {
2031
+ opacity: 1,
2032
+ transform: "translateY(0)",
2033
+ transition: {
2034
+ delay: .1,
2035
+ duration: .3
2036
+ }
2037
+ }
2038
+ }, {
2039
+ default: withCtx(() => [createVNode(_component_UIcon, {
2040
+ name: unref(check_circle_default),
2041
+ class: normalizeClass(ui.value.successfullySentIcon({ class: props.ui?.successfullySentIcon }))
2042
+ }, null, 8, ["name", "class"])]),
2043
+ _: 1
2044
+ }),
2045
+ createVNode(unref(motion).p, {
2046
+ initial: {
2047
+ opacity: 0,
2048
+ transform: "translateY(4px)"
2049
+ },
2050
+ animate: {
2051
+ opacity: 1,
2052
+ transform: "translateY(0)",
2053
+ transition: {
2054
+ delay: .2,
2055
+ duration: .3
2056
+ }
2057
+ },
2058
+ class: "mt-3"
2059
+ }, {
2060
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("FeedbackCard.success.received")), 1)]),
2061
+ _: 1
2062
+ }),
2063
+ createVNode(unref(motion).p, {
2064
+ initial: {
2065
+ opacity: 0,
2066
+ transform: "translateY(4px)"
2067
+ },
2068
+ animate: {
2069
+ opacity: 1,
2070
+ transform: "translateY(0)",
2071
+ transition: {
2072
+ delay: .3,
2073
+ duration: .3
2074
+ }
2075
+ },
2076
+ class: "mt-1"
2077
+ }, {
2078
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("FeedbackCard.success.thanks")), 1)]),
2079
+ _: 1
2080
+ })
2081
+ ], 2)], 64)) : createCommentVNode("v-if", true), createVNode(_component_UFormField, { error: formattedError.value }, {
2082
+ error: withCtx(({ error: formFieldError }) => [formFieldError ? (openBlock(), createBlock(unref(motion).div, {
2083
+ key: 0,
2084
+ initial: { height: 0 },
2085
+ animate: { height: "auto" },
2086
+ class: "text-sm"
2087
+ }, {
2088
+ default: withCtx(() => [createTextVNode(toDisplayString(formFieldError), 1)]),
2089
+ _: 2
2090
+ }, 1024)) : createCommentVNode("v-if", true)]),
2091
+ default: withCtx(() => [createVNode(_component_UTextarea, {
2092
+ modelValue: content.value,
2093
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => content.value = $event),
2094
+ placeholder: unref(t)("FeedbackCard.placeholder"),
2095
+ variant: "soft",
2096
+ class: normalizeClass(ui.value.input({ class: props.ui?.input }))
2097
+ }, null, 8, [
2098
+ "modelValue",
2099
+ "placeholder",
2100
+ "class"
2101
+ ])]),
2102
+ _: 1
2103
+ }, 8, ["error"])]),
2104
+ _: 1
2105
+ }, 8, ["class"]);
2106
+ };
2107
+ }
2108
+ });
2109
+ //#endregion
2110
+ //#region src/components/Feedback/Feedback.vue
2111
+ const feedback = tv({ slots: { base: "px-0 py-1 text-dimmed text-sm" } });
2112
+ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
2113
+ __name: "Feedback",
2114
+ props: {
2115
+ id: {},
2116
+ class: {},
2117
+ ui: {}
2118
+ },
2119
+ setup(__props) {
2120
+ const props = __props;
2121
+ const { t } = useLocale();
2122
+ const { track } = useUmami();
2123
+ function onClick() {
2124
+ track("feedback_click");
2125
+ }
2126
+ const content = ref("");
2127
+ const rating = ref("");
2128
+ function onSuccess() {
2129
+ content.value = "";
2130
+ rating.value = "";
2131
+ }
2132
+ const ui = computed(() => feedback());
2133
+ return (_ctx, _cache) => {
2134
+ const _component_UButton = resolveComponent("UButton");
2135
+ const _component_UPopover = resolveComponent("UPopover");
2136
+ return openBlock(), createBlock(_component_UPopover, { ui: { content: "ring-0 data-[state=open]:animate-[scale-up_100ms_ease-out]" } }, {
2137
+ content: withCtx(() => [createVNode(_sfc_main$3, {
2138
+ id: props.id,
2139
+ rating: rating.value,
2140
+ "onUpdate:rating": _cache[0] || (_cache[0] = ($event) => rating.value = $event),
2141
+ content: content.value,
2142
+ "onUpdate:content": _cache[1] || (_cache[1] = ($event) => content.value = $event),
2143
+ onSuccess
2144
+ }, null, 8, [
2145
+ "id",
2146
+ "rating",
2147
+ "content"
2148
+ ])]),
2149
+ default: withCtx(() => [createVNode(_component_UButton, {
2150
+ variant: "link",
2151
+ color: "neutral",
2152
+ label: unref(t)("Feedback.action"),
2153
+ size: "sm",
2154
+ icon: unref(thumbs_up_default),
2155
+ class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })),
2156
+ onClick
2157
+ }, null, 8, [
2158
+ "label",
2159
+ "icon",
2160
+ "class"
2161
+ ])]),
2162
+ _: 1
2163
+ });
2164
+ };
2165
+ }
2166
+ });
2167
+ //#endregion
2168
+ //#region ~icons/simple-icons/discord
2169
+ const _hoisted_1$8 = {
2170
+ viewBox: "0 0 24 24",
2171
+ width: "1.2em",
2172
+ height: "1.2em"
2173
+ };
2174
+ function render$5(_ctx, _cache) {
2175
+ return openBlock(), createElementBlock("svg", _hoisted_1$8, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
2176
+ fill: "currentColor",
2177
+ d: "M20.317 4.37a19.8 19.8 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.3 18.3 0 0 0-5.487 0a13 13 0 0 0-.617-1.25a.08.08 0 0 0-.079-.037A19.7 19.7 0 0 0 3.677 4.37a.1.1 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.08.08 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.08.08 0 0 0 .084-.028a14 14 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13 13 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10 10 0 0 0 .372-.292a.07.07 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.07.07 0 0 1 .078.01q.181.149.373.292a.077.077 0 0 1-.006.127a12.3 12.3 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.08.08 0 0 0 .084.028a19.8 19.8 0 0 0 6.002-3.03a.08.08 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.06.06 0 0 0-.031-.03M8.02 15.33c-1.182 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418m7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418"
2178
+ }, null, -1)])]);
2179
+ }
2180
+ var discord_default = markRaw({
2181
+ name: "simple-icons-discord",
2182
+ render: render$5
2183
+ });
2184
+ //#endregion
2185
+ //#region ~icons/simple-icons/github
2186
+ const _hoisted_1$7 = {
2187
+ viewBox: "0 0 24 24",
2188
+ width: "1.2em",
2189
+ height: "1.2em"
2190
+ };
2191
+ function render$4(_ctx, _cache) {
2192
+ return openBlock(), createElementBlock("svg", _hoisted_1$7, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
2193
+ fill: "currentColor",
2194
+ d: "M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
2195
+ }, null, -1)])]);
2196
+ }
2197
+ var github_default = markRaw({
2198
+ name: "simple-icons-github",
2199
+ render: render$4
2200
+ });
2201
+ //#endregion
2202
+ //#region ~icons/simple-icons/linkedin
2203
+ const _hoisted_1$6 = {
2204
+ viewBox: "0 0 24 24",
2205
+ width: "1.2em",
2206
+ height: "1.2em"
2207
+ };
2208
+ function render$3(_ctx, _cache) {
2209
+ return openBlock(), createElementBlock("svg", _hoisted_1$6, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
2210
+ fill: "currentColor",
2211
+ d: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037c-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85c3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.06 2.06 0 0 1-2.063-2.065a2.064 2.064 0 1 1 2.063 2.065m1.782 13.019H3.555V9h3.564zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0z"
2212
+ }, null, -1)])]);
2213
+ }
2214
+ var linkedin_default = markRaw({
2215
+ name: "simple-icons-linkedin",
2216
+ render: render$3
2217
+ });
2218
+ //#endregion
2219
+ //#region ~icons/simple-icons/twitch
2220
+ const _hoisted_1$5 = {
2221
+ viewBox: "0 0 24 24",
2222
+ width: "1.2em",
2223
+ height: "1.2em"
2224
+ };
2225
+ function render$2(_ctx, _cache) {
2226
+ return openBlock(), createElementBlock("svg", _hoisted_1$5, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
2227
+ fill: "currentColor",
2228
+ d: "M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"
2229
+ }, null, -1)])]);
2230
+ }
2231
+ var twitch_default = markRaw({
2232
+ name: "simple-icons-twitch",
2233
+ render: render$2
2234
+ });
2235
+ //#endregion
2236
+ //#region ~icons/simple-icons/x
2237
+ const _hoisted_1$4 = {
2238
+ viewBox: "0 0 24 24",
2239
+ width: "1.2em",
2240
+ height: "1.2em"
2241
+ };
2242
+ function render$1(_ctx, _cache) {
2243
+ return openBlock(), createElementBlock("svg", _hoisted_1$4, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
2244
+ fill: "currentColor",
2245
+ d: "M14.234 10.162L22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299l-.929-1.329L3.076 1.56h3.182l5.965 8.532l.929 1.329l7.754 11.09h-3.182z"
2246
+ }, null, -1)])]);
2247
+ }
2248
+ var x_default = markRaw({
2249
+ name: "simple-icons-x",
2250
+ render: render$1
2251
+ });
2252
+ //#endregion
2253
+ //#region src/components/Header.vue
2254
+ const header = tv({ slots: {
2255
+ base: "h-(--ui-header-height) flex flex-row gap-4 items-center justify-end",
2256
+ link: "p-0 text-dimmed"
2257
+ } });
2258
+ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
2259
+ __name: "Header",
2260
+ props: {
2261
+ links: {},
2262
+ class: {},
2263
+ ui: {}
2264
+ },
2265
+ setup(__props) {
2266
+ const props = __props;
2267
+ const { track } = useUmami();
2268
+ function trackClick(label) {
2269
+ track("header_click", { label });
2270
+ }
2271
+ const ui = computed(() => header());
2272
+ return (_ctx, _cache) => {
2273
+ const _component_UButton = resolveComponent("UButton");
2274
+ const _component_UTooltip = resolveComponent("UTooltip");
2275
+ const _component_USeparator = resolveComponent("USeparator");
2276
+ return openBlock(), createBlock(_sfc_main$1, { ui: { inner: "max-w-5xl" } }, {
2277
+ default: withCtx(() => [createElementVNode("header", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [
2278
+ (openBlock(true), createElementBlock(Fragment, null, renderList(props.links, (link) => {
2279
+ return openBlock(), createBlock(_component_UTooltip, {
2280
+ key: link.label,
2281
+ text: link.label
2282
+ }, {
2283
+ default: withCtx(() => [createVNode(_component_UButton, {
2284
+ variant: "link",
2285
+ color: "neutral",
2286
+ to: link.to,
2287
+ href: link.href,
2288
+ "aria-label": link.label,
2289
+ icon: link.icon,
2290
+ target: link.target,
2291
+ rel: link.rel,
2292
+ class: normalizeClass(ui.value.link({ class: props.ui?.link })),
2293
+ onClick: ($event) => trackClick(link.label)
2294
+ }, null, 8, [
2295
+ "to",
2296
+ "href",
2297
+ "aria-label",
2298
+ "icon",
2299
+ "target",
2300
+ "rel",
2301
+ "class",
2302
+ "onClick"
2303
+ ])]),
2304
+ _: 2
2305
+ }, 1032, ["text"]);
2306
+ }), 128)),
2307
+ createVNode(_component_USeparator, {
2308
+ orientation: "vertical",
2309
+ class: "h-5"
2310
+ }),
2311
+ createVNode(_component_UTooltip, { text: "LinkedIn" }, {
2312
+ default: withCtx(() => [createVNode(_component_UButton, {
2313
+ href: "https://www.linkedin.com/in/esteban25/",
2314
+ target: "_blank",
2315
+ variant: "link",
2316
+ color: "neutral",
2317
+ "aria-label": "LinkedIn",
2318
+ icon: unref(linkedin_default),
2319
+ class: normalizeClass(ui.value.link({ class: props.ui?.link })),
2320
+ ui: { leadingIcon: "size-4" },
2321
+ onClick: _cache[0] || (_cache[0] = ($event) => trackClick("LinkedIn"))
2322
+ }, null, 8, ["icon", "class"])]),
2323
+ _: 1
2324
+ }),
2325
+ createVNode(_component_UTooltip, { text: "X" }, {
2326
+ default: withCtx(() => [createVNode(_component_UButton, {
2327
+ href: "https://x.com/soubiran_",
2328
+ target: "_blank",
2329
+ variant: "link",
2330
+ color: "neutral",
2331
+ "aria-label": "X",
2332
+ icon: unref(x_default),
2333
+ class: normalizeClass(ui.value.link({ class: props.ui?.link })),
2334
+ ui: { leadingIcon: "size-4" },
2335
+ onClick: _cache[1] || (_cache[1] = ($event) => trackClick("X"))
2336
+ }, null, 8, ["icon", "class"])]),
2337
+ _: 1
2338
+ }),
2339
+ createVNode(_component_UTooltip, { text: "Twitch" }, {
2340
+ default: withCtx(() => [createVNode(_component_UButton, {
2341
+ href: "https://www.twitch.tv/barbapapazes",
2342
+ target: "_blank",
2343
+ variant: "link",
2344
+ color: "neutral",
2345
+ "aria-label": "Twitch",
2346
+ icon: unref(twitch_default),
2347
+ class: normalizeClass(ui.value.link({ class: props.ui?.link })),
2348
+ ui: { leadingIcon: "size-4" },
2349
+ onClick: _cache[2] || (_cache[2] = ($event) => trackClick("Twitch"))
2350
+ }, null, 8, ["icon", "class"])]),
2351
+ _: 1
2352
+ }),
2353
+ createVNode(_component_UTooltip, { text: "GitHub" }, {
2354
+ default: withCtx(() => [createVNode(_component_UButton, {
2355
+ href: "https://github.com/barbapapazes",
2356
+ target: "_blank",
2357
+ variant: "link",
2358
+ color: "neutral",
2359
+ "aria-label": "GitHub",
2360
+ icon: unref(github_default),
2361
+ class: normalizeClass(ui.value.link({ class: props.ui?.link })),
2362
+ ui: { leadingIcon: "size-4" },
2363
+ onClick: _cache[3] || (_cache[3] = ($event) => trackClick("GitHub"))
2364
+ }, null, 8, ["icon", "class"])]),
2365
+ _: 1
2366
+ }),
2367
+ createVNode(_component_UTooltip, { text: "Discord" }, {
2368
+ default: withCtx(() => [createVNode(_component_UButton, {
2369
+ href: "https://discord.gg/q2ghCGUuFR",
2370
+ target: "_blank",
2371
+ variant: "link",
2372
+ color: "neutral",
2373
+ "aria-label": "Discord",
2374
+ icon: unref(discord_default),
2375
+ class: normalizeClass(ui.value.link({ class: props.ui?.link })),
2376
+ ui: { leadingIcon: "size-4" },
2377
+ onClick: _cache[4] || (_cache[4] = ($event) => trackClick("Discord"))
2378
+ }, null, 8, ["icon", "class"])]),
2379
+ _: 1
2380
+ })
2381
+ ], 2)]),
2382
+ _: 1
2383
+ });
2384
+ };
2385
+ }
2386
+ });
2387
+ //#endregion
2388
+ //#region src/components/LoginCallback.vue
2389
+ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
2390
+ __name: "LoginCallback",
2391
+ props: { class: {} },
2392
+ setup(__props) {
2393
+ const props = __props;
2394
+ const { t } = useLocale();
2395
+ const status = ref("pending");
2396
+ onMounted(() => {
2397
+ const nonce = new URL(window.location.href).searchParams.get(LOGIN_CALLBACK_NONCE_PARAM);
2398
+ if (!nonce || !window.opener || window.opener.closed) {
2399
+ status.value = "error";
2400
+ return;
2401
+ }
2402
+ try {
2403
+ window.opener.postMessage({
2404
+ type: LOGIN_CALLBACK_MESSAGE_TYPE,
2405
+ nonce
2406
+ }, window.location.origin);
2407
+ status.value = "success";
2408
+ window.close();
2409
+ } catch {
2410
+ status.value = "error";
2411
+ }
2412
+ });
2413
+ return (_ctx, _cache) => {
2414
+ return openBlock(), createElementBlock("div", { class: normalizeClass(props.class) }, [status.value === "error" ? renderSlot(_ctx.$slots, "error", {
2415
+ key: 0,
2416
+ message: unref(t)("LoginCallback.error")
2417
+ }, () => [createElementVNode("p", null, toDisplayString(unref(t)("LoginCallback.error")), 1)]) : renderSlot(_ctx.$slots, "success", {
2418
+ key: 1,
2419
+ message: unref(t)("LoginCallback.success")
2420
+ }, () => [createElementVNode("p", null, toDisplayString(unref(t)("LoginCallback.success")), 1)])], 2);
2421
+ };
2422
+ }
2423
+ });
2424
+ //#endregion
2425
+ //#region src/components/Page/Page.vue
2426
+ const page = tv({ slots: {
2427
+ base: "py-12",
2428
+ header: "max-w-3xl mx-auto xl:max-w-none xl:grid xl:grid-cols-[256px_768px_256px]",
2429
+ headerInner: "xl:col-start-2",
2430
+ content: "mt-6 max-w-3xl mx-auto xl:max-w-none xl:grid xl:grid-cols-[256px_768px_256px] xl:mx-auto",
2431
+ contentInner: "xl:col-start-2",
2432
+ right: "hidden xl:block xl:col-start-3 xl:pl-8 xl:h-full",
2433
+ rightInner: "sticky top-4"
2434
+ } });
2435
+ const _sfc_main$6 = /* @__PURE__ */ defineComponent({
2436
+ __name: "Page",
2437
+ props: {
2438
+ class: {},
2439
+ ui: {}
2440
+ },
2441
+ setup(__props) {
2442
+ const props = __props;
2443
+ const ui = computed(() => page());
2444
+ return (_ctx, _cache) => {
2445
+ return openBlock(), createElementBlock("div", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [createVNode(_sfc_main$1, null, {
2446
+ default: withCtx(() => [createElementVNode("div", { class: normalizeClass(ui.value.header({ class: props.ui?.header })) }, [createElementVNode("div", { class: normalizeClass(ui.value.headerInner({ class: props.ui?.headerInner })) }, [renderSlot(_ctx.$slots, "header")], 2)], 2), createElementVNode("div", { class: normalizeClass(ui.value.content({ class: props.ui?.content })) }, [createElementVNode("div", { class: normalizeClass(ui.value.contentInner({ class: props.ui?.contentInner })) }, [renderSlot(_ctx.$slots, "default")], 2), createElementVNode("div", { class: normalizeClass(ui.value.right({ class: props.ui?.right })) }, [createElementVNode("div", { class: normalizeClass(ui.value.rightInner({ class: props.ui?.rightInner })) }, [renderSlot(_ctx.$slots, "right")], 2)], 2)], 2)]),
2447
+ _: 3
2448
+ }), renderSlot(_ctx.$slots, "bottom")], 2);
2449
+ };
2450
+ }
2451
+ });
2452
+ //#endregion
2453
+ //#region src/components/Page/PageTitle.vue
2454
+ const pageTitle = tv({ slots: { base: "text-xl font-bold text-highlighted" } });
2455
+ const _sfc_main$8 = /* @__PURE__ */ defineComponent({
2456
+ __name: "PageTitle",
2457
+ props: {
2458
+ title: {},
2459
+ class: {},
2460
+ ui: {}
2461
+ },
2462
+ setup(__props) {
2463
+ const props = __props;
2464
+ const ui = computed(() => pageTitle());
2465
+ return (_ctx, _cache) => {
2466
+ return openBlock(), createElementBlock("h1", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, toDisplayString(__props.title), 3);
2467
+ };
2468
+ }
2469
+ });
2470
+ //#endregion
2471
+ //#region src/components/Page/PageHeader.vue
2472
+ const pageHeader = tv({ slots: { base: "" } });
2473
+ const _sfc_main$7 = /* @__PURE__ */ defineComponent({
2474
+ __name: "PageHeader",
2475
+ props: {
2476
+ title: {},
2477
+ class: {},
2478
+ ui: {}
2479
+ },
2480
+ setup(__props) {
2481
+ const props = __props;
2482
+ const ui = computed(() => pageHeader());
2483
+ return (_ctx, _cache) => {
2484
+ return openBlock(), createElementBlock("div", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [createVNode(_sfc_main$8, { title: props.title }, null, 8, ["title"]), renderSlot(_ctx.$slots, "after")], 2);
2485
+ };
2486
+ }
2487
+ });
2488
+ //#endregion
2489
+ //#region ~icons/simple-icons/youtube
2490
+ const _hoisted_1$3 = {
2491
+ viewBox: "0 0 24 24",
2492
+ width: "1.2em",
2493
+ height: "1.2em"
2494
+ };
2495
+ function render(_ctx, _cache) {
2496
+ return openBlock(), createElementBlock("svg", _hoisted_1$3, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
2497
+ fill: "currentColor",
2498
+ d: "M23.498 6.186a3.02 3.02 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.02 3.02 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.02 3.02 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.02 3.02 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814M9.545 15.568V8.432L15.818 12z"
2499
+ }, null, -1)])]);
2500
+ }
2501
+ var youtube_default = markRaw({
2502
+ name: "simple-icons-youtube",
2503
+ render
2504
+ });
2505
+ //#endregion
2506
+ //#region src/components/Socials.vue
2507
+ const socials = tv({ slots: {
2508
+ base: "flex flex-wrap gap-2",
2509
+ link: "flex items-center gap-1",
2510
+ icon: "size-4"
2511
+ } });
2512
+ const _sfc_main$9 = /* @__PURE__ */ defineComponent({
2513
+ __name: "Socials",
2514
+ props: {
2515
+ class: {},
2516
+ ui: {}
2517
+ },
2518
+ setup(__props) {
2519
+ const props = __props;
2520
+ const ui = computed(() => socials());
2521
+ return (_ctx, _cache) => {
2522
+ const _component_UIcon = resolveComponent("UIcon");
2523
+ return openBlock(), createElementBlock("p", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [
2524
+ createElementVNode("a", {
2525
+ href: "https://www.linkedin.com/in/esteban25/",
2526
+ rel: "noopener noreferrer",
2527
+ target: "_blank",
2528
+ class: normalizeClass(ui.value.link({ class: props.ui?.link }))
2529
+ }, [createVNode(_component_UIcon, {
2530
+ name: unref(linkedin_default),
2531
+ class: normalizeClass(ui.value.icon({ class: props.ui?.icon }))
2532
+ }, null, 8, ["name", "class"]), _cache[0] || (_cache[0] = createElementVNode("span", null, "LinkedIn", -1))], 2),
2533
+ createElementVNode("a", {
2534
+ href: "https://x.com/soubiran_",
2535
+ rel: "noopener noreferrer",
2536
+ target: "_blank",
2537
+ class: normalizeClass(ui.value.link({ class: props.ui?.link }))
2538
+ }, [createVNode(_component_UIcon, {
2539
+ name: unref(x_default),
2540
+ class: normalizeClass(ui.value.icon({ class: props.ui?.icon }))
2541
+ }, null, 8, ["name", "class"]), _cache[1] || (_cache[1] = createElementVNode("span", null, "X (Twitter)", -1))], 2),
2542
+ createElementVNode("a", {
2543
+ href: "https://github.com/barbapapazes",
2544
+ rel: "noopener noreferrer",
2545
+ target: "_blank",
2546
+ class: normalizeClass(ui.value.link({ class: props.ui?.link }))
2547
+ }, [createVNode(_component_UIcon, {
2548
+ name: unref(github_default),
2549
+ class: normalizeClass(ui.value.icon({ class: props.ui?.icon }))
2550
+ }, null, 8, ["name", "class"]), _cache[2] || (_cache[2] = createElementVNode("span", null, "GitHub", -1))], 2),
2551
+ createElementVNode("a", {
2552
+ href: "https://www.twitch.tv/barbapapazes",
2553
+ rel: "noopener noreferrer",
2554
+ target: "_blank",
2555
+ class: normalizeClass(ui.value.link({ class: props.ui?.link }))
2556
+ }, [createVNode(_component_UIcon, {
2557
+ name: unref(twitch_default),
2558
+ class: normalizeClass(ui.value.icon({ class: props.ui?.icon }))
2559
+ }, null, 8, ["name", "class"]), _cache[3] || (_cache[3] = createElementVNode("span", null, "Twitch", -1))], 2),
2560
+ createElementVNode("a", {
2561
+ href: "https://www.youtube.com/@barbapapazes",
2562
+ target: "_blank",
2563
+ rel: "noopener noreferrer",
2564
+ class: normalizeClass(ui.value.link({ class: props.ui?.link }))
2565
+ }, [createVNode(_component_UIcon, {
2566
+ name: unref(youtube_default),
2567
+ class: normalizeClass(ui.value.icon({ class: props.ui?.icon }))
2568
+ }, null, 8, ["name", "class"]), _cache[4] || (_cache[4] = createElementVNode("span", null, "YouTube", -1))], 2)
2569
+ ], 2);
2570
+ };
2571
+ }
2572
+ });
2573
+ //#endregion
2574
+ //#region src/components/Sponsors.vue
2575
+ const _hoisted_1$2 = {
2576
+ href: "https://soubiran.dev/sponsorship?utm_source=infra.soubiran.dev&utm_medium=link",
2577
+ target: "_blank"
2578
+ };
2579
+ const sponsors = tv({ slots: {
2580
+ base: "not-prose",
2581
+ img: "border-0"
2582
+ } });
2583
+ const _sfc_main$10 = /* @__PURE__ */ defineComponent({
2584
+ __name: "Sponsors",
2585
+ props: {
2586
+ class: {},
2587
+ ui: {}
2588
+ },
2589
+ setup(__props) {
2590
+ const props = __props;
2591
+ const ui = computed(() => sponsors());
2592
+ return (_ctx, _cache) => {
2593
+ return openBlock(), createElementBlock("p", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [createElementVNode("a", _hoisted_1$2, [createElementVNode("img", {
2594
+ src: "https://raw.githubusercontent.com/Barbapapazes/static/refs/heads/main/sponsors.svg",
2595
+ alt: "Sponsors",
2596
+ class: normalizeClass(ui.value.img({ class: props.ui?.img }))
2597
+ }, null, 2)])], 2);
2598
+ };
2599
+ }
2600
+ });
2601
+ //#endregion
2602
+ //#region src/composables/useTableOfContents.ts
2603
+ function _useTableOfContents() {
2604
+ const activeHeadings = ref([]);
2605
+ function setActive(id) {
2606
+ activeHeadings.value.push(id);
2607
+ }
2608
+ function unsetActive(id) {
2609
+ activeHeadings.value = activeHeadings.value.filter((h) => h !== id);
2610
+ }
2611
+ return {
2612
+ setActive,
2613
+ unsetActive,
2614
+ activeHeadings: readonly(activeHeadings)
2615
+ };
2616
+ }
2617
+ var useTableOfContents_default = createSharedComposable(_useTableOfContents);
2618
+ //#endregion
2619
+ //#region src/components/TableOfContents.vue
2620
+ const _hoisted_1$1 = [
2621
+ "href",
2622
+ "data-active",
2623
+ "onClick"
2624
+ ];
2625
+ const tableOfContents = tv({ slots: {
2626
+ base: "space-y-1 text-sm text-dimmed font-medium",
2627
+ title: "",
2628
+ list: "font-sofia",
2629
+ link: "flex gap-1 px-0 py-1 text-dimmed hover:text-default active:text-default transition-colors focus:outline-none focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-inverted data-active:text-default rounded-md"
2630
+ } });
2631
+ const _sfc_main$11 = /* @__PURE__ */ defineComponent({
2632
+ __name: "TableOfContents",
2633
+ props: {
2634
+ toc: {},
2635
+ class: {},
2636
+ ui: {}
2637
+ },
2638
+ setup(__props) {
2639
+ const props = __props;
2640
+ const { track } = useUmami();
2641
+ function trackClick(text, hash) {
2642
+ track("table_of_contents_click", {
2643
+ toc_text: text,
2644
+ toc_hash: hash
2645
+ });
2646
+ }
2647
+ const { activeHeadings } = useTableOfContents_default();
2648
+ const ui = computed(() => tableOfContents());
2649
+ return (_ctx, _cache) => {
2650
+ return openBlock(), createElementBlock("div", { class: normalizeClass(ui.value.base({ class: [props.ui?.base, props.class] })) }, [createElementVNode("div", { class: normalizeClass(ui.value.title({ class: props.ui?.title })) }, " Table of Contents ", 2), createElementVNode("ul", { class: normalizeClass(ui.value.list({ class: props.ui?.list })) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(props.toc, (item, index) => {
2651
+ return openBlock(), createElementBlock("li", { key: item.anchor || index }, [createCommentVNode(" Cannot use RouterLink as the anchor will be prefixed with the route path. "), item.anchor ? (openBlock(), createElementBlock("a", {
2652
+ key: 0,
2653
+ custom: "",
2654
+ variant: "link",
2655
+ color: "neutral",
2656
+ href: `#${item.anchor}`,
2657
+ "data-active": unref(activeHeadings).includes(item.anchor || "") ? true : void 0,
2658
+ class: normalizeClass(ui.value.link({ class: props.ui?.link })),
2659
+ onClick: ($event) => trackClick(item.text, item.anchor)
2660
+ }, [createElementVNode("span", null, toDisplayString(index + 1) + ".", 1), createTextVNode(" " + toDisplayString(item.text), 1)], 10, _hoisted_1$1)) : createCommentVNode("v-if", true)]);
2661
+ }), 128))], 2)], 2);
2662
+ };
2663
+ }
2664
+ });
2665
+ //#endregion
2666
+ //#region src/components/ViewersCounter.vue
2667
+ const _hoisted_1 = ["title"];
2668
+ const viewersCounter = tv({ slots: {
2669
+ base: "flex items-center justify-center gap-[0.375rem] border border-green-400 rounded-full px-[0.375rem] py-[0.125rem] text-xs text-muted font-light leading-3 dark:border-green-700",
2670
+ dot: "inline-block h-[0.375rem] w-[0.375rem] animate-[pulse_4s_cubic-bezier(0.4,_0,_0.6,_1)_infinite] rounded-full bg-green-400 ring ring-2 ring-green-200 dark:bg-green-700 dark:ring-green-500"
2671
+ } });
2672
+ const _sfc_main$12 = /* @__PURE__ */ defineComponent({
2673
+ __name: "ViewersCounter",
2674
+ props: {
2675
+ class: {},
2676
+ ui: {}
2677
+ },
2678
+ setup(__props) {
2679
+ const props = __props;
2680
+ const count = ref(0);
2681
+ const connections = ref({});
2682
+ let socket;
2683
+ function connect() {
2684
+ return new PartySocket({
2685
+ host: import.meta.env.VITE_PARTYKIT_URL,
2686
+ room: "soubiran-dev"
2687
+ });
2688
+ }
2689
+ function handleMessage(event) {
2690
+ if (typeof event.data !== "string") return;
2691
+ let data;
2692
+ try {
2693
+ data = JSON.parse(event.data);
2694
+ } catch {
2695
+ return;
2696
+ }
2697
+ count.value = data.count;
2698
+ connections.value = data.connections;
2699
+ }
2700
+ onMounted(() => {
2701
+ socket = connect();
2702
+ socket.addEventListener("message", handleMessage);
2703
+ });
2704
+ onBeforeUnmount(() => {
2705
+ if (!socket) return;
2706
+ socket.removeEventListener("message", handleMessage);
2707
+ socket.close();
2708
+ socket = void 0;
2709
+ });
2710
+ const title = computed(() => {
2711
+ return `${count.value} viewer${count.value > 1 ? "s" : ""} currently online (${Object.keys(connections.value).map((host) => `${connections.value[host]} from ${host}`).join(", ")})`;
2712
+ });
2713
+ const ui = computed(() => viewersCounter());
2714
+ return (_ctx, _cache) => {
2715
+ return openBlock(), createBlock(Transition, { name: "fade" }, {
2716
+ default: withCtx(() => [count.value ? (openBlock(), createElementBlock("div", {
2717
+ key: 0,
2718
+ title: title.value,
2719
+ class: normalizeClass(ui.value.base({ class: [props.class, props.ui?.base] }))
2720
+ }, [createElementVNode("span", null, toDisplayString(count.value), 1), createElementVNode("span", {
2721
+ class: normalizeClass(ui.value.dot({ class: props.ui?.dot })),
2722
+ "aria-hidden": "true"
2723
+ }, null, 2)], 10, _hoisted_1)) : createCommentVNode("v-if", true)]),
2724
+ _: 1
2725
+ });
2726
+ };
2727
+ }
2728
+ });
2729
+ //#endregion
2730
+ export { _sfc_main as Comments, _sfc_main$1 as Container, _sfc_main$2 as Feedback, _sfc_main$3 as FeedbackCard, _sfc_main$4 as Header, _sfc_main$5 as LoginCallback, _sfc_main$6 as Page, _sfc_main$7 as PageHeader, _sfc_main$8 as PageTitle, _sfc_main$9 as Socials, _sfc_main$10 as Sponsors, _sfc_main$11 as TableOfContents, _sfc_main$12 as ViewersCounter, useLogin, useTableOfContents_default as useTableOfContents, useUmami };