@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
package/types.ts CHANGED
@@ -2,15 +2,130 @@ import { type HttpBindings } from '@hono/node-server'
2
2
  import { type Context } from 'hono'
3
3
  import { type HonoOptions } from 'hono/hono-base'
4
4
  import { type BlankInput, type BlankEnv } from 'hono/types'
5
+ import {
6
+ BlockFieldInstance,
7
+ BlockInstance,
8
+ FieldInstance,
9
+ } from './utils/define.ts'
10
+
11
+ export type PrimitiveField = {
12
+ name: string
13
+ label: string
14
+ type: 'text' | 'slug' | 'markdown' | 'image'
15
+ }
16
+
17
+ export type BlockField = {
18
+ name: string
19
+ label: string
20
+ type: 'blocks'
21
+ children: Record<string, Field | Block>
22
+ }
23
+
24
+ export type Field = PrimitiveField | BlockField
5
25
 
6
26
  export type Block = {
7
27
  name: string
8
28
  label: string
9
29
  type: string
10
- fields?: Block[]
30
+ fields: Record<string, Field | Block>
31
+ }
32
+
33
+ export type Structure = Record<string, BlockDefStructure>
34
+
35
+ // --- Field & block definitions ---
36
+ type FieldType = 'text' | 'slug' | 'markdown' | 'image'
37
+
38
+ interface BaseField {
39
+ label: string
40
+ type: FieldType
41
+ description?: string
42
+ }
43
+
44
+ interface TextField extends BaseField {
45
+ type: 'text' | 'slug' | 'markdown'
46
+ }
47
+
48
+ interface TextFieldStructure extends TextField {
49
+ instanceOf: typeof FieldInstance
50
+ }
51
+
52
+ interface ImageField extends BaseField {
53
+ type: 'image'
54
+ }
55
+
56
+ interface ImageFieldStructure extends ImageField {
57
+ instanceOf: typeof FieldInstance
58
+ }
59
+
60
+ export interface BlocksFieldDef {
61
+ label: string
62
+ type: 'blocks'
63
+ description?: string
64
+ children: Record<string, BlockDefStructure | FieldDefStructure>
65
+ }
66
+
67
+ export interface BlocksFieldDefStructure extends BlocksFieldDef {
68
+ instanceOf: typeof BlockFieldInstance
69
+ }
70
+
71
+ export type FieldDef = TextField | ImageField
72
+ export type FieldDefStructure = TextFieldStructure | ImageFieldStructure
73
+
74
+ export interface BlockDef {
75
+ label: string
76
+ type: string
77
+ fields: Record<string, FieldDefStructure | BlocksFieldDefStructure>
78
+ description?: string
79
+ }
80
+
81
+ export interface BlockDefStructure extends BlockDef {
82
+ instanceOf: typeof BlockInstance
83
+ }
84
+
85
+ // type DBDefaults = {
86
+ // id: number
87
+ // created_at: string
88
+ // updated_at: string
89
+ // name: string
90
+ // label: string
91
+ // // type: string
92
+ // sort_order: number
93
+ // value: string
94
+ // options: string | null
95
+ // status: 'enabled' | 'disabled'
96
+ // parent_id: number | null
97
+ // depth: number
98
+ // }
99
+
100
+ type BaseDBResult = {
101
+ id: number
102
+ created_at: string
103
+ updated_at: string
104
+ name: string
105
+ label: string
106
+ sort_order: number
107
+ value: string | null
108
+ options: any
109
+ status: 'enabled' | 'disabled'
110
+ parent_id: number | null
111
+ depth: number
112
+ }
113
+
114
+ export type DBPrimitiveFieldResult = BaseDBResult & {
115
+ type: FieldDef
116
+ }
117
+
118
+ export type DBBlockFieldResult = BaseDBResult & {
119
+ type: 'blocks'
120
+ children: DBBlockResult[]
121
+ }
122
+
123
+ export type DBBlockResult = BaseDBResult & {
124
+ type: string
125
+ fields: Record<string, DBFieldResult>
11
126
  }
12
127
 
13
- export type Structure = Block[]
128
+ export type DBFieldResult = DBPrimitiveFieldResult & DBBlockFieldResult
14
129
 
