@blogmcp/community 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/dist/blogmcp-community.global.js +10 -0
  4. package/dist/chunks/mount-DzjwnHyx.js +4597 -0
  5. package/dist/core/attrs.d.ts +73 -0
  6. package/dist/core/avatars.d.ts +16 -0
  7. package/dist/core/client.d.ts +27 -0
  8. package/dist/core/discussion.d.ts +144 -0
  9. package/dist/core/download.d.ts +4 -0
  10. package/dist/core/format.d.ts +17 -0
  11. package/dist/core/i18n.d.ts +28 -0
  12. package/dist/core/names.d.ts +15 -0
  13. package/dist/core/notice.d.ts +50 -0
  14. package/dist/core/onboarding.d.ts +52 -0
  15. package/dist/core/outcomes.d.ts +28 -0
  16. package/dist/core/panel.d.ts +80 -0
  17. package/dist/core/receipt.d.ts +5 -0
  18. package/dist/core/text.d.ts +22 -0
  19. package/dist/core/turnstile.d.ts +33 -0
  20. package/dist/element.d.ts +26 -0
  21. package/dist/global.d.ts +8 -0
  22. package/dist/index.d.ts +25 -0
  23. package/dist/index.js +22 -0
  24. package/dist/mount.d.ts +65 -0
  25. package/dist/react.d.ts +17 -0
  26. package/dist/react.js +35 -0
  27. package/dist/ui/AuthorAvatar.vue.d.ts +11 -0
  28. package/dist/ui/ConfirmDialog.vue.d.ts +20 -0
  29. package/dist/ui/DiscussionView.vue.d.ts +33 -0
  30. package/dist/ui/LinkedText.vue.d.ts +5 -0
  31. package/dist/ui/MessageComposer.vue.d.ts +24 -0
  32. package/dist/ui/MessageItem.vue.d.ts +6 -0
  33. package/dist/ui/MessageMenu.vue.d.ts +10 -0
  34. package/dist/ui/MyCommunityView.vue.d.ts +16 -0
  35. package/dist/ui/NoticeFormView.vue.d.ts +26 -0
  36. package/dist/ui/NoticeStatusView.vue.d.ts +15 -0
  37. package/dist/ui/OnboardingSteps.vue.d.ts +11 -0
  38. package/dist/ui/ReactionBar.vue.d.ts +13 -0
  39. package/dist/ui/ReportDialog.vue.d.ts +15 -0
  40. package/dist/ui/StatementView.vue.d.ts +15 -0
  41. package/dist/ui/context.d.ts +21 -0
  42. package/dist/ui/discussion-context.d.ts +19 -0
  43. package/dist/ui/types.d.ts +7 -0
  44. package/dist/version.d.ts +1 -0
  45. package/dist/vue.d.ts +89 -0
  46. package/dist/vue.js +97 -0
  47. package/package.json +80 -4
  48. package/src/core/attrs.ts +147 -0
  49. package/src/core/avatars.ts +44 -0
  50. package/src/core/client.ts +54 -0
  51. package/src/core/discussion.ts +691 -0
  52. package/src/core/download.ts +21 -0
  53. package/src/core/format.ts +90 -0
  54. package/src/core/i18n.ts +49 -0
  55. package/src/core/names.ts +51 -0
  56. package/src/core/notice.ts +178 -0
  57. package/src/core/onboarding.ts +345 -0
  58. package/src/core/outcomes.ts +74 -0
  59. package/src/core/panel.ts +411 -0
  60. package/src/core/receipt.ts +28 -0
  61. package/src/core/text.ts +142 -0
  62. package/src/core/turnstile.ts +98 -0
  63. package/src/element.ts +109 -0
  64. package/src/global.ts +12 -0
  65. package/src/index.ts +35 -0
  66. package/src/mount.ts +127 -0
  67. package/src/react.ts +53 -0
  68. package/src/shims.d.ts +11 -0
  69. package/src/ui/AuthorAvatar.vue +26 -0
  70. package/src/ui/ConfirmDialog.vue +38 -0
  71. package/src/ui/DiscussionView.vue +294 -0
  72. package/src/ui/LinkedText.vue +18 -0
  73. package/src/ui/MessageComposer.vue +130 -0
  74. package/src/ui/MessageItem.vue +205 -0
  75. package/src/ui/MessageMenu.vue +67 -0
  76. package/src/ui/MyCommunityView.vue +369 -0
  77. package/src/ui/NoticeFormView.vue +244 -0
  78. package/src/ui/NoticeStatusView.vue +82 -0
  79. package/src/ui/OnboardingSteps.vue +163 -0
  80. package/src/ui/ReactionBar.vue +41 -0
  81. package/src/ui/ReportDialog.vue +88 -0
  82. package/src/ui/StatementView.vue +141 -0
  83. package/src/ui/context.ts +75 -0
  84. package/src/ui/discussion-context.ts +30 -0
  85. package/src/ui/styles.css +243 -0
  86. package/src/ui/types.ts +7 -0
  87. package/src/version.ts +3 -0
  88. package/src/vue.ts +116 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * What the elements read from their attributes: the host's page routes (`routes`, JSON), the notice form's
