@alstar/studio 0.0.0-beta.15 → 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 (52) hide show
  1. package/api/block.ts +0 -14
  2. package/components/AdminPanel.ts +11 -5
  3. package/components/BlockFieldRenderer.ts +26 -20
  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 +14 -8
  8. package/components/LivePreview.ts +37 -0
  9. package/components/Render.ts +8 -3
  10. package/components/SiteLayout.ts +1 -4
  11. package/components/fields/Markdown.ts +10 -3
  12. package/components/fields/Reference.ts +71 -0
  13. package/components/fields/Slug.ts +6 -6
  14. package/components/fields/Text.ts +13 -8
  15. package/components/fields/index.ts +2 -1
  16. package/components/icons.ts +3 -0
  17. package/components/settings/ApiKeys.ts +4 -4
  18. package/components/settings/Backup.ts +3 -3
  19. package/components/settings/Users.ts +1 -1
  20. package/index.ts +11 -10
  21. package/package.json +5 -6
  22. package/pages/entry/[id].ts +7 -1
  23. package/pages/error.ts +7 -6
  24. package/pages/login.ts +1 -1
  25. package/pages/register.ts +2 -2
  26. package/public/studio/css/admin-panel.css +27 -9
  27. package/public/studio/css/blocks-field.css +25 -0
  28. package/public/studio/css/entry-page.css +4 -0
  29. package/public/studio/css/entry.css +35 -0
  30. package/public/studio/css/field.css +14 -0
  31. package/public/studio/css/live-preview.css +25 -0
  32. package/public/studio/css/settings.css +4 -0
  33. package/public/studio/js/live-preview.js +26 -0
  34. package/public/studio/js/markdown-editor.js +6 -0
  35. package/public/studio/js/sortable-list.js +6 -4
  36. package/public/studio/main.css +11 -12
  37. package/public/studio/main.js +1 -0
  38. package/queries/block.ts +127 -105
  39. package/queries/index.ts +3 -2
  40. package/schemas.ts +1 -1
  41. package/types.ts +51 -75
  42. package/utils/define.ts +3 -1
  43. package/utils/get-or-create-row.ts +2 -1
  44. package/utils/refresher.ts +56 -0
  45. package/utils/renderSSE.ts +8 -3
  46. package/utils/startup-log.ts +4 -4
  47. package/queries/block-2.ts +0 -339
  48. package/queries/db-types.ts +0 -15
  49. package/queries/getBlockTrees-2.ts +0 -71
  50. package/queries/getBlocks.ts +0 -214
  51. package/queries/structure-types.ts +0 -97
  52. package/utils/buildBlocksTree.ts +0 -44
package/index.ts CHANGED
@@ -6,7 +6,6 @@ import { serveStatic } from '@hono/node-server/serve-static'
6
6
  import { HTTPException } from 'hono/http-exception'
7
7
 
8
8
  import { loadDb } from '@alstar/db'
9
- import { createRefresher } from '@alstar/refresher'
10
9
 
11
10
  import { createStudioTables } from './utils/create-studio-tables.ts'
12
11
  import { fileBasedRouter } from './utils/file-based-router.ts'
@@ -20,21 +19,20 @@ import auth from './utils/auth.ts'
20
19
  import ErrorPage from './pages/error.ts'
21
20
 
22
21
  import * as types from './types.ts'
22
+ import { refresher, refreshClient } from './utils/refresher.ts'
23
23
 
24
24
  export let rootdir = './node_modules/@alstar/studio'
25
25
 
26
26
  export let studioStructure: types.Structure = {}
27
27
  export let studioConfig: types.StudioConfig = {
28
28
  siteName: '',
29
+ honoConfig: {},
29
30
  fileBasedRouter: true,
30
31
  port: 3000,
31
32
  structure: {},
32
33
  }
33
34
 
34
- const createStudio = async (config: types.StudioConfig) => {
35
- // const refresher = await createRefresher({ rootdir: ['.', import.meta.dirname] })
36
- const refresher = await createRefresher({ rootdir: '.' })
37
-
35
+ const createStudio = async (config: types.StudioConfigInput) => {
38
36
  loadDb('./studio.db')
39
37
  createStudioTables()
40
38
 
@@ -48,6 +46,8 @@ const createStudio = async (config: types.StudioConfig) => {
48
46
 
49
47
  const app = new Hono(studioConfig.honoConfig)
50
48
 
49
+ app.get('/refresh', refresher({ root: '.', exclude: '.db' }))
50
+
51
51
  /**
52
52
  * Static folders
53
53
  */
@@ -104,8 +104,6 @@ const createStudio = async (config: types.StudioConfig) => {
104
104
  }),
105
105
  )
106
106
 
107
- // console.log(app.routes)
108
-
109
107
  /**
110
108
  * Run server
111
109
  */
@@ -129,9 +127,12 @@ const createStudio = async (config: types.StudioConfig) => {
129
127
  })
130
128
  })
131
129
 
132
- startupLog({ port: studioConfig.port || 3000, refresherPort: refresher.port })
130
+ startupLog({ port: studioConfig.port })
133
131
 
134
- return app
132
+ return {
133
+ app,
134
+ refreshClient: refreshClient(studioConfig.port)
135
+ }
135
136
  }
136
137
 
137
138
  export {
@@ -144,6 +145,6 @@ export {
144
145
  } from './utils/define.ts'
145
146
  export { type RequestContext } from './types.ts'
146
147
  export { createStudio }
147
- export { html, type HtmlEscapedString } from './utils/html.ts'
148
+ export { html, raw, type HtmlEscapedString } from './utils/html.ts'
148
149
  export { query } from './queries/index.ts'
149
150
  export const version = packageJSON.version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alstar/studio",
3
- "version": "0.0.0-beta.15",
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",
16
- "@alstar/ui": "0.0.0-beta.2",
17
- "@alstar/refresher": "0.0.0-beta.3",
18
- "@alstar/db": "0.0.0-beta.1"
15
+ "hono": "4.8.12",
16
+ "@alstar/db": "0.0.0-beta.1",
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,7 +1,16 @@
1
1
  @import 'https://esm.sh/@alstar/ui/red.css';
2
+ /* @import '../node_modules/@alstar/ui/red.css'; */
2
3
  @import './css/admin-panel.css';
3
4
  @import './css/blocks-field.css';
4
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
+ }
5
14
 
6
15
  body {
7
16
  padding: 0;
@@ -12,13 +21,11 @@ body {
12
21
  align-content: baseline;
13
22
 
14
23
  > main {
15
- padding: 40px;
16
- height: 100vh;
17
- overflow: auto;
18
24
  width: 100%;
25
+ height: 100vh;
26
+ padding: 0;
19
27
 
20
28
  section {
21
- max-width: 900px;
22
29
  margin: 0 auto;
23
30
  }
24
31
  }
@@ -28,14 +35,6 @@ button svg, a svg {
28
35
  pointer-events: none;
29
36
  }
30
37
 
31
- .entry > .fields > .block {
32
- padding-block: var(--pico-spacing);
33
-
34
- > header {
35
- margin-block: var(--pico-block-spacing-vertical);
36
- }
37
- }
38
-
39
38
  .block {
40
39
  > header {
41
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,