g-comments 0.1.0 → 0.2.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/react.d.cts CHANGED
@@ -1,9 +1,12 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { g as Comment, R as Pending, o as Commenter, r as CommentsError, P as Messages, C as CreateClientOptions, a as CommentsClient, e as AuthStatus, c as AuthProvider, Z as SignInOptions, Q as OpenThreadOptions, V as PostResult, a5 as ThreadHandle, a7 as ThreadSnapshot, b as CountRef } from './messages-BlBkT6Kr.cjs';
3
+ import { g as Comment, V as Pending, o as Commenter, r as CommentsError, T as Messages, C as CreateClientOptions, a as CommentsClient, e as AuthStatus, c as AuthProvider, a1 as SignInOptions, U as OpenThreadOptions, Z as PostResult, a9 as ThreadHandle, ab as ThreadSnapshot, b as CountRef, N as ForumTheme } from './messages-C9bMljl3.cjs';
4
4
 
5
- /** `auto` follows `prefers-color-scheme`; `inherit` leaves the colours to the page. */
6
- type Theme = "auto" | "light" | "dark" | "inherit";
5
+ /**
6
+ * `auto` follows `prefers-color-scheme`, `inherit` leaves the colours to the
7
+ * page, and `forum` applies the forum's own palettes, fonts and theme CSS.
8
+ */
9
+ type Theme = "auto" | "light" | "dark" | "inherit" | "forum";
7
10
  /** Design tokens; each is set as `--gc-<kebab-case name>` on the widget root. */
8
11
  type AppearanceVariable = "font" | "fg" | "fgMuted" | "bg" | "bgSubtle" | "border" | "accent" | "accentFg" | "danger" | "radius" | "shadow" | "space" | "avatarSize";
9
12
  /** Elements that accept extra class names through `appearance.elements`. */
@@ -16,7 +19,7 @@ interface Appearance {
16
19
  }
17
20
 
18
21
  /** What the widget was doing when an `error` event fired. */
19
- type CommentsAction = "open" | "load" | "loadMore" | "loadReplies" | "context" | "post" | "edit" | "delete" | "like" | "flag" | "pin" | "upload" | "preview" | "signIn";
22
+ type CommentsAction = "open" | "load" | "loadMore" | "loadReplies" | "context" | "post" | "edit" | "delete" | "like" | "flag" | "pin" | "upload" | "preview" | "signIn" | "theme";
20
23
  /** Delivered to `<DiscourseComments onEvent>`. */
