Skip to content

Commit 524c6b6

Browse files
authored
feat(examples): add dock switcher UI to minimal hub examples (#28)
1 parent 34d68eb commit 524c6b6

17 files changed

Lines changed: 660 additions & 152 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Next Demo Tool B</title>
7+
<style>
8+
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
9+
body { margin: 0; padding: 2rem; }
10+
h1 { margin-top: 0; }
11+
code { font-family: ui-monospace, monospace; }
12+
</style>
13+
</head>
14+
<body>
15+
<h1>Next Demo Tool B</h1>
16+
<p>Served from <code id="loc"></code></p>
17+
<p>A second demo devframe, mounted alongside the first to demonstrate dock switching.</p>
18+
<script>
19+
document.getElementById('loc').textContent = location.pathname
20+
</script>
21+
</body>
22+
</html>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Next Demo Tool</title>
7+
<style>
8+
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
9+
body { margin: 0; padding: 2rem; }
10+
h1 { margin-top: 0; }
11+
code { font-family: ui-monospace, monospace; }
12+
</style>
13+
</head>
14+
<body>
15+
<h1>Next Demo Tool</h1>
16+
<p>Served from <code id="loc"></code></p>
17+
<p>This SPA is mounted by <code>minimal-next-devframe-hub</code> via <code>DevframeHost.mountStatic()</code>.</p>
18+
<script>
19+
document.getElementById('loc').textContent = location.pathname
20+
</script>
21+
</body>
22+
</html>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { createReadStream } from 'node:fs'
2+
import { stat } from 'node:fs/promises'
3+
import { Readable } from 'node:stream'
4+
import { extname, join, normalize, resolve, sep } from 'pathe'
5+
import { ensureMinimalNextDevframeHub, getStaticMount } from '../../../devframe/minimal-next-devframe-hub'
6+
7+
export const runtime = 'nodejs'
8+
export const dynamic = 'force-dynamic'
9+
10+
const CONTENT_TYPES: Record<string, string> = {
11+
'.html': 'text/html; charset=utf-8',
12+
'.htm': 'text/html; charset=utf-8',
13+
'.css': 'text/css; charset=utf-8',
14+
'.js': 'application/javascript; charset=utf-8',
15+
'.mjs': 'application/javascript; charset=utf-8',
16+
'.json': 'application/json; charset=utf-8',
17+
'.svg': 'image/svg+xml',
18+
'.png': 'image/png',
19+
'.jpg': 'image/jpeg',
20+
'.jpeg': 'image/jpeg',
21+
'.gif': 'image/gif',
22+
'.webp': 'image/webp',
23+
'.ico': 'image/x-icon',
24+
}
25+
26+
interface ResolvedFile {
27+
abs: string
28+
size: number
29+
mtime: Date
30+
}
31+
32+
async function statFile(abs: string): Promise<ResolvedFile | null> {
33+
try {
34+
const s = await stat(abs)
35+
if (!s.isFile())
36+
return null
37+
return { abs, size: s.size, mtime: s.mtime }
38+
}
39+
catch {
40+
return null
41+
}
42+
}
43+
44+
async function resolveTarget(absDir: string, urlPath: string): Promise<ResolvedFile | null> {
45+
let cleaned = decodeURIComponent(urlPath || '/').replace(/[?#].*$/, '')
46+
if (cleaned.endsWith('/'))
47+
cleaned = cleaned.slice(0, -1)
48+
if (cleaned.startsWith('/'))
49+
cleaned = cleaned.slice(1)
50+
51+
const abs = normalize(join(absDir, cleaned))
52+
if (abs !== absDir && !abs.startsWith(absDir + sep))
53+
return null
54+
55+
const direct = await statFile(abs)
56+
if (direct)
57+
return direct
58+
59+
// Directory → index.html
60+
try {
61+
const s = await stat(abs)
62+
if (s.isDirectory()) {
63+
const candidate = await statFile(join(abs, 'index.html'))
64+
if (candidate)
65+
return candidate
66+
}
67+
}
68+
catch {
69+
// not found / not a directory — continue
70+
}
71+
72+
// SPA fallback for extensionless paths
73+
if (!/\.[a-z0-9]+$/i.test(cleaned)) {
74+
const fallback = await statFile(join(absDir, 'index.html'))
75+
if (fallback)
76+
return fallback
77+
}
78+
79+
return null
80+
}
81+
82+
export async function GET(request: Request): Promise<Response> {
83+
await ensureMinimalNextDevframeHub()
84+
85+
const pathname = new URL(request.url).pathname
86+
const hit = getStaticMount(pathname)
87+
if (!hit)
88+
return new Response(null, { status: 404 })
89+
90+
const file = await resolveTarget(resolve(hit.distDir), hit.relative)
91+
if (!file)
92+
return new Response(null, { status: 404 })
93+
94+
return new Response(Readable.toWeb(createReadStream(file.abs)) as ReadableStream, {
95+
status: 200,
96+
headers: {
97+
'Content-Type': CONTENT_TYPES[extname(file.abs).toLowerCase()] ?? 'application/octet-stream',
98+
'Content-Length': String(file.size),
99+
'Last-Modified': file.mtime.toUTCString(),
100+
'Cache-Control': 'no-store',
101+
},
102+
})
103+
}

examples/minimal-next-devframe-hub/src/client/app/globals.css

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,99 @@
66

77
body {
88
margin: 0;
9-
padding: 1.5rem 2rem;
9+
height: 100vh;
10+
overflow: hidden;
1011
}
1112

12-
main {
13-
max-width: 800px;
14-
margin-inline: auto;
13+
.app-shell {
14+
display: grid;
15+
grid-template-columns: 220px 1fr;
16+
grid-template-rows: auto 1fr auto;
17+
grid-template-areas:
18+
"header header"
19+
"sidebar main"
20+
"footer footer";
21+
height: 100vh;
1522
}
1623

17-
header h1 {
18-
margin-bottom: 0.25rem;
24+
.app-header {
25+
grid-area: header;
26+
padding: 0.75rem 1rem;
27+
border-bottom: 1px solid color-mix(in srgb, currentcolor 20%, transparent);
1928
}
2029

21-
header p {
22-
margin-top: 0;
30+
.app-header h1 {
31+
margin: 0;
32+
font-size: 1rem;
33+
letter-spacing: 0.05em;
34+
text-transform: uppercase;
35+
}
36+
37+
.app-header p {
38+
margin: 0.25rem 0 0;
39+
font-family: ui-monospace, monospace;
40+
font-size: 0.8rem;
2341
opacity: 0.7;
2442
}
2543

26-
section {
27-
margin-block: 1.5rem;
44+
.app-sidebar {
45+
grid-area: sidebar;
46+
border-right: 1px solid color-mix(in srgb, currentcolor 20%, transparent);
47+
padding: 0.75rem;
48+
overflow: auto;
49+
}
50+
51+
.app-main {
52+
grid-area: main;
53+
overflow: hidden;
54+
background: color-mix(in srgb, currentcolor 3%, transparent);
55+
}
56+
57+
.app-main iframe {
58+
width: 100%;
59+
height: 100%;
60+
border: 0;
61+
display: block;
62+
}
63+
64+
.app-footer {
65+
grid-area: footer;
66+
display: flex;
67+
gap: 1rem;
68+
padding: 0.75rem 1rem;
69+
border-top: 1px solid color-mix(in srgb, currentcolor 20%, transparent);
70+
max-height: 30vh;
71+
overflow: auto;
72+
}
73+
74+
.app-footer section {
75+
flex: 1;
76+
min-width: 0;
2877
}
2978

3079
h2 {
31-
font-size: 1rem;
80+
font-size: 0.75rem;
3281
text-transform: uppercase;
3382
letter-spacing: 0.05em;
3483
opacity: 0.8;
35-
border-bottom: 1px solid currentcolor;
36-
padding-bottom: 0.25rem;
84+
margin: 0 0 0.5rem;
3785
}
3886

3987
ul {
4088
list-style: none;
4189
padding-left: 0;
90+
margin: 0;
91+
display: flex;
92+
flex-direction: column;
93+
gap: 0.25rem;
4294
}
4395

4496
li {
45-
padding: 0.5rem 0.75rem;
97+
padding: 0.4rem 0.6rem;
4698
border: 1px solid color-mix(in srgb, currentcolor 15%, transparent);
47-
border-radius: 0.5rem;
48-
margin-bottom: 0.5rem;
99+
border-radius: 0.4rem;
49100
font-family: ui-monospace, monospace;
50-
font-size: 0.9rem;
101+
font-size: 0.8rem;
51102
}
52103

53104
li.muted {
@@ -62,29 +113,54 @@ code {
62113
border-radius: 0.25em;
63114
}
64115

65-
button {
116+
.app-sidebar ul li {
117+
padding: 0;
118+
border: 0;
119+
background: transparent;
120+
}
121+
122+
.app-sidebar button {
123+
width: 100%;
124+
text-align: left;
125+
padding: 0.5rem 0.6rem;
66126
font: inherit;
67-
padding: 0.5rem 1rem;
68-
border-radius: 0.5rem;
69-
border: 1px solid currentcolor;
127+
font-size: 0.85rem;
70128
background: transparent;
129+
border: 1px solid transparent;
130+
border-radius: 0.4rem;
71131
cursor: pointer;
132+
color: inherit;
72133
}
73134

74-
button:hover {
75-
background: color-mix(in srgb, currentcolor 10%, transparent);
135+
.app-sidebar button:hover {
136+
background: color-mix(in srgb, currentcolor 8%, transparent);
137+
}
138+
139+
.app-sidebar button.active {
140+
background: color-mix(in srgb, currentcolor 15%, transparent);
141+
border-color: color-mix(in srgb, currentcolor 30%, transparent);
76142
}
77143

78144
.actions {
79145
display: flex;
80146
flex-wrap: wrap;
81147
gap: 0.75rem;
148+
align-items: flex-start;
82149
}
83150

84-
#status {
85-
font-family: ui-monospace, monospace;
151+
button {
152+
font: inherit;
86153
font-size: 0.85rem;
87-
opacity: 0.7;
154+
padding: 0.4rem 0.8rem;
155+
border-radius: 0.4rem;
156+
border: 1px solid currentcolor;
157+
background: transparent;
158+
cursor: pointer;
159+
color: inherit;
160+
}
161+
162+
button:hover {
163+
background: color-mix(in srgb, currentcolor 10%, transparent);
88164
}
89165

90166
#status.error span {

0 commit comments

Comments
 (0)