@blogmcp/community 0.1.0 → 0.2.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 (68) hide show
  1. package/README.md +108 -11
  2. package/dist/blogmcp-community.global.js +8 -8
  3. package/dist/chunks/{mount-DzjwnHyx.js → mount-WadUnaTa.js} +3909 -2070
  4. package/dist/core/attrs.d.ts +27 -1
  5. package/dist/core/badge.d.ts +30 -0
  6. package/dist/core/centre.d.ts +28 -0
  7. package/dist/core/digest-token.d.ts +2 -0
  8. package/dist/core/discussion.d.ts +36 -5
  9. package/dist/core/format.d.ts +7 -0
  10. package/dist/core/onboarding.d.ts +42 -2
  11. package/dist/core/outcomes.d.ts +2 -1
  12. package/dist/core/panel.d.ts +17 -1
  13. package/dist/core/profile.d.ts +40 -0
  14. package/dist/core/push.d.ts +63 -0
  15. package/dist/core/realtime.d.ts +35 -0
  16. package/dist/core/settings.d.ts +53 -0
  17. package/dist/core/text.d.ts +13 -0
  18. package/dist/element.d.ts +22 -1
  19. package/dist/global.d.ts +7 -4
  20. package/dist/index.d.ts +10 -2
  21. package/dist/index.js +34 -11
  22. package/dist/mount.d.ts +40 -0
  23. package/dist/react.d.ts +14 -3
  24. package/dist/react.js +24 -4
  25. package/dist/ui/BadgeView.vue.d.ts +13 -0
  26. package/dist/ui/DiscussionView.vue.d.ts +11 -0
  27. package/dist/ui/LinkedText.vue.d.ts +1 -0
  28. package/dist/ui/MemberSettingsView.vue.d.ts +13 -0
  29. package/dist/ui/MessageComposer.vue.d.ts +2 -0
  30. package/dist/ui/NotificationCentreView.vue.d.ts +12 -0
  31. package/dist/ui/OnboardingSteps.vue.d.ts +1 -1
  32. package/dist/ui/ProfileView.vue.d.ts +13 -0
  33. package/dist/ui/WelcomeView.vue.d.ts +12 -0
  34. package/dist/vue.d.ts +114 -3
  35. package/dist/vue.js +109 -5
  36. package/package.json +3 -3
  37. package/src/core/attrs.ts +41 -2
  38. package/src/core/badge.ts +120 -0
  39. package/src/core/centre.ts +136 -0
  40. package/src/core/digest-token.ts +26 -0
  41. package/src/core/discussion.ts +203 -18
  42. package/src/core/format.ts +22 -0
  43. package/src/core/onboarding.ts +165 -18
  44. package/src/core/outcomes.ts +13 -1
  45. package/src/core/panel.ts +39 -4
  46. package/src/core/profile.ts +144 -0
  47. package/src/core/push.ts +174 -0
  48. package/src/core/realtime.ts +39 -0
  49. package/src/core/settings.ts +363 -0
  50. package/src/core/text.ts +32 -0
  51. package/src/element.ts +62 -6
  52. package/src/global.ts +18 -4
  53. package/src/index.ts +36 -1
  54. package/src/mount.ts +108 -1
  55. package/src/react.ts +47 -3
  56. package/src/ui/BadgeView.vue +51 -0
  57. package/src/ui/DiscussionView.vue +43 -3
  58. package/src/ui/LinkedText.vue +11 -5
  59. package/src/ui/MemberSettingsView.vue +175 -0
  60. package/src/ui/MessageComposer.vue +15 -4
  61. package/src/ui/MessageItem.vue +24 -6
  62. package/src/ui/MyCommunityView.vue +9 -0
  63. package/src/ui/NotificationCentreView.vue +159 -0
  64. package/src/ui/OnboardingSteps.vue +17 -6
  65. package/src/ui/ProfileView.vue +91 -0
  66. package/src/ui/WelcomeView.vue +72 -0
  67. package/src/ui/styles.css +16 -0
  68. package/src/vue.ts +129 -3
