@alstar/studio 0.0.0-beta.16 → 0.0.0-beta.18

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 (49) hide show
  1. package/api/block.ts +0 -14
  2. package/components/AdminPanel.ts +11 -5
  3. package/components/BlockFieldRenderer.ts +25 -19
  4. package/components/BlockRenderer.ts +4 -4
  5. package/components/Entries.ts +1 -1
  6. package/components/Entry.ts +13 -7
  7. package/components/FieldRenderer.ts +8 -7
  8. package/components/LivePreview.ts +37 -0
  9. package/components/Render.ts +2 -2
  10. package/components/SiteLayout.ts +1 -4
  11. package/components/fields/Markdown.ts +10 -3
  12. package/components/fields/Reference.ts +11 -7
  13. package/components/fields/Slug.ts +6 -6
  14. package/components/fields/Text.ts +13 -8
  15. package/components/icons.ts +3 -0
  16. package/components/settings/ApiKeys.ts +4 -4
  17. package/components/settings/Backup.ts +3 -3
  18. package/components/settings/Users.ts +1 -1
  19. package/index.ts +11 -10
  20. package/package.json +4 -5
  21. package/pages/entry/[id].ts +7 -1
  22. package/pages/error.ts +7 -6
  23. package/pages/login.ts +1 -1
  24. package/pages/register.ts +2 -2
  25. package/public/studio/css/admin-panel.css +27 -9
  26. package/public/studio/css/blocks-field.css +25 -0
  27. package/public/studio/css/entry-page.css +4 -0
  28. package/public/studio/css/entry.css +35 -0
  29. package/public/studio/css/field.css +14 -0
  30. package/public/studio/css/live-preview.css +25 -0
  31. package/public/studio/css/settings.css +4 -0
  32. package/public/studio/js/live-preview.js +26 -0
  33. package/public/studio/js/markdown-editor.js +6 -0
  34. package/public/studio/js/sortable-list.js +6 -4
  35. package/public/studio/main.css +11 -13
  36. package/public/studio/main.js +1 -0
  37. package/queries/block.ts +127 -105
  38. package/queries/index.ts +3 -2
  39. package/types.ts +39 -69
  40. package/utils/define.ts +3 -1
  41. package/utils/refresher.ts +56 -0
  42. package/utils/renderSSE.ts +8 -3
  43. package/utils/startup-log.ts +4 -4
  44. package/queries/block-2.ts +0 -339
  45. package/queries/db-types.ts +0 -15
  46. package/queries/getBlockTrees-2.ts +0 -71
  47. package/queries/getBlocks.ts +0 -214
  48. package/queries/structure-types.ts +0 -97
  49. package/utils/buildBlocksTree.ts +0 -44
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alstar/studio",
3
- "version": "0.0.0-beta.16",
3
+ "version": "0.0.0-beta.18",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "bin": {
@@ -10,12 +10,11 @@
10
10
  "node": ">=23.8"
11
11
  },
12
12
  "dependencies": {
13
- "@hono/node-server": "^1.18.1",
13
+ "@hono/node-server": "1.18.1",
14
14
  "@starfederation/datastar-sdk": "1.0.0-RC.1",
15
- "hono": "^4.8.12",
15
+ "hono": "4.8.12",
16
16
  "@alstar/db": "0.0.0-beta.1",
17
- "@alstar/refresher": "0.0.0-beta.3",
18
- "@alstar/ui": "0.0.0-beta.3"
17
+ "@alstar/ui": "0.0.0-beta.4"
19
18
  },