21
24
  type CommentsEvent = {
22
25
  type: "comment.posted";
@@ -317,4 +320,16 @@ declare function useCommentCount(ref: CountRef | null, options?: {
317
320
  forum?: string;
318
321
  }): CommentCountState;
319
322
 
320
- export { type Appearance, type AppearanceElement, type AppearanceVariable, CommentCount, type CommentCountProps, type CommentCountState, type CommenterState, Comments, type CommentsAction, type CommentsEvent, type CommentsProps, type ComposerProps, type CountProps, DiscourseComments, type DiscourseCommentsProps, type HeaderProps, type ItemProps, type ListProps, type RenderComment, type RootProps, type SignInMode, type SignInProps, type Theme, type UseThreadOptions, type UseThreadResult, useClient, useCommentCount, useCommenter, useThread };
323
+ interface ForumThemeState {
324
+ /** `null` until loaded (or when `enabled` is false). */
325
+ theme: ForumTheme | null;
326
+ error: CommentsError | null;
327
+ }
328
+ /**
329
+ * The forum's palettes, fonts and theme stylesheets (`GET /comments/theme`),
330
+ * for sites that style their own UI after the forum. `<Comments theme="forum">`
331
+ * applies the same data to the widget.
332
+ */
333
+ declare function useForumTheme(): ForumThemeState;
334
+
335
+ export { type Appearance, type AppearanceElement, type AppearanceVariable, CommentCount, type CommentCountProps, type CommentCountState, type CommenterState, Comments, type CommentsAction, type CommentsEvent, type CommentsProps, type ComposerProps, type CountProps, DiscourseComments, type DiscourseCommentsProps, type ForumThemeState, type HeaderProps, type ItemProps, type ListProps, type RenderComment, type RootProps, type SignInMode, type SignInProps, type Theme, type UseThreadOptions, type UseThreadResult, useClient, useCommentCount, useCommenter, useForumTheme, useThread };
package/dist/react.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { g as Comment, R as Pending, o as Commenter, r as CommentsError, P as Messages, C as CreateClientOptions, a as CommentsClient, e as AuthStatus, c as AuthProvider, Z as SignInOptions, Q as OpenThreadOptions, V as PostResult, a5 as ThreadHandle, a7 as ThreadSnapshot, b as CountRef } from './messages-BlBkT6Kr.js';
3
+ import { g as Comment, V as Pending, o as Commenter, r as CommentsError, T as Messages, C as CreateClientOptions, a as CommentsClient, e as AuthStatus, c as AuthProvider, a1 as SignInOptions, U as OpenThreadOptions, Z as PostResult, a9 as ThreadHandle, ab as ThreadSnapshot, b as CountRef, N as ForumTheme } from './messages-C9bMljl3.js';
4
4
 
5
- /** `auto` follows `prefers-color-scheme`; `inherit` leaves the colours to the page. */
6
- type Theme = "auto" | "light" | "dark" | "inherit";
5
+ /**
6
+ * `auto` follows `prefers-color-scheme`, `inherit` leaves the colours to the
7
+ * page, and `forum` applies the forum's own palettes, fonts and theme CSS.
8
+ */
9
+ type Theme = "auto" | "light" | "dark" | "inherit" | "forum";
7
10
  /** Design tokens; each is set as `--gc-<kebab-case name>` on the widget root. */
8
11
  type AppearanceVariable = "font" | "fg" | "fgMuted" | "bg" | "bgSubtle" | "border" | "accent" | "accentFg" | "danger" | "radius" | "shadow" | "space" | "avatarSize";
9
12
  /** Elements that accept extra class names through `appearance.elements`. */
@@ -16,7 +19,7 @@ interface Appearance {
16
19
  }
17
20
 
18
21
  /** What the widget was doing when an `error` event fired. */
19
- type CommentsAction = "open" | "load" | "loadMore" | "loadReplies" | "context" | "post" | "edit" | "delete" | "like" | "flag" | "pin" | "upload" | "preview" | "signIn";
22
+ type CommentsAction = "open" | "load" | "loadMore" | "loadReplies" | "context" | "post" | "edit" | "delete" | "like" | "flag" | "pin" | "upload" | "preview" | "signIn" | "theme";
20
23
  /** Delivered to `<DiscourseComments onEvent>`. */
21
24
  type CommentsEvent = {
22
25
  type: "comment.posted";
@@ -317,4 +320,16 @@ declare function useCommentCount(ref: CountRef | null, options?: {
317
320
  forum?: string;
318
321
  }): CommentCountState;
319
322
 
320
- export { type Appearance, type AppearanceElement, type AppearanceVariable, CommentCount, type CommentCountProps, type CommentCountState, type CommenterState, Comments, type CommentsAction, type CommentsEvent, type CommentsProps, type ComposerProps, type CountProps, DiscourseComments, type DiscourseCommentsProps, type HeaderProps, type ItemProps, type ListProps, type RenderComment, type RootProps, type SignInMode, type SignInProps, type Theme, type UseThreadOptions, type UseThreadResult, useClient, useCommentCount, useCommenter, useThread };
323
+ interface ForumThemeState {
324
+ /** `null` until loaded (or when `enabled` is false). */
325
+ theme: ForumTheme | null;
326
+ error: CommentsError | null;
327
+ }
328
+ /**
329
+ * The forum's palettes, fonts and theme stylesheets (`GET /comments/theme`),
330
+ * for sites that style their own UI after the forum. `<Comments theme="forum">`
331
+ * applies the same data to the widget.
332
+ */
333
+ declare function useForumTheme(): ForumThemeState;
334
+
335
+ export { type Appearance, type AppearanceElement, type AppearanceVariable, CommentCount, type CommentCountProps, type CommentCountState, type CommenterState, Comments, type CommentsAction, type CommentsEvent, type CommentsProps, type ComposerProps, type CountProps, DiscourseComments, type DiscourseCommentsProps, type ForumThemeState, type HeaderProps, type ItemProps, type ListProps, type RenderComment, type RootProps, type SignInMode, type SignInProps, type Theme, type UseThreadOptions, type UseThreadResult, useClient, useCommentCount, useCommenter, useForumTheme, useThread };
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { createTranslator, PACKAGE_NAME, currentUrlWithoutHash, findComment, createComposerTimer, normalizeForum, resolveLocale, clientRegistry, STORAGE_PREFIX, isCommentsError, CommentsError, encodeRef, defaultPageUrl } from './chunk-6E3M3PQN.js';
2
+ import { createTranslator, PACKAGE_NAME, currentUrlWithoutHash, findComment, createComposerTimer, normalizeForum, resolveLocale, clientRegistry, STORAGE_PREFIX, isCommentsError, CommentsError, encodeRef, defaultPageUrl } from './chunk-WACVXJHD.js';
3
3
  import { createContext, useState, useRef, useCallback, useEffect, useMemo, Fragment as Fragment$1, useId, useContext, useLayoutEffect, useSyncExternalStore } from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
@@ -282,6 +282,150 @@ function ImageIcon() {
282
282
  function BackIcon() {
283
283
  return /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) });
284
284
  }
