@alstar/studio 0.0.0-beta.1 → 0.0.0-beta.10

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 (58) hide show
  1. package/api/api-key.ts +73 -0
  2. package/api/backup.ts +38 -0
  3. package/api/block.ts +96 -30
  4. package/api/index.ts +12 -1
  5. package/api/mcp.ts +53 -0
  6. package/components/AdminPanel.ts +74 -0
  7. package/components/Backup.ts +10 -0
  8. package/components/BlockFieldRenderer.ts +121 -0
  9. package/components/BlockRenderer.ts +22 -0
  10. package/components/Entries.ts +19 -11
  11. package/components/Entry.ts +13 -21
  12. package/components/FieldRenderer.ts +35 -0
  13. package/components/Render.ts +46 -0
  14. package/components/Settings.ts +101 -0
  15. package/components/{layout.ts → SiteLayout.ts} +22 -17
  16. package/components/fields/Markdown.ts +44 -0
  17. package/components/fields/Text.ts +42 -0
  18. package/components/fields/index.ts +4 -0
  19. package/components/icons.ts +100 -7
  20. package/index.ts +66 -34
  21. package/package.json +8 -8
  22. package/pages/entry/[id].ts +17 -0
  23. package/{components → pages}/index.ts +7 -4
  24. package/pages/settings.ts +10 -0
  25. package/public/studio/css/admin-panel.css +103 -0
  26. package/public/studio/css/blocks-field.css +53 -0
  27. package/public/studio/css/settings.css +24 -0
  28. package/public/studio/js/markdown-editor.js +34 -0
  29. package/public/studio/js/sortable-list.js +50 -0
  30. package/public/studio/main.css +161 -0
  31. package/public/studio/main.js +9 -0
  32. package/queries/block-with-children.ts +74 -0
  33. package/queries/block.ts +289 -0
  34. package/queries/db-types.ts +15 -0
  35. package/queries/getBlockTrees-2.ts +0 -0
  36. package/queries/getBlockTrees.ts +316 -0
  37. package/queries/getBlocks.ts +214 -0
  38. package/queries/index.ts +2 -98
  39. package/queries/structure-types.ts +97 -0
  40. package/readme.md +205 -0
  41. package/schemas.ts +15 -54
  42. package/types.ts +133 -5
  43. package/utils/buildBlocksTree.ts +4 -4
  44. package/utils/define.ts +41 -0
  45. package/utils/file-based-router.ts +11 -2
  46. package/utils/get-config.ts +8 -9
  47. package/utils/get-or-create-row.ts +41 -0
  48. package/utils/html.ts +247 -0
  49. package/utils/startup-log.ts +10 -0
  50. package/components/AdminPanel/AdminPanel.css +0 -59
  51. package/components/AdminPanel/AdminPanel.ts +0 -57
  52. package/components/Block.ts +0 -116
  53. package/components/Entry.css +0 -7
  54. package/components/Field.ts +0 -164
  55. package/components/Fields.ts +0 -43
  56. package/public/main.css +0 -92
  57. package/public/main.js +0 -43
  58. /package/public/{favicon.svg → studio/favicon.svg} +0 -0
