@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.
Files changed (36) hide show
  1. package/api/block.ts +63 -15
  2. package/components/AdminPanel.ts +46 -24
  3. package/components/{fields/Blocks.ts → BlockFieldRenderer.ts} +33 -30
  4. package/components/BlockRenderer.ts +22 -0
  5. package/components/Entries.ts +3 -2
  6. package/components/Entry.ts +13 -15
  7. package/components/FieldRenderer.ts +35 -0
  8. package/components/Render.ts +46 -0
  9. package/components/{layout.ts → SiteLayout.ts} +8 -12
  10. package/components/fields/Markdown.ts +44 -0
  11. package/components/fields/Text.ts +10 -10
  12. package/components/fields/index.ts +2 -2
  13. package/index.ts +31 -28
  14. package/package.json +3 -3
  15. package/pages/entry/[id].ts +17 -0
  16. package/{components → pages}/index.ts +7 -4
  17. package/pages/settings.ts +10 -0
  18. package/public/{admin-panel.css → studio/admin-panel.css} +14 -1
  19. package/public/studio/main.css +162 -0
  20. package/public/studio/main.js +10 -0
  21. package/public/studio/markdown-editor.js +34 -0
  22. package/public/studio/sortable-list.js +40 -0
  23. package/queries/block.ts +2 -0
  24. package/types.ts +70 -32
  25. package/utils/define.ts +21 -9
  26. package/utils/file-based-router.ts +1 -0
  27. package/utils/get-or-create-row.ts +20 -7
  28. package/utils/startup-log.ts +2 -2
  29. package/bin/alstar.ts +0 -42
  30. package/components/Block.ts +0 -55
  31. package/public/main.css +0 -103
  32. package/public/main.js +0 -48
  33. /package/public/{blocks.css → studio/blocks.css} +0 -0
  34. /package/public/{entry.css → studio/entry.css} +0 -0
  35. /package/public/{favicon.svg → studio/favicon.svg} +0 -0
  36. /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 structure: types.Structure
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
- structure: {}
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
- structure = config.structure
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
- app.use('*', serveStatic({ root: './' }))
41
+ /**
42
+ * Static folders
43
+ */
44
+ app.use('*', serveStatic({ root: path.join(rootdir, 'public') }))
47
45
  app.use('*', serveStatic({ root: './public' }))
48
46
 
49
- app.get('/admin', (c) => c.html(Layout({ structure, content: IndexPage() })))
50
- app.get('/admin/settings', (c) => c.html(Layout({ structure, content: SettingsPage() })))
51
- app.get('/admin/entry/:id', (c) => {
52
- return c.html(
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(app)
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.6",
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 { structure } from '../index.ts'
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 html`${!structure ? Discamer : ''}`
30
- }
32
+ return SiteLayout({ content: !Object.values(studioStructure).length ? Discamer : '' })
33
+ })
@@ -0,0 +1,10 @@
1
+ import { defineEntry } from '../utils/define.ts'
2
+
3
+ import SiteLayout from '../components/SiteLayout.ts'
4
+ import Settings from '../components/Settings.ts'
5
+
6
+ export default defineEntry(() => {
7
+ return SiteLayout({
8
+ content: Settings(),
9
+ })
10
+ })
@@ -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,10 @@
1
+ import barba from '@barba/core'
2
+
3
+ barba.init({
4
+ cacheIgnore: true,
5
+ views: [
6
+ {
7
+ namespace: 'default',
8
+ },
9
+ ],
10
+ })
@@ -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
@@ -281,6 +281,8 @@ export function blocks(params: Record<string, any>) {
281
281
  blocks
282
282
  where
283
283
  ${filterSql}
284
+ order by
285
+ sort_order
284
286
  `
285
287
 
286
288
  return db.database.prepare(query).all(sqlParams) as unknown as DBBlockResult[]
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, BlockDef>
33
+ export type Structure = Record<string, BlockDefStructure>
29
34
 
30
35
  // --- Field & block definitions ---
31
- type FieldType = 'text' | 'slug' | 'markdown' | 'blocks' | 'image';
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 BlocksField extends BaseField {
48
- type: 'blocks';
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 | BlocksField;
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, FieldDef>;
75
+ label: string
76
+ type: string
77
+ fields: Record<string, FieldDefStructure | BlocksFieldDefStructure>
58
78
  description?: string
59
79
  }
60
80
 
61
- type DBDefaults = {
62
- id: number
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
- export type DBBlockResult = {
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: string
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: string
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 field
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.BlockDef>,
38
+ structure: Record<string, types.BlockDefStructure>,
27
39
  ) {
28
40
  return structure
29
41
  }
@@ -15,6 +15,7 @@ export const fileBasedRouter = async (rootdir: string) => {
15
15
  try {
16
16
  dirs = await fs.readdir(root, { recursive: true })
17
17
  } catch (error) {
18
+ console.log('No files in:', root)
18
19
  return
19
20
  }
20
21