20
19
  "devDependencies": {
21
20
  "@types/node": "^24.1.0",
@@ -3,6 +3,7 @@ import { defineEntry } from '../../utils/define.ts'
3
3
 
4
4
  import SiteLayout from '../../components/SiteLayout.ts'
5
5
  import Entry from '../../components/Entry.ts'
6
+ import LivePreview from '../../components/LivePreview.ts'
6
7
 
7
8
  export default defineEntry((c) => {
8
9
  const id = c.req.param('id')
@@ -11,5 +12,10 @@ export default defineEntry((c) => {
11
12
  return html`<p>Entry page url needs an ID param: "${id}"</p>`
12
13
  }
13
14
 
14
- return SiteLayout(Entry({ entryId: parseInt(id) }))
15
+ return SiteLayout(
16
+ html`<div class="entry-page">
17
+ ${Entry({ entryId: id })}
18
+ ${LivePreview({ entryId: id })}</div>
19
+ `
20
+ )
15
21
  })
package/pages/error.ts CHANGED
@@ -3,12 +3,13 @@ import SiteLayout from "../components/SiteLayout.ts";
3
3
  import type { HTTPResponseError } from "hono/types";
4
4
 
5
5
  export default ((err?: Error | HTTPResponseError) => {
6
-
7
6
  return SiteLayout(html`
8
- <article>
9
- <header>Something went wrong</header>
10
- <p>Try again</p>
11
- <p>${err?.message || '404 - Not found'}</p>
12
- </article>
7
+ <div class="container">
8
+ <article style="margin: 100px">
9
+ <header>Something went wrong</header>
10
+ <p>Try again</p>
11
+ <p>${err?.message || '404 - Not found'}</p>
12
+ </article>
13
+ </div>
13
14
  `, false)
14
15
  })
package/pages/login.ts CHANGED
@@ -7,7 +7,7 @@ export default defineEntry(c => {
7
7
  <div class="login-form">
8
8
  <article>
9
9
  <header>Login</header>
10
- <form data-on-submit="@post('/studio/api/auth/login', { contentType: 'form' })">
10
+ <form data-on:submit="@post('/studio/api/auth/login', { contentType: 'form' })">
11
11
  <label for="email">Email</label>
12
12
  <input id="email" name="email" type="text" placeholder="Email">
13
13
  <label for="password">Password</label>
package/pages/register.ts CHANGED
@@ -10,8 +10,8 @@ export default defineEntry((c) => {
10
10
  <header>Register user</header>
11
11
  <form
12
12
  data-signals="{ status: 0 }"
13
- data-on-submit="@post('/studio/api/auth/register', { contentType: 'form' })"
14
- data-on-signal-patch="patch.status === 200 && window.location.reload()"
13
+ data-on:submit="@post('/studio/api/auth/register', { contentType: 'form' })"
14
+ data-on:signal-patch="patch.status === 200 && window.location.reload()"
15
15
  >
16
16
  <label for="email">Email</label>
17
17
  <input id="email" name="email" type="text" placeholder="Email" style="width: 100%" />
@@ -1,28 +1,46 @@
1
1
  .admin-panel {
2
- /* background: hsla(0, 0%, 0%, 0.1); */
3
2
  padding: 40px;
4
3
  margin-bottom: 0;
5
4
  display: flex;
6
5
  flex-direction: column;
7
6
  align-items: flex-start;
8
7
 
8
+ position: relative;
9
9
  height: 100%;
10
10
  min-height: inherit;
11
-
12
11
  min-width: 250px;
13
12
 
14
- > h1 {
15
- padding-bottom: 1rem;
13
+ border-right: 1px solid var(--pico-muted-border-color);
16
14
 
17
- a {
18
- display: flex;
19
- }
15
+ /* .toggle-button {
16
+ position: absolute;
17
+ top: 0;
18
+ right: 0;
19
+ margin: 0.5rem;
20
+ padding: 0.2rem;
21
+ } */
20
22
 
21
- svg {
22
- height: 1.6rem;
23
+ header {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 1rem;
27
+ padding-bottom: 1rem;
28
+
29
+ .title {
30
+ margin: 0;
31
+
32
+ a {
33
+ display: flex;
34
+ }
35
+
36
+ svg {
37
+ height: 1.6rem;
38
+ width: auto;
39
+ }
23
40
  }
24
41
  }
25
42
 
43
+
26
44
  form {
27
45
  padding-bottom: 1rem;
28
46
 
@@ -1,9 +1,22 @@
1
1
  .blocks-field {
2
2
  > header {
3
+ position: relative;
3
4
  display: flex;
4
5
  justify-content: space-between;
5
6
  align-items: center;
6
7
 
8
+ position: sticky;
9
+ top: 0;
10
+ z-index: 3;
11
+ background: var(--pico-background-color);
12
+ border-bottom: 1px solid var(--pico-muted-border-color);
13
+
14
+ padding: 1rem;
15
+
16
+ p {
17
+ margin: 0;
18
+ }
19
+
7
20
  > form fieldset {
8
21
  button {
9
22
  padding-inline: 1rem;
@@ -11,6 +24,8 @@
11
24
  }
12
25
 
13
26
  details {
27
+ margin: 0;
28
+
14
29
  form {
15
30
  display: flex;
16
31
  }
@@ -28,6 +43,16 @@
28
43
  }
29
44
  }
30
45
 
46
+ > header:after {
47
+ content: '';
48
+ position: absolute;
49
+ background-color: var(--pico-background-color);
50
+ height: 100%;
51
+ width: 200%;
52
+ left: -50%;
53
+ z-index: -1;
54
+ }
55
+
31
56
  article {
32
57
  > header {
33
58
  display: flex;
@@ -0,0 +1,4 @@
1
+ .entry-page:has(#live_preview) {
2
+ display: grid;
3
+ grid-template-columns: 500px 1fr;
4
+ }
@@ -0,0 +1,35 @@
1
+ .entry{
2
+ height: 100vh;
3
+ display: grid;
4
+ grid-template-rows: auto 1fr;
5
+
6
+ > header {
7
+ background-color: var(--pico-background-color);
8
+ border-bottom: 1px solid var(--pico-muted-border-color);
9
+ padding: 1rem;
10
+
11
+ h1 {
12
+ font-size: 1rem;
13
+ font-weight: normal;
14
+ margin: 0;
15
+ font-family: var(--pico-font-family-monospace);
16
+ }
17
+ }
18
+
19
+ .content {
20
+ overflow: auto;
21
+ }
22
+
23
+ > .fields > .block {
24
+ padding: var(--pico-spacing);
25
+
26
+ > header {
27
+ margin-block: var(--pico-block-spacing-vertical);
28
+ }
29
+ }
30
+
31
+ .field {
32
+ padding-top: 1rem;
33
+ padding-inline: 1rem;
34
+ }
35
+ }
@@ -0,0 +1,14 @@
1
+ .field {
2
+ .field-text {
3
+ output {
4
+ display: grid;
5
+ max-height: 300px;
6
+ margin-bottom: 1rem;
7
+
8
+ svg {
9
+ height: 100%;
10
+ width: 100%;
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,25 @@
1
+ .live-preview {
2
+ width: 100%;
3
+ height: 100%;
4
+ border-left: 1px solid var(--pico-muted-border-color);
5
+
6
+ display: grid;
7
+ grid-template-rows: auto 1fr;
8
+
9
+ header {
10
+ border-bottom: 1px solid var(--pico-muted-border-color);
11
+ padding: 1rem;
12
+
13
+ h1 {
14
+ font-size: 1rem;
15
+ font-weight: normal;
16
+ margin: 0;
17
+ font-family: var(--pico-font-family-monospace);
18
+ }
19
+ }
20
+
21
+ iframe {
22
+ width: 100%;
23
+ height: 100%;
24
+ }
25
+ }
@@ -1,4 +1,8 @@
1
1
  #settings {
2
+ padding: 1rem;
3
+ overflow: auto;
4
+ height: 100vh;
5
+
2
6
  tr {
3
7
  input {
4
8
  width: 100%;
@@ -0,0 +1,26 @@
1
+ class LivePreview extends HTMLElement {
2
+ abortController = new AbortController()
3
+ mutationObserver
4
+ iframe
5
+
6
+ reload() {
7
+ this.iframe.contentWindow.location.reload()
8
+ }
9
+
10
+ connectedCallback() {
11
+ this.mutationObserver = new MutationObserver(this.reload.bind(this))
12
+ this.mutationObserver.observe(this, { attributes: true })
13
+
14
+ this.iframe = this.querySelector('iframe')
15
+
16
+ window.addEventListener('markdown-editor:update', this.reload.bind(this), { signal: this.abortController.signal })
17
+ window.addEventListener('sortable-list:update', this.reload.bind(this), { signal: this.abortController.signal })
18
+ }
19
+
20
+ disconnectedCallback() {
21
+ this.abortController?.abort()
22
+ this.mutationObserver?.disconnect()
23
+ }
24
+ }
25
+
26
+ customElements.define('live-preview', LivePreview)
@@ -11,11 +11,17 @@ class MarkdownEditor extends HTMLElement {
11
11
  afterUpdate: async (e) => {
12
12
  await fetch('/studio/api/value', {
13
13
  method: 'PATCH',
14
+ headers: {
15
+ render: 'LivePreview',
16
+ props: this.dataset.entryId
17
+ },
14
18
  body: JSON.stringify({
15
19
  value: e,
16
20
  id: this.dataset.id,
17
21
  }),
18
22
  })
23
+
24
+ window.dispatchEvent(new CustomEvent('markdown-editor:update'))
19
25
  },
20
26
  },
21
27
  interface: {
@@ -13,23 +13,25 @@ class SortableList extends HTMLElement {
13
13
  this.instance = Sortable.create(this, {
14
14
  animation: 250,
15
15
  handle: `[data-handle-for="${id}"]`,
16
- onEnd: (e) => {
16
+ onEnd: async (e) => {
17
17
  const items = [...e.target.children]
18
18
 
19
- items.forEach(async (child, idx) => {
19
+ await Promise.all(items.map(async (child, idx) => {
20
20
  const searchParams = new URLSearchParams()
21
21
 
22
22
  searchParams.set('id', child.dataset.id)
23
23
  searchParams.set('sort-order', idx)
24
24
 
25
- await fetch(`/studio/api/sort-order?${searchParams.toString()}`, {
25
+ return fetch(`/studio/api/sort-order?${searchParams.toString()}`, {
26
26
  method: 'post',
27
27
  })
28
- })
28
+ }))
29
29
 
30
30
  this.querySelectorAll('[name="sort_order"]').forEach((input, idx) => {
31
31
  input.value = idx.toString()
32
32
  })
33
+
34
+ window.dispatchEvent(new CustomEvent('sortable-list:update'))
33
35
  },
34
36
  })
35
37
  }
@@ -1,8 +1,16 @@
1
1
  @import 'https://esm.sh/@alstar/ui/red.css';
2
- /* @import '../node_modules/@alstar/ui/slate.css'; */
2
+ /* @import '../node_modules/@alstar/ui/red.css'; */
3
3
  @import './css/admin-panel.css';
4
4
  @import './css/blocks-field.css';
5
5
  @import './css/settings.css';
6
+ @import './css/live-preview.css';
7
+ @import './css/entry-page.css';
8
+ @import './css/entry.css';
9
+ @import './css/field.css';
10
+
11
+ :root {
12
+ --pico-font-size: 80%;
13
+ }
6
14
 
7
15
  body {
8
16
  padding: 0;
@@ -13,13 +21,11 @@ body {
13
21
  align-content: baseline;
14
22
 
15
23
  > main {
16
- padding: 40px;
17
- height: 100vh;
18
- overflow: auto;
19
24
  width: 100%;
25
+ height: 100vh;
26
+ padding: 0;
20
27
 
21
28
  section {
22
- max-width: 900px;
23
29
  margin: 0 auto;
24
30
  }
25
31
  }
@@ -29,14 +35,6 @@ button svg, a svg {
29
35
  pointer-events: none;
30
36
  }
31
37
 
32
- .entry > .fields > .block {
33
- padding-block: var(--pico-spacing);
34
-
35
- > header {
36
- margin-block: var(--pico-block-spacing-vertical);
37
- }
38
- }
39
-
40
38
  .block {
41
39
  > header {
42
40
  margin-bottom: var(--pico-spacing);
@@ -2,6 +2,7 @@ import barba from '@barba/core'
2
2
 
3
3
  import './js/markdown-editor.js'
4
4
  import './js/sortable-list.js'
5
+ import './js/live-preview.js'
5
6
 
6
7
  barba.init({
7
8
  debug: true,
package/queries/block.ts CHANGED
@@ -2,97 +2,69 @@ import { db } from '@alstar/db'
2
2
  import { sql } from '../utils/sql.ts'
3
3
  import { type DBBlockResult } from '../types.ts'
4
4
 
5
- function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
6
- const map = new Map<number, DBBlockResult>()
7
- const roots: DBBlockResult[] = []
8
-
9
- for (const block of blocks) {
10
- block.children = []
11
- map.set(block.id, block)
12
- }
13
-
14
- for (const block of blocks) {
15
- if (block.parent_id === null) {
16
- roots.push(block)
17
- } else {
18
- const parent = map.get(block.parent_id)
19
- if (parent) parent.children!.push(block)
20
- }
21
- }
22
-
23
- // Sort children by sort_order recursively
24
- const sortChildren = (node: DBBlockResult) => {
25
- node.children!.sort((a, b) => a.sort_order - b.sort_order)
26
- node.children!.forEach(sortChildren)
27
- }
28
- roots.forEach(sortChildren)
29
-
30
- return roots
31
- }
32
-
33
- function buildTree(blocks: DBBlockResult[]): DBBlockResult {
34
- const map = new Map<number, DBBlockResult>()
35
- const roots: DBBlockResult[] = []
36
-
37
- for (const block of blocks) {
38
- block.children = []
39
- map.set(block.id, block)
40
- }
41
-
42
- for (const block of blocks) {
43
- if (block.parent_id === null) {
44
- roots.push(block)
45
- } else {
46
- const parent = map.get(block.parent_id)
47
- if (parent) parent.children!.push(block)
48
- }
49
- }
50
-
51
- // Sort children by sort_order recursively
52
- const sortChildren = (node: DBBlockResult) => {
53
- node.children!.sort((a, b) => a.sort_order - b.sort_order)
54
- node.children!.forEach(sortChildren)
55
- }
56
- roots.forEach(sortChildren)
57
-
58
- return roots[0]
59
- }
60
-
61
- function transformBlocksTree(
62
- block: DBBlockResult,
63
- isBlocksChild?: boolean,
64
- ): DBBlockResult {
65
- const fields: Record<string, DBBlockResult> = {}
66
- let hasFields = false
67
-
68
- for (const child of block.children ?? []) {
69
- const transformedChild = transformBlocksTree(
70
- child,
71
- child.type === 'blocks',
72
- )
73
-
74
- if (!isBlocksChild) {
75
- hasFields = true
76
- fields[transformedChild.name] = transformedChild
77
- }
78
- }
79
-
80
- if(hasFields) {
81
- block.fields = fields
82
- }
5
+ // function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
6
+ // const map = new Map<number, DBBlockResult>()
7
+ // const roots: DBBlockResult[] = []
8
+
9
+ // for (const block of blocks) {
10
+ // block.blocks = []
11
+ // map.set(block.id, block)
12
+ // }
13
+
14
+ // for (const block of blocks) {
15
+ // if (block.parent_id === null) {
16
+ // roots.push(block)
17
+ // } else {
18
+ // const parent = map.get(block.parent_id)
19
+ // if (parent) parent.blocks!.push(block)
20
+ // }
21
+ // }
22
+
23
+ // // Sort blocks by sort_order recursively
24
+ // const sortChildren = (node: DBBlockResult) => {
25
+ // node.blocks!.sort((a, b) => a.sort_order - b.sort_order)
26
+ // node.blocks!.forEach(sortChildren)
27
+ // }
28
+ // roots.forEach(sortChildren)
29
+
30
+ // return roots
31
+ // }
32
+
33
+ // function transformBlocksTree(
34
+ // block: DBBlockResult,
35
+ // isBlocksChild?: boolean,
36
+ // ): DBBlockResult {
37
+ // const fields: Record<string, DBBlockResult> = {}
38
+ // let hasFields = false
39
+
40
+ // for (const child of block.blocks ?? []) {
41
+ // const transformedChild = transformBlocksTree(
42
+ // child,
43
+ // child.type === 'blocks',
44
+ // )
45
+
46
+ // if (!isBlocksChild) {
47
+ // hasFields = true
48
+ // fields[transformedChild.name] = transformedChild
49
+ // }
50
+ // }
51
+
52
+ // if(hasFields) {
53
+ // block.fields = fields
54
+ // }
83
55
 
84
- if (!isBlocksChild) {
85
- delete block.children
86
- } else {
87
- delete block.fields
88
- }
56
+ // if (!isBlocksChild) {
57
+ // delete block.blocks
58
+ // } else {
59
+ // delete block.fields
60
+ // }
89
61
 
90
- return block
91
- }
62
+ // return block
63
+ // }
92
64
 
93
- function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
94
- return blocks.map((block) => transformBlocksTree(block))
95
- }
65
+ // function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
66
+ // return blocks.map((block) => transformBlocksTree(block))
67
+ // }
96
68
 
97
69
  function rootQuery(filterSql: string, depthLimit?: number) {
98
70
  const depthLimitClause =
@@ -218,24 +190,72 @@ function buildFilterSql(params: Record<string, any>) {
218
190
  return { filterSql, sqlParams }
219
191
  }
220
192
 
221
- export function roots(
222
- params: Record<string, any>,
223
- options?: {
224
- depth?: number
225
- },
226
- ): DBBlockResult[] | [] {
227
- const { filterSql, sqlParams } = buildFilterSql(params)
193
+ // export function roots(
194
+ // params: Record<string, any>,
195
+ // options?: {
196
+ // depth?: number
197
+ // },
198
+ // ): DBBlockResult[] | [] {
199
+ // const { filterSql, sqlParams } = buildFilterSql(params)
200
+
201
+ // const query = rootQuery(filterSql, options?.depth)
202
+ // const rows = db.database
203
+ // .prepare(query)
204
+ // .all(sqlParams) as unknown as DBBlockResult[]
205
+
206
+ // if (!rows.length) return []
207
+
208
+ // const forest = buildForest(rows)
209
+
210
+ // return transformForest(forest)
211
+ // }
212
+
213
+ type DBRow = {
214
+ id: number
215
+ created_at: string
216
+ updated_at: string
217
+ name: string
218
+ label: string
219
+ type: string
220
+ sort_order: number
221
+ value: string | null
222
+ options: string | null
223
+ status: 'enabled' | 'disabled'
224
+ parent_id: number | null
225
+ depth: number
226
+ }
228
227
 
229
- const query = rootQuery(filterSql, options?.depth)
230
- const rows = db.database
231
- .prepare(query)
232
- .all(sqlParams) as unknown as DBBlockResult[]
228
+ type TODO = any
229
+
230
+ function buildTree2(items: DBRow[]): TODO | null {
231
+ const map = new Map<number, TODO>();
233
232
 
234
- if (!rows.length) return []
233
+ // First pass: clone items into map
234
+ for (const item of items) {
235
+ map.set(item.id, { ...item });
236
+ }
235
237
 
236
- const forest = buildForest(rows)
238
+ let root: TODO | null = null;
237
239
 
238
- return transformForest(forest)
240
+ // Second pass: assign blocks to parents
241
+ for (const item of map.values()) {
242
+ if (item.parent_id === null) {
243
+ root = item; // Root node
244
+ } else {
245
+ const parent = map.get(item.parent_id);
246
+ if (parent) {
247
+ if(parent.type === 'blocks') {
248
+ if (!parent.blocks) parent.blocks = [];
249
+ parent.blocks.push(item);
250
+ } else {
251
+ if (!parent.fields) parent.fields = {};
252
+ parent.fields[item.name] = item;
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ return root;
239
259
  }
240
260
 
241
261
  export function root(
@@ -247,13 +267,15 @@ export function root(
247
267
  const query = rootQuery(filterSql, options?.depth)
248
268
  const rows = db.database
249
269
  .prepare(query)
250
- .all(sqlParams) as unknown as DBBlockResult[]
270
+ .all(sqlParams) as unknown as DBRow[]
251
271
 
252
272
  if (!rows.length) return null
253
273
 
254
- const tree = buildTree(rows)
274
+ // const tree = buildTree(rows)
275
+
276
+ const tree = buildTree2(rows)
255
277
 
256
- return transformBlocksTree(tree)
278
+ return tree // transformBlocksTree(tree)
257
279
  }
258
280
 
259
281
  export function block(params: Record<string, any>) {