@alstar/studio 0.0.0-beta.5 → 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 (53) hide show
  1. package/api/api-key.ts +74 -0
  2. package/api/block.ts +82 -42
  3. package/api/index.ts +9 -1
  4. package/api/mcp.ts +53 -0
  5. package/components/AdminPanel.ts +74 -0
  6. package/components/BlockFieldRenderer.ts +121 -0
  7. package/components/BlockRenderer.ts +22 -0
  8. package/components/Entries.ts +5 -4
  9. package/components/Entry.ts +13 -21
  10. package/components/FieldRenderer.ts +35 -0
  11. package/components/Render.ts +46 -0
  12. package/components/Settings.ts +98 -0
  13. package/components/{layout.ts → SiteLayout.ts} +9 -13
  14. package/components/fields/Markdown.ts +44 -0
  15. package/components/fields/Text.ts +42 -0
  16. package/components/fields/index.ts +4 -0
  17. package/components/icons.ts +59 -0
  18. package/index.ts +52 -30
  19. package/package.json +3 -2
  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/studio/admin-panel.css +103 -0
  24. package/public/studio/blocks.css +53 -0
  25. package/public/studio/main.css +162 -0
  26. package/public/studio/main.js +10 -0
  27. package/public/studio/markdown-editor.js +34 -0
  28. package/public/studio/settings.css +24 -0
  29. package/public/studio/sortable-list.js +40 -0
  30. package/queries/block-with-children.ts +74 -0
  31. package/queries/block.ts +31 -40
  32. package/queries/db-types.ts +15 -0
  33. package/queries/getBlockTrees-2.ts +0 -0
  34. package/queries/getBlockTrees.ts +316 -0
  35. package/queries/getBlocks.ts +214 -0
  36. package/queries/index.ts +1 -0
  37. package/queries/structure-types.ts +97 -0
  38. package/schemas.ts +14 -3
  39. package/types.ts +123 -6
  40. package/utils/buildBlocksTree.ts +4 -4
  41. package/utils/define.ts +31 -5
  42. package/utils/file-based-router.ts +1 -0
  43. package/utils/get-or-create-row.ts +41 -0
  44. package/utils/startup-log.ts +9 -0
  45. package/components/AdminPanel/AdminPanel.css +0 -78
  46. package/components/AdminPanel/AdminPanel.ts +0 -57
  47. package/components/Block.ts +0 -116
  48. package/components/Field.ts +0 -168
  49. package/components/Fields.ts +0 -43
  50. package/public/main.css +0 -95
  51. package/public/main.js +0 -44
  52. /package/{components/Entry.css → public/studio/entry.css} +0 -0
  53. /package/public/{favicon.svg → studio/favicon.svg} +0 -0
