@alstar/studio 0.0.0-beta.6 → 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/block.ts +63 -15
- package/components/AdminPanel.ts +46 -24
- package/components/{fields/Blocks.ts → BlockFieldRenderer.ts} +33 -30
- package/components/BlockRenderer.ts +22 -0
- package/components/Entries.ts +3 -2
- package/components/Entry.ts +13 -15
- package/components/FieldRenderer.ts +35 -0
- package/components/Render.ts +46 -0
- package/components/{layout.ts → SiteLayout.ts} +8 -12
- package/components/fields/Markdown.ts +44 -0
- package/components/fields/Text.ts +10 -10
- package/components/fields/index.ts +2 -2
- package/index.ts +31 -28
- package/package.json +3 -3
- package/pages/entry/[id].ts +17 -0
- package/{components → pages}/index.ts +7 -4
- package/pages/settings.ts +10 -0
- package/public/{admin-panel.css → studio/admin-panel.css} +14 -1
- 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/sortable-list.js +40 -0
- package/queries/block.ts +2 -0
- package/types.ts +70 -32
- package/utils/define.ts +21 -9
- package/utils/file-based-router.ts +1 -0
- package/utils/get-or-create-row.ts +20 -7
- package/utils/startup-log.ts +2 -2
- package/bin/alstar.ts +0 -42
- package/components/Block.ts +0 -55
- package/public/main.css +0 -103
- package/public/main.js +0 -48
- /package/public/{blocks.css → studio/blocks.css} +0 -0
- /package/public/{entry.css → studio/entry.css} +0 -0
- /package/public/{favicon.svg → studio/favicon.svg} +0 -0
- /package/public/{settings.css → studio/settings.css} +0 -0
package/index.ts
CHANGED
|
@@ -4,11 +4,6 @@ import { serve } from '@hono/node-server'
|
|
|
4
4
|
import { serveStatic } from '@hono/node-server/serve-static'
|
|
5
5
|
import { createRefresher } from '@alstar/refresher'
|
|
6
6
|
|
|
7
|
-
import Layout from './components/layout.ts'
|
|
8
|
-
import IndexPage from './components/index.ts'
|
|
9
|
-
import SettingsPage from './components/Settings.ts'
|
|
10
|
-
import Entry from './components/Entry.ts'
|
|
11
|
-
|
|
12
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'
|
|
@@ -16,18 +11,18 @@ import { getConfig } from './utils/get-config.ts'
|
|
|
16
11
|
import startupLog from './utils/startup-log.ts'
|
|
17
12
|
import { api } from './api/index.ts'
|
|
18
13
|
import mcp from './api/mcp.ts'
|
|
14
|
+
import path from 'path'
|
|
19
15
|
|
|
20
|
-
export let
|
|
21
|
-
export let rootdir = '/node_modules/@alstar/studio'
|
|
16
|
+
export let rootdir = './node_modules/@alstar/studio'
|
|
22
17
|
|
|
18
|
+
export let studioStructure: types.Structure = {}
|
|
23
19
|
export let studioConfig: types.StudioConfig = {
|
|
24
20
|
siteName: '',
|
|
25
|
-
|
|
21
|
+
port: 3000,
|
|
22
|
+
structure: {},
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
const createStudio = async (config: types.StudioConfig) => {
|
|
29
|
-
startupLog()
|
|
30
|
-
|
|
31
26
|
createRefresher({ rootdir: '.' })
|
|
32
27
|
|
|
33
28
|
loadDb('./studio.db')
|
|
@@ -36,37 +31,43 @@ const createStudio = async (config: types.StudioConfig) => {
|
|
|
36
31
|
// const configFile = await getConfig<types.StudioConfig>()
|
|
37
32
|
|
|
38
33
|
if (config.structure) {
|
|
39
|
-
|
|
34
|
+
studioStructure = config.structure
|
|
40
35
|
}
|
|
41
36
|
|
|
42
37
|
studioConfig = { ...studioConfig, ...config }
|
|
43
38
|
|
|
44
39
|
const app = new Hono(studioConfig.honoConfig)
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Static folders
|
|
43
|
+
*/
|
|
44
|
+
app.use('*', serveStatic({ root: path.join(rootdir, 'public') }))
|
|
47
45
|
app.use('*', serveStatic({ root: './public' }))
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Layout({
|
|
54
|
-
structure,
|
|
55
|
-
content: Entry({ entryId: parseInt(c.req.param('id')) }),
|
|
56
|
-
}),
|
|
57
|
-
)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
app.route('/admin/api', api(structure))
|
|
47
|
+
/**
|
|
48
|
+
* Studio API routes
|
|
49
|
+
*/
|
|
50
|
+
app.route('/admin/api', api(studioStructure))
|
|
61
51
|
app.route('/admin/mcp', mcp())
|
|
62
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Studio pages
|
|
55
|
+
*/
|
|
56
|
+
const adminPages = await fileBasedRouter(path.join(rootdir, 'pages'))
|
|
57
|
+
|
|
58
|
+
if (adminPages) app.route('/admin', adminPages)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* User pages
|
|
62
|
+
*/
|
|
63
63
|
const pages = await fileBasedRouter('./pages')
|
|
64
64
|
|
|
65
|
-
if (pages)
|
|
66
|
-
app.route('/', pages)
|
|
67
|
-
}
|
|
65
|
+
if (pages) app.route('/', pages)
|
|
68
66
|
|
|
69
|
-
const server = serve(
|
|
67
|
+
const server = serve({
|
|
68
|
+
fetch: app.fetch,
|
|
69
|
+
port: studioConfig.port,
|
|
70
|
+
})
|
|
70
71
|
|
|
71
72
|
// graceful shutdown
|
|
72
73
|
process.on('SIGINT', () => {
|
|
@@ -83,6 +84,8 @@ const createStudio = async (config: types.StudioConfig) => {
|
|
|
83
84
|
})
|
|
84
85
|
})
|
|
85
86
|
|
|
87
|
+
startupLog({ port: studioConfig.port || 3000 })
|
|
88
|
+
|
|
86
89
|
return app
|
|
87
90
|
}
|
|
88
91
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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": {
|
|
7
7
|
"@hono/node-server": "^1.18.1",
|
|
8
8
|
"@starfederation/datastar-sdk": "1.0.0-RC.1",
|
|
9
9
|
"hono": "^4.8.12",
|
|
10
|
+
"@alstar/ui": "0.0.0-beta.1",
|
|
10
11
|
"@alstar/db": "0.0.0-beta.1",
|
|
11
|
-
"@alstar/refresher": "0.0.0-beta.2"
|
|
12
|
-
"@alstar/ui": "0.0.0-beta.1"
|
|
12
|
+
"@alstar/refresher": "0.0.0-beta.2"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
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
|
+
})
|
|
@@ -29,11 +29,23 @@
|
|
|
29
29
|
button {
|
|
30
30
|
margin: 10px 0px 20px;
|
|
31
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
|
+
}
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
#entries {
|
|
35
46
|
width: 100%;
|
|
36
|
-
|
|
47
|
+
user-select: none;
|
|
48
|
+
|
|
37
49
|
ul {
|
|
38
50
|
padding: 0;
|
|
39
51
|
margin-inline: -1rem;
|
|
@@ -54,6 +66,7 @@
|
|
|
54
66
|
justify-content: space-between;
|
|
55
67
|
align-items: stretch;
|
|
56
68
|
list-style: none;
|
|
69
|
+
padding: 0;
|
|
57
70
|
|
|
58
71
|
a {
|
|
59
72
|
text-decoration: none;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/* @import './../node_modules/@alstar/ui/red.css'; */
|
|
2
|
+
@import 'https://esm.sh/@alstar/ui/red.css';
|
|
3
|
+
@import './admin-panel.css';
|
|
4
|
+
@import './entry.css';
|
|
5
|
+
@import './blocks.css';
|
|
6
|
+
@import './settings.css';
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
padding: 0;
|
|
10
|
+
min-height: 100vh;
|
|
11
|
+
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: auto 1fr;
|
|
14
|
+
align-content: baseline;
|
|
15
|
+
|
|
16
|
+
> main {
|
|
17
|
+
padding: 40px;
|
|
18
|
+
height: 100vh;
|
|
19
|
+
overflow: auto;
|
|
20
|
+
|
|
21
|
+
section {
|
|
22
|
+
max-width: 900px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.entry > .fields > .block {
|
|
29
|
+
padding-block: var(--pico-spacing);
|
|
30
|
+
|
|
31
|
+
> header {
|
|
32
|
+
margin-block: var(--pico-block-spacing-vertical);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.block {
|
|
37
|
+
> header {
|
|
38
|
+
margin-bottom: var(--pico-spacing);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
header {
|
|
42
|
+
display: flex;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
align-items: center;
|
|
45
|
+
|
|
46
|
+
background-color: inherit;
|
|
47
|
+
|
|
48
|
+
h6,
|
|
49
|
+
h5,
|
|
50
|
+
fieldset {
|
|
51
|
+
margin-bottom: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
button {
|
|
55
|
+
padding: 0;
|
|
56
|
+
background-color: white;
|
|
57
|
+
|
|
58
|
+
svg {
|
|
59
|
+
padding: 8px;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[data-sortable] {
|
|
66
|
+
> article {
|
|
67
|
+
transition: opacity 200ms;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.sortable-ghost {
|
|
71
|
+
opacity: 0.3;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
div.ink-mde {
|
|
76
|
+
border: var(--pico-border-width) solid #2a3140;
|
|
77
|
+
border-radius: var(--pico-border-radius);
|
|
78
|
+
outline: 0;
|
|
79
|
+
background-color: var(--pico-background-color);
|
|
80
|
+
box-shadow: var(--pico-box-shadow);
|
|
81
|
+
color: var(--pico-color);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.ͼ1 .cm-content.ink-mde-editor-content {
|
|
85
|
+
min-height: 10lh;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
div.ink-mde {
|
|
89
|
+
--pico-border-color: var(--pico-form-element-border-color);
|
|
90
|
+
|
|
91
|
+
border: var(--pico-border-width) solid var(--pico-border-color);
|
|
92
|
+
border-radius: var(--pico-border-radius);
|
|
93
|
+
|
|
94
|
+
padding: var(--pico-form-element-spacing-vertical)
|
|
95
|
+
var(--pico-form-element-spacing-horizontal);
|
|
96
|
+
margin-bottom: var(--pico-spacing);
|
|
97
|
+
|
|
98
|
+
--pico-background-color: var(--pico-form-element-background-color);
|
|
99
|
+
|
|
100
|
+
--pico-color: var(--pico-form-element-color);
|
|
101
|
+
--pico-box-shadow: none;
|
|
102
|
+
|
|
103
|
+
outline: 0;
|
|
104
|
+
background-color: var(--pico-background-color);
|
|
105
|
+
box-shadow: var(--pico-box-shadow);
|
|
106
|
+
color: var(--pico-color);
|
|
107
|
+
font-weight: var(--pico-font-weight);
|
|
108
|
+
transition:
|
|
109
|
+
background-color var(--pico-transition),
|
|
110
|
+
border-color var(--pico-transition),
|
|
111
|
+
color var(--pico-transition),
|
|
112
|
+
box-shadow var(--pico-transition);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.ink-mde-textarea {
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
div.ink-mde:has(.cm-focused) {
|
|
119
|
+
--pico-outline-width: 0.1rem;
|
|
120
|
+
--pico-box-shadow: 0 0 0 var(--pico-outline-width)
|
|
121
|
+
var(--pico-form-element-focus-color);
|
|
122
|
+
|
|
123
|
+
box-shadow: var(--pico-box-shadow);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.ͼ1.cm-focused {
|
|
127
|
+
outline: none;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ink-mde-textarea {
|
|
131
|
+
width: 100%;
|
|
132
|
+
|
|
133
|
+
--pico-background-color: var(--pico-form-element-background-color);
|
|
134
|
+
|
|
135
|
+
--pico-color: var(--pico-form-element-color);
|
|
136
|
+
--pico-box-shadow: none;
|
|
137
|
+
|
|
138
|
+
outline: 0;
|
|
139
|
+
background-color: var(--pico-background-color);
|
|
140
|
+
box-shadow: var(--pico-box-shadow);
|
|
141
|
+
color: var(--pico-color);
|
|
142
|
+
font-weight: var(--pico-font-weight);
|
|
143
|
+
transition:
|
|
144
|
+
background-color var(--pico-transition),
|
|
145
|
+
border-color var(--pico-transition),
|
|
146
|
+
color var(--pico-transition),
|
|
147
|
+
box-shadow var(--pico-transition);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.disclamer {
|
|
151
|
+
display: flex;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.text-secondary {
|
|
156
|
+
color: var(--pico-secondary);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.markdown {
|
|
160
|
+
width: 100%;
|
|
161
|
+
height: 10lh;
|
|
162
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ink } from 'ink-mde'
|
|
2
|
+
|
|
3
|
+
class MarkdownEditor extends HTMLElement {
|
|
4
|
+
instance = null
|
|
5
|
+
|
|
6
|
+
connectedCallback() {
|
|
7
|
+
this.style.width = '100%'
|
|
8
|
+
|
|
9
|
+
this.instance = ink(this, {
|
|
10
|
+
hooks: {
|
|
11
|
+
afterUpdate: async (e) => {
|
|
12
|
+
await fetch('/admin/api/value', {
|
|
13
|
+
method: 'PATCH',
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
value: e,
|
|
16
|
+
id: this.dataset.id,
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
interface: {
|
|
22
|
+
attribution: false,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
this.instance.update(this.dataset.content)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
disconnectedCallback() {
|
|
30
|
+
// this.instance.destroy()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
customElements.define('markdown-editor', MarkdownEditor)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Sortable from 'sortablejs'
|
|
2
|
+
|
|
3
|
+
class SortableList extends HTMLElement {
|
|
4
|
+
instance = null
|
|
5
|
+
|
|
6
|
+
connectedCallback() {
|
|
7
|
+
if (!this.children.length) return
|
|
8
|
+
|
|
9
|
+
const { id } = this.dataset
|
|
10
|
+
|
|
11
|
+
this.instance = Sortable.create(this, {
|
|
12
|
+
animation: 250,
|
|
13
|
+
handle: `[data-handle-for="${id}"]`,
|
|
14
|
+
onEnd: (e) => {
|
|
15
|
+
const items = [...e.target.children]
|
|
16
|
+
|
|
17
|
+
items.forEach(async (child, idx) => {
|
|
18
|
+
const searchParams = new URLSearchParams()
|
|
19
|
+
|
|
20
|
+
searchParams.set('id', child.dataset.id)
|
|
21
|
+
searchParams.set('sort-order', idx)
|
|
22
|
+
|
|
23
|
+
await fetch(`/admin/api/sort-order?${searchParams.toString()}`, {
|
|
24
|
+
method: 'post',
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
this.querySelectorAll('[name="sort_order"]').forEach((input, idx) => {
|
|
29
|
+
input.value = idx.toString()
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
disconnectedCallback() {
|
|
36
|
+
this.instance?.destroy()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
customElements.define('sortable-list', SortableList)
|
package/queries/block.ts
CHANGED
package/types.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { type HttpBindings } from '@hono/node-server'
|
|
|
2
2
|
import { type Context } from 'hono'
|
|
3
3
|
import { type HonoOptions } from 'hono/hono-base'
|
|
4
4
|
import { type BlankInput, type BlankEnv } from 'hono/types'
|
|
5
|
+
import {
|
|
6
|
+
BlockFieldInstance,
|
|
7
|
+
BlockInstance,
|
|
8
|
+
FieldInstance,
|
|
9
|
+
} from './utils/define.ts'
|
|
5
10
|
|
|
6
11
|
export type PrimitiveField = {
|
|
7
12
|
name: string
|
|
@@ -25,71 +30,103 @@ export type Block = {
|
|
|
25
30
|
fields: Record<string, Field | Block>
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
export type Structure = Record<string,
|
|
33
|
+
export type Structure = Record<string, BlockDefStructure>
|
|
29
34
|
|
|
30
35
|
// --- Field & block definitions ---
|
|
31
|
-
type FieldType = 'text' | 'slug' | 'markdown' | '
|
|
36
|
+
type FieldType = 'text' | 'slug' | 'markdown' | 'image'
|
|
32
37
|
|
|
33
38
|
interface BaseField {
|
|
34
|
-
label: string
|
|
35
|
-
type: FieldType
|
|
39
|
+
label: string
|
|
40
|
+
type: FieldType
|
|
36
41
|
description?: string
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
interface TextField extends BaseField {
|
|
40
|
-
type: 'text' | 'slug' | 'markdown'
|
|
45
|
+
type: 'text' | 'slug' | 'markdown'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface TextFieldStructure extends TextField {
|
|
49
|
+
instanceOf: typeof FieldInstance
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
interface ImageField extends BaseField {
|
|
44
|
-
type: 'image'
|
|
53
|
+
type: 'image'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ImageFieldStructure extends ImageField {
|
|
57
|
+
instanceOf: typeof FieldInstance
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BlocksFieldDef {
|
|
61
|
+
label: string
|
|
62
|
+
type: 'blocks'
|
|
63
|
+
description?: string
|
|
64
|
+
children: Record<string, BlockDefStructure | FieldDefStructure>
|
|
45
65
|
}
|
|
46
66
|
|
|
47
|
-
export interface
|
|
48
|
-
|
|
49
|
-
children: Record<string, BlockDef | FieldDef>;
|
|
67
|
+
export interface BlocksFieldDefStructure extends BlocksFieldDef {
|
|
68
|
+
instanceOf: typeof BlockFieldInstance
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
export type FieldDef = TextField | ImageField
|
|
71
|
+
export type FieldDef = TextField | ImageField
|
|
72
|
+
export type FieldDefStructure = TextFieldStructure | ImageFieldStructure
|
|
53
73
|
|
|
54
74
|
export interface BlockDef {
|
|
55
|
-
label: string
|
|
56
|
-
type: string
|
|
57
|
-
fields: Record<string,
|
|
75
|
+
label: string
|
|
76
|
+
type: string
|
|
77
|
+
fields: Record<string, FieldDefStructure | BlocksFieldDefStructure>
|
|
58
78
|
description?: string
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
created_at: string
|
|
64
|
-
updated_at: string
|
|
65
|
-
name: string
|
|
66
|
-
label: string
|
|
67
|
-
// type: string
|
|
68
|
-
sort_order: number
|
|
69
|
-
value: string
|
|
70
|
-
options: string | null
|
|
71
|
-
status: 'enabled' | 'disabled'
|
|
72
|
-
parent_id: number | null
|
|
73
|
-
depth: number
|
|
81
|
+
export interface BlockDefStructure extends BlockDef {
|
|
82
|
+
instanceOf: typeof BlockInstance
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
// type DBDefaults = {
|
|
86
|
+
// id: number
|
|
87
|
+
// created_at: string
|
|
88
|
+
// updated_at: string
|
|
89
|
+
// name: string
|
|
90
|
+
// label: string
|
|
91
|
+
// // type: string
|
|
92
|
+
// sort_order: number
|
|
93
|
+
// value: string
|
|
94
|
+
// options: string | null
|
|
95
|
+
// status: 'enabled' | 'disabled'
|
|
96
|
+
// parent_id: number | null
|
|
97
|
+
// depth: number
|
|
98
|
+
// }
|
|
99
|
+
|
|
100
|
+
type BaseDBResult = {
|
|
77
101
|
id: number
|
|
78
102
|
created_at: string
|
|
79
103
|
updated_at: string
|
|
80
104
|
name: string
|
|
81
105
|
label: string
|
|
82
|
-
type: string
|
|
83
106
|
sort_order: number
|
|
84
107
|
value: string | null
|
|
85
108
|
options: any
|
|
86
|
-
status:
|
|
109
|
+
status: 'enabled' | 'disabled'
|
|
87
110
|
parent_id: number | null
|
|
88
111
|
depth: number
|
|
89
|
-
children?: DBBlockResult[]
|
|
90
|
-
fields?: Record<string, DBBlockResult>
|
|
91
112
|
}
|
|
92
113
|
|
|
114
|
+
export type DBPrimitiveFieldResult = BaseDBResult & {
|
|
115
|
+
type: FieldDef
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type DBBlockFieldResult = BaseDBResult & {
|
|
119
|
+
type: 'blocks'
|
|
120
|
+
children: DBBlockResult[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type DBBlockResult = BaseDBResult & {
|
|
124
|
+
type: string
|
|
125
|
+
fields: Record<string, DBFieldResult>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type DBFieldResult = DBPrimitiveFieldResult & DBBlockFieldResult
|
|
129
|
+
|
|
93
130
|
export type DBBlock = Block & {
|
|
94
131
|
id: number
|
|
95
132
|
created_at: string
|
|
@@ -103,8 +140,9 @@ export type DBBlock = Block & {
|
|
|
103
140
|
export type BlockStatus = 'enabled' | 'disabled'
|
|
104
141
|
|
|
105
142
|
export type StudioConfig = {
|
|
106
|
-
siteName
|
|
143
|
+
siteName?: string
|
|
107
144
|
honoConfig?: HonoOptions<BlankEnv>
|
|
145
|
+
port?: number
|
|
108
146
|
structure: Structure
|
|
109
147
|
}
|
|
110
148
|
|
package/utils/define.ts
CHANGED
|
@@ -3,27 +3,39 @@ import { type HtmlEscapedString } from './html.ts'
|
|
|
3
3
|
|
|
4
4
|
export const defineConfig = (config: types.StudioConfig) => config
|
|
5
5
|
|
|
6
|
-
// export const defineStructure = (structure: types.Block[]) => structure
|
|
7
|
-
// export const defineField = (field: types.Field) => field
|
|
8
|
-
// export const defineBlock = (block: types.Block) => block
|
|
9
|
-
|
|
10
6
|
export const defineEntry = (
|
|
11
7
|
fn: (
|
|
12
8
|
c: types.RequestContext,
|
|
13
9
|
) => HtmlEscapedString | Promise<HtmlEscapedString>,
|
|
14
10
|
) => fn
|
|
15
11
|
|
|
12
|
+
export const FieldInstance = Symbol('field')
|
|
13
|
+
export const BlockFieldInstance = Symbol('blockfield')
|
|
14
|
+
export const BlockInstance = Symbol('block')
|
|
15
|
+
|
|
16
16
|
// --- Identity helpers (preserve literal types) ---
|
|
17
|
-
export function defineField(field: types.FieldDef) {
|
|
18
|
-
return
|
|
17
|
+
export function defineField(field: types.FieldDef): types.FieldDefStructure {
|
|
18
|
+
return {
|
|
19
|
+
...field,
|
|
20
|
+
instanceOf: FieldInstance,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function defineBlockField(
|
|
25
|
+
field: types.BlocksFieldDef,
|
|
26
|
+
): types.BlocksFieldDefStructure {
|
|
27
|
+
return {
|
|
28
|
+
...field,
|
|
29
|
+
instanceOf: BlockFieldInstance,
|
|
30
|
+
}
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
export function defineBlock(block: types.BlockDef) {
|
|
22
|
-
return block
|
|
33
|
+
export function defineBlock(block: types.BlockDef): types.BlockDefStructure {
|
|
34
|
+
return { ...block, instanceOf: BlockInstance }
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
export function defineStructure(
|
|
26
|
-
structure: Record<string, types.
|
|
38
|
+
structure: Record<string, types.BlockDefStructure>,
|
|
27
39
|
) {
|
|
28
40
|
return structure
|
|
29
41
|
}
|