@alstar/studio 0.0.0-beta.5 → 0.0.0-beta.7
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/api/api-key.ts +74 -0
- package/api/block.ts +82 -42
- package/api/index.ts +9 -1
- package/api/mcp.ts +53 -0
- package/components/AdminPanel.ts +74 -0
- package/components/BlockFieldRenderer.ts +121 -0
- package/components/BlockRenderer.ts +22 -0
- package/components/Entries.ts +5 -4
- package/components/Entry.ts +13 -21
- package/components/FieldRenderer.ts +35 -0
- package/components/Render.ts +46 -0
- package/components/Settings.ts +98 -0
- package/components/{layout.ts → SiteLayout.ts} +9 -13
- package/components/fields/Markdown.ts +44 -0
- package/components/fields/Text.ts +42 -0
- package/components/fields/index.ts +4 -0
- package/components/icons.ts +59 -0
- package/index.ts +52 -30
- package/package.json +3 -2
- package/pages/entry/[id].ts +17 -0
- package/{components → pages}/index.ts +7 -4
- package/pages/settings.ts +10 -0
- package/public/studio/admin-panel.css +103 -0
- package/public/studio/blocks.css +53 -0
- package/public/studio/main.css +162 -0
- package/public/studio/main.js +10 -0
- package/public/studio/markdown-editor.js +34 -0
- package/public/studio/settings.css +24 -0
- package/public/studio/sortable-list.js +40 -0
- package/queries/block-with-children.ts +74 -0
- package/queries/block.ts +31 -40
- package/queries/db-types.ts +15 -0
- package/queries/getBlockTrees-2.ts +0 -0
- package/queries/getBlockTrees.ts +316 -0
- package/queries/getBlocks.ts +214 -0
- package/queries/index.ts +1 -0
- package/queries/structure-types.ts +97 -0
- package/schemas.ts +14 -3
- package/types.ts +123 -6
- package/utils/buildBlocksTree.ts +4 -4
- package/utils/define.ts +31 -5
- package/utils/file-based-router.ts +1 -0
- package/utils/get-or-create-row.ts +41 -0
- package/utils/startup-log.ts +9 -0
- package/components/AdminPanel/AdminPanel.css +0 -78
- package/components/AdminPanel/AdminPanel.ts +0 -57
- package/components/Block.ts +0 -116
- package/components/Field.ts +0 -168
- package/components/Fields.ts +0 -43
- package/public/main.css +0 -95
- package/public/main.js +0 -44
- /package/{components/Entry.css → public/studio/entry.css} +0 -0
- /package/public/{favicon.svg → studio/favicon.svg} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import type { HtmlEscapedString } from 'hono/utils/html'
|
|
3
|
+
import FieldRenderer from './FieldRenderer.ts'
|
|
4
|
+
import BlockFieldRenderer from './BlockFieldRenderer.ts'
|
|
5
|
+
import BlockRenderer from './BlockRenderer.ts'
|
|
6
|
+
import {
|
|
7
|
+
BlockFieldInstance,
|
|
8
|
+
BlockInstance,
|
|
9
|
+
FieldInstance,
|
|
10
|
+
} from '../utils/define.ts'
|
|
11
|
+
import type {
|
|
12
|
+
BlockDefStructure,
|
|
13
|
+
BlocksFieldDefStructure,
|
|
14
|
+
FieldDefStructure,
|
|
15
|
+
} from '../types.ts'
|
|
16
|
+
|
|
17
|
+
export default (props: {
|
|
18
|
+
entryId: number
|
|
19
|
+
parentId: number
|
|
20
|
+
structure: BlockDefStructure | FieldDefStructure | BlocksFieldDefStructure
|
|
21
|
+
id?: number
|
|
22
|
+
name: string
|
|
23
|
+
sortOrder?: number
|
|
24
|
+
}): HtmlEscapedString | Promise<HtmlEscapedString> | string => {
|
|
25
|
+
const { entryId, parentId, structure, name, id } = props
|
|
26
|
+
|
|
27
|
+
if (!structure) return html`<p>No block</p>`
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
switch (structure.instanceOf) {
|
|
31
|
+
case FieldInstance: {
|
|
32
|
+
return FieldRenderer({ entryId, parentId, id, structure, name })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case BlockFieldInstance: {
|
|
36
|
+
return BlockFieldRenderer({ entryId, parentId, id, structure, name })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case BlockInstance: {
|
|
40
|
+
return BlockRenderer({ entryId, parentId, structure, id })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return html`<p>Error rendering "${name}"</p>`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { db } from '@alstar/db'
|
|
2
|
+
import { html } from 'hono/html'
|
|
3
|
+
import { sql } from '../utils/sql.ts'
|
|
4
|
+
import * as icons from './icons.ts'
|
|
5
|
+
|
|
6
|
+
export default () => {
|
|
7
|
+
const apiKeys = db.database
|
|
8
|
+
.prepare(sql`
|
|
9
|
+
select
|
|
10
|
+
*
|
|
11
|
+
from
|
|
12
|
+
api_keys
|
|
13
|
+
`)
|
|
14
|
+
.all()
|
|
15
|
+
|
|
16
|
+
return html`
|
|
17
|
+
<div id="settings" data-signals="{ apiKey: '', copied: false }">
|
|
18
|
+
<!-- <h1>Settings</h1> -->
|
|
19
|
+
<article>
|
|
20
|
+
<header>API Keys</header>
|
|
21
|
+
|
|
22
|
+
<ul>
|
|
23
|
+
${apiKeys.map((apiKey) => {
|
|
24
|
+
return html`<li>
|
|
25
|
+
<p>${apiKey.name}</p>
|
|
26
|
+
<input type="text" disabled value="${apiKey.hint}" />
|
|
27
|
+
<form
|
|
28
|
+
data-on-submit="@delete('/admin/api/api-key', { contentType: 'form' })"
|
|
29
|
+
>
|
|
30
|
+
<button
|
|
31
|
+
data-tooltip="Delete API key"
|
|
32
|
+
data-placement="left"
|
|
33
|
+
type="submit"
|
|
34
|
+
class="ghost"
|
|
35
|
+
>
|
|
36
|
+
${icons.trash}
|
|
37
|
+
</button>
|
|
38
|
+
|
|
39
|
+
<input type="hidden" name="value" value="${apiKey.value}" />
|
|
40
|
+
</form>
|
|
41
|
+
</li>`
|
|
42
|
+
})}
|
|
43
|
+
</ul>
|
|
44
|
+
|
|
45
|
+
<form
|
|
46
|
+
data-on-submit="@post('/admin/api/api-key', { contentType: 'form' })"
|
|
47
|
+
>
|
|
48
|
+
<label for="api_key_name">Generate API Key</label>
|
|
49
|
+
|
|
50
|
+
<input
|
|
51
|
+
data-bind="name"
|
|
52
|
+
type="text"
|
|
53
|
+
name="name"
|
|
54
|
+
id="api_key_name"
|
|
55
|
+
placeholder="Name"
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<button type="submit" class="ghost">Generate key</button>
|
|
59
|
+
</form>
|
|
60
|
+
|
|
61
|
+
<dialog data-attr="{ open: $apiKey !== '' }">
|
|
62
|
+
<article>
|
|
63
|
+
<header>
|
|
64
|
+
<p>API Key</p>
|
|
65
|
+
</header>
|
|
66
|
+
<p>Be sure to save this key, as it wont be shown again.</p>
|
|
67
|
+
|
|
68
|
+
<div style="display: flex; gap: 1rem; align-items: center;">
|
|
69
|
+
<h3 style="margin: 0;">
|
|
70
|
+
<code data-text="$apiKey"></code>
|
|
71
|
+
</h3>
|
|
72
|
+
|
|
73
|
+
<button
|
|
74
|
+
style="display: flex; align-items: center;"
|
|
75
|
+
data-attr="{ id: $apiKey }"
|
|
76
|
+
data-on-click="navigator.clipboard.writeText($apiKey); $copied = true"
|
|
77
|
+
class="ghost"
|
|
78
|
+
aria-label="Copy key to clipboard"
|
|
79
|
+
>
|
|
80
|
+
${icons.clipboard}
|
|
81
|
+
<span style="display: none; margin-left: 0.5rem; color: green;" data-style="{ display: $copied && 'block' }">Copied</span>
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<footer>
|
|
86
|
+
<button
|
|
87
|
+
class="ghost"
|
|
88
|
+
data-on-click="$apiKey = ''; $copied = false; evt.target.closest('dialog')?.close()"
|
|
89
|
+
>
|
|
90
|
+
Close
|
|
91
|
+
</button>
|
|
92
|
+
</footer>
|
|
93
|
+
</article>
|
|
94
|
+
</dialog>
|
|
95
|
+
</article>
|
|
96
|
+
</div>
|
|
97
|
+
`
|
|
98
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import adminPanel from './AdminPanel
|
|
1
|
+
import adminPanel from './AdminPanel.ts'
|
|
2
2
|
import { html } from 'hono/html'
|
|
3
3
|
import { type HtmlEscapedString } from 'hono/utils/html'
|
|
4
|
-
import {
|
|
5
|
-
import { type Structure } from '../types.ts'
|
|
4
|
+
import { studioConfig } from '../index.ts'
|
|
6
5
|
|
|
7
6
|
export default (props: {
|
|
8
7
|
content:
|
|
@@ -10,7 +9,6 @@ export default (props: {
|
|
|
10
9
|
| Promise<string>
|
|
11
10
|
| HtmlEscapedString
|
|
12
11
|
| Promise<HtmlEscapedString>
|
|
13
|
-
structure: Structure
|
|
14
12
|
}) => {
|
|
15
13
|
return html`
|
|
16
14
|
<!DOCTYPE html>
|
|
@@ -23,15 +21,11 @@ export default (props: {
|
|
|
23
21
|
Studio
|
|
24
22
|
</title>
|
|
25
23
|
|
|
26
|
-
<link
|
|
27
|
-
rel="icon"
|
|
28
|
-
type="image/svg"
|
|
29
|
-
href="${rootdir}/public/favicon.svg"
|
|
30
|
-
/>
|
|
24
|
+
<link rel="icon" type="image/svg" href="/studio/favicon.svg" />
|
|
31
25
|
|
|
32
26
|
<meta name="color-scheme" content="light dark" />
|
|
33
27
|
|
|
34
|
-
<link rel="stylesheet" href="
|
|
28
|
+
<link rel="stylesheet" href="/studio/main.css" />
|
|
35
29
|
|
|
36
30
|
<script
|
|
37
31
|
type="module"
|
|
@@ -47,18 +41,20 @@ export default (props: {
|
|
|
47
41
|
}
|
|
48
42
|
}
|
|
49
43
|
</script>
|
|
44
|
+
|
|
45
|
+
<script src="/studio/markdown-editor.js" type="module"></script>
|
|
46
|
+
<script src="/studio/sortable-list.js" type="module"></script>
|
|
47
|
+
<script src="/studio/main.js" type="module"></script>
|
|
50
48
|
</head>
|
|
51
49
|
|
|
52
50
|
<body data-barba="wrapper">
|
|
53
|
-
<section>${adminPanel(
|
|
51
|
+
<section style="margin-bottom: 0;">${adminPanel()}</section>
|
|
54
52
|
|
|
55
53
|
<main>
|
|
56
54
|
<section data-barba="container" data-barba-namespace="default">
|
|
57
55
|
${props.content}
|
|
58
56
|
</section>
|
|
59
57
|
</main>
|
|
60
|
-
|
|
61
|
-
<script src="${rootdir}/public/main.js" type="module"></script>
|
|
62
58
|
</body>
|
|
63
59
|
</html>
|
|
64
60
|
`
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { FieldDefStructure } from '../../types.ts'
|
|
2
|
+
import { getOrCreateRow } from '../../utils/get-or-create-row.ts'
|
|
3
|
+
import { html } from '../../utils/html.ts'
|
|
4
|
+
|
|
5
|
+
export default (props: {
|
|
6
|
+
entryId: number
|
|
7
|
+
parentId: number
|
|
8
|
+
name: string
|
|
9
|
+
id?: number
|
|
10
|
+
structure: FieldDefStructure
|
|
11
|
+
}) => {
|
|
12
|
+
const { entryId, parentId, name, structure, id } = props
|
|
13
|
+
|
|
14
|
+
const data = getOrCreateRow({ parentId, name, field: structure, id })
|
|
15
|
+
|
|
16
|
+
if (!data) return html`<p>No block</p>`
|
|
17
|
+
|
|
18
|
+
return html`
|
|
19
|
+
<form
|
|
20
|
+
data-on-input="@patch('/admin/api/block', { contentType: 'form' })"
|
|
21
|
+
>
|
|
22
|
+
<hgroup>
|
|
23
|
+
<label for="block-${data.id}">${structure.label}</label>
|
|
24
|
+
<p><small>${structure.description}</small></p>
|
|
25
|
+
</hgroup>
|
|
26
|
+
|
|
27
|
+
<markdown-editor
|
|
28
|
+
data-content="${data.value?.trim()}"
|
|
29
|
+
data-id="${data.id}"
|
|
30
|
+
>
|
|
31
|
+
<!-- <textarea id="block-{data.id}" name="value" class="markdown">
|
|
32
|
+
{data.value}
|
|
33
|
+
</textarea
|
|
34
|
+
> -->
|
|
35
|
+
</markdown-editor>
|
|
36
|
+
|
|
37
|
+
<input type="hidden" name="type" value="${structure.type}" />
|
|
38
|
+
<input type="hidden" name="id" value="${data.id}" />
|
|
39
|
+
<input type="hidden" name="entryId" value="${entryId}" />
|
|
40
|
+
<input type="hidden" name="parentId" value="${parentId}" />
|
|
41
|
+
<input type="hidden" name="name" value="${name}" />
|
|
42
|
+
</form>
|
|
43
|
+
`
|
|
44
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getOrCreateRow } from '../../utils/get-or-create-row.ts'
|
|
2
|
+
import { html } from '../../utils/html.ts'
|
|
3
|
+
import type { FieldDefStructure } from '../../types.ts'
|
|
4
|
+
|
|
5
|
+
export default (props: {
|
|
6
|
+
entryId: number
|
|
7
|
+
parentId: number
|
|
8
|
+
name: string
|
|
9
|
+
id?: number
|
|
10
|
+
structure: FieldDefStructure
|
|
11
|
+
sortOrder?: number
|
|
12
|
+
}) => {
|
|
13
|
+
const { entryId, parentId, name, structure, sortOrder = 0, id } = props
|
|
14
|
+
|
|
15
|
+
const data = getOrCreateRow({ parentId, name, field: structure, sortOrder, id })
|
|
16
|
+
|
|
17
|
+
if (!data) return html`<p>No block</p>`
|
|
18
|
+
|
|
19
|
+
return html`
|
|
20
|
+
<form
|
|
21
|
+
data-on-input="@patch('/admin/api/block', { contentType: 'form' })"
|
|
22
|
+
>
|
|
23
|
+
<hgroup>
|
|
24
|
+
<label for="block-${data.id}">${structure.label}</label>
|
|
25
|
+
<p><small>${structure.description}</small></p>
|
|
26
|
+
</hgroup>
|
|
27
|
+
|
|
28
|
+
<input
|
|
29
|
+
id="block-${data.id}"
|
|
30
|
+
name="value"
|
|
31
|
+
type="text"
|
|
32
|
+
value="${data.value}"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<input type="hidden" name="type" value="${structure.type}" />
|
|
36
|
+
<input type="hidden" name="id" value="${data.id}" />
|
|
37
|
+
<input type="hidden" name="entryId" value="${entryId}" />
|
|
38
|
+
<input type="hidden" name="parentId" value="${parentId}" />
|
|
39
|
+
<input type="hidden" name="name" value="${name}" />
|
|
40
|
+
</form>
|
|
41
|
+
`
|
|
42
|
+
}
|
package/components/icons.ts
CHANGED
|
@@ -135,3 +135,62 @@ export const trash = html`
|
|
|
135
135
|
/>
|
|
136
136
|
</svg>
|
|
137
137
|
`
|
|
138
|
+
|
|
139
|
+
export const x = html`
|
|
140
|
+
<svg
|
|
141
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
142
|
+
width="26"
|
|
143
|
+
height="26"
|
|
144
|
+
viewBox="0 0 24 24"
|
|
145
|
+
>
|
|
146
|
+
<!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
|
|
147
|
+
<path
|
|
148
|
+
fill="none"
|
|
149
|
+
stroke="currentColor"
|
|
150
|
+
stroke-linecap="round"
|
|
151
|
+
stroke-linejoin="round"
|
|
152
|
+
stroke-width="1.5"
|
|
153
|
+
d="M6 18L18 6M6 6l12 12"
|
|
154
|
+
/>
|
|
155
|
+
</svg>
|
|
156
|
+
`
|
|
157
|
+
|
|
158
|
+
export const cog = html`<svg
|
|
159
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
160
|
+
width="16"
|
|
161
|
+
height="16"
|
|
162
|
+
viewBox="0 0 24 24"
|
|
163
|
+
>
|
|
164
|
+
<!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
|
|
165
|
+
<g
|
|
166
|
+
fill="none"
|
|
167
|
+
stroke="currentColor"
|
|
168
|
+
stroke-linecap="round"
|
|
169
|
+
stroke-linejoin="round"
|
|
170
|
+
stroke-width="1.5"
|
|
171
|
+
>
|
|
172
|
+
<path
|
|
173
|
+
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93c.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204s.505.71.93.78l.893.15c.543.09.94.559.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.894.149c-.424.07-.764.383-.929.78c-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107c-.398.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93c-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.272-.806.108-1.204s-.506-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78c.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107s.71-.505.78-.929z"
|
|
174
|
+
/>
|
|
175
|
+
<path d="M15 12a3 3 0 1 1-6 0a3 3 0 0 1 6 0" />
|
|
176
|
+
</g>
|
|
177
|
+
</svg>`
|
|
178
|
+
|
|
179
|
+
export const clipboard = html`
|
|
180
|
+
<svg
|
|
181
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
182
|
+
width="16"
|
|
183
|
+
height="16"
|
|
184
|
+
viewBox="0 0 24 24"
|
|
185
|
+
>
|
|
186
|
+
<!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
|
|
187
|
+
<path
|
|
188
|
+
fill="none"
|
|
189
|
+
stroke="currentColor"
|
|
190
|
+
stroke-linecap="round"
|
|
191
|
+
stroke-linejoin="round"
|
|
192
|
+
stroke-width="1.5"
|
|
193
|
+
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0q.083.292.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0q.002-.32.084-.612m7.332 0q.969.073 1.927.184c1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48 48 0 0 1 1.927-.184"
|
|
194
|
+
/>
|
|
195
|
+
</svg>
|
|
196
|
+
`
|
package/index.ts
CHANGED
|
@@ -1,59 +1,73 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
2
|
import { loadDb } from '@alstar/db'
|
|
3
|
-
import { sectionRoutes } from './api/index.ts'
|
|
4
|
-
import { getConfig } from './utils/get-config.ts'
|
|
5
|
-
import * as types from './types.ts'
|
|
6
|
-
|
|
7
|
-
import Layout from './components/layout.ts'
|
|
8
|
-
import IndexPage from './components/index.ts'
|
|
9
|
-
import Entry from './components/Entry.ts'
|
|
10
|
-
|
|
11
3
|
import { serve } from '@hono/node-server'
|
|
12
4
|
import { serveStatic } from '@hono/node-server/serve-static'
|
|
5
|
+
import { createRefresher } from '@alstar/refresher'
|
|
6
|
+
|
|
7
|
+
import * as types from './types.ts'
|
|
13
8
|
import { createStudioTables } from './utils/create-studio-tables.ts'
|
|
14
9
|
import { fileBasedRouter } from './utils/file-based-router.ts'
|
|
10
|
+
import { getConfig } from './utils/get-config.ts'
|
|
11
|
+
import startupLog from './utils/startup-log.ts'
|
|
12
|
+
import { api } from './api/index.ts'
|
|
13
|
+
import mcp from './api/mcp.ts'
|
|
14
|
+
import path from 'path'
|
|
15
15
|
|
|
16
|
-
export let
|
|
17
|
-
export let rootdir = '/node_modules/@alstar/studio'
|
|
16
|
+
export let rootdir = './node_modules/@alstar/studio'
|
|
18
17
|
|
|
18
|
+
export let studioStructure: types.Structure = {}
|
|
19
19
|
export let studioConfig: types.StudioConfig = {
|
|
20
20
|
siteName: '',
|
|
21
|
+
port: 3000,
|
|
22
|
+
structure: {},
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
const createStudio = async (
|
|
25
|
+
const createStudio = async (config: types.StudioConfig) => {
|
|
26
|
+
createRefresher({ rootdir: '.' })
|
|
27
|
+
|
|
24
28
|
loadDb('./studio.db')
|
|
25
29
|
createStudioTables()
|
|
26
30
|
|
|
27
|
-
const configFile = await getConfig<types.StudioConfig>()
|
|
31
|
+
// const configFile = await getConfig<types.StudioConfig>()
|
|
32
|
+
|
|
33
|
+
if (config.structure) {
|
|
34
|
+
studioStructure = config.structure
|
|
35
|
+
}
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
studioConfig = { ...studioConfig, ...configFile }
|
|
37
|
+
studioConfig = { ...studioConfig, ...config }
|
|
31
38
|
|
|
32
39
|
const app = new Hono(studioConfig.honoConfig)
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Static folders
|
|
43
|
+
*/
|
|
44
|
+
app.use('*', serveStatic({ root: path.join(rootdir, 'public') }))
|
|
35
45
|
app.use('*', serveStatic({ root: './public' }))
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Studio API routes
|
|
49
|
+
*/
|
|
50
|
+
app.route('/admin/api', api(studioStructure))
|
|
51
|
+
app.route('/admin/mcp', mcp())
|
|
38
52
|
|
|
39
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Studio pages
|
|
55
|
+
*/
|
|
56
|
+
const adminPages = await fileBasedRouter(path.join(rootdir, 'pages'))
|
|
40
57
|
|
|
41
|
-
app.
|
|
42
|
-
return c.html(
|
|
43
|
-
Layout({
|
|
44
|
-
structure,
|
|
45
|
-
content: Entry({ structure, entryId: c.req.param('id') }),
|
|
46
|
-
}),
|
|
47
|
-
)
|
|
48
|
-
})
|
|
58
|
+
if (adminPages) app.route('/admin', adminPages)
|
|
49
59
|
|
|
60
|
+
/**
|
|
61
|
+
* User pages
|
|
62
|
+
*/
|
|
50
63
|
const pages = await fileBasedRouter('./pages')
|
|
51
64
|
|
|
52
|
-
if (pages)
|
|
53
|
-
app.route('/', pages)
|
|
54
|
-
}
|
|
65
|
+
if (pages) app.route('/', pages)
|
|
55
66
|
|
|
56
|
-
const server = serve(
|
|
67
|
+
const server = serve({
|
|
68
|
+
fetch: app.fetch,
|
|
69
|
+
port: studioConfig.port,
|
|
70
|
+
})
|
|
57
71
|
|
|
58
72
|
// graceful shutdown
|
|
59
73
|
process.on('SIGINT', () => {
|
|
@@ -70,10 +84,18 @@ const createStudio = async (studioStructure?: types.Structure) => {
|
|
|
70
84
|
})
|
|
71
85
|
})
|
|
72
86
|
|
|
87
|
+
startupLog({ port: studioConfig.port || 3000 })
|
|
88
|
+
|
|
73
89
|
return app
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
export {
|
|
92
|
+
export {
|
|
93
|
+
defineConfig,
|
|
94
|
+
defineEntry,
|
|
95
|
+
defineStructure,
|
|
96
|
+
defineBlock,
|
|
97
|
+
defineField,
|
|
98
|
+
} from './utils/define.ts'
|
|
77
99
|
export { type RequestContext } from './types.ts'
|
|
78
100
|
export { createStudio }
|
|
79
101
|
export { html, type HtmlEscapedString } from './utils/html.ts'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alstar/studio",
|
|
3
|
-
"version": "0.0.0-beta.
|
|
3
|
+
"version": "0.0.0-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"@starfederation/datastar-sdk": "1.0.0-RC.1",
|
|
9
9
|
"hono": "^4.8.12",
|
|
10
10
|
"@alstar/ui": "0.0.0-beta.1",
|
|
11
|
-
"@alstar/db": "0.0.0-beta.1"
|
|
11
|
+
"@alstar/db": "0.0.0-beta.1",
|
|
12
|
+
"@alstar/refresher": "0.0.0-beta.2"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"@types/node": "^24.1.0",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import { defineEntry } from '../../utils/define.ts'
|
|
3
|
+
|
|
4
|
+
import SiteLayout from '../../components/SiteLayout.ts'
|
|
5
|
+
import Entry from '../../components/Entry.ts'
|
|
6
|
+
|
|
7
|
+
export default defineEntry((c) => {
|
|
8
|
+
const id = c.req.param('id')
|
|
9
|
+
|
|
10
|
+
if (!id) {
|
|
11
|
+
return html`<p>Entry page url needs an ID param: "${id}"</p>`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return SiteLayout({
|
|
15
|
+
content: Entry({ entryId: parseInt(id) }),
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { html } from 'hono/html'
|
|
2
|
-
import {
|
|
2
|
+
import { defineEntry } from '../utils/define.ts'
|
|
3
|
+
|
|
4
|
+
import SiteLayout from '../components/SiteLayout.ts'
|
|
5
|
+
import { studioStructure } from '../index.ts'
|
|
3
6
|
|
|
4
7
|
const codeBlock = html`<code><span style="color: #c678dd;">await</span> <span style="color: #61aeee;">createStudio</span>([
|
|
5
8
|
{
|
|
@@ -16,7 +19,7 @@ const codeBlock = html`<code><span style="color: #c678dd;">await</span> <span st
|
|
|
16
19
|
}
|
|
17
20
|
])</code>`
|
|
18
21
|
|
|
19
|
-
export default () => {
|
|
22
|
+
export default defineEntry(() => {
|
|
20
23
|
const Discamer = html`
|
|
21
24
|
<div class="disclamer">
|
|
22
25
|
<article>
|
|
@@ -26,5 +29,5 @@ export default () => {
|
|
|
26
29
|
</article>
|
|
27
30
|
</div>`
|
|
28
31
|
|
|
29
|
-
return
|
|
30
|
-
}
|
|
32
|
+
return SiteLayout({ content: !Object.values(studioStructure).length ? Discamer : '' })
|
|
33
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
.admin-panel {
|
|
2
|
+
/* background: hsla(0, 0%, 0%, 0.1); */
|
|
3
|
+
padding: 40px;
|
|
4
|
+
margin-bottom: 0;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
align-items: flex-start;
|
|
8
|
+
|
|
9
|
+
height: 100%;
|
|
10
|
+
min-height: inherit;
|
|
11
|
+
|
|
12
|
+
min-width: 250px;
|
|
13
|
+
|
|
14
|
+
> h1 {
|
|
15
|
+
padding-bottom: 1rem;
|
|
16
|
+
|
|
17
|
+
a {
|
|
18
|
+
display: flex;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
svg {
|
|
22
|
+
height: 1.6rem;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
form {
|
|
27
|
+
padding-bottom: 1rem;
|
|
28
|
+
|
|
29
|
+
button {
|
|
30
|
+
margin: 10px 0px 20px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
p {
|
|
34
|
+
margin: 0;
|
|
35
|
+
|
|
36
|
+
small {
|
|
37
|
+
text-transform: uppercase;
|
|
38
|
+
letter-spacing: 0.4px;
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
opacity: 0.6;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#entries {
|
|
46
|
+
width: 100%;
|
|
47
|
+
user-select: none;
|
|
48
|
+
|
|
49
|
+
ul {
|
|
50
|
+
padding: 0;
|
|
51
|
+
margin-inline: -1rem;
|
|
52
|
+
|
|
53
|
+
form {
|
|
54
|
+
display: flex;
|
|
55
|
+
padding-bottom: 0;
|
|
56
|
+
|
|
57
|
+
button {
|
|
58
|
+
margin: 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
> li {
|
|
63
|
+
margin-bottom: 0px;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
display: flex;
|
|
66
|
+
justify-content: space-between;
|
|
67
|
+
align-items: stretch;
|
|
68
|
+
list-style: none;
|
|
69
|
+
padding: 0;
|
|
70
|
+
|
|
71
|
+
a {
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
width: 100%;
|
|
74
|
+
padding: 0.5rem 1rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
button {
|
|
78
|
+
border-radius: 7px;
|
|
79
|
+
opacity: 0;
|
|
80
|
+
transition: opacity 100px;
|
|
81
|
+
|
|
82
|
+
svg {
|
|
83
|
+
margin: 0.5rem 1rem;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
> footer {
|
|
91
|
+
margin-top: auto;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#entries ul {
|
|
96
|
+
> li:hover {
|
|
97
|
+
background-color: var(--pico-form-element-background-color);
|
|
98
|
+
|
|
99
|
+
button {
|
|
100
|
+
opacity: 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|