@@ -0,0 +1,53 @@
1
+ .blocks-field {
2
+ > header {
3
+ display: flex;
4
+ justify-content: space-between;
5
+ align-items: center;
6
+
7
+ > form fieldset {
8
+ button {
9
+ padding-inline: 1rem;
10
+ }
11
+ }
12
+
13
+ details {
14
+ form {
15
+ display: flex;
16
+ }
17
+
18
+ button {
19
+ width: 100%;
20
+ margin-bottom: 0;
21
+ text-align: left;
22
+ }
23
+
24
+ summary + ul {
25
+ right: 0;
26
+ left: auto;
27
+ }
28
+ }
29
+ }
30
+
31
+ article {
32
+ > header {
33
+ display: flex;
34
+ justify-content: space-between;
35
+ align-items: center;
36
+
37
+ aside {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 5px;
41
+ }
42
+
43
+ button {
44
+ margin: 0;
45
+ padding: 0;
46
+
47
+ svg {
48
+ padding: 5px;
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
@@ -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,24 @@
1
+ #settings {
2
+ ul {
3
+ padding: 0;
4
+ }
5
+
6
+ li {
7
+ width: 100%;
8
+ display: grid;
9
+ align-items: center;
10
+ grid-template-columns: 15% 1fr auto;
11
+ gap: 1rem;
12
+ border-bottom: 1px solid var(--pico-form-element-border-color);
13
+ margin-bottom: 0;
14
+ padding: 1rem 0;
15
+
16
+ input {
17
+ font-family: var(--pico-font-family-monospace);
18
+ }
19
+
20
+ * {
21
+ margin-bottom: 0;
22
+ }
23
+ }
24
+ }
@@ -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)
@@ -0,0 +1,74 @@
1
+ import { sql } from '../utils/sql.ts'
2
+
3
+ export const blockWithChildren = sql`
4
+ with recursive
5
+ block_tree as (
6
+ -- Start from the root block you want
7
+ select
8
+ id,
9
+ name,
10
+ label,
11
+ type,
12
+ sort_order,
13
+ value,
14
+ options,
15
+ status,
16
+ parent_id,
17
+ 0 as depth
18
+ from
19
+ blocks
20
+ where
21
+ id = ? -- <-- put your starting block id here
22
+ union all
23
+ -- Recursively select children
24
+ select
25
+ b.id,
26
+ b.name,
27
+ b.label,
28
+ b.type,
29
+ b.sort_order,
30
+ b.value,
31
+ b.options,
32
+ b.status,
33
+ b.parent_id,
34
+ bt.depth + 1
35
+ from
36
+ blocks b
37
+ inner join block_tree bt on b.parent_id = bt.id
38
+ )
39
+ select
40
+ *
41
+ from
42
+ block_tree
43
+ order by
44
+ depth,
45
+ sort_order;
46
+ `
47
+
48
+ export const deleteBlockWithChildren = sql`
49
+ with recursive
50
+ block_tree as (
51
+ -- start from the root block you want to delete
52
+ select
53
+ id
54
+ from
55
+ blocks
56
+ where
57
+ id = ? -- <-- put your root block id here
58
+ union all
59
+ -- recursively select children
60
+ select
61
+ b.id
62
+ from
63
+ blocks b
64
+ inner join block_tree bt on b.parent_id = bt.id
65
+ )
66
+ delete from blocks
67
+ where
68
+ id in (
69
+ select
70
+ id
71
+ from
72
+ block_tree
73
+ );
74
+ `
package/queries/block.ts CHANGED
@@ -1,22 +1,6 @@
1
1
  import { db } from '@alstar/db'
2
2
  import { sql } from '../utils/sql.ts'
3
-
4
- type DBBlockResult = {
5
- id: number
6
- created_at: string
7
- updated_at: string
8
- name: string
9
- label: string
10
- type: string
11
- sort_order: number
12
- value: string | null
13
- options: any
14
- status: string
15
- parent_block_id: number | null
16
- depth: number
17
- children: DBBlockResult[]
18
- fields: Record<string, DBBlockResult>
19
- }
3
+ import { type DBBlockResult } from '../types.ts'
20
4
 
21
5
  function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
22
6
  const map = new Map<number, DBBlockResult>()
@@ -28,10 +12,10 @@ function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
28
12
  }
29
13
 
30
14
  for (const block of blocks) {
31
- if (block.parent_block_id === null) {
15
+ if (block.parent_id === null) {
32
16
  roots.push(block)
33
17
  } else {
34
- const parent = map.get(block.parent_block_id)
18
+ const parent = map.get(block.parent_id)
35
19
  if (parent) parent.children!.push(block)
36
20
  }
37
21
  }
@@ -56,10 +40,10 @@ function buildTree(blocks: DBBlockResult[]): DBBlockResult {
56
40
  }
57
41
 
58
42
  for (const block of blocks) {
59
- if (block.parent_block_id === null) {
43
+ if (block.parent_id === null) {
60
44
  roots.push(block)
61
45
  } else {
62
- const parent = map.get(block.parent_block_id)
46
+ const parent = map.get(block.parent_id)
63
47
  if (parent) parent.children!.push(block)
64
48
  }
65
49
  }
@@ -74,35 +58,40 @@ function buildTree(blocks: DBBlockResult[]): DBBlockResult {
74
58
  return roots[0]
75
59
  }
76
60
 
77
- function transformDBBlockResultTree(
61
+ function transformBlocksTree(
78
62
  block: DBBlockResult,
79
63
  isBlocksChild?: boolean,
80
64
  ): DBBlockResult {
81
65
  const fields: Record<string, DBBlockResult> = {}
66
+ let hasFields = false
82
67
 
83
68
  for (const child of block.children ?? []) {
84
- const transformedChild = transformDBBlockResultTree(
69
+ const transformedChild = transformBlocksTree(
85
70
  child,
86
71
  child.type === 'blocks',
87
72
  )
88
73
 
89
- if (isBlocksChild) {
90
- } else {
74
+ if (!isBlocksChild) {
75
+ hasFields = true
91
76
  fields[transformedChild.name] = transformedChild
92
77
  }
93
78
  }
94
79
 
95
- block.fields = fields
96
-
80
+ if(hasFields) {
81
+ block.fields = fields
82
+ }
83
+
97
84
  if (!isBlocksChild) {
98
- block.children = [] // clear children array, since all children moved into fields
85
+ delete block.children
86
+ } else {
87
+ delete block.fields
99
88
  }
100
89
 
101
90
  return block
102
91
  }
103
92
 
104
93
  function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
105
- return blocks.map((block) => transformDBBlockResultTree(block))
94
+ return blocks.map((block) => transformBlocksTree(block))
106
95
  }
107
96
 
108
97
  function rootQuery(filterSql: string, depthLimit?: number) {
@@ -123,7 +112,7 @@ function rootQuery(filterSql: string, depthLimit?: number) {
123
112
  value,
124
113
  options,
125
114
  status,
126
- parent_block_id,
115
+ parent_id,
127
116
  0 as depth
128
117
  from
129
118
  blocks
@@ -141,11 +130,11 @@ function rootQuery(filterSql: string, depthLimit?: number) {
141
130
  b.value,
142
131
  b.options,
143
132
  b.status,
144
- b.parent_block_id,
133
+ b.parent_id,
145
134
  a.depth + 1
146
135
  from
147
136
  blocks b
148
- inner join ancestors a on b.id = a.parent_block_id
137
+ inner join ancestors a on b.id = a.parent_id
149
138
  ),
150
139
  roots as (
151
140
  select
@@ -159,12 +148,12 @@ function rootQuery(filterSql: string, depthLimit?: number) {
159
148
  value,
160
149
  options,
161
150
  status,
162
- parent_block_id,
151
+ parent_id,
163
152
  0 as depth
164
153
  from
165
154
  ancestors
166
155
  where
167
- parent_block_id is null
156
+ parent_id is null
168
157
  ),
169
158
  descendants as (
170
159
  select
@@ -178,7 +167,7 @@ function rootQuery(filterSql: string, depthLimit?: number) {
178
167
  value,
179
168
  options,
180
169
  status,
181
- parent_block_id,
170
+ parent_id,
182
171
  depth
183
172
  from
184
173
  roots
@@ -194,19 +183,19 @@ function rootQuery(filterSql: string, depthLimit?: number) {
194
183
  b.value,
195
184
  b.options,
196
185
  b.status,
197
- b.parent_block_id,
186
+ b.parent_id,
198
187
  d.depth + 1
199
188
  from
200
189
  blocks b
201
- inner join descendants d on b.parent_block_id = d.id ${depthLimitClause}
190
+ inner join descendants d on b.parent_id = d.id ${depthLimitClause}
202
191
  )
203
192
  select
204
193
  *
205
194
  from
206
195
  descendants
207
196
  order by
208
- parent_block_id,
209
- sort_order;
197
+ sort_order,
198
+ id;
210
199
  `
211
200
  }
212
201
 
@@ -264,7 +253,7 @@ export function root(
264
253
 
265
254
  const tree = buildTree(rows)
266
255
 
267
- return transformDBBlockResultTree(tree)
256
+ return transformBlocksTree(tree)
268
257
  }
269
258
 
270
259
  export function block(params: Record<string, any>) {
@@ -292,6 +281,8 @@ export function blocks(params: Record<string, any>) {
292
281
  blocks
293
282
  where
294
283
  ${filterSql}
284
+ order by
285
+ sort_order
295
286
  `
296
287
 
297
288
  return db.database.prepare(query).all(sqlParams) as unknown as DBBlockResult[]
@@ -0,0 +1,15 @@
1
+ // db-types.ts
2
+ export type DBBase = {
3
+ id: number;
4
+ created_at: string;
5
+ updated_at: string;
6
+ name: string;
7
+ label: string;
8
+ type: string;
9
+ sort_order: number;
10
+ value: string | null;
11
+ options: any | null;
12
+ status: string;
13
+ parent_id: number | null;
14
+ depth: number;
15
+ };
File without changes