@alstar/studio 0.0.0-beta.4 → 0.0.0-beta.6

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 (43) hide show
  1. package/api/api-key.ts +74 -0
  2. package/api/block.ts +39 -21
  3. package/api/index.ts +9 -1
  4. package/api/mcp.ts +53 -0
  5. package/bin/alstar.ts +42 -0
  6. package/components/{AdminPanel/AdminPanel.ts → AdminPanel.ts} +22 -27
  7. package/components/Block.ts +51 -112
  8. package/components/Entries.ts +17 -10
  9. package/components/Entry.ts +9 -15
  10. package/components/Settings.ts +98 -0
  11. package/components/fields/Blocks.ts +118 -0
  12. package/components/fields/Text.ts +42 -0
  13. package/components/fields/index.ts +4 -0
  14. package/components/icons.ts +97 -0
  15. package/components/index.ts +1 -1
  16. package/components/layout.ts +8 -5
  17. package/index.ts +48 -20
  18. package/package.json +2 -1
  19. package/public/admin-panel.css +90 -0
  20. package/public/blocks.css +53 -0
  21. package/public/main.css +11 -1
  22. package/public/main.js +4 -0
  23. package/public/settings.css +24 -0
  24. package/queries/block-with-children.ts +74 -0
  25. package/queries/block.ts +287 -0
  26. package/queries/db-types.ts +15 -0
  27. package/queries/getBlockTrees-2.ts +0 -0
  28. package/queries/getBlockTrees.ts +316 -0
  29. package/queries/getBlocks.ts +214 -0
  30. package/queries/index.ts +2 -98
  31. package/queries/structure-types.ts +97 -0
  32. package/schemas.ts +15 -54
  33. package/types.ts +95 -5
  34. package/utils/buildBlocksTree.ts +4 -4
  35. package/utils/define.ts +21 -5
  36. package/utils/file-based-router.ts +9 -1
  37. package/utils/get-config.ts +8 -9
  38. package/utils/get-or-create-row.ts +28 -0
  39. package/utils/startup-log.ts +9 -0
  40. package/components/AdminPanel/AdminPanel.css +0 -59
  41. package/components/Field.ts +0 -164
  42. package/components/Fields.ts +0 -43
  43. /package/{components/Entry.css → public/entry.css} +0 -0