package/package.json CHANGED
@@ -1,18 +1,15 @@
1
1
  {
2
2
  "name": "@alstar/studio",
3
- "version": "0.0.0-beta.1",
3
+ "version": "0.0.0-beta.10",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
- "scripts": {
7
- "dev": "node --watch index.ts"
8
- },
9
6
  "dependencies": {
10
- "@alstar/db": "workspace:*",
11
- "@alstar/ui": "workspace:*",
12
7
  "@hono/node-server": "^1.18.1",
13
8
  "@starfederation/datastar-sdk": "1.0.0-RC.1",
14
9
  "hono": "^4.8.12",
15
- "sortablejs": "^1.15.6"
10
+ "@alstar/refresher": "0.0.0-beta.3",
11
+ "@alstar/ui": "0.0.0-beta.1",
12
+ "@alstar/db": "0.0.0-beta.1"
16
13
  },
17
14
  "devDependencies": {
18
15
  "@types/node": "^24.1.0",
@@ -35,5 +32,8 @@
35
32
  },
36
33
  "publishConfig": {
37
34
  "access": "public"
35
+ },
36
+ "scripts": {
37
+ "dev": "node --watch index.ts"
38
38
  }
39
- }
39
+ }
@@ -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.length ? 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
+ })
@@ -0,0 +1,103 @@
1
+ .admin-panel {
2
+ /* background: hsla(0, 0%, 0%, 0.1); */
3
+ padding: 40px;
4
+ margin-bottom: 0;
5
+ display: flex;
6
+ flex-direction: column;
7
+ align-items: flex-start;
8
+
9
+ height: 100%;
10
+ min-height: inherit;
11
+
12
+ min-width: 250px;
13
+
14
+ > h1 {
15
+ padding-bottom: 1rem;
16
+
17
+ a {
18
+ display: flex;
19
+ }
20
+
21
+ svg {
22
+ height: 1.6rem;
23
+ }
24
+ }
25
+
26
+ form {
27
+ padding-bottom: 1rem;
28
+
29
+ button {
30
+ margin: 10px 0px 20px;
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
+ }
43
+ }
44
+
45
+ #entries {
46
+ width: 100%;
47
+ user-select: none;
48
+
49
+ ul {
50
+ padding: 0;
51
+ margin-inline: -1rem;
52
+
53
+ form {
54
+ display: flex;
55
+ padding-bottom: 0;
56
+
57
+ button {
58
+ margin: 0;
59
+ }
60
+ }
61
+
62
+ > li {
63
+ margin-bottom: 0px;
64
+ border-radius: 8px;
65
+ display: flex;
66
+ justify-content: space-between;
67
+ align-items: stretch;
68
+ list-style: none;
69
+ padding: 0;
70
+
71
+ a {
72
+ text-decoration: none;
73
+ width: 100%;
74
+ padding: 0.5rem 1rem;
75
+ }
76
+
77
+ button {
78
+ border-radius: 7px;
79
+ opacity: 0;
80
+ transition: opacity 100px;
81
+
82
+ svg {
83
+ margin: 0.5rem 1rem;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ > footer {
91
+ margin-top: auto;
92
+ }
93
+ }
94
+
95
+ #entries ul {
96
+ > li:hover {
97
+ background-color: var(--pico-form-element-background-color);
98
+
99
+ button {
100
+ opacity: 1;
101
+ }
102
+ }
103
+ }
@@ -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,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,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,50 @@
1
+ import Sortable from 'sortablejs'
2
+
3
+ class SortableList extends HTMLElement {
4
+ instance = null
5
+
6
+ mutationObserver
7
+
8
+ init() {
9
+ if (this.instance || !this.children.length) return
10
+
11
+ const { id } = this.dataset
12
+
13
+ this.instance = Sortable.create(this, {
14
+ animation: 250,
15
+ handle: `[data-handle-for="${id}"]`,
16
+ onEnd: (e) => {
17
+ const items = [...e.target.children]
18
+
19
+ items.forEach(async (child, idx) => {
20
+ const searchParams = new URLSearchParams()
21
+
22
+ searchParams.set('id', child.dataset.id)
23
+ searchParams.set('sort-order', idx)
24
+
25
+ await fetch(`/admin/api/sort-order?${searchParams.toString()}`, {
26
+ method: 'post',
27
+ })
28
+ })
29
+
30
+ this.querySelectorAll('[name="sort_order"]').forEach((input, idx) => {
31
+ input.value = idx.toString()
32
+ })
33
+ },
34
+ })
35
+ }
36
+
37
+ connectedCallback() {
38
+ this.mutationObserver = new MutationObserver(this.init.bind(this))
39
+ this.mutationObserver.observe(this, { childList: true })
40
+
41
+ this.init()
42
+ }
43
+
44
+ disconnectedCallback() {
45
+ this.instance?.destroy()
46
+ this.mutationObserver?.disconnect()
47
+ }
48
+ }
49
+
50
+ customElements.define('sortable-list', SortableList)
@@ -0,0 +1,161 @@
1
+ /* @import './../node_modules/@alstar/ui/red.css'; */
2
+ @import 'https://esm.sh/@alstar/ui/red.css';
3
+ @import './css/admin-panel.css';
4
+ @import './css/blocks-field.css';
5
+ @import './css/settings.css';
6
+
7
+ body {
8
+ padding: 0;
9
+ min-height: 100vh;
10
+
11
+ display: grid;
12
+ grid-template-columns: auto 1fr;
13
+ align-content: baseline;
14
+
15
+ > main {
16
+ padding: 40px;
17
+ height: 100vh;
18
+ overflow: auto;
19
+
20
+ section {
21
+ max-width: 900px;
22
+ margin: 0 auto;
23
+ }
24
+ }
25
+ }
26
+
27
+ .entry > .fields > .block {
28
+ padding-block: var(--pico-spacing);
29
+
30
+ > header {
31
+ margin-block: var(--pico-block-spacing-vertical);
32
+ }
33
+ }
34
+
35
+ .block {
36
+ > header {
37
+ margin-bottom: var(--pico-spacing);
38
+ }
39
+
40
+ header {
41
+ display: flex;
42
+ justify-content: space-between;
43
+ align-items: center;
44
+
45
+ background-color: inherit;
46
+
47
+ h6,
48
+ h5,
49
+ fieldset {
50
+ margin-bottom: 0;
51
+ }
52
+
53
+ button {
54
+ padding: 0;
55
+ background-color: white;
56
+
57
+ svg {
58
+ padding: 8px;
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ [data-sortable] {
65
+ > article {
66
+ transition: opacity 200ms;
67
+ }
68
+
69
+ .sortable-ghost {
70
+ opacity: 0.3;
71
+ }
72
+ }
73
+
74
+ div.ink-mde {
75
+ border: var(--pico-border-width) solid #2a3140;
76
+ border-radius: var(--pico-border-radius);
77
+ outline: 0;
78
+ background-color: var(--pico-background-color);
79
+ box-shadow: var(--pico-box-shadow);
80
+ color: var(--pico-color);
81
+ }
82
+
83
+ .ͼ1 .cm-content.ink-mde-editor-content {
84
+ min-height: 10lh;
85
+ }
86
+
87
+ div.ink-mde {
88
+ --pico-border-color: var(--pico-form-element-border-color);
89
+
90
+ border: var(--pico-border-width) solid var(--pico-border-color);
91
+ border-radius: var(--pico-border-radius);
92
+
93
+ padding: var(--pico-form-element-spacing-vertical)
94
+ var(--pico-form-element-spacing-horizontal);
95
+ margin-bottom: var(--pico-spacing);
96
+
97
+ --pico-background-color: var(--pico-form-element-background-color);
98
+
99
+ --pico-color: var(--pico-form-element-color);
100
+ --pico-box-shadow: none;
101
+
102
+ outline: 0;
103
+ background-color: var(--pico-background-color);
104
+ box-shadow: var(--pico-box-shadow);
105
+ color: var(--pico-color);
106
+ font-weight: var(--pico-font-weight);
107
+ transition:
108
+ background-color var(--pico-transition),
109
+ border-color var(--pico-transition),
110
+ color var(--pico-transition),
111
+ box-shadow var(--pico-transition);
112
+ }
113
+
114
+ .ink-mde-textarea {
115
+ }
116
+
117
+ div.ink-mde:has(.cm-focused) {
118
+ --pico-outline-width: 0.1rem;
119
+ --pico-box-shadow: 0 0 0 var(--pico-outline-width)
120
+ var(--pico-form-element-focus-color);
121
+
122
+ box-shadow: var(--pico-box-shadow);
123
+ }
124
+
125
+ .ͼ1.cm-focused {
126
+ outline: none;
127
+ }
128
+
129
+ .ink-mde-textarea {
130
+ width: 100%;
131
+
132
+ --pico-background-color: var(--pico-form-element-background-color);
133
+
134
+ --pico-color: var(--pico-form-element-color);
135
+ --pico-box-shadow: none;
136
+
137
+ outline: 0;
138
+ background-color: var(--pico-background-color);
139
+ box-shadow: var(--pico-box-shadow);
140
+ color: var(--pico-color);
141
+ font-weight: var(--pico-font-weight);
142
+ transition:
143
+ background-color var(--pico-transition),
144
+ border-color var(--pico-transition),
145
+ color var(--pico-transition),
146
+ box-shadow var(--pico-transition);
147
+ }
148
+
149
+ .disclamer {
150
+ display: flex;
151
+ justify-content: center;
152
+ }
153
+
154
+ .text-secondary {
155
+ color: var(--pico-secondary);
156
+ }
157
+
158
+ .markdown {
159
+ width: 100%;
160
+ height: 10lh;
161
+ }
@@ -0,0 +1,9 @@
1
+ import barba from '@barba/core'
2
+
3
+ import './js/markdown-editor.js'
4
+ import './js/sortable-list.js'
5
+
6
+ barba.init({
7
+ cacheIgnore: true,
8
+ views: [{ namespace: 'default' }],
9
+ })
@@ -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
+ `