@commonpub/layer 0.21.4 → 0.21.6
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/components/CookieConsent.vue +1 -1
- package/components/RemoteFollowDialog.vue +2 -2
- package/components/SearchFilters.vue +1 -1
- package/components/ShareToHubModal.vue +10 -2
- package/components/editors/MarkdownImportDialog.vue +5 -2
- package/components/homepage/HomepageSectionRenderer.vue +1 -1
- package/error.vue +1 -1
- package/package.json +11 -9
- package/pages/search.vue +1 -1
- package/pages/u/[username]/index.vue +1 -1
- package/server/api/realtime/stream.get.ts +18 -3
|
@@ -34,7 +34,7 @@ const visible = computed(() => !hasConsented.value && hasNonEssentialCookies.val
|
|
|
34
34
|
z-index: var(--z-toast);
|
|
35
35
|
background: var(--surface);
|
|
36
36
|
border-top: var(--border-width-default) solid var(--border);
|
|
37
|
-
box-shadow: 0 -
|
|
37
|
+
box-shadow: 0 -2px 0 var(--border);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
.cpub-consent-inner {
|
|
@@ -79,14 +79,14 @@ useFocusTrap(dialogRef, () => open.value, close);
|
|
|
79
79
|
<style scoped>
|
|
80
80
|
.cpub-rfd-overlay {
|
|
81
81
|
position: fixed; inset: 0; z-index: 9999;
|
|
82
|
-
background: rgba(0, 0, 0, 0.5); display: flex;
|
|
82
|
+
background: var(--color-surface-overlay, rgba(0, 0, 0, 0.5)); display: flex;
|
|
83
83
|
align-items: center; justify-content: center;
|
|
84
84
|
padding: 16px;
|
|
85
85
|
}
|
|
86
86
|
.cpub-rfd-dialog {
|
|
87
87
|
background: var(--bg); border: var(--border-width-default) solid var(--border);
|
|
88
88
|
width: 100%; max-width: 420px; padding: 24px;
|
|
89
|
-
box-shadow:
|
|
89
|
+
box-shadow: var(--shadow-md);
|
|
90
90
|
}
|
|
91
91
|
.cpub-rfd-header {
|
|
92
92
|
display: flex; align-items: center; justify-content: space-between;
|
|
@@ -156,7 +156,7 @@ function removeTag(tag: string): void {
|
|
|
156
156
|
padding: 7px 10px;
|
|
157
157
|
font-size: 12px;
|
|
158
158
|
color: var(--text);
|
|
159
|
-
font-family:
|
|
159
|
+
font-family: var(--font-sans);
|
|
160
160
|
outline: none;
|
|
161
161
|
margin-bottom: 6px;
|
|
162
162
|
transition: border-color 0.15s;
|
|
@@ -14,6 +14,14 @@ const { data: userHubs } = useLazyFetch<{ items: Array<{ id: string; name: strin
|
|
|
14
14
|
const sharing = ref(false);
|
|
15
15
|
const selectedHub = ref('');
|
|
16
16
|
|
|
17
|
+
// Parent mounts/unmounts this modal via v-if, so it's always "open" while
|
|
18
|
+
// mounted. A local ref flipped on mount drives useFocusTrap's watcher
|
|
19
|
+
// (false -> true), activating the trap, Esc handler and scroll lock.
|
|
20
|
+
const contentRef = ref<HTMLElement | null>(null);
|
|
21
|
+
const visible = ref(false);
|
|
22
|
+
onMounted(() => { visible.value = true; });
|
|
23
|
+
useFocusTrap(contentRef, () => visible.value, () => emit('close'));
|
|
24
|
+
|
|
17
25
|
async function handleShare(): Promise<void> {
|
|
18
26
|
if (!selectedHub.value) return;
|
|
19
27
|
sharing.value = true;
|
|
@@ -35,9 +43,9 @@ async function handleShare(): Promise<void> {
|
|
|
35
43
|
|
|
36
44
|
<template>
|
|
37
45
|
<div class="cpub-modal-backdrop" @click.self="emit('close')">
|
|
38
|
-
<div class="cpub-modal-content">
|
|
46
|
+
<div ref="contentRef" class="cpub-modal-content" role="dialog" aria-modal="true" aria-labelledby="cpub-share-hub-title">
|
|
39
47
|
<div class="cpub-modal-header">
|
|
40
|
-
<h3 class="cpub-modal-title">Share to Hub</h3>
|
|
48
|
+
<h3 id="cpub-share-hub-title" class="cpub-modal-title">Share to Hub</h3>
|
|
41
49
|
<button class="cpub-modal-close" aria-label="Close" @click="emit('close')"><i class="fa-solid fa-xmark"></i></button>
|
|
42
50
|
</div>
|
|
43
51
|
|
|
@@ -10,6 +10,9 @@ const emit = defineEmits<{
|
|
|
10
10
|
import: [md: string, mode: 'append' | 'replace'];
|
|
11
11
|
}>();
|
|
12
12
|
|
|
13
|
+
const dialogRef = ref<HTMLElement | null>(null);
|
|
14
|
+
useFocusTrap(dialogRef, () => props.show, () => emit('close'));
|
|
15
|
+
|
|
13
16
|
const activeTab = ref<'paste' | 'file'>('paste');
|
|
14
17
|
const markdownText = ref('');
|
|
15
18
|
const mode = ref<'append' | 'replace'>('append');
|
|
@@ -58,9 +61,9 @@ async function readFile(file: File): Promise<void> {
|
|
|
58
61
|
<template>
|
|
59
62
|
<Teleport to="body">
|
|
60
63
|
<div v-if="show" class="md-import-overlay" @click.self="emit('close')">
|
|
61
|
-
<div class="md-import-dialog">
|
|
64
|
+
<div ref="dialogRef" class="md-import-dialog" role="dialog" aria-modal="true" aria-labelledby="md-import-title">
|
|
62
65
|
<div class="md-import-header">
|
|
63
|
-
<h2><i class="fa-brands fa-markdown"></i> Import Markdown</h2>
|
|
66
|
+
<h2 id="md-import-title"><i class="fa-brands fa-markdown"></i> Import Markdown</h2>
|
|
64
67
|
<button class="md-import-close" @click="emit('close')"><i class="fa-solid fa-xmark"></i></button>
|
|
65
68
|
</div>
|
|
66
69
|
|
|
@@ -21,7 +21,7 @@ const features = useFeatures();
|
|
|
21
21
|
|
|
22
22
|
function isFeatureEnabled(featureGate?: string): boolean {
|
|
23
23
|
if (!featureGate) return true;
|
|
24
|
-
return (features.features as unknown as Record<string, boolean>)?.[featureGate] ?? true;
|
|
24
|
+
return (features.features.value as unknown as Record<string, boolean>)?.[featureGate] ?? true;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Section types that render in the full-width zone (above the 2-column layout) */
|
package/error.vue
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"vue": "^3.4.0",
|
|
51
51
|
"vue-router": "^4.3.0",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
-
"@commonpub/editor": "0.7.9",
|
|
54
|
-
"@commonpub/protocol": "0.9.10",
|
|
55
|
-
"@commonpub/schema": "0.16.0",
|
|
56
53
|
"@commonpub/docs": "0.6.3",
|
|
54
|
+
"@commonpub/config": "0.12.0",
|
|
55
|
+
"@commonpub/learning": "0.5.2",
|
|
57
56
|
"@commonpub/auth": "0.6.0",
|
|
58
57
|
"@commonpub/explainer": "0.7.12",
|
|
59
|
-
"@commonpub/
|
|
60
|
-
"@commonpub/
|
|
61
|
-
"@commonpub/
|
|
62
|
-
"@commonpub/server": "2.53.
|
|
58
|
+
"@commonpub/schema": "0.16.0",
|
|
59
|
+
"@commonpub/editor": "0.7.9",
|
|
60
|
+
"@commonpub/protocol": "0.9.10",
|
|
61
|
+
"@commonpub/server": "2.53.1",
|
|
62
|
+
"@commonpub/ui": "0.8.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -68,5 +68,7 @@
|
|
|
68
68
|
"jsdom": "^25.0.1",
|
|
69
69
|
"vitest": "^3.2.4"
|
|
70
70
|
},
|
|
71
|
-
"scripts": {
|
|
71
|
+
"scripts": {
|
|
72
|
+
"test": "vitest run"
|
|
73
|
+
}
|
|
72
74
|
}
|
package/pages/search.vue
CHANGED
|
@@ -512,7 +512,7 @@ const { data: relatedCommunities } = await useFetch('/api/hubs', {
|
|
|
512
512
|
font-size: 15px;
|
|
513
513
|
font-weight: 500;
|
|
514
514
|
color: var(--text);
|
|
515
|
-
font-family:
|
|
515
|
+
font-family: var(--font-sans);
|
|
516
516
|
outline: none;
|
|
517
517
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
518
518
|
}
|
|
@@ -679,7 +679,7 @@ async function handleReport(): Promise<void> {
|
|
|
679
679
|
background: none;
|
|
680
680
|
cursor: pointer;
|
|
681
681
|
border-bottom: 3px solid transparent;
|
|
682
|
-
font-family:
|
|
682
|
+
font-family: var(--font-sans);
|
|
683
683
|
display: flex;
|
|
684
684
|
align-items: center;
|
|
685
685
|
gap: 6px;
|
|
@@ -47,6 +47,23 @@ export default defineEventHandler(async (event) => {
|
|
|
47
47
|
}
|
|
48
48
|
userConnections.set(userId, current + 1);
|
|
49
49
|
|
|
50
|
+
// Decrement exactly once on disconnect. Registered at handler scope (not
|
|
51
|
+
// inside ReadableStream.start) because start() is invoked lazily when the
|
|
52
|
+
// runtime begins pulling the stream — if the client aborts before that,
|
|
53
|
+
// start() never runs and an in-start cleanup would never fire, leaking
|
|
54
|
+
// the slot permanently and eventually 429-locking the user for the
|
|
55
|
+
// process lifetime. cleanup() also calls release(); the guard makes it
|
|
56
|
+
// idempotent so there is no double-decrement.
|
|
57
|
+
let released = false;
|
|
58
|
+
const release = (): void => {
|
|
59
|
+
if (released) return;
|
|
60
|
+
released = true;
|
|
61
|
+
const next = (userConnections.get(userId) ?? 1) - 1;
|
|
62
|
+
if (next <= 0) userConnections.delete(userId);
|
|
63
|
+
else userConnections.set(userId, next);
|
|
64
|
+
};
|
|
65
|
+
event.node.req.on('close', release);
|
|
66
|
+
|
|
50
67
|
const encoder = new TextEncoder();
|
|
51
68
|
const stream = new ReadableStream({
|
|
52
69
|
async start(controller) {
|
|
@@ -65,9 +82,7 @@ export default defineEventHandler(async (event) => {
|
|
|
65
82
|
unsubscribe = null;
|
|
66
83
|
}
|
|
67
84
|
try { controller.close(); } catch { /* already closed */ }
|
|
68
|
-
|
|
69
|
-
if (next <= 0) userConnections.delete(userId);
|
|
70
|
-
else userConnections.set(userId, next);
|
|
85
|
+
release();
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
async function sendCounts(): Promise<void> {
|