3
+ * target (`target`, JSON, or `article-id`), and the link the discussion gives a reader without a session
4
+ * to report a message on the host's notice page.
5
+ *
6
+ * Every path is the HOST's (m1-contracts.md §10.2: the elements take the host's routes as attributes) and
7
+ * must be a path on its own site — one leading `/`, never `//` or `/\`, no blank, control character or
8
+ * backslash; anything else is dropped and the managed blog's default (COMMUNITY_ROUTES) applies.
9
+ *
10
+ * The defaults are router-relative (the SDK's COMMUNITY_ROUTES: "the blog prepends its own base"). A site served
11
+ * under a path — a managed blog under `/blog`, whose host's own `/api/` goes straight to kb-api — names it in
12
+ * `site-base`, and the defaults (the pages and the api-base) are taken under it; what the host names itself
13
+ * (`routes`, `api-base`, the tenant's MyState.links) is a full site path already and is used as it is.
14
+ */
15
+ import { type NoticeTarget } from '@blogmcp/sdk/community';
16
+ /** The host's community pages, as the `routes` attribute names them (all optional). */
17
+ export interface CommunityRoutes {
18
+ /** A statement of reasons; `{id}` is the decision's id (`/komunita/rozhodnuti/{id}`). */
19
+ statementPath?: string;
20
+ /** The „Moje komunita" panel. */
21
+ myCommunityPath?: string;
22
+ /** The rules page (rendered by the host without JavaScript). */
23
+ rulesPath?: string;
24
+ /** The notice form. */
25
+ reportPath?: string;
26
+ /** A notifier's status page (the receipt goes in its fragment). */
27
+ noticesPath?: string;
28
+ /**
29
+ * The site's consultation page (drhoubicka's „poradna"): the regulated preset's `report_to_manufacturer` note
30
+ * becomes a link to it (m1-contracts.md §7.6). No default — without it the note stays text.
31
+ */
32
+ consultationPath?: string;
33
+ }
34
+ /** A path on the host's own site (the SDK's links.ts rule). */
35
+ export declare function isSitePath(path: unknown): path is string;
36
+ /** The `routes` attribute: the site paths it names; anything malformed is left out. */
37
+ export declare function parseRoutes(value: unknown): CommunityRoutes;
38
+ /**
39
+ * The `site-base` attribute: the path the site is served under, without a trailing `/` (`/blog`; '' for the root,
40
+ * also when absent) — or null when it is no plain path (a URL, a query, a dot segment): the element then renders
41
+ * nothing, as for an absolute api-base.
42
+ */
43
+ export declare function siteBaseOf(value: unknown): string | null;
44
+ /**
45
+ * The pages with the managed blog's defaults filled in under the site's base (statementPath stays a template).
46
+ * A page the host or the tenant names is its own full path.
47
+ */
48
+ export declare function routesWithDefaults(routes: CommunityRoutes, tenant?: {
49
+ statementPath?: string | null;
50
+ myCommunityPath?: string | null;
51
+ } | null, siteBase?: string): {
52
+ statementPath: string;
53
+ myCommunityPath: string;
54
+ rulesPath: string;
55
+ reportPath: string;
56
+ noticesPath: string;
57
+ consultationPath: string | null;
58
+ };
59
+ /** A notice target of one of kb-api's four shapes, or null (the form then has nothing to report). */
60
+ export declare function parseTarget(value: unknown): NoticeTarget | null;
61
+ export type NoticeTargetKind = 'message' | 'profile' | 'article';
62
+ export declare function targetKind(target: NoticeTarget): NoticeTargetKind;
63
+ /**
64
+ * The notice page's link for a message, for a reader without a session (who cannot use the member's
65
+ * report): `${reportPath}#c=<conversationId>&s=<seq>`. The ids are public; they travel in the fragment so
66
+ * the notice page stays one address (no crawlable variants — the storefront's rule for the sign-in link).
67
+ */
68
+ export declare function reportLink(reportPath: string, target: {
69
+ conversationId: string;
70
+ seq: number;
71
+ }): string | null;
72
+ /** The target the discussion's report link carries (`#c=…&s=…`), or `#a=<articleId>`; null for anything else. */
73
+ export declare function targetFromHash(hash: string): NoticeTarget | null;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The avatar presets a member picks from (kb-api stores the id, `^[a-z0-9_]{1,64}$`, and knows no list).
3
+ * Each is one emoji: a screen reader names it in the reader's own language, so the picker needs no words of
4
+ * ours. An id this list does not know (a newer element's) shows the name's initial instead.
5
+ */
6
+ export interface AvatarPreset {
7
+ readonly id: string;
8
+ readonly emoji: string;
9
+ }
10
+ export declare const AVATAR_PRESETS: readonly AvatarPreset[];
11
+ /** The preset's emoji, or null for none or an unknown id. */
12
+ export declare function avatarEmoji(preset: string | null | undefined): string | null;
13
+ /** The first letter of a name (the avatar of a member without a known preset). */
14
+ export declare function initialOf(name: string | null | undefined): string;
15
+ /** A stable hue for a member id, so the same member keeps the same initial's colour (decoration only). */
16
+ export declare function hueOf(id: string | null | undefined): number;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The client an element talks through: the SDK's `createCommunityClient` over the HOST's relative base
3
+ * (`api-base`; default `/api/community` under the site's `site-base` — a headless site's BFF, the storefront's,
4
+ * or with `site-base="/blog"` a managed blog's own proxy). Never kb-api directly: its community namespaces answer
5
+ * no CORS (m1-contracts.md §7.1), and the client refuses an absolute base. On a managed blog the tenant host's
6
+ * `/api/` IS kb-api, so `/api/community` there would skip the blog's proxy and its session. An element with an
7
+ * unusable base warns once and renders nothing.
8
+ */
9
+ import { type CommunityClient, type CommunityTransport } from '@blogmcp/sdk/community';
10
+ /** The base the hosts serve the community under, below their site base (§12; the BFF's default basePath). */
11
+ export declare const DEFAULT_API_BASE = "/api/community";
12
+ /**
13
+ * One console line per problem and page, for the site's developer — never a value from the page. The
14
+ * message starts with `[blogmcp-community]` (test/text-only.spec.ts tells developer lines from reader text
15
+ * by it).
16
+ */
17
+ export declare function warnOnce(key: string, message: `[blogmcp-community] ${string}`): void;
18
+ /**
19
+ * The `site-base` attribute as a path prefix ('' at the site's root), or null — the developer told once — when it
20
+ * is no path on this site.
21
+ */
22
+ export declare function siteBaseFor(value: string | null | undefined): string | null;
23
+ /**
24
+ * The client for an element's `api-base` (and an injected transport), or null when the base is not a relative
25
+ * path. Without an `api-base`: `/api/community` under the site's base.
26
+ */
27
+ export declare function communityClientFor(apiBase: string | null | undefined, transport?: CommunityTransport | null, siteBase?: string): CommunityClient | null;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `<blogmcp-discussion>`'s brain, without any view (m1-contracts.md §10.4): the SDK's discussion store fed by
3
+ * `discussions/list` pages and the `discussions/head` poll, the composer (a new message, a reply, an edit)
4
+ * with its optimistic rows, the Russmedia confirmation and the cooldowns, reactions, blocks, reports, and the
5
+ * inline onboarding. The element renders `controller.state`; tests drive it directly over a fake transport.
6
+ *
7
+ * Every request goes through the SDK client (the host's relative base); nothing here builds a path.
8
+ */
9
+ import { type ShallowRef } from 'vue';
10
+ import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type DiscussionMessage, type DiscussionState, type DiscussionStore, type MyState, type ReactionTarget, type ViewerNeeds } from '@blogmcp/sdk/community';
11
+ import type { Lang, TextRef } from './i18n';
12
+ import { type OnboardingModel } from './onboarding';
13
+ export type DiscussionPhase = 'loading' | 'ready' | 'failed' | 'hidden';
14
+ /** One composer's text and what happened to it. */
15
+ export interface Draft {
16
+ body: string;
17
+ sending: boolean;
18
+ failure: CommunityFailure | null;
19
+ /** CONFIRMATION_REQUIRED: the codes the author confirms before the same text goes again. */
20
+ confirm: {
21
+ codes: string[];
22
+ checked: string[];
23
+ } | null;
24
+ /** A cooldown: the epoch milliseconds until which this write waits. */
25
+ waitUntil: number | null;
26
+ /** A post's id, kept until kb-api took it: a retry of a write whose answer was lost cannot post twice. */
27
+ clientMsgId: string | null;
28
+ }
29
+ export type InlineKind = 'reply' | 'edit';
30
+ /** The one reply or edit open under a message (opening another closes it). */
31
+ export interface InlineDraft extends Draft {
32
+ kind: InlineKind;
33
+ /** The message edited, or the TOP-LEVEL message a reply goes under (two levels only). */
34
+ seq: number;
35
+ /** A reply to a reply names whom it answers. */
36
+ replyingTo: string | null;
37
+ }
38
+ export interface ReportDraft {
39
+ conversationId: string;
40
+ seq: number;
41
+ category: CaseCategory | '';
42
+ ruleId: string;
43
+ note: string;
44
+ rules: CommunityRule[] | null;
45
+ rulesFailed: boolean;
46
+ sending: boolean;
47
+ failure: CommunityFailure | null;
48
+ done: boolean;
49
+ }
50
+ /**
51
+ * Where the composer stands: `write`; `signIn` (no session that can write); `onboarding` (the inline
52
+ * onboarding waits); `ageRefused` (the age band declared in it was below the minimum: reading only, the
53
+ * onboarding is not offered again); `sanction` (a restriction, suspension or ban in effect); `notMoved` (the
54
+ * discussion takes no writes yet — one sentence); `restricted` (the member is read-only or deleting their
55
+ * profile); `pending` (being decided).
56
+ */
57
+ export type ComposerMode = 'write' | 'signIn' | 'onboarding' | 'ageRefused' | 'sanction' | 'notMoved' | 'restricted' | 'pending';
58
+ export interface DiscussionUi {
59
+ phase: DiscussionPhase;
60
+ /** Why the first load failed (`failed`). */
61
+ failure: CommunityFailure | null;
62
+ thread: DiscussionState;
63
+ loadingMore: boolean;
64
+ refreshing: boolean;
65
+ /** A head poll saw messages the page does not show yet. */
66
+ newMessages: boolean;
67
+ me: MyState | null;
68
+ /** What a write or the member's state taught: no writes yet, or none for this member. */
69
+ block: 'notMoved' | 'restricted' | null;
70
+ composer: Draft;
71
+ inline: InlineDraft | null;
72
+ report: ReportDraft | null;
73
+ /** The last outcome to announce (aria-live), and a counter so the same text is announced again. */
74
+ announcement: TextRef | null;
75
+ announceId: number;
76
+ /** Now, ticking while a cooldown runs. */
77
+ clock: number;
78
+ /** Actions on their way, by key (`react:…`, `delete:<seq>`, `block:<memberId>`). */
79
+ busy: Record<string, boolean>;
80
+ /**
81
+ * The minimum age a declared band fell below (AGE_BELOW_MINIMUM). kb-api keeps nothing of a refused band
82
+ * (§7.4.2), so the element does: the onboarding is not offered again while it lives.
83
+ */
84
+ refusedAge: number | null;
85
+ }
86
+ export interface DiscussionControllerOptions {
87
+ client: CommunityClient;
88
+ articleId: string;
89
+ lang: Lang;
90
+ /** Head poll interval (default 20 s; 0 = no polling). */
91
+ pollMs?: number;
92
+ now?: () => number;
93
+ /** The viewer's message was posted (`held`: it waits for review). */
94
+ onPosted?(state: 'visible' | 'held'): void;
95
+ /** A refusal the element saw before (a discussion started again in another language keeps it). */
96
+ refusedAge?: number | null;
97
+ /** The onboarding's age band was refused: the element keeps it for its next discussions. */
98
+ onAgeRefused?(minAge: number): void;
99
+ }
100
+ export interface DiscussionController {
101
+ readonly state: DiscussionUi;
102
+ readonly store: DiscussionStore;
103
+ readonly onboarding: ShallowRef<OnboardingModel | null>;
104
+ /** The first load, then the poll. */
105
+ start(): Promise<void>;
106
+ refresh(): Promise<void>;
107
+ loadMore(): Promise<void>;
108
+ mode(): ComposerMode;
109
+ needs(): ViewerNeeds;
110
+ isOwn(m: Pick<DiscussionMessage, 'author'>): boolean;
111
+ canReply(m: DiscussionMessage): boolean;
112
+ canEdit(m: DiscussionMessage): boolean;
113
+ canDelete(m: DiscussionMessage): boolean;
114
+ canReact(): boolean;
115
+ /** A member's report (`messages/report`): any signed-in reader, on someone else's message. */
116
+ canReport(m: DiscussionMessage): boolean;
117
+ /** A reader without a session reports through the host's notice page instead (reportLink). */
118
+ canReportAsNotifier(m: DiscussionMessage): boolean;
119
+ canBlock(m: DiscussionMessage): boolean;
120
+ /** The poll may run (the element is on screen). */
121
+ setInView(inView: boolean): void;
122
+ submit(): Promise<void>;
123
+ /** Reply under `seq`'s thread (a reply to a reply goes under its top-level message, naming its author). */
124
+ openReply(seq: number, replyingTo?: string | null): void;
125
+ openEdit(seq: number): void;
126
+ closeInline(): void;
127
+ submitInline(): Promise<void>;
128
+ /** Tick or untick one code of a draft's confirmation. */
129
+ setConfirmed(draft: Draft, code: string, checked: boolean): void;
130
+ /** Send the draft again with every confirmed code (the Russmedia confirmation). */
131
+ sendConfirmed(draft: Draft): Promise<void>;
132
+ remove(seq: number): Promise<boolean>;
133
+ react(target: ReactionTarget, emoji: string): Promise<void>;
134
+ setBlocked(memberId: string, blocked: boolean): Promise<void>;
135
+ openReport(m: DiscussionMessage): void;
136
+ closeReport(): void;
137
+ submitReport(): Promise<void>;
138
+ beginOnboarding(): void;
139
+ cancelOnboarding(): void;
140
+ /** Seconds a draft still waits (0 when it may go). */
141
+ waitLeft(draft: Draft): number;
142
+ dispose(): void;
143
+ }
144
+ export declare function createDiscussionController(o: DiscussionControllerOptions): DiscussionController;
@@ -0,0 +1,4 @@
1
+ /** The member's export as a file the browser saves (§8.3: the download is the SDK's). Nothing leaves the page. */
2
+ export declare function saveJson(data: unknown, fileName: string): void;
3
+ /** `<name>-YYYY-MM-DD.json`, the name being the catalogue's (`panel.exportFileName`). */
4
+ export declare function exportFileName(name: string, now?: Date): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Dates and a statement's territorial scope as the reader's language writes them (Intl does the words; the
3
+ * catalogue holds the sentence around them).
4
+ */
5
+ import { type Lang } from './i18n';
6
+ /** An ISO timestamp as a date (and time); '' for anything that is no date. */
7
+ export declare function formatDate(lang: Lang, iso: string | null | undefined, withTime?: boolean): string;
8
+ /** Whether an ISO timestamp lies in the past (an appeal window, a sanction's end). */
9
+ export declare function isPast(iso: string | null | undefined, now?: number): boolean;
10
+ /**
11
+ * A statement's territorial scope (DSA art. 17(3)(a)) as the reader reads it. kb-api stores `all` — the measure
12
+ * applies without a territorial limit, the catalogue's `statement.scopeAll` — or the regions it is limited to as
13
+ * ISO 3166-1 alpha-2 codes (`CZ,SK`), which Intl names and joins („Česko a Slovensko"; a runtime without Intl reads
14
+ * `CZ, SK`). Any other value — one a newer kb-api sends — is shown as it came, never dropped and never an
15
+ * exception; '' when there is none.
16
+ */
17
+ export declare function formatTerritorialScope(lang: Lang, scope: string | null | undefined): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The elements' language and their texts (m1-contracts.md §10.4): every string a reader sees comes from
3
+ * the SDK's catalogue, `communityMessage` — one catalogue, one tone test. The elements add no words of
4
+ * their own; test/text-only.spec.ts scans src/ for any.
5
+ */
6
+ import { type CommunityMessageKey } from '@blogmcp/sdk/community';
7
+ export type Lang = 'cs' | 'en';
8
+ /**
9
+ * The review layer's rule: the first candidate that is Czech or Slovak reads Czech, the first that is
10
+ * English reads English; anything else is passed over; English when none decides. The candidates are
11
+ * the `lang` attribute, then `<html lang>`.
12
+ */
13
+ export declare function pickLang(...candidates: Array<string | null | undefined>): Lang;
14
+ /** The page's own language attribute, when there is a page (the elements mount in a browser only). */
15
+ export declare function documentLang(): string | null;
16
+ export type TextParams = Record<string, string | number>;
17
+ export type Translate = (key: CommunityMessageKey, params?: TextParams) => string;
18
+ export declare function translator(lang: Lang): Translate;
19
+ /**
20
+ * A key built from one of kb-api's open codes (`restriction.<code>`, `caseStatus.<status>`, …) when the
21
+ * catalogue has a text for it, else null: a code a newer kb-api sends is skipped, never shown raw.
22
+ */
23
+ export declare function knownKey(key: string): CommunityMessageKey | null;
24
+ /** A message key and its params: what a controller hands the view, which turns it into text. */
25
+ export interface TextRef {
26
+ readonly key: CommunityMessageKey;
27
+ readonly params?: TextParams;
28
+ }
@@ -0,0 +1,15 @@
1
+ /** The display name as kb-api will fold it: NFKC, white space collapsed, trimmed. */
2
+ export declare function normalizeName(name: string): string;
3
+ export declare function isDisplayNameOk(name: string): boolean;
4
+ /** A handle's length as COMMUNITY_HANDLE_RE takes it (3–30), for the hint and the field's maxlength. */
5
+ export declare const HANDLE_MIN = 3;
6
+ export declare const HANDLE_MAX = 30;
7
+ /** A handle as typed: lower case, trimmed; '' when none was typed. */
8
+ export declare function normalizeHandle(handle: string): string;
9
+ export declare function isHandleOk(handle: string): boolean;
10
+ /**
11
+ * The handle kb-api would make of a display name when the member gives none (its stem: fold → ASCII →
12
+ * words joined by `_`, at most 24 characters) — shown as a suggestion; null when the name gives too little.
13
+ * kb-api may still add a number on a clash, or leave out a reserved word.
14
+ */
15
+ export declare function suggestedHandle(displayName: string): string | null;
@@ -0,0 +1,50 @@
1
+ import { type CaseCategory, type CommunityClient, type CommunityFailure, type CommunityRule, type NoticeReceipt, type NoticeTarget } from '@blogmcp/sdk/community';
2
+ import type { Lang } from './i18n';
3
+ /** `off`: no site key (development, a host without the widget); kb-api then decides alone. */
4
+ export type CaptchaState = 'off' | 'loading' | 'ready' | 'unavailable';
5
+ export interface NoticeFormUi {
6
+ target: NoticeTarget | null;
7
+ category: CaseCategory | '';
8
+ ruleId: string;
9
+ rules: CommunityRule[] | null;
10
+ description: string;
11
+ name: string;
12
+ email: string;
13
+ goodFaith: boolean;
14
+ captcha: CaptchaState;
15
+ /** The widget's one-shot token, or null. */
16
+ token: string | null;
17
+ /** Bumped after every failed send: the widget issues a new token. */
18
+ resetSignal: number;
19
+ sending: boolean;
20
+ failure: CommunityFailure | null;
21
+ receipt: NoticeReceipt | null;
22
+ /** The notifier's status page with the receipt in the fragment. */
23
+ statusLink: string | null;
24
+ }
25
+ export interface NoticeFormController {
26
+ readonly state: NoticeFormUi;
27
+ /** The name and e-mail fields apply (not for an endangered child). */
28
+ contactAllowed(): boolean;
29
+ canSubmit(): boolean;
30
+ descriptionLength(): number;
31
+ setCategory(category: CaseCategory | ''): void;
32
+ setToken(token: string | null): void;
33
+ setCaptcha(state: CaptchaState): void;
34
+ submit(): Promise<void>;
35
+ dispose(): void;
36
+ }
37
+ export interface NoticeFormOptions {
38
+ client: Pick<CommunityClient, 'notices' | 'legal'>;
39
+ lang: Lang;
40
+ target: NoticeTarget | null;
41
+ /** A Turnstile site key was given: a token is needed before the form can be sent. */
42
+ captcha: boolean;
43
+ /** The host's status page (the receipt goes in its fragment). */
44
+ noticesPath: string;
45
+ /** The form was sent (the receipt is the page's: it is not passed on). */
46
+ onSubmitted?(answer: {
47
+ contactKept: boolean;
48
+ }): void;
49
+ }
50
+ export declare function createNoticeForm(o: NoticeFormOptions): NoticeFormController;
@@ -0,0 +1,52 @@
1
+ import { type AgeBand, type CommunityClient, type CommunityFailure, type CommunityRule, type CurrentLegalText, type MyState } from '@blogmcp/sdk/community';
2
+ import type { Lang } from './i18n';
3
+ export type OnboardingStep = 'age' | 'rules' | 'art9' | 'profile';
4
+ export type OnboardingPhase = 'loading' | 'ready' | 'sending' | 'done' | 'failed' | 'refused';
5
+ export interface OnboardingState {
6
+ /** `refused`: the age band is below the minimum — nothing was stored, the reader may go on reading. */
7
+ phase: OnboardingPhase;
8
+ steps: OnboardingStep[];
9
+ index: number;
10
+ me: MyState | null;
11
+ rulesText: CurrentLegalText | null;
12
+ rules: CommunityRule[];
13
+ art9Text: CurrentLegalText | null;
14
+ art9Paragraphs: string[];
15
+ /** The member accepted an older rules text; this one asks again. */
16
+ reconsent: boolean;
17
+ ageBand: AgeBand | null;
18
+ rulesAccepted: boolean;
19
+ art9Accepted: boolean;
20
+ displayName: string;
21
+ handle: string;
22
+ avatarPreset: string | null;
23
+ failure: CommunityFailure | null;
24
+ /** The fields a refusal named (`displayName`, `handle`, …). */
25
+ fields: string[];
26
+ /** The minimum age, once a refusal told it (a refused `16_17` means 18). */
27
+ minAge: number | null;
28
+ }
29
+ export interface OnboardingModel {
30
+ readonly state: OnboardingState;
31
+ load(): Promise<void>;
32
+ step(): OnboardingStep | null;
33
+ isLast(): boolean;
34
+ canContinue(): boolean;
35
+ /** The next step, or — on the last one — `me/onboard`. */
36
+ next(): Promise<void>;
37
+ back(): void;
38
+ suggestedHandle(): string | null;
39
+ hasFieldError(field: string): boolean;
40
+ }
41
+ export interface OnboardingOptions {
42
+ client: Pick<CommunityClient, 'me' | 'legal'>;
43
+ lang: Lang;
44
+ /** Called with the member's state once kb-api took the onboarding. */
45
+ onDone(state: MyState): void;
46
+ /** The session cannot act as a member any more (signed out meanwhile). */
47
+ onSignIn(): void;
48
+ /** The age band was below the tenant's minimum (AGE_BELOW_MINIMUM): kb-api kept nothing, the element keeps this. */
49
+ onRefused?(minAge: number): void;
50
+ signal?: AbortSignal;
51
+ }
52
+ export declare function createOnboarding(o: OnboardingOptions): OnboardingModel;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * How a refusal reads (m1-contracts.md §10.2, §10.4): the client turns every kb-api and host code into one
3
+ * of COMMUNITY_UI_CODES; this turns that into the catalogue's sentence — `error.<code>`, or a sanction's
4
+ * own sentence with its end date — and says what the element does about it besides showing it.
5
+ */
6
+ import type { CommunityFailure, CommunityUiCode } from '@blogmcp/sdk/community';
7
+ import type { Lang, TextRef } from './i18n';
8
+ /** The sentence a refusal shows. Total over COMMUNITY_UI_CODES (test/outcomes.spec.ts). */
9
+ export declare function failureText(failure: Pick<CommunityFailure, 'code' | 'until'>, lang: Lang): TextRef;
10
+ /** Seconds a refusal asks to wait before the same write may go (the burst, a quota, the limiter). */
11
+ export declare const DEFAULT_WAIT_S: Partial<Record<CommunityUiCode, number>>;
12
+ /** The epoch milliseconds until which a write waits after `failure`, or null when it may go again at once. */
13
+ export declare function waitUntilOf(failure: Pick<CommunityFailure, 'code' | 'retryAfterS'>, now: number): number | null;
14
+ /** A countdown shows only for a short wait (a quota until midnight reads `error.quota` alone). */
15
+ export declare const COUNTDOWN_MAX_S = 120;
16
+ /**
17
+ * What the element does with a refusal of a write, besides showing its sentence:
18
+ * - `confirm` the author confirms the codes and sends again (the Russmedia confirmation);
19
+ * - `wait` the write waits (a cooldown);
20
+ * - `onboard` the inline onboarding opens (ONBOARDING_REQUIRED);
21
+ * - `reload` the discussion is read again (the viewer's state changed: a sanction, a session);
22
+ * - `notMoved` the discussion does not take writes yet: one sentence instead of the composer;
23
+ * - `hide` discussions are switched off: the element renders nothing;
24
+ * - `retry` nothing was decided (no answer, the ledger): the same write may go again as it is;
25
+ * - `show` anything else: the sentence, the text stays for the author to change.
26
+ */
27
+ export type FailureEffect = 'confirm' | 'wait' | 'onboard' | 'reload' | 'notMoved' | 'hide' | 'retry' | 'show';
28
+ export declare function failureEffect(code: CommunityUiCode): FailureEffect;
@@ -0,0 +1,80 @@
1
+ import { type AppealItem, type BlockEntry, type CommunityClient, type CommunityFailure, type DecisionSummary, type MemberExport, type MyState, type NoticeStatus, type NotificationItem, type ReportItem, type Statement } from '@blogmcp/sdk/community';
2
+ import type { TextRef } from './i18n';
3
+ export type LoadPhase = 'idle' | 'loading' | 'ready' | 'failed';
4
+ export interface AppealDraft {
5
+ body: string;
6
+ sending: boolean;
7
+ failure: CommunityFailure | null;
8
+ }
9
+ export interface ProfileDraft {
10
+ displayName: string;
11
+ handle: string;
12
+ avatarPreset: string | null;
13
+ sending: boolean;
14
+ failure: CommunityFailure | null;
15
+ fields: string[];
16
+ }
17
+ export interface PanelUi {
18
+ /** The member's part: `signIn` when the session cannot act as a member (or there is none). */
19
+ phase: LoadPhase | 'signIn';
20
+ failure: CommunityFailure | null;
21
+ me: MyState | null;
22
+ notifications: NotificationItem[];
23
+ decisions: DecisionSummary[];
24
+ appeals: AppealItem[];
25
+ reports: ReportItem[];
26
+ blocks: BlockEntry[];
27
+ /** The statement the page opens (`decision-id`). */
28
+ statement: {
29
+ phase: LoadPhase | 'signIn';
30
+ value: Statement | null;
31
+ failure: CommunityFailure | null;
32
+ appeal: AppealDraft;
33
+ };
34
+ /** The notifier's part (`#t=` in the address). `missing`: a `#t=` that holds no receipt. */
35
+ notice: {
36
+ phase: LoadPhase | 'missing';
37
+ value: NoticeStatus | null;
38
+ failure: CommunityFailure | null;
39
+ appeal: AppealDraft;
40
+ };
41
+ profile: ProfileDraft | null;
42
+ /** The profile's deletion: its date once requested here (kb-api's MyState does not carry it). */
43
+ purgeAfter: string | null;
44
+ exporting: boolean;
45
+ busy: Record<string, boolean>;
46
+ announcement: TextRef | null;
47
+ announceId: number;
48
+ }
49
+ export interface PanelController {
50
+ readonly state: PanelUi;
51
+ /** The member's sections (me/get and the lists). */
52
+ loadMember(): Promise<void>;
53
+ loadStatement(decisionId: string): Promise<void>;
54
+ loadNotice(receipt: string | null): Promise<void>;
55
+ unreadCount(): number;
56
+ markAllRead(): Promise<void>;
57
+ fileAppeal(): Promise<void>;
58
+ withdrawAppeal(): Promise<void>;
59
+ fileNoticeAppeal(): Promise<void>;
60
+ withdrawNoticeAppeal(): Promise<void>;
61
+ withdrawConsent(): Promise<boolean>;
62
+ unblock(memberId: string): Promise<void>;
63
+ editProfile(): void;
64
+ cancelProfile(): void;
65
+ saveProfile(): Promise<void>;
66
+ /** `me/export` as a JSON file the browser saves (`save` does the download; tests pass their own). */
67
+ exportData(save: (data: MemberExport) => void): Promise<void>;
68
+ erase(): Promise<boolean>;
69
+ cancelErase(): Promise<void>;
70
+ /** The member asked for the deletion (here or before). */
71
+ erasing(): boolean;
72
+ appealLength(draft: AppealDraft): number;
73
+ canAppealWith(draft: AppealDraft): boolean;
74
+ dispose(): void;
75
+ }
76
+ export interface PanelOptions {
77
+ client: CommunityClient;
78
+ lang: string;
79
+ }
80
+ export declare function createPanel(o: PanelOptions): PanelController;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The receipt this element's page carries: a string; `null` for a `#t=` that holds none (a cut link — "incomplete",
3
+ * never an older receipt of the tab); `undefined` when the page carries none for it.
4
+ */
5
+ export declare function receiptForElement(owner: object | null): string | null | undefined;
@@ -0,0 +1,22 @@
1
+ export type TextSegment = {
2
+ readonly kind: 'text';
3
+ readonly text: string;
4
+ } | {
5
+ readonly kind: 'link';
6
+ readonly text: string;
7
+ readonly href: string;
8
+ };
9
+ /** The rel every link to what a member wrote carries (user-generated, no referrer, no opener). */
10
+ export declare const UGC_LINK_REL = "nofollow ugc noopener noreferrer";
11
+ /** A member's text as text runs and links. */
12
+ export declare function linkify(text: string): TextSegment[];
13
+ /** Characters in code points, as kb-api counts a body (after NFC and trimming). */
14
+ export declare function codePointLength(text: string): number;
15
+ /** A body as kb-api will measure it: NFC, trimmed. */
16
+ export declare function normalizeBody(text: string): string;
17
+ /**
18
+ * The paragraphs of the tenant's own legal text (`legal/current`'s `body` is HTML with anchors) as plain
19
+ * text, for the art. 9 consent step: tags are dropped and entities decoded AFTER that, so nothing in the
20
+ * text is ever parsed as markup again. String work only — no HTML parser, no live DOM.
21
+ */
22
+ export declare function htmlToParagraphs(html: string): string[];
@@ -0,0 +1,33 @@
1
+ export declare const TURNSTILE_SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
2
+ interface TurnstileRenderOptions {
3
+ sitekey: string;
4
+ action?: string;
5
+ language?: string;
6
+ appearance?: 'always' | 'execute' | 'interaction-only';
7
+ callback?: (token: string) => void;
8
+ 'expired-callback'?: () => void;
9
+ 'error-callback'?: () => void;
10
+ 'timeout-callback'?: () => void;
11
+ }
12
+ export interface TurnstileApi {
13
+ render(el: HTMLElement, options: TurnstileRenderOptions): string;
14
+ reset(widgetId: string): void;
15
+ remove(widgetId: string): void;
16
+ }
17
+ /** One `<script>` per page however many forms mount; a failed load may be tried again later. */
18
+ export declare function loadTurnstile(nonce?: string): Promise<TurnstileApi>;
19
+ export interface TurnstileWidget {
20
+ /** A fresh token (the last one is spent). */
21
+ reset(): void;
22
+ remove(): void;
23
+ }
24
+ export interface RenderTurnstileOptions {
25
+ siteKey: string;
26
+ language: string;
27
+ /** A new token, or null when it expired or failed (the form cannot be sent until a new one comes). */
28
+ onToken(token: string | null): void;
29
+ nonce?: string;
30
+ }
31
+ /** The widget in `container`; rejects when the script cannot load (an ad blocker, the network). */
32
+ export declare function renderTurnstile(container: HTMLElement, o: RenderTurnstileOptions): Promise<TurnstileWidget>;
33
+ export {};
@@ -0,0 +1,26 @@
1
+ export declare const DISCUSSION_ELEMENT_TAG = "blogmcp-discussion";
2
+ export declare const NOTICE_FORM_ELEMENT_TAG = "blogmcp-notice-form";
3
+ export declare const MY_COMMUNITY_ELEMENT_TAG = "blogmcp-my-community";
4
+ export interface DefineCommunityElementOptions {
5
+ /** Another tag name for the element. */
6
+ tag?: string;
7
+ /** CSP nonce for the element's `<style>` (else each element's own `nonce`). */
8
+ nonce?: string;
9
+ }
10
+ export interface DefineCommunityElementsOptions {
11
+ /** CSP nonce for the elements' `<style>`. */
12
+ nonce?: string;
13
+ tags?: {
14
+ discussion?: string;
15
+ noticeForm?: string;
16
+ myCommunity?: string;
17
+ };
18
+ }
19
+ /** Register `<blogmcp-discussion>` (once; later calls are no-ops). Browser only. */
20
+ export declare function defineDiscussionElement(opts?: DefineCommunityElementOptions): void;
21
+ /** Register `<blogmcp-notice-form>` (once). Browser only. */
22
+ export declare function defineNoticeFormElement(opts?: DefineCommunityElementOptions): void;
23
+ /** Register `<blogmcp-my-community>` (once). Browser only. */
24
+ export declare function defineMyCommunityElement(opts?: DefineCommunityElementOptions): void;
25
+ /** Register all three (once). Browser only. */
26
+ export declare function defineCommunityElements(opts?: DefineCommunityElementsOptions): void;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The `<script>` build: registers `<blogmcp-discussion>`, `<blogmcp-notice-form>` and `<blogmcp-my-community>`
3
+ * with the script tag's own CSP nonce, and exposes `window.BlogmcpCommunity`.
4
+ */
5
+ import { defineCommunityElements } from './element';
6
+ import { mountDiscussion, mountMyCommunity, mountNoticeForm } from './mount';
7
+ import { VERSION } from './version';
8
+ export { defineCommunityElements, mountDiscussion, mountMyCommunity, mountNoticeForm, VERSION };