285
+
286
+ // src/react/forum-theme.ts
287
+ var FORUM_ROOT = '.gc-root[data-gc-theme="forum"]';
288
+ var FORUM_STYLED_ROOT = '.gc-root--styled[data-gc-theme="forum"]';
289
+ var TOKEN_SOURCES = [
290
+ ["fg", "primary"],
291
+ ["fg-muted", "primary-medium"],
292
+ ["bg", "secondary"],
293
+ ["bg-subtle", "primary-very-low"],
294
+ ["border", "primary-low"],
295
+ ["accent", "tertiary"],
296
+ ["accent-fg", "secondary"],
297
+ ["danger", "danger"]
298
+ ];
299
+ var SAFE_NAME = /^[\w-]+$/;
300
+ var SAFE_VALUE = /^[^;{}<>]+$/;
301
+ var SAFE_FAMILY = /^[^"'\;{}<>]+$/;
302
+ var FONT_SOURCE = /^url\("https?:\/\/[^"\s<>]+"\)(?:\s+format\("[\w-]+"\))?$|^local\("[^"\\<>]+"\)$/;
303
+ var FONT_WEIGHT = /^(?:[1-9]\d{0,2}|normal|bold)$/;
304
+ var SCHEME_TYPES = /* @__PURE__ */ new Set(["light", "dark"]);
305
+ var STYLE_ATTRIBUTE = "data-gc-forum-theme";
306
+ var FONTS_ATTRIBUTE = "data-gc-forum-fonts";
307
+ function colorDeclarations(scheme) {
308
+ let css = "";
309
+ for (const [name, value] of Object.entries(scheme.colors)) {
310
+ if (SAFE_NAME.test(name) && SAFE_VALUE.test(value)) {
311
+ css += `--gc-forum-${name}:${value};`;
312
+ }
313
+ }
314
+ if (SCHEME_TYPES.has(scheme.type)) {
315
+ css += `color-scheme:${scheme.type};`;
316
+ }
317
+ return css;
318
+ }
319
+ function fontDeclarations(fonts) {
320
+ let css = "";
321
+ for (const [role, font] of Object.entries(fonts)) {
322
+ if (font && SAFE_NAME.test(role) && SAFE_VALUE.test(font.stack)) {
323
+ css += `--gc-forum-font-${role}:${font.stack};`;
324
+ }
325
+ }
326
+ return css;
327
+ }
328
+ function tokenDeclarations(theme) {
329
+ let css = "";
330
+ for (const [token, color] of TOKEN_SOURCES) {
331
+ if (color in theme.schemes.light.colors) {
332
+ css += `--_gc-${token}:var(--gc-${token},var(--gc-forum-${color}));`;
333
+ }
334
+ }
335
+ if (theme.fonts.body) {
336
+ css += "--_gc-font:var(--gc-font,var(--gc-forum-font-body));";
337
+ }
338
+ return css;
339
+ }
340
+ function forumThemeCss(theme) {
341
+ const { light, dark } = theme.schemes;
342
+ let css = `${FORUM_ROOT}{${colorDeclarations(light)}${fontDeclarations(theme.fonts)}}
343
+ `;
344
+ css += `${FORUM_STYLED_ROOT}{${tokenDeclarations(theme)}}
345
+ `;
346
+ if (dark) {
347
+ css += `@media (prefers-color-scheme: dark){${FORUM_ROOT}{${colorDeclarations(dark)}}}
348
+ `;
349
+ }
350
+ return css;
351
+ }
352
+ function forumFontFacesCss(theme) {
353
+ let css = "";
354
+ const seen = /* @__PURE__ */ new Set();
355
+ for (const font of Object.values(theme.fonts)) {
356
+ for (const face of font?.faces ?? []) {
357
+ const weight = String(face.weight);
358
+ const rule = `@font-face{font-family:"${face.family}";src:${face.src};font-weight:${weight};font-display:swap;}
359
+ `;
360
+ if (SAFE_FAMILY.test(face.family) && FONT_SOURCE.test(face.src) && FONT_WEIGHT.test(weight) && !seen.has(rule)) {
361
+ seen.add(rule);
362
+ css += rule;
363
+ }
364
+ }
365
+ }
366
+ return css;
367
+ }
368
+ function forumStylesheetUrls(theme, forum) {
369
+ const secure = forum.startsWith("https:");
370
+ const urls = [];
371
+ for (const href of theme.stylesheets) {
372
+ let url;
373
+ try {
374
+ url = new URL(href);
375
+ } catch {
376
+ continue;
377
+ }
378
+ if (url.protocol === "https:" || !secure && url.protocol === "http:") {
379
+ urls.push(url.href);
380
+ }
381
+ }
382
+ return urls;
383
+ }
384
+ function forumThemeStyleText(theme, forum) {
385
+ const imports = forumStylesheetUrls(theme, forum).map((href) => `@import url(${JSON.stringify(href)}) layer(gc-forum);
386
+ `).join("");
387
+ return `@layer gc-defaults, gc-forum;
388
+ ${imports}@layer gc-forum{
389
+ ${forumThemeCss(theme)}}
390
+ `;
391
+ }
392
+ function documentOf(root) {
393
+ return root.nodeType === 9 ? root : root.ownerDocument;
394
+ }
395
+ function replaceNodes(container, attribute, forum, key, create) {
396
+ let current = false;
397
+ for (const node of container.querySelectorAll(`[${attribute}]`)) {
398
+ const value = node.getAttribute(attribute);
399
+ if (value === key) {
400
+ current = true;
401
+ } else if (value?.startsWith(`${forum}#`)) {
402
+ node.remove();
403
+ }
404
+ }
405
+ if (!current) {
406
+ container.append(...create());
407
+ }
408
+ }
409
+ function applyForumTheme(root, theme, forum) {
410
+ const doc = documentOf(root);
411
+ const container = root.nodeType === 9 ? doc.head : root;
412
+ const key = `${forum}#${theme.version}`;
413
+ replaceNodes(container, STYLE_ATTRIBUTE, forum, key, () => {
414
+ const style = doc.createElement("style");
415
+ style.setAttribute(STYLE_ATTRIBUTE, key);
416
+ style.textContent = forumThemeStyleText(theme, forum);
417
+ return [style];
418
+ });
419
+ const faces = forumFontFacesCss(theme);
420
+ if (faces) {
421
+ replaceNodes(doc.head, FONTS_ATTRIBUTE, forum, key, () => {
422
+ const style = doc.createElement("style");
423
+ style.setAttribute(FONTS_ATTRIBUTE, key);
424
+ style.textContent = faces;
425
+ return [style];
426
+ });
427
+ }
428
+ }
285
429
  var loaded = /* @__PURE__ */ new WeakMap();
286
430
  function initial(client) {
287
431
  return { client, config: loaded.get(client) ?? null, error: null };
@@ -351,6 +495,49 @@ function useCommenterFor(client, config) {
351
495
  [state, providers, actions]
352
496
  );
353
497
  }
498
+ var loaded2 = /* @__PURE__ */ new WeakMap();
499
+ function initial2(client) {
500
+ return { client, theme: loaded2.get(client) ?? null, error: null };
501
+ }
502
+ function useForumThemeFor(client, enabled) {
503
+ const [stored, setState] = useState(() => initial2(client));
504
+ let state = stored;
505
+ if (stored.client !== client) {
506
+ state = initial2(client);
507
+ setState(state);
508
+ }
509
+ useEffect(() => {
510
+ if (!enabled) {
511
+ return;
512
+ }
513
+ let active = true;
514
+ client.theme().then(
515
+ (theme) => {
516
+ loaded2.set(client, theme);
517
+ if (active) {
518
+ setState(
519
+ (current) => current.client === client && current.theme !== theme ? { client, theme, error: null } : current
520
+ );
521
+ }
522
+ },
523
+ (error) => {
524
+ if (active) {
525
+ setState(
526
+ (current) => current.client === client ? { ...current, error: toCommentsError(error) } : current
527
+ );
528
+ }
529
+ }
530
+ );
531
+ return () => {
532
+ active = false;
533
+ };
534
+ }, [client, enabled]);
535
+ return { theme: state.theme, error: state.error };
536
+ }
537
+ function useForumTheme() {
538
+ const { client } = useCommentsContext();
539
+ return useForumThemeFor(client, true);
540
+ }
354
541
  function useThread(options = {}) {
355
542
  const { client } = useCommentsContext(options.forum);
356
543
  return useThreadFor(client, options);
@@ -484,6 +671,19 @@ function ConfiguredRoot(props) {
484
671
  threadIdRef.current = thread?.id;
485
672
  const { config, error: configError, reload: reloadConfig } = useConfig(client);
486
673
  const commenter = useCommenterFor(client, config);
674
+ const forumTheme = useForumThemeFor(client, theme === "forum");
675
+ useEffect(() => {
676
+ const element = rootRef.current;
677
+ if (theme === "forum" && forumTheme.theme && element) {
678
+ applyForumTheme(rootNodeOf(element), forumTheme.theme, client.forum);
679
+ }
680
+ }, [theme, forumTheme.theme, client.forum]);
681
+ const themeError = theme === "forum" ? forumTheme.error : null;
682
+ useEffect(() => {
683
+ if (themeError) {
684
+ reportError("theme", themeError);
685
+ }
686
+ }, [themeError, reportError]);
487
687
  const retry = useCallback(() => {
488
688
  retryThread();
489
689
  if (configError) {
@@ -1455,8 +1655,8 @@ function UserAvatar({
1455
1655
  }
1456
1656
  );
1457
1657
  }
1458
- const initial2 = (user?.name || user?.username || "").charAt(0).toUpperCase();
1459
- return /* @__PURE__ */ jsx("span", { className: cx("gc-avatar", "gc-avatar--placeholder", className), "aria-hidden": "true", children: initial2 });
1658
+ const initial3 = (user?.name || user?.username || "").charAt(0).toUpperCase();
1659
+ return /* @__PURE__ */ jsx("span", { className: cx("gc-avatar", "gc-avatar--placeholder", className), "aria-hidden": "true", children: initial3 });
1460
1660
  }
1461
1661
  function Meta({ className }) {
1462
1662
  const { comment } = useItemContext("Comments.Meta");
@@ -2274,8 +2474,8 @@ function Replies() {
2274
2474
  root.reportError("loadReplies", error);
2275
2475
  return;
2276
2476
  }
2277
- const loaded2 = findComment(thread.snapshot().comments, comment.id)?.node.children ?? [];
2278
- const first = loaded2.find((child) => !before.has(child.id));
2477
+ const loaded3 = findComment(thread.snapshot().comments, comment.id)?.node.children ?? [];
2478
+ const first = loaded3.find((child) => !before.has(child.id));
2279
2479
  if (first) {
2280
2480
  root.focusComment(first.id);
2281
2481
  }
@@ -2544,6 +2744,6 @@ function CommentCount({
2544
2744
  return /* @__PURE__ */ jsx("span", { className: cx("gc-count", className), "aria-busy": count === null || void 0, children: count === null ? null : render ? render(count) : t("commentCount", { count }) });
2545
2745
  }
2546
2746
 
2547
- export { CommentCount, Comments, DiscourseComments, useClient, useCommentCount, useCommenter, useThread };
2747
+ export { CommentCount, Comments, DiscourseComments, useClient, useCommentCount, useCommenter, useForumTheme, useThread };
2548
2748
  //# sourceMappingURL=react.js.map
2549
2749
  //# sourceMappingURL=react.js.map