@soubiran/ui 0.9.2 → 0.10.1

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