@@ -0,0 +1,91 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * `<blogmcp-profile handle>`: a member's public profile (m2-contracts.md §3.2, §3.10) — the name and avatar, since
4
+ * when they are a member (a month), the bio as text, and the trust level as its label („Nový člen" … „Moderátor"),
5
+ * never its number. An unknown handle reads one line; a visitor gets the sign-in sentence (profiles are for signed-in
6
+ * readers, kb-api's rule).
7
+ */
8
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
9
+ import { signInLink, type CommunityTransport } from '@blogmcp/sdk/community'
10
+ import { communityClientFor, siteBaseFor } from '../core/client'
11
+ import { formatMonth } from '../core/format'
12
+ import { documentLang, pickLang, type Lang } from '../core/i18n'
13
+ import { failureText } from '../core/outcomes'
14
+ import { createProfile, type ProfileController } from '../core/profile'
15
+ import AuthorAvatar from './AuthorAvatar.vue'
16
+ import { provideText, uid } from './context'
17
+
18
+ defineOptions({ inheritAttrs: false })
19
+
20
+ const props = defineProps<{
21
+ /** The member's handle (`jana_n`, an `@` in front is fine). */
22
+ handle?: string
23
+ apiBase?: string
24
+ siteBase?: string
25
+ lang?: string
26
+ routes?: string | Record<string, unknown>
27
+ signInHref?: string
28
+ transport?: CommunityTransport
29
+ }>()
30
+
31
+ const lang = ref<Lang>(pickLang(props.lang, documentLang()))
32
+ const { t } = provideText(lang)
33
+ const id = uid('bcm-profile')
34
+
35
+ const siteBase = siteBaseFor(props.siteBase)
36
+ const client = siteBase === null ? null : communityClientFor(props.apiBase, props.transport, siteBase)
37
+ const ctl: ProfileController | null = client ? createProfile({ client }) : null
38
+ const state = ctl?.state ?? null
39
+
40
+ onMounted(() => void ctl?.load(props.handle ?? ''))
41
+ // A host that reuses the element for another member (client-side navigation) reads that one.
42
+ watch(
43
+ () => props.handle,
44
+ (next, before) => {
45
+ if (next !== before) void ctl?.load(next ?? '')
46
+ },
47
+ )
48
+ onBeforeUnmount(() => ctl?.dispose())
49
+
50
+ const since = computed(() => (state?.memberSince ? formatMonth(lang.value, state.memberSince) : ''))
51
+ const failure = computed(() => {
52
+ const f = state?.failure
53
+ if (!f) return ''
54
+ const r = failureText(f, lang.value)
55
+ return t(r.key, r.params)
56
+ })
57
+
58
+ function signInHref(): string | null {
59
+ return signInLink(props.signInHref, typeof location === 'undefined' ? null : location.pathname)
60
+ }
61
+ </script>
62
+
63
+ <template>
64
+ <section v-if="ctl && state" class="bcm bcm-profile" :lang="lang" :aria-labelledby="`${id}-title`">
65
+ <p v-if="state.phase === 'loading' || state.phase === 'idle'" class="bcm-muted" role="status">{{ t('common.loading') }}</p>
66
+ <p v-else-if="state.phase === 'notFound'" :id="`${id}-title`" class="bcm-note">{{ t('profile.notFound') }}</p>
67
+ <div v-else-if="state.phase === 'signIn'" class="bcm-row">
68
+ <p :id="`${id}-title`">{{ t('profile.signIn') }}</p>
69
+ <a v-if="signInHref()" class="bcm-btn bcm-primary" :href="signInHref() ?? undefined">{{ t('common.signIn') }}</a>
70
+ </div>
71
+ <div v-else-if="state.phase === 'failed'" class="bcm-banner" role="alert">
72
+ <span :id="`${id}-title`">{{ failure }}</span>
73
+ <button type="button" class="bcm-btn" @click="ctl.load(props.handle ?? '')">{{ t('common.retry') }}</button>
74
+ </div>
75
+ <template v-else-if="state.author && state.author.kind === 'member'">
76
+ <header class="bcm-profile-head">
77
+ <AuthorAvatar :author="state.author" />
78
+ <div class="bcm-msg-main">
79
+ <h2 :id="`${id}-title`">{{ state.author.displayName }}</h2>
80
+ <span class="bcm-handle">{{ state.author.handle }}</span>
81
+ </div>
82
+ </header>
83
+ <p v-if="state.trustKey"><span class="bcm-badge">{{ t(state.trustKey) }}</span></p>
84
+ <p v-if="since" class="bcm-muted">{{ t('profile.memberSince', { date: since }) }}</p>
85
+ <section v-if="state.bio" class="bcm-section" :aria-labelledby="`${id}-bio`">
86
+ <h3 :id="`${id}-bio`">{{ t('profile.bio') }}</h3>
87
+ <p class="bcm-text">{{ state.bio }}</p>
88
+ </section>
89
+ </template>
90
+ </section>
91
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * `<blogmcp-welcome>`: the welcome page after joining (m2-contracts.md §3.10; the storefront's `/komunita/vitejte`) —
4
+ * the tenant's rules in summary (as the onboarding shows them, with the link to the rules page) and three links: the
5
+ * discussions, the settings, the member's own profile. A visitor gets the sign-in link in the profile's place.
6
+ */
7
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
8
+ import { profilePath, signInLink, type CommunityTransport } from '@blogmcp/sdk/community'
9
+ import { parseRoutes, routesWithDefaults } from '../core/attrs'
10
+ import { communityClientFor, siteBaseFor } from '../core/client'
11
+ import { documentLang, pickLang, type Lang } from '../core/i18n'
12
+ import { createWelcome, type WelcomeController } from '../core/profile'
13
+ import { provideText, uid } from './context'
14
+
15
+ defineOptions({ inheritAttrs: false })
16
+
17
+ const props = defineProps<{
18
+ apiBase?: string
19
+ siteBase?: string
20
+ lang?: string
21
+ /** JSON (or an object): `rulesPath`, `discussionsPath`, `settingsPath`, `profilePath`. */
22
+ routes?: string | Record<string, unknown>
23
+ signInHref?: string
24
+ transport?: CommunityTransport
25
+ }>()
26
+
27
+ const lang = ref<Lang>(pickLang(props.lang, documentLang()))
28
+ const { t } = provideText(lang)
29
+ const id = uid('bcm-welcome')
30
+
31
+ const siteBase = siteBaseFor(props.siteBase)
32
+ const client = siteBase === null ? null : communityClientFor(props.apiBase, props.transport, siteBase)
33
+ const ctl: WelcomeController | null = client ? createWelcome({ client, lang: lang.value }) : null
34
+ const state = ctl?.state ?? null
35
+ const routes = computed(() => routesWithDefaults(parseRoutes(props.routes), null, siteBase ?? ''))
36
+ const ownProfile = computed(() => (state?.handle ? profilePath(routes.value.profilePath, state.handle) : null))
37
+
38
+ onMounted(() => void ctl?.load())
39
+ onBeforeUnmount(() => ctl?.dispose())
40
+
41
+ function signInHref(): string | null {
42
+ return signInLink(props.signInHref, typeof location === 'undefined' ? null : location.pathname)
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <section v-if="ctl && state" class="bcm bcm-welcome" :lang="lang" :aria-labelledby="`${id}-title`">
48
+ <h2 :id="`${id}-title`">{{ t('welcome.title') }}</h2>
49
+ <p>{{ t('welcome.lede') }}</p>
50
+
51
+ <p v-if="state.phase !== 'ready'" class="bcm-muted" role="status">{{ t('common.loading') }}</p>
52
+ <template v-else>
53
+ <section class="bcm-section" :aria-labelledby="`${id}-rules`">
54
+ <h3 :id="`${id}-rules`">{{ t('welcome.rules') }}</h3>
55
+ <ol v-if="state.rules.length" class="bcm-rules">
56
+ <li v-for="rule in state.rules" :key="rule.id">
57
+ <strong>{{ rule.title }}</strong>
58
+ <span v-if="rule.summary" class="bcm-hint">{{ rule.summary }}</span>
59
+ </li>
60
+ </ol>
61
+ <p><a :href="routes.rulesPath">{{ t('onboarding.rulesLink') }}</a></p>
62
+ </section>
63
+
64
+ <ul class="bcm-links">
65
+ <li><a class="bcm-btn" :href="routes.discussionsPath">{{ t('welcome.discussions') }}</a></li>
66
+ <li><a class="bcm-btn" :href="routes.settingsPath">{{ t('welcome.settings') }}</a></li>
67
+ <li v-if="ownProfile"><a class="bcm-btn" :href="ownProfile">{{ t('welcome.profile') }}</a></li>
68
+ <li v-else-if="state.visitor && signInHref()"><a class="bcm-btn bcm-primary" :href="signInHref() ?? undefined">{{ t('common.signIn') }}</a></li>
69
+ </ul>
70
+ </template>
71
+ </section>
72
+ </template>
package/src/ui/styles.css CHANGED
@@ -220,6 +220,22 @@ span.bcm-react { cursor: default; }
220
220
  .bcm-receipt { display: flex; flex-direction: column; gap: 8px; }
221
221
  .bcm-turnstile { min-height: 0; }
222
222
 
223
+ /* ── M2: mentions, the badge, the centre, settings, profiles, the welcome page ── */
224
+ .bcm-mention { color: var(--bcm-accent); font-weight: 600; text-decoration: none; }
225
+ .bcm-mention:hover { text-decoration: underline; }
226
+ :host(blogmcp-badge) { display: inline-block; }
227
+ .bcm-badge-link {
228
+ display: inline-flex; align-items: center; justify-content: center; min-width: 1.5em; height: 1.5em; padding: 0 6px;
229
+ border-radius: 999px; background: var(--bcm-accent); color: var(--bcm-accent-fg); font-size: 0.8125em; font-weight: 700;
230
+ line-height: 1; text-decoration: none; font-variant-numeric: tabular-nums;
231
+ }
232
+ .bcm-switches { list-style: none; display: flex; flex-direction: column; gap: 10px; }
233
+ .bcm-switches li { display: flex; flex-direction: column; gap: 2px; }
234
+ .bcm-profile-head { display: flex; align-items: center; gap: 12px; }
235
+ .bcm-profile-head .bcm-avatar { width: 48px; height: 48px; font-size: 26px; }
236
+ .bcm-links { list-style: none; display: flex; flex-direction: column; gap: 8px; }
237
+ .bcm-url { overflow-wrap: anywhere; font-family: ui-monospace, monospace; font-size: 0.875em; }
238
+
223
239
  /* ── dialogs (native <dialog>: the top layer, Esc, focus kept inside) ── */
224
240
  .bcm-dialog {
225
241
  border: 1px solid var(--bcm-line); border-radius: 14px; padding: 0;
package/src/vue.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * `<KbDiscussion>`, `<KbNoticeForm>`, `<KbMyCommunity>` for Vue / Nuxt: each mounts its element on the
2
+ * `<KbDiscussion>`, `<KbNoticeForm>`, `<KbMyCommunity>` and (M2) `<KbNotificationCentre>`, `<KbBadge>`,
3
+ * `<KbMemberSettings>`, `<KbProfile>`, `<KbWelcome>` for Vue / Nuxt: each mounts its element on the
3
4
  * client, after hydration, and removes it on unmount. Each renders one empty `<div>` as the mount point —
4
5
  * nothing else, on the server or the client — so the element sits where the component does and the page's
5
6
  * markup is otherwise the site's alone.
@@ -12,7 +13,17 @@
12
13
  import { defineComponent, h, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
13
14
  import type { CommunityTransport } from '@blogmcp/sdk/community'
14
15
  import type { CommunityRoutes } from './core/attrs'
15
- import type { MountDiscussionOptions, MountMyCommunityOptions, MountNoticeFormOptions } from './mount'
16
+ import type { DiscussionRealtime } from './core/realtime'
17
+ import type {
18
+ MountBadgeOptions,
19
+ MountDiscussionOptions,
20
+ MountMemberSettingsOptions,
21
+ MountMyCommunityOptions,
22
+ MountNoticeFormOptions,
23
+ MountNotificationCentreOptions,
24
+ MountProfileOptions,
25
+ MountWelcomeOptions,
26
+ } from './mount'
16
27
 
17
28
  type Mounter<O> = (o: O & { container: Element }) => () => void
18
29
 
@@ -60,6 +71,8 @@ export const KbDiscussion = defineComponent({
60
71
  pollMs: Number,
61
72
  nonce: String,
62
73
  transport: Function as PropType<CommunityTransport>,
74
+ /** M2: `createRealtimeSession` of `@blogmcp/sdk/community/realtime`. */
75
+ realtime: Function as PropType<DiscussionRealtime>,
63
76
  },
64
77
  setup(props) {
65
78
  return useMount<MountDiscussionOptions>(
@@ -113,4 +126,117 @@ export const KbMyCommunity = defineComponent({
113
126
  },
114
127
  })
115
128
 
116
- export type { MountDiscussionOptions, MountNoticeFormOptions, MountMyCommunityOptions, CommunityRoutes }
129
+ export const KbNotificationCentre = defineComponent({
130
+ name: 'KbNotificationCentre',
131
+ props: {
132
+ apiBase: String,
133
+ siteBase: String,
134
+ lang: String,
135
+ signInHref: String,
136
+ routes: Object as PropType<CommunityRoutes>,
137
+ nonce: String,
138
+ transport: Function as PropType<CommunityTransport>,
139
+ },
140
+ setup(props) {
141
+ return useMount<MountNotificationCentreOptions>(
142
+ 'notification-centre',
143
+ () => defined({ ...props }) as MountNotificationCentreOptions,
144
+ async () => (await import('./mount')).mountNotificationCentre as Mounter<MountNotificationCentreOptions>,
145
+ )
146
+ },
147
+ })
148
+
149
+ export const KbBadge = defineComponent({
150
+ name: 'KbBadge',
151
+ props: {
152
+ apiBase: String,
153
+ siteBase: String,
154
+ lang: String,
155
+ routes: Object as PropType<CommunityRoutes>,
156
+ pollMs: Number,
157
+ nonce: String,
158
+ transport: Function as PropType<CommunityTransport>,
159
+ },
160
+ setup(props) {
161
+ return useMount<MountBadgeOptions>(
162
+ 'badge',
163
+ () => defined({ ...props }) as MountBadgeOptions,
164
+ async () => (await import('./mount')).mountBadge as Mounter<MountBadgeOptions>,
165
+ )
166
+ },
167
+ })
168
+
169
+ export const KbMemberSettings = defineComponent({
170
+ name: 'KbMemberSettings',
171
+ props: {
172
+ apiBase: String,
173
+ siteBase: String,
174
+ lang: String,
175
+ swPath: String,
176
+ signInHref: String,
177
+ routes: Object as PropType<CommunityRoutes>,
178
+ nonce: String,
179
+ transport: Function as PropType<CommunityTransport>,
180
+ },
181
+ setup(props) {
182
+ return useMount<MountMemberSettingsOptions>(
183
+ 'member-settings',
184
+ () => defined({ ...props }) as MountMemberSettingsOptions,
185
+ async () => (await import('./mount')).mountMemberSettings as Mounter<MountMemberSettingsOptions>,
186
+ )
187
+ },
188
+ })
189
+
190
+ export const KbProfile = defineComponent({
191
+ name: 'KbProfile',
192
+ props: {
193
+ handle: { type: String, required: true },
194
+ apiBase: String,
195
+ siteBase: String,
196
+ lang: String,
197
+ signInHref: String,
198
+ routes: Object as PropType<CommunityRoutes>,
199
+ nonce: String,
200
+ transport: Function as PropType<CommunityTransport>,
201
+ },
202
+ setup(props) {
203
+ return useMount<MountProfileOptions>(
204
+ 'profile',
205
+ () => defined({ ...props }) as MountProfileOptions,
206
+ async () => (await import('./mount')).mountProfile as Mounter<MountProfileOptions>,
207
+ )
208
+ },
209
+ })
210
+
211
+ export const KbWelcome = defineComponent({
212
+ name: 'KbWelcome',
213
+ props: {
214
+ apiBase: String,
215
+ siteBase: String,
216
+ lang: String,
217
+ signInHref: String,
218
+ routes: Object as PropType<CommunityRoutes>,
219
+ nonce: String,
220
+ transport: Function as PropType<CommunityTransport>,
221
+ },
222
+ setup(props) {
223
+ return useMount<MountWelcomeOptions>(
224
+ 'welcome',
225
+ () => defined({ ...props }) as MountWelcomeOptions,
226
+ async () => (await import('./mount')).mountWelcome as Mounter<MountWelcomeOptions>,
227
+ )
228
+ },
229
+ })
230
+
231
+ export type {
232
+ MountDiscussionOptions,
233
+ MountNoticeFormOptions,
234
+ MountMyCommunityOptions,
235
+ MountNotificationCentreOptions,
236
+ MountBadgeOptions,
237
+ MountMemberSettingsOptions,
238
+ MountProfileOptions,
239
+ MountWelcomeOptions,
240
+ CommunityRoutes,
241
+ DiscussionRealtime,
242
+ }