15
130
  export type DBBlock = Block & {
16
131
  id: number
@@ -18,17 +133,19 @@ export type DBBlock = Block & {
18
133
  updated_at: string
19
134
  value: string | null
20
135
  sort_order: number | null
21
- parent_block_id: number | null
136
+ parent_id: number | null
22
137
  options: number | null
23
138
  }
24
139
 
140
+ export type BlockStatus = 'enabled' | 'disabled'
141
+
25
142
  export type StudioConfig = {
26
- siteName: string
143
+ siteName?: string
27
144
  honoConfig?: HonoOptions<BlankEnv>
145
+ port?: number
146
+ structure: Structure
28
147
  }
29
148
 
30
- export type BlockStatus = 'enabled' | 'disabled'
31
-
32
149
  export type RequestContext = Context<
33
150
  { Bindings: HttpBindings },
34
151
  string,
@@ -6,7 +6,7 @@ type Block = {
6
6
  sort_order: number
7
7
  value: string | null
8
8
  options: any // JSON-parsed if necessary
9
- parent_block_id: number | null
9
+ parent_id: number | null
10
10
  depth: number
11
11
  // ... you can add other fields if needed
12
12
  }
@@ -26,13 +26,13 @@ export function buildBlockTree(blocks: Block[]): BlockWithChildren {
26
26
  for (const block of blocks) {
27
27
  const current = blockMap.get(block.id)!
28
28
 
29
- if (block.parent_block_id != null) {
30
- const parent = blockMap.get(block.parent_block_id)
29
+ if (block.parent_id != null) {
30
+ const parent = blockMap.get(block.parent_id)
31
31
  if (parent) {
32
32
  parent.fields.push(current)
33
33
  } else {
34
34
  console.warn(
35
- `Parent with id ${block.parent_block_id} not found for block ${block.id}`,
35
+ `Parent with id ${block.parent_id} not found for block ${block.id}`,
36
36
  )
37
37
  }
38
38
  } else {
package/utils/define.ts CHANGED
@@ -1,15 +1,41 @@
1
- import { type Context } from 'hono'
2
1
  import * as types from '../types.ts'
3
- import { type HttpBindings } from '@hono/node-server'
4
- import { type BlankInput } from 'hono/types'
5
2
  import { type HtmlEscapedString } from './html.ts'
6
3
 
7
4
  export const defineConfig = (config: types.StudioConfig) => config
8
5
 
9
- export const defineStructure = (structure: types.Structure) => structure
10
-
11
6
  export const defineEntry = (
12
7
  fn: (
13
8
  c: types.RequestContext,
14
9
  ) => HtmlEscapedString | Promise<HtmlEscapedString>,
15
10
  ) => fn
11
+
12
+ export const FieldInstance = Symbol('field')
13
+ export const BlockFieldInstance = Symbol('blockfield')
14
+ export const BlockInstance = Symbol('block')
15
+
16
+ // --- Identity helpers (preserve literal types) ---
17
+ export function defineField(field: types.FieldDef): types.FieldDefStructure {
18
+ return {
19
+ ...field,
20
+ instanceOf: FieldInstance,
21
+ }
22
+ }
23
+
24
+ export function defineBlockField(
25
+ field: types.BlocksFieldDef,
26
+ ): types.BlocksFieldDefStructure {
27
+ return {
28
+ ...field,
29
+ instanceOf: BlockFieldInstance,
30
+ }
31
+ }
32
+
33
+ export function defineBlock(block: types.BlockDef): types.BlockDefStructure {
34
+ return { ...block, instanceOf: BlockInstance }
35
+ }
36
+
37
+ export function defineStructure(
38
+ structure: Record<string, types.BlockDefStructure>,
39
+ ) {
40
+ return structure
41
+ }
@@ -15,6 +15,7 @@ export const fileBasedRouter = async (rootdir: string) => {
15
15
  try {
16
16
  dirs = await fs.readdir(root, { recursive: true })
17
17
  } catch (error) {
18
+ console.log('No files in:', root)
18
19
  return
19
20
  }
20
21
 
@@ -0,0 +1,41 @@
1
+ import { db } from '@alstar/db'
2
+ import { query } from '../queries/index.ts'
3
+ import type {
4
+ BlockDefStructure,
5
+ BlocksFieldDefStructure,
6
+ FieldDefStructure,
7
+ } from '../types.ts'
8
+
9
+ export function getOrCreateRow(props: {
10
+ parentId: string | number | null
11
+ name: string
12
+ field: BlockDefStructure | BlocksFieldDefStructure | FieldDefStructure
13
+ sortOrder?: number
14
+ id?: number
15
+ }) {
16
+ const { parentId, name, field, sortOrder = 0, id } = props
17
+
18
+ if (id) {
19
+ return query.block({
20
+ id: id?.toString(),
21
+ })
22
+ }
23
+
24
+ const data = query.block({
25
+ parent_id: parentId?.toString() || null,
26
+ name: name,
27
+ sort_order: sortOrder.toString(),
28
+ })
29
+
30
+ if (data) return data
31
+
32
+ const change = db.insertInto('blocks', {
33
+ name: name?.toString(),
34
+ label: field.label?.toString(),
35
+ type: field.type?.toString(),
36
+ sort_order: sortOrder,
37
+ parent_id: parentId,
38
+ })
39
+
40
+ return query.block({ id: change.lastInsertRowid.toString() })
41
+ }
@@ -0,0 +1,9 @@
1
+ import packageJSON from '../package.json' with { type: 'json' }
2
+
3
+ export default ({ port }: { port: number }) => {
4
+ console.log('\x1b[32m%s\x1b[0m', '╭───────────────────────╮')
5
+ console.log('\x1b[32m%s\x1b[0m', '│ Alstar Studio │')
6
+ console.log('\x1b[32m%s\x1b[0m', `│ ${packageJSON.version}${' '.repeat(22 - packageJSON.version.length)}│`)
7
+ console.log('\x1b[32m%s\x1b[0m', `│ http://localhost:${port} │`)
8
+ console.log('\x1b[32m%s\x1b[0m', '╰───────────────────────╯')
9
+ }
@@ -1,78 +0,0 @@
1
- .admin-panel {
2
- /* background: hsla(0, 0%, 0%, 0.1); */
3
- padding: 40px;
4
-
5
- height: 100%;
6
- min-height: inherit;
7
-
8
- min-width: 250px;
9
-
10
- > h1 {
11
- padding-bottom: 1rem;
12
-
13
- a {
14
- display: flex;
15
- }
16
-
17
- svg {
18
- height: 1.6rem;
19
- }
20
- }
21
-
22
- form {
23
- padding-bottom: 1rem;
24
-
25
- button {
26
- margin: 10px 0px 20px;
27
- }
28
- }
29
-
30
- #entries ul {
31
- padding: 0;
32
- margin-inline: -1rem;
33
-
34
- form {
35
- display: flex;
36
- padding-bottom: 0;
37
-
38
- button {
39
- margin: 0;
40
- }
41
- }
42
-
43
- > li {
44
- margin-bottom: 0px;
45
- border-radius: 8px;
46
- display: flex;
47
- justify-content: space-between;
48
- align-items: stretch;
49
- list-style: none;
50
-
51
- a {
52
- text-decoration: none;
53
- width: 100%;
54
- padding: 0.5rem 1rem;
55
- }
56
-
57
- button {
58
- border-radius: 7px;
59
- opacity: 0;
60
- transition: opacity 100px;
61
-
62
- svg {
63
- margin: 0.5rem 1rem;
64
- }
65
- }
66
- }
67
- }
68
- }
69
-
70
- #entries ul {
71
- > li:hover {
72
- background-color: var(--pico-form-element-background-color);
73
-
74
- button {
75
- opacity: 1;
76
- }
77
- }
78
- }
@@ -1,57 +0,0 @@
1
- import { html } from 'hono/html'
2
- import type { Structure } from '../../types.ts'
3
- import { logo } from '../icons.ts'
4
- import { rootdir } from '../../index.ts'
5
- import Entries from '../Entries.ts'
6
- import * as icons from '../icons.ts'
7
-
8
- export default (structure: Structure) => {
9
- return html`
10
- <link
11
- rel="stylesheet"
12
- href="${rootdir}/components/AdminPanel/AdminPanel.css"
13
- />
14
-
15
- <div class="admin-panel">
16
- <h1>
17
- <a href="/admin" aria-label="Go to dashboard"> ${logo} </a>
18
- </h1>
19
-
20
- <aside>
21
- <form
22
- data-on-submit="@post('/admin/api/block', { contentType: 'form' })"
23
- >
24
- <!-- <select name="type">
25
- ${structure.map(
26
- (block) => html`
27
- <option value="${block.type}">
28
- ${block.label}
29
- </option>
30
- `,
31
- )}
32
- </select>
33
- <input type="submit" value="Create" /> -->
34
-
35
- <!-- TODO: currently only handles a single entry type -->
36
- ${structure.length
37
- ? html`<input
38
- type="hidden"
39
- name="type"
40
- value="${structure[0].type}"
41
- />
42
- <button
43
- class="ghost"
44
- style="padding: 10px; margin: 0 -13px; display: flex;"
45
- data-tooltip="New entry"
46
- data-placement="right"
47
- >
48
- ${icons.newDocument}
49
- </button>`
50
- : ''}
51
- </form>
52
- </aside>
53
-
54
- ${Entries()}
55
- </div>
56
- `
57
- }
@@ -1,116 +0,0 @@
1
- import { html } from 'hono/html'
2
- import { query } from '../queries/index.ts'
3
- import Fields from './Fields.ts'
4
- import * as icons from './icons.ts'
5
- import { type Block } from '../types.ts'
6
- import Field from './Field.ts'
7
- import {
8
- buildStructurePath,
9
- type StructurePath,
10
- } from '../utils/build-structure-path.ts'
11
-
12
- export default (
13
- entryId: string | number,
14
- id: string | number,
15
- fieldStructure: Block,
16
- structurePath: StructurePath,
17
- ) => {
18
- const data = query.block({ id: id.toString() })
19
- const blocks = query.blocks({ parent_block_id: id })
20
-
21
- if (!data) return html`<p>No block data</p>`
22
-
23
- const fieldStructurePath = buildStructurePath(fieldStructure, structurePath)
24
-
25
- return html`
26
-
27
- <section class="block">
28
- <header>
29
- <h5>${fieldStructure.label}</h5>
30
-
31
- <form
32
- data-on-submit="@post('/admin/api/new-block', { contentType: 'form' })"
33
- >
34
- <input type="hidden" name="parent_block_id" value="${id}" />
35
- <input
36
- type="hidden"
37
- name="sort_order"
38
- value="${blocks?.length || 0}"
39
- />
40
-
41
- <fieldset role="group">
42
- <select name="block">
43
- ${fieldStructure.fields?.map((field) => {
44
- return html`<option
45
- value="entry_id:${entryId};type:${field.type};name:${field.name};label:${field.label};sort_order:${blocks?.length ||
46
- 0}"
47
- >
48
- ${field.label}
49
- </option>`
50
- })}
51
- </select>
52
-
53
- <button
54
- class="outline"
55
- style="padding: 0 1rem"
56
- data-tooltip="New entry"
57
- data-placement="right"
58
- type="submit"
59
- >
60
- Add
61
- </button>
62
- </fieldset>
63
- </form>
64
- </header>
65
-
66
- <div data-sortable="${id}">
67
- ${blocks?.map((block, idx) => {
68
- const structure = fieldStructure.fields?.find(
69
- (field) => field.name === block.name,
70
- )
71
-
72
- if (!structure) return
73
-
74
- return html`
75
- <article
76
- data-signals="{ open${block.id}: true }"
77
- >
78
- <header>
79
- <h6>${structure.label}</h6>
80
- <div>
81
- <button
82
- data-on-click="$open${block.id} = !$open${block.id}"
83
- style="margin-right: 0.5rem"
84
- data-style-rotate="$open${block.id} ? '0deg' : '180deg'"
85
- class="shadow"
86
- >
87
- ${icons.chevron}
88
- </button>
89
- <button class="shadow">${icons.bars}</button>
90
- </div>
91
- </header>
92
-
93
- <div data-show="$open${block.id}">
94
- ${structure.fields
95
- ? Fields({
96
- entryId,
97
- parentId: block.id,
98
- blockStructure: structure,
99
- structurePath: [...fieldStructurePath, structure.name],
100
- })
101
- : Field({
102
- entryId,
103
- parentId: block.id,
104
- blockStructure: structure,
105
- sortOrder: idx,
106
- structurePath: [...fieldStructurePath, structure.name],
107
- })}
108
-
109
- </div>
110
- </article>
111
- `
112
- })}
113
- </div>
114
- </section>
115
- `
116
- }