@alstar/studio 0.0.0-beta.6 → 0.0.0-beta.8

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 (41) hide show
  1. package/api/api-key.ts +1 -2
  2. package/api/backup.ts +38 -0
  3. package/api/block.ts +63 -15
  4. package/api/index.ts +6 -3
  5. package/components/AdminPanel.ts +46 -24
  6. package/components/Backup.ts +10 -0
  7. package/components/{fields/Blocks.ts → BlockFieldRenderer.ts} +33 -30
  8. package/components/BlockRenderer.ts +22 -0
  9. package/components/Entries.ts +3 -2
  10. package/components/Entry.ts +13 -15
  11. package/components/FieldRenderer.ts +35 -0
  12. package/components/Render.ts +46 -0
  13. package/components/Settings.ts +3 -0
  14. package/components/{layout.ts → SiteLayout.ts} +8 -12
  15. package/components/fields/Markdown.ts +44 -0
  16. package/components/fields/Text.ts +10 -10
  17. package/components/fields/index.ts +2 -2
  18. package/index.ts +31 -28
  19. package/package.json +3 -3
  20. package/pages/entry/[id].ts +17 -0
  21. package/{components → pages}/index.ts +7 -4
  22. package/pages/settings.ts +10 -0
  23. package/public/{admin-panel.css → studio/admin-panel.css} +14 -1
  24. package/public/studio/main.css +162 -0
  25. package/public/studio/main.js +10 -0
  26. package/public/studio/markdown-editor.js +34 -0
  27. package/public/studio/sortable-list.js +40 -0
  28. package/queries/block.ts +2 -0
  29. package/types.ts +70 -32
  30. package/utils/define.ts +21 -9
  31. package/utils/file-based-router.ts +1 -0
  32. package/utils/get-or-create-row.ts +20 -7
  33. package/utils/startup-log.ts +2 -2
  34. package/bin/alstar.ts +0 -42
  35. package/components/Block.ts +0 -55
  36. package/public/main.css +0 -103
  37. package/public/main.js +0 -48
  38. /package/public/{blocks.css → studio/blocks.css} +0 -0
  39. /package/public/{entry.css → studio/entry.css} +0 -0
  40. /package/public/{favicon.svg → studio/favicon.svg} +0 -0
  41. /package/public/{settings.css → studio/settings.css} +0 -0
@@ -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
+ }
@@ -1,17 +1,18 @@
1
- import type { FieldDef } from '../../types.ts'
2
1
  import { getOrCreateRow } from '../../utils/get-or-create-row.ts'
3
2
  import { html } from '../../utils/html.ts'
3
+ import type { FieldDefStructure } from '../../types.ts'
4
4
 
5
5
  export default (props: {
6
6
  entryId: number
7
7
  parentId: number
8
8
  name: string
9
- field: FieldDef
10
- sortOrder: number
9
+ id?: number
10
+ structure: FieldDefStructure
11
+ sortOrder?: number
11
12
  }) => {
12
- const { entryId, parentId, name, field, sortOrder = 0 } = props
13
+ const { entryId, parentId, name, structure, sortOrder = 0, id } = props
13
14
 
14
- const data = getOrCreateRow(parentId, name, field, sortOrder)
15
+ const data = getOrCreateRow({ parentId, name, field: structure, sortOrder, id })
15
16
 
16
17
  if (!data) return html`<p>No block</p>`
17
18
 
@@ -20,10 +21,10 @@ export default (props: {
20
21
  data-on-input="@patch('/admin/api/block', { contentType: 'form' })"
21
22
  >
22
23
  <hgroup>
23
- <label for="block-${data.id}">${field.label}</label>
24
- <p><small>${field.description}</small></p>
24
+ <label for="block-${data.id}">${structure.label}</label>
25
+ <p><small>${structure.description}</small></p>
25
26
  </hgroup>
26
-
27
+
27
28
  <input
28
29
  id="block-${data.id}"
29
30
  name="value"
@@ -31,11 +32,10 @@ export default (props: {
31
32
  value="${data.value}"
32
33
  />
33
34
 
34
- <input type="hidden" name="type" value="${field.type}" />
35
+ <input type="hidden" name="type" value="${structure.type}" />
35
36
  <input type="hidden" name="id" value="${data.id}" />
36
37
  <input type="hidden" name="entryId" value="${entryId}" />
37
38
  <input type="hidden" name="parentId" value="${parentId}" />
38
- <input type="hidden" name="sort_order" value="${sortOrder}" />
39
39
  <input type="hidden" name="name" value="${name}" />
40
40
  </form>
41
41
  `
@@ -1,4 +1,4 @@
1
1
  import Text from './Text.ts'
2
- import Blocks from './Blocks.ts'
2
+ import Markdown from './Markdown.ts'
3
3
 
4
- export const Field = { Text, Blocks }
4
+ export const Field = { Text, Markdown }
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.8",
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/db": "0.0.0-beta.1",
11
10
  "@alstar/refresher": "0.0.0-beta.2",
12
- "@alstar/ui": "0.0.0-beta.1"
11
+ "@alstar/ui": "0.0.0-beta.1",
12
+ "@alstar/db": "0.0.0-beta.1"
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[]