@@ -0,0 +1,98 @@
1
+ import { db } from '@alstar/db'
2
+ import { html } from 'hono/html'
3
+ import { sql } from '../utils/sql.ts'
4
+ import * as icons from './icons.ts'
5
+
6
+ export default () => {
7
+ const apiKeys = db.database
8
+ .prepare(sql`
9
+ select
10
+ *
11
+ from
12
+ api_keys
13
+ `)
14
+ .all()
15
+
16
+ return html`
17
+ <div id="settings" data-signals="{ apiKey: '', copied: false }">
18
+ <!-- <h1>Settings</h1> -->
19
+ <article>
20
+ <header>API Keys</header>
21
+
22
+ <ul>
23
+ ${apiKeys.map((apiKey) => {
24
+ return html`<li>
25
+ <p>${apiKey.name}</p>
26
+ <input type="text" disabled value="${apiKey.hint}" />
27
+ <form
28
+ data-on-submit="@delete('/admin/api/api-key', { contentType: 'form' })"
29
+ >
30
+ <button
31
+ data-tooltip="Delete API key"
32
+ data-placement="left"
33
+ type="submit"
34
+ class="ghost"
35
+ >
36
+ ${icons.trash}
37
+ </button>
38
+
39
+ <input type="hidden" name="value" value="${apiKey.value}" />
40
+ </form>
41
+ </li>`
42
+ })}
43
+ </ul>
44
+
45
+ <form
46
+ data-on-submit="@post('/admin/api/api-key', { contentType: 'form' })"
47
+ >
48
+ <label for="api_key_name">Generate API Key</label>
49
+
50
+ <input
51
+ data-bind="name"
52
+ type="text"
53
+ name="name"
54
+ id="api_key_name"
55
+ placeholder="Name"
56
+ />
57
+
58
+ <button type="submit" class="ghost">Generate key</button>
59
+ </form>
60
+
61
+ <dialog data-attr="{ open: $apiKey !== '' }">
62
+ <article>
63
+ <header>
64
+ <p>API Key</p>
65
+ </header>
66
+ <p>Be sure to save this key, as it wont be shown again.</p>
67
+
68
+ <div style="display: flex; gap: 1rem; align-items: center;">
69
+ <h3 style="margin: 0;">
70
+ <code data-text="$apiKey"></code>
71
+ </h3>
72
+
73
+ <button
74
+ style="display: flex; align-items: center;"
75
+ data-attr="{ id: $apiKey }"
76
+ data-on-click="navigator.clipboard.writeText($apiKey); $copied = true"
77
+ class="ghost"
78
+ aria-label="Copy key to clipboard"
79
+ >
80
+ ${icons.clipboard}
81
+ <span style="display: none; margin-left: 0.5rem; color: green;" data-style="{ display: $copied && 'block' }">Copied</span>
82
+ </button>
83
+ </div>
84
+
85
+ <footer>
86
+ <button
87
+ class="ghost"
88
+ data-on-click="$apiKey = ''; $copied = false; evt.target.closest('dialog')?.close()"
89
+ >
90
+ Close
91
+ </button>
92
+ </footer>
93
+ </article>
94
+ </dialog>
95
+ </article>
96
+ </div>
97
+ `
98
+ }
@@ -0,0 +1,118 @@
1
+ import { query } from '../../queries/index.ts'
2
+ import type { Field, Block as BlockType, BlocksField } from '../../types.ts'
3
+ import { getOrCreateRow } from '../../utils/get-or-create-row.ts'
4
+ import { html } from '../../utils/html.ts'
5
+ import Block from '../Block.ts'
6
+ import * as icons from '../icons.ts'
7
+
8
+ export default (props: {
9
+ entryId: number
10
+ parentId: number
11
+ name: string
12
+ field: BlocksField
13
+ sortOrder: number
14
+ }) => {
15
+ const { entryId, parentId, name, field, sortOrder = 0 } = props
16
+
17
+ const data = getOrCreateRow(parentId, name, field, sortOrder)
18
+
19
+ if (!data) return html`<p>No block</p>`
20
+
21
+ const entries = Object.entries(field.children) as [
22
+ string,
23
+ BlockType | Field,
24
+ ][]
25
+ const rows = query.blocks({ parent_id: data.id })
26
+
27
+ return html`
28
+ <div class="blocks-field">
29
+ <header>
30
+ <p>${field.label}</p>
31
+
32
+ <details class="dropdown">
33
+ <summary>Add</summary>
34
+ <ul>
35
+ ${entries.map(([name, block]) => {
36
+ return html`
37
+ <li>
38
+ <form
39
+ data-on-submit="@post('/admin/api/new-block', { contentType: 'form' })"
40
+ >
41
+ <button type="submit" class="ghost">${block.label}</button>
42
+ <input type="hidden" name="type" value="${block.type}" />
43
+ <input type="hidden" name="name" value="${name}" />
44
+ <input type="hidden" name="label" value="${block.label}" />
45
+ <input type="hidden" name="parent_id" value="${data.id}" />
46
+ <input type="hidden" name="entry_id" value="${entryId}" />
47
+ <input
48
+ type="hidden"
49
+ name="sort_order"
50
+ value="${rows.length}"
51
+ />
52
+ </form>
53
+ </li>
54
+ `
55
+ })}
56
+ </ul>
57
+ </details>
58
+ </header>
59
+
60
+ <hr style="margin-top: 0;" />
61
+
62
+ <div data-sortable="${data.id}">
63
+ ${rows.map((row, idx) => {
64
+ const [name, struct] = (entries.find(([name]) => name === row.name) ||
65
+ []) as [string, BlockType]
66
+
67
+ if (!name) return html``
68
+
69
+ return html`
70
+ <article>
71
+ <header>
72
+ ${struct.label}
73
+
74
+ <!-- data-tooltip="Move"
75
+ data-placement="top" -->
76
+ <aside>
77
+ <button
78
+ data-handle="${data.id}"
79
+ class="ghost handle text-secondary"
80
+ style="cursor: grab"
81
+ >
82
+ ${icons.bars}
83
+ </button>
84
+
85
+ <form
86
+ data-on-submit="@delete('/admin/api/block', { contentType: 'form' })"
87
+ >
88
+ <button
89
+ type="submit"
90
+ class="ghost text-secondary"
91
+ data-tooltip="Remove"
92
+ data-placement="top"
93
+ aria-label="Delete block"
94
+ >
95
+ ${icons.x}
96
+ </button>
97
+ <input type="hidden" name="id" value="${row.id}" />
98
+ <input type="hidden" name="entry_id" value="${entryId}" />
99
+ </form>
100
+ </aside>
101
+ </header>
102
+
103
+ ${Block({
104
+ entryId,
105
+ parentId: struct.fields ? row.id : data.id,
106
+ blockStructure: struct,
107
+ name: name,
108
+ sortOrder: row.sort_order,
109
+ })}
110
+
111
+ <!-- <pre><code>{JSON.stringify(struct, null, 2)} - {row.sort_order}</code></pre> -->
112
+ </article>
113
+ `
114
+ })}
115
+ </div>
116
+ </div>
117
+ `
118
+ }
@@ -0,0 +1,42 @@
1
+ import type { FieldDef } from '../../types.ts'
2
+ import { getOrCreateRow } from '../../utils/get-or-create-row.ts'
3
+ import { html } from '../../utils/html.ts'
4
+
5
+ export default (props: {
6
+ entryId: number
7
+ parentId: number
8
+ name: string
9
+ field: FieldDef
10
+ sortOrder: number
11
+ }) => {
12
+ const { entryId, parentId, name, field, sortOrder = 0 } = props
13
+
14
+ const data = getOrCreateRow(parentId, name, field, sortOrder)
15
+
16
+ if (!data) return html`<p>No block</p>`
17
+
18
+ return html`
19
+ <form
20
+ data-on-input="@patch('/admin/api/block', { contentType: 'form' })"
21
+ >
22
+ <hgroup>
23
+ <label for="block-${data.id}">${field.label}</label>
24
+ <p><small>${field.description}</small></p>
25
+ </hgroup>
26
+
27
+ <input
28
+ id="block-${data.id}"
29
+ name="value"
30
+ type="text"
31
+ value="${data.value}"
32
+ />
33
+
34
+ <input type="hidden" name="type" value="${field.type}" />
35
+ <input type="hidden" name="id" value="${data.id}" />
36
+ <input type="hidden" name="entryId" value="${entryId}" />
37
+ <input type="hidden" name="parentId" value="${parentId}" />
38
+ <input type="hidden" name="sort_order" value="${sortOrder}" />
39
+ <input type="hidden" name="name" value="${name}" />
40
+ </form>
41
+ `
42
+ }
@@ -0,0 +1,4 @@
1
+ import Text from './Text.ts'
2
+ import Blocks from './Blocks.ts'
3
+
4
+ export const Field = { Text, Blocks }
@@ -97,3 +97,100 @@ export const newDocument = html`
97
97
  />
98
98
  </svg>
99
99
  `
100
+
101
+ export const dots = html`
102
+ <svg
103
+ xmlns="http://www.w3.org/2000/svg"
104
+ width="16"
105
+ height="16"
106
+ viewBox="0 0 24 24"
107
+ >
108
+ <!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
109
+ <path
110
+ fill="none"
111
+ stroke="currentColor"
112
+ stroke-linecap="round"
113
+ stroke-linejoin="round"
114
+ stroke-width="1.5"
115
+ d="M6.75 12a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m6 0a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m6 0a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0"
116
+ />
117
+ </svg>
118
+ `
119
+
120
+ export const trash = html`
121
+ <svg
122
+ xmlns="http://www.w3.org/2000/svg"
123
+ width="16"
124
+ height="16"
125
+ viewBox="0 0 24 24"
126
+ >
127
+ <!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
128
+ <path
129
+ fill="none"
130
+ stroke="currentColor"
131
+ stroke-linecap="round"
132
+ stroke-linejoin="round"
133
+ stroke-width="1.5"
134
+ d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21q.512.078 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48 48 0 0 0-3.478-.397m-12 .562q.51-.088 1.022-.165m0 0a48 48 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a52 52 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a49 49 0 0 0-7.5 0"
135
+ />
136
+ </svg>
137
+ `
138
+
139
+ export const x = html`
140
+ <svg
141
+ xmlns="http://www.w3.org/2000/svg"
142
+ width="26"
143
+ height="26"
144
+ viewBox="0 0 24 24"
145
+ >
146
+ <!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
147
+ <path
148
+ fill="none"
149
+ stroke="currentColor"
150
+ stroke-linecap="round"
151
+ stroke-linejoin="round"
152
+ stroke-width="1.5"
153
+ d="M6 18L18 6M6 6l12 12"
154
+ />
155
+ </svg>
156
+ `
157
+
158
+ export const cog = html`<svg
159
+ xmlns="http://www.w3.org/2000/svg"
160
+ width="16"
161
+ height="16"
162
+ viewBox="0 0 24 24"
163
+ >
164
+ <!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
165
+ <g
166
+ fill="none"
167
+ stroke="currentColor"
168
+ stroke-linecap="round"
169
+ stroke-linejoin="round"
170
+ stroke-width="1.5"
171
+ >
172
+ <path
173
+ d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93c.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204s.505.71.93.78l.893.15c.543.09.94.559.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.894.149c-.424.07-.764.383-.929.78c-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107c-.398.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93c-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.272-.806.108-1.204s-.506-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78c.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107s.71-.505.78-.929z"
174
+ />
175
+ <path d="M15 12a3 3 0 1 1-6 0a3 3 0 0 1 6 0" />
176
+ </g>
177
+ </svg>`
178
+
179
+ export const clipboard = html`
180
+ <svg
181
+ xmlns="http://www.w3.org/2000/svg"
182
+ width="16"
183
+ height="16"
184
+ viewBox="0 0 24 24"
185
+ >
186
+ <!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
187
+ <path
188
+ fill="none"
189
+ stroke="currentColor"
190
+ stroke-linecap="round"
191
+ stroke-linejoin="round"
192
+ stroke-width="1.5"
193
+ d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0q.083.292.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0q.002-.32.084-.612m7.332 0q.969.073 1.927.184c1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48 48 0 0 1 1.927-.184"
194
+ />
195
+ </svg>
196
+ `
@@ -26,5 +26,5 @@ export default () => {
26
26
  </article>
27
27
  </div>`
28
28
 
29
- return html`${!structure.length ? Discamer : ''}`
29
+ return html`${!structure ? Discamer : ''}`
30
30
  }
@@ -1,7 +1,7 @@
1
- import adminPanel from './AdminPanel/AdminPanel.ts'
1
+ import adminPanel from './AdminPanel.ts'
2
2
  import { html } from 'hono/html'
3
3
  import { type HtmlEscapedString } from 'hono/utils/html'
4
- import { rootdir } from '../index.ts'
4
+ import { rootdir, studioConfig } from '../index.ts'
5
5
  import { type Structure } from '../types.ts'
6
6
 
7
7
  export default (props: {
@@ -18,7 +18,10 @@ export default (props: {
18
18
  <head>
19
19
  <meta charset="UTF-8" />
20
20
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
21
- <title>Alster CMS</title>
21
+ <title>
22
+ ${studioConfig.siteName ? studioConfig.siteName + ' | ' : ''}Alstar
23
+ Studio
24
+ </title>
22
25
 
23
26
  <link
24
27
  rel="icon"
@@ -47,9 +50,9 @@ export default (props: {
47
50
  </head>
48
51
 
49
52
  <body data-barba="wrapper">
50
- <section>${adminPanel(props.structure)}</section>
53
+ <section style="margin-bottom: 0;">${adminPanel(props.structure)}</section>
51
54
 
52
- <main style="height: 100vh; overflow: auto">
55
+ <main>
53
56
  <section data-barba="container" data-barba-namespace="default">
54
57
  ${props.content}
55
58
  </section>
package/index.ts CHANGED
@@ -1,52 +1,71 @@
1
1
  import { Hono } from 'hono'
2
2
  import { loadDb } from '@alstar/db'
3
- import { query } from './queries/index.ts'
4
- import { sectionRoutes } from './api/index.ts'
5
- import { getConfig } from './utils/get-config.ts'
6
- import * as types from './types.ts'
3
+ import { serve } from '@hono/node-server'
4
+ import { serveStatic } from '@hono/node-server/serve-static'
5
+ import { createRefresher } from '@alstar/refresher'
7
6
 
8
7
  import Layout from './components/layout.ts'
9
8
  import IndexPage from './components/index.ts'
9
+ import SettingsPage from './components/Settings.ts'
10
10
  import Entry from './components/Entry.ts'
11
11
 
12
- import { serve } from '@hono/node-server'
13
- import { serveStatic } from '@hono/node-server/serve-static'
12
+ import * as types from './types.ts'
14
13
  import { createStudioTables } from './utils/create-studio-tables.ts'
15
14
  import { fileBasedRouter } from './utils/file-based-router.ts'
16
-
17
- export { html, type HtmlEscapedString } from './utils/html.ts'
15
+ import { getConfig } from './utils/get-config.ts'
16
+ import startupLog from './utils/startup-log.ts'
17
+ import { api } from './api/index.ts'
18
+ import mcp from './api/mcp.ts'
18
19
 
19
20
  export let structure: types.Structure
20
21
  export let rootdir = '/node_modules/@alstar/studio'
21
22
 
22
- const createStudio = async (studioStructure?: types.Structure) => {
23
- const config = await getConfig<types.StudioConfig>()
23
+ export let studioConfig: types.StudioConfig = {
24
+ siteName: '',
25
+ structure: {}
26
+ }
27
+
28
+ const createStudio = async (config: types.StudioConfig) => {
29
+ startupLog()
24
30
 
25
- structure = studioStructure || []
31
+ createRefresher({ rootdir: '.' })
26
32
 
27
33
  loadDb('./studio.db')
28
-
29
34
  createStudioTables()
30
35
 
31
- const app = new Hono(config.honoConfig)
36
+ // const configFile = await getConfig<types.StudioConfig>()
32
37
 
33
- app.use('*', serveStatic({ root: './' }))
34
- app.use('*', serveStatic({ root: './public' }))
38
+ if (config.structure) {
39
+ structure = config.structure
40
+ }
35
41
 
36
- app.route('/', await fileBasedRouter('./pages'))
42
+ studioConfig = { ...studioConfig, ...config }
37
43
 
38
- app.route('/admin/api', sectionRoutes(structure))
44
+ const app = new Hono(studioConfig.honoConfig)
45
+
46
+ app.use('*', serveStatic({ root: './' }))
47
+ app.use('*', serveStatic({ root: './public' }))
39
48
 
40
49
  app.get('/admin', (c) => c.html(Layout({ structure, content: IndexPage() })))
50
+ app.get('/admin/settings', (c) => c.html(Layout({ structure, content: SettingsPage() })))
41
51
  app.get('/admin/entry/:id', (c) => {
42
52
  return c.html(
43
53
  Layout({
44
54
  structure,
45
- content: Entry({ structure, entryId: c.req.param('id') }),
55
+ content: Entry({ entryId: parseInt(c.req.param('id')) }),
46
56
  }),
47
57
  )
48
58
  })
49
59
 
60
+ app.route('/admin/api', api(structure))
61
+ app.route('/admin/mcp', mcp())
62
+
63
+ const pages = await fileBasedRouter('./pages')
64
+
65
+ if (pages) {
66
+ app.route('/', pages)
67
+ }
68
+
50
69
  const server = serve(app)
51
70
 
52
71
  // graceful shutdown
@@ -67,5 +86,14 @@ const createStudio = async (studioStructure?: types.Structure) => {
67
86
  return app
68
87
  }
69
88
 
70
- export { defineConfig, defineEntry, defineStructure } from './utils/define.ts'
71
- export { createStudio, query }
89
+ export {
90
+ defineConfig,
91
+ defineEntry,
92
+ defineStructure,
93
+ defineBlock,
94
+ defineField,
95
+ } from './utils/define.ts'
96
+ export { type RequestContext } from './types.ts'
97
+ export { createStudio }
98
+ export { html, type HtmlEscapedString } from './utils/html.ts'
99
+ export { query } from './queries/index.ts'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alstar/studio",
3
- "version": "0.0.0-beta.4",
3
+ "version": "0.0.0-beta.6",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "dependencies": {
@@ -8,6 +8,7 @@
8
8
  "@starfederation/datastar-sdk": "1.0.0-RC.1",
9
9
  "hono": "^4.8.12",
10
10
  "@alstar/db": "0.0.0-beta.1",
11
+ "@alstar/refresher": "0.0.0-beta.2",
11
12
  "@alstar/ui": "0.0.0-beta.1"
12
13
  },
13
14
  "devDependencies": {
@@ -0,0 +1,90 @@
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
+
34
+ #entries {
35
+ width: 100%;
36
+
37
+ ul {
38
+ padding: 0;
39
+ margin-inline: -1rem;
40
+
41
+ form {
42
+ display: flex;
43
+ padding-bottom: 0;
44
+
45
+ button {
46
+ margin: 0;
47
+ }
48
+ }
49
+
50
+ > li {
51
+ margin-bottom: 0px;
52
+ border-radius: 8px;
53
+ display: flex;
54
+ justify-content: space-between;
55
+ align-items: stretch;
56
+ list-style: none;
57
+
58
+ a {
59
+ text-decoration: none;
60
+ width: 100%;
61
+ padding: 0.5rem 1rem;
62
+ }
63
+
64
+ button {
65
+ border-radius: 7px;
66
+ opacity: 0;
67
+ transition: opacity 100px;
68
+
69
+ svg {
70
+ margin: 0.5rem 1rem;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ > footer {
78
+ margin-top: auto;
79
+ }
80
+ }
81
+
82
+ #entries ul {
83
+ > li:hover {
84
+ background-color: var(--pico-form-element-background-color);
85
+
86
+ button {
87
+ opacity: 1;
88
+ }
89
+ }
90
+ }
@@ -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
+ }