@stacksjs/defaults 0.70.159 → 0.70.161
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.
- package/app/Actions/Buddy/CommandsAction.ts +1 -10
- package/app/Actions/Dashboard/DashboardHomeAction.test.ts +19 -0
- package/app/Actions/Dashboard/DashboardHomeAction.ts +49 -12
- package/app/Models/Tag.ts +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/CodeEditor.stx +2 -2
- package/resources/components/Dashboard/FileViewer/MediaCard.stx +1 -1
- package/resources/components/Dashboard/FileViewer/MediaInfoPanel.stx +2 -2
- package/resources/components/Dashboard/Settings/Forms/LibraryForm.stx +5 -5
- package/resources/components/Dashboard/Settings/SettingsFormManager.stx +21 -170
- package/resources/components/Dashboard/UI/RouterLink.stx +2 -2
- package/resources/components/Docs/Demo/ComboboxCode.md +1 -1
- package/resources/components/Docs/Demo/DialogCode.md +2 -2
- package/resources/components/Docs/Demo/DropdownCode.md +1 -1
- package/resources/components/Docs/Demo/ListboxCode.md +1 -1
- package/resources/components/Docs/Demo/NotificationCode.md +1 -1
- package/resources/components/Docs/Demo/PopoverCode.md +1 -1
- package/resources/components/Docs/Demo/RadioGroupCode.md +1 -1
- package/resources/components/Docs/Demo/StepperCode.md +1 -1
- package/resources/components/Docs/Demo/SwitchCode.md +1 -1
- package/resources/components/HelloWorld.stx +1 -1
- package/resources/components/README.md +9 -79
- package/resources/components/index.ts +0 -5
- package/resources/functions/dashboard/library.ts +2 -2
- package/resources/functions/dashboard/sidebar.ts +3 -3
- package/resources/functions/system-tray.ts +81 -0
- package/resources/layouts/README.md +12 -9
- package/resources/views/index.stx +1 -7
|
@@ -270,7 +270,7 @@ export default new Action({
|
|
|
270
270
|
},
|
|
271
271
|
{
|
|
272
272
|
signature: 'buddy build:components',
|
|
273
|
-
description: 'Builds
|
|
273
|
+
description: 'Builds the STX component library and Web Component library',
|
|
274
274
|
notes: '',
|
|
275
275
|
synopsis: 'buddy build:components',
|
|
276
276
|
aliases: [],
|
|
@@ -607,15 +607,6 @@ export default new Action({
|
|
|
607
607
|
arguments: [],
|
|
608
608
|
options: [],
|
|
609
609
|
},
|
|
610
|
-
{
|
|
611
|
-
signature: 'buddy example:vue',
|
|
612
|
-
description: 'Runs the Vue example',
|
|
613
|
-
notes: '',
|
|
614
|
-
synopsis: 'buddy example:vue',
|
|
615
|
-
aliases: [],
|
|
616
|
-
arguments: [],
|
|
617
|
-
options: [],
|
|
618
|
-
},
|
|
619
610
|
{
|
|
620
611
|
signature: 'buddy example:web-components',
|
|
621
612
|
description: 'Runs the Web Component example',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { summarizeHttpRequests } from './DashboardHomeAction'
|
|
3
|
+
|
|
4
|
+
describe('dashboard HTTP metrics', () => {
|
|
5
|
+
it('summarizes captured requests without fake values', () => {
|
|
6
|
+
const metrics = summarizeHttpRequests(12_345, [
|
|
7
|
+
{ duration: 10, status: 200 },
|
|
8
|
+
{ duration: 20, status: 302 },
|
|
9
|
+
{ duration: 30, status: 404 },
|
|
10
|
+
{ duration: 40, status: 500 },
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
expect(metrics.map(metric => metric.value)).toEqual(['12,345', '25ms', '50.0%', '50.0%'])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns explicit empty-state metrics', () => {
|
|
17
|
+
expect(summarizeHttpRequests(0, []).map(metric => metric.value)).toEqual(['0', '0ms', '100%', '0%'])
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
2
|
+
import { Order, Post, Product, Request, User } from '@stacksjs/orm'
|
|
3
|
+
|
|
4
|
+
interface HttpRequestSample {
|
|
5
|
+
duration: number
|
|
6
|
+
status: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function summarizeHttpRequests(total: number, requests: HttpRequestSample[]) {
|
|
10
|
+
const successful = requests.filter(request => request.status >= 200 && request.status < 400).length
|
|
11
|
+
const failed = requests.filter(request => request.status >= 400).length
|
|
12
|
+
const averageDuration = requests.length > 0
|
|
13
|
+
? Math.round(requests.reduce((sum, request) => sum + request.duration, 0) / requests.length)
|
|
14
|
+
: 0
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
{ title: 'HTTP Requests', value: total.toLocaleString(), detail: 'All captured requests', icon: 'i-hugeicons-global' },
|
|
18
|
+
{ title: 'Average Response', value: `${averageDuration}ms`, detail: `Latest ${requests.length.toLocaleString()} requests`, icon: 'i-hugeicons-clock-01' },
|
|
19
|
+
{ title: 'Success Rate', value: requests.length > 0 ? `${((successful / requests.length) * 100).toFixed(1)}%` : '100%', detail: '2xx and 3xx responses', icon: 'i-hugeicons-checkmark-circle-02' },
|
|
20
|
+
{ title: 'Error Rate', value: requests.length > 0 ? `${((failed / requests.length) * 100).toFixed(1)}%` : '0%', detail: '4xx and 5xx responses', icon: 'i-hugeicons-alert-02' },
|
|
21
|
+
]
|
|
22
|
+
}
|
|
3
23
|
|
|
4
24
|
export default new Action({
|
|
5
25
|
name: 'DashboardHomeAction',
|
|
@@ -14,6 +34,8 @@ export default new Action({
|
|
|
14
34
|
let totalRevenue = 0
|
|
15
35
|
let recentOrderRows: any[] = []
|
|
16
36
|
let recentUserRows: any[] = []
|
|
37
|
+
let databaseStatus = 'offline'
|
|
38
|
+
let httpMetrics = summarizeHttpRequests(0, [])
|
|
17
39
|
|
|
18
40
|
try {
|
|
19
41
|
const results = await Promise.all([
|
|
@@ -35,16 +57,31 @@ export default new Action({
|
|
|
35
57
|
// Calculate total revenue from all orders
|
|
36
58
|
const allOrders = await Order.all()
|
|
37
59
|
totalRevenue = allOrders.reduce((sum, o) => sum + (Number(o.get('total_amount')) || 0), 0)
|
|
60
|
+
databaseStatus = 'healthy'
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// A new project may not have run its migrations yet.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const [requestCount, recentRequests] = await Promise.all([
|
|
68
|
+
Request.count(),
|
|
69
|
+
Request.orderBy('created_at', 'desc').limit(1000).get(),
|
|
70
|
+
])
|
|
71
|
+
httpMetrics = summarizeHttpRequests(requestCount, recentRequests.map(request => ({
|
|
72
|
+
duration: Number(request.get('duration_ms')) || 0,
|
|
73
|
+
status: Number(request.get('status_code')) || 500,
|
|
74
|
+
})))
|
|
38
75
|
}
|
|
39
76
|
catch {
|
|
40
|
-
//
|
|
77
|
+
// Request capture is optional, so its empty state is valid.
|
|
41
78
|
}
|
|
42
79
|
|
|
43
80
|
const stats = [
|
|
44
|
-
{ label: 'Total Users', value: String(userCount),
|
|
45
|
-
{ label: 'Products', value: String(productCount),
|
|
46
|
-
{ label: 'Revenue', value: `$${totalRevenue.toLocaleString()}`,
|
|
47
|
-
{ label: 'Orders', value: String(orderCount),
|
|
81
|
+
{ label: 'Total Users', value: String(userCount), color: 'blue' },
|
|
82
|
+
{ label: 'Products', value: String(productCount), color: 'green' },
|
|
83
|
+
{ label: 'Revenue', value: `$${totalRevenue.toLocaleString()}`, color: 'orange' },
|
|
84
|
+
{ label: 'Orders', value: String(orderCount), color: 'red' },
|
|
48
85
|
]
|
|
49
86
|
|
|
50
87
|
const quickLinks = [
|
|
@@ -55,11 +92,11 @@ export default new Action({
|
|
|
55
92
|
]
|
|
56
93
|
|
|
57
94
|
const services = [
|
|
58
|
-
{ name: 'Database', status:
|
|
59
|
-
{ name: 'Cache', status: '
|
|
60
|
-
{ name: 'Queue', status: '
|
|
61
|
-
{ name: 'Storage', status: '
|
|
62
|
-
{ name: 'Email', status: '
|
|
95
|
+
{ name: 'Database', status: databaseStatus, latency: 'N/A' },
|
|
96
|
+
{ name: 'Cache', status: 'configured', latency: 'N/A' },
|
|
97
|
+
{ name: 'Queue', status: 'configured', latency: 'N/A' },
|
|
98
|
+
{ name: 'Storage', status: 'configured', latency: 'N/A' },
|
|
99
|
+
{ name: 'Email', status: 'configured', latency: 'N/A' },
|
|
63
100
|
]
|
|
64
101
|
|
|
65
102
|
// Build activities from real data
|
|
@@ -78,6 +115,6 @@ export default new Action({
|
|
|
78
115
|
})),
|
|
79
116
|
].slice(0, 5)
|
|
80
117
|
|
|
81
|
-
return { stats, quickLinks, services, activities }
|
|
118
|
+
return { stats, httpMetrics, quickLinks, services, activities }
|
|
82
119
|
},
|
|
83
120
|
})
|
package/app/Models/Tag.ts
CHANGED
|
@@ -32,7 +32,7 @@ export default defineModel({
|
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
factory: faker => faker.helpers.arrayElement([
|
|
35
|
-
'javascript', 'typescript', '
|
|
35
|
+
'javascript', 'typescript', 'stx', 'react', 'nodejs',
|
|
36
36
|
'database', 'performance', 'security', 'api', 'testing',
|
|
37
37
|
'devops', 'cloud', 'frontend', 'backend', 'mobile',
|
|
38
38
|
]),
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.161",
|
|
6
6
|
"description": "Default Stacks scaffold resources (views, layouts, components, preloader) — shipped so apps consuming the framework from node_modules get the same fallback resources a vendored checkout provides. Source of truth: storage/framework/defaults.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
|
@@ -25,7 +25,7 @@ cp`
|
|
|
25
25
|
@endif
|
|
26
26
|
</header>
|
|
27
27
|
|
|
28
|
-
<div class="bg-[#0d1117] rounded-b-lg
|
|
28
|
+
<div class="bg-[#0d1117] rounded-b-lg stx-code-editor" style="height: 300px; font-size: 14px;">
|
|
29
29
|
<pre class="p-4 font-mono text-[#c9d1d9] text-sm">{{ content }}</pre>
|
|
30
30
|
</div>
|
|
31
31
|
<style>
|
|
@@ -33,7 +33,7 @@ select {
|
|
|
33
33
|
margin-right: 15px;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
.
|
|
36
|
+
.stx-code-editor {
|
|
37
37
|
font-size: 14px;
|
|
38
38
|
flex: 1;
|
|
39
39
|
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
MediaCard — Meema-style visual file preview
|
|
3
3
|
|
|
4
|
-
Ported from meemalabs/file-manager
|
|
4
|
+
Ported from the meemalabs/file-manager media card components.
|
|
5
5
|
to stx + Crosswind. Presentational: it renders the preview + label for one
|
|
6
6
|
file item and derives the media "kind" from `mime_type` (preferred) or the
|
|
7
7
|
file extension in `type`. Real thumbnails render when the item carries a
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
MediaInfoPanel — file preview + metadata
|
|
3
3
|
|
|
4
|
-
Ported from meemalabs/file-manager
|
|
5
|
-
|
|
4
|
+
Ported from the meemalabs/file-manager file preview, metadata, and sharing
|
|
5
|
+
sections to STX and Crosswind. Presentational: it
|
|
6
6
|
renders a large preview and a metadata list for one selected file. The parent
|
|
7
7
|
owns the slide-over shell (visibility, positioning, close button) and passes
|
|
8
8
|
the current item in.
|
|
@@ -189,10 +189,10 @@ const { libraryName = 'hello-world', libraryOwner = '@stacksjs', libraryReposito
|
|
|
189
189
|
<div class="grid gap-x-8 gap-y-10 grid-cols-1 md:grid-cols-3 py-16 max-w-7xl">
|
|
190
190
|
<div>
|
|
191
191
|
<h2 class="font-semibold leading-7 text-base text-gray-900 dark:text-neutral-100">
|
|
192
|
-
|
|
192
|
+
STX Component Configuration
|
|
193
193
|
</h2>
|
|
194
194
|
<p class="mt-1 leading-6 text-gray-600 text-sm">
|
|
195
|
-
Update your
|
|
195
|
+
Update your STX component package
|
|
196
196
|
</p>
|
|
197
197
|
</div>
|
|
198
198
|
|
|
@@ -223,15 +223,15 @@ const { libraryName = 'hello-world', libraryOwner = '@stacksjs', libraryReposito
|
|
|
223
223
|
<div class="flex justify-between mb-4">
|
|
224
224
|
<div>
|
|
225
225
|
<h2 class="font-semibold leading-7 text-base text-gray-900 dark:text-neutral-100">
|
|
226
|
-
|
|
226
|
+
STX Component Tags
|
|
227
227
|
</h2>
|
|
228
228
|
<p class="mt-1 leading-6 text-gray-600 text-sm">
|
|
229
|
-
Update
|
|
229
|
+
Update STX component tags or add a new custom element tag
|
|
230
230
|
</p>
|
|
231
231
|
</div>
|
|
232
232
|
|
|
233
233
|
<div class="sm:flex-none mt-4 sm:ml-16 sm:mt-0">
|
|
234
|
-
<button type="button" class="block px-3 py-2 font-semibold text-center text-sm text-white bg-gray-600 hover:bg-indigo-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-600 focus-visible:outline-offset-2 shadow-sm" data-action="add-
|
|
234
|
+
<button type="button" class="block px-3 py-2 font-semibold text-center text-sm text-white bg-gray-600 hover:bg-indigo-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-600 focus-visible:outline-offset-2 shadow-sm" data-action="add-stx-tag">
|
|
235
235
|
Add Tag
|
|
236
236
|
</button>
|
|
237
237
|
</div>
|
|
@@ -5,177 +5,28 @@ interface SettingsFormManagerProps {
|
|
|
5
5
|
|
|
6
6
|
const { name = '' } = defineProps<SettingsFormManagerProps>()
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import DatabaseForm from './Forms/DatabaseForm.vue'
|
|
15
|
-
import DNSForm from './Forms/DNSForm.vue'
|
|
16
|
-
import EmailForm from './Forms/EmailForm.vue'
|
|
17
|
-
import FileSystemForm from './Forms/FileSystemForm.vue'
|
|
18
|
-
import HashingForm from './Forms/HashingForm.vue'
|
|
19
|
-
import LibraryForm from './Forms/LibraryForm.vue'
|
|
20
|
-
import QueueForm from './Forms/QueueForm.vue'
|
|
21
|
-
import SearchEngineForm from './Forms/SearchEngineForm.vue'
|
|
22
|
-
import SecurityForm from './Forms/SecurityForm.vue'
|
|
23
|
-
import ServicesForm from './Forms/ServicesForm.vue'
|
|
24
|
-
import StorageForm from './Forms/StorageForm.vue'
|
|
25
|
-
import TeamsForm from './Forms/TeamsForm.vue'
|
|
26
|
-
|
|
27
|
-
const options = [
|
|
28
|
-
{
|
|
29
|
-
key: 'ai',
|
|
30
|
-
label: 'AI',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
key: 'analytics',
|
|
34
|
-
label: 'Analytics',
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
key: 'app',
|
|
38
|
-
label: 'App',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
key: 'cache',
|
|
42
|
-
label: 'Cache',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
key: 'cli',
|
|
46
|
-
label: 'CLI',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
key: 'cloud',
|
|
50
|
-
label: 'Cloud',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
key: 'database',
|
|
54
|
-
label: 'Database',
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
key: 'dns',
|
|
58
|
-
label: 'DNS',
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
key: 'email',
|
|
62
|
-
label: 'Email',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
key: 'file-system',
|
|
66
|
-
label: 'File System',
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
key: 'hashing',
|
|
70
|
-
label: 'Hashing',
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
key: 'languages',
|
|
74
|
-
label: 'Languages',
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
key: 'library',
|
|
78
|
-
label: 'Library',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
key: 'native',
|
|
82
|
-
label: 'Native',
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
key: 'mail',
|
|
86
|
-
label: 'Mail',
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
key: 'queue',
|
|
90
|
-
label: 'Queue',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
key: 'search-engine',
|
|
94
|
-
label: 'Search Engine',
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
key: 'security',
|
|
98
|
-
label: 'Security',
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
key: 'services',
|
|
102
|
-
label: 'Services',
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
key: 'storage',
|
|
106
|
-
label: 'Storage',
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
key: 'team',
|
|
110
|
-
label: 'Team',
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
key: 'ui',
|
|
114
|
-
label: 'UI',
|
|
115
|
-
},
|
|
116
|
-
]
|
|
117
|
-
function pageTitle() { return options.find(option => option.key === name }?.label || name)
|
|
8
|
+
function title(): string {
|
|
9
|
+
return name
|
|
10
|
+
.split(/[-_]/)
|
|
11
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
12
|
+
.join(' ')
|
|
13
|
+
}
|
|
118
14
|
</script>
|
|
119
|
-
<main>
|
|
120
|
-
<div class="md:col-span-2 px-4 py-6 sm:px-6 lg:px-8 bg-white ring-1 ring-gray-900/5 sm:rounded-xl shadow-sm">
|
|
121
|
-
<h2 class="font-semibold leading-7 text-base text-gray-900">
|
|
122
|
-
{{ pageTitle }} Configuration
|
|
123
|
-
</h2>
|
|
124
|
-
<p class="mt-1 leading-6 text-gray-600 text-sm">
|
|
125
|
-
This configuration defines all of your {{ pageTitle }} options.
|
|
126
|
-
</p>
|
|
127
15
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
<DatabaseForm />
|
|
143
|
-
@elseif(name === 'dns')
|
|
144
|
-
<DNSForm />
|
|
145
|
-
@elseif(name === 'email')
|
|
146
|
-
<EmailForm />
|
|
147
|
-
@elseif(name === 'file-system')
|
|
148
|
-
<FileSystemForm />
|
|
149
|
-
@elseif(name === 'hashing')
|
|
150
|
-
<HashingForm />
|
|
151
|
-
@elseif(name === 'library')
|
|
152
|
-
<LibraryForm />
|
|
153
|
-
@elseif(name === 'queue')
|
|
154
|
-
<QueueForm />
|
|
155
|
-
@elseif(name === 'search-engine')
|
|
156
|
-
<SearchEngineForm />
|
|
157
|
-
@elseif(name === 'security')
|
|
158
|
-
<SecurityForm />
|
|
159
|
-
@elseif(name === 'services')
|
|
160
|
-
<ServicesForm />
|
|
161
|
-
@elseif(name === 'storage')
|
|
162
|
-
<StorageForm />
|
|
163
|
-
@elseif(name === 'team')
|
|
164
|
-
<TeamsForm />
|
|
165
|
-
@else
|
|
166
|
-
<div class="text-center">
|
|
167
|
-
<div class="h-12 w-12 text-gray-400 dark:text-neutral-200 duration-150 ease-in-out transition-all i-hugeicons-cog-8-tooth" />
|
|
168
|
-
<h3 class="mt-2 font-semibold text-gray-900 text-sm">
|
|
169
|
-
Settings <span class="font-bold">{{ pageTitle }}</span> Form not created yet
|
|
170
|
-
</h3>
|
|
171
|
-
<p class="mt-1 text-gray-500 text-sm">
|
|
172
|
-
Please ask the maintenance team to fix.
|
|
173
|
-
</p>
|
|
174
|
-
</div>
|
|
175
|
-
@endif
|
|
16
|
+
<Card variant="default" padding="lg">
|
|
17
|
+
<div class="flex gap-4 items-start">
|
|
18
|
+
<div class="flex flex-none items-center justify-center h-10 w-10 text-blue-600 dark:text-blue-400 bg-blue-500/10 rounded-lg">
|
|
19
|
+
<span class="h-5 w-5 i-hugeicons-settings-02"></span>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="flex-1 min-w-0">
|
|
22
|
+
<h2 class="font-semibold text-neutral-900 dark:text-white">{{ title() }} configuration</h2>
|
|
23
|
+
<p class="mt-1 text-neutral-500 text-sm dark:text-neutral-400">
|
|
24
|
+
Configuration editing now lives in the unified Settings workspace.
|
|
25
|
+
</p>
|
|
26
|
+
<a href="/settings" class="inline-flex gap-2 items-center mt-4 font-medium text-blue-600 text-sm dark:text-blue-400 hover:underline">
|
|
27
|
+
Open Settings
|
|
28
|
+
<span class="h-4 w-4 i-hugeicons-arrow-right-02"></span>
|
|
29
|
+
</a>
|
|
176
30
|
</div>
|
|
177
31
|
</div>
|
|
178
|
-
</
|
|
179
|
-
<style scoped>
|
|
180
|
-
|
|
181
|
-
</style>
|
|
32
|
+
</Card>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script server>
|
|
2
2
|
/**
|
|
3
|
-
* RouterLink
|
|
3
|
+
* RouterLink bridge component for templates that use declarative
|
|
4
4
|
* `<RouterLink to="…">` syntax. The dashboard is server-rendered MPA, so
|
|
5
5
|
* there's no actual client router; this just maps the `to` prop to a
|
|
6
6
|
* standard `href` and forwards the rest. Keeps existing pages from
|
|
7
7
|
* breaking with `[Error loading component: 'router-link']` while we
|
|
8
8
|
* migrate them off the syntax.
|
|
9
9
|
*
|
|
10
|
-
* `replace` is accepted for
|
|
10
|
+
* `replace` is accepted for API compatibility; it has no
|
|
11
11
|
* effect under MPA navigation but makes drop-in usage error-free.
|
|
12
12
|
*/
|
|
13
13
|
interface RouterLinkProps {
|
|
@@ -69,7 +69,7 @@ console.log('HelloWorld component mounted & the greeting is', greeting)
|
|
|
69
69
|
<div class="ml-12">
|
|
70
70
|
<div class="mt-2 leading-6 text-gray-600 text-sm dark:text-neutral-400">
|
|
71
71
|
The easiest way to build component libraries and distribute them on npm. Stacks automatically imports all of your components & makes them available to you in your templates.
|
|
72
|
-
And it automatically builds framework agnostic (e.g. Web
|
|
72
|
+
And it automatically builds framework agnostic (e.g. Web and STX components) component libraries out of those.
|
|
73
73
|
</div>
|
|
74
74
|
</div>
|
|
75
75
|
</div>
|
|
@@ -1,87 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Components
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create reusable STX components in this directory. Components are discovered by
|
|
4
|
+
the STX build pipeline and can be used directly from pages and other templates.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## 💡 Get Started
|
|
9
|
-
|
|
10
|
-
It's easy to get started. The only prerequisite is a basic understanding of HTML, sprinkled with *minimal* JavaScript. In other words, there is virtually no learning curve.
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
# you may use this GitHub template or the following command
|
|
14
|
-
bunx stacks new hello-world
|
|
15
|
-
cd hello-world
|
|
16
|
-
|
|
17
|
-
buddy install # install deps for all packages
|
|
18
|
-
buddy dev:components # starts the component dev server
|
|
19
|
-
buddy build:components # builds the component library for production-ready use
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Additionally, the `package.json` contains some useful snippets you likely want to be aware of.
|
|
23
|
-
|
|
24
|
-
## 🤖 Usage
|
|
25
|
-
|
|
26
|
-
Because this project is optimized toward the development of easily reusable & composable component libraries, it’s very easy to use (and distribute):
|
|
27
|
-
|
|
28
|
-
```vue
|
|
29
|
-
<script setup lang="ts">
|
|
30
|
-
import HelloWorld from 'hello-world-stack'
|
|
6
|
+
```html
|
|
7
|
+
<script server lang="ts">
|
|
8
|
+
const props = defineProps<{ name: string }>()
|
|
31
9
|
</script>
|
|
32
10
|
|
|
33
11
|
<template>
|
|
34
|
-
<
|
|
12
|
+
<p>Hello, {{ props.name }}.</p>
|
|
35
13
|
</template>
|
|
36
14
|
```
|
|
37
15
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
This project also includes a simple way to handle your versioning. Through semantic commit names, it will also generate the two changelogs: one as part of the GitHub releases & the one markdown file that's stored within the root of the project.
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
# how to create a git commit
|
|
44
|
-
git add . # select the changes you want to commit
|
|
45
|
-
buddy commit # then simply follow the prompts
|
|
46
|
-
|
|
47
|
-
# after you successfully committed, you may create a "release"
|
|
48
|
-
buddy release # automates git commits, versioning, and CHANGELOG generation
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Read more about these tips in the docs.
|
|
52
|
-
|
|
53
|
-
### Coding Style
|
|
54
|
-
|
|
55
|
-
You may want to consider using the "Composition API" with [`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227), as it is perfectly suited for building component libraries via Stacks. [ESLint](https://github.com/eslint) also comes pre-configured to statically analyze, fix and format your code without the need for Prettier.
|
|
56
|
-
|
|
57
|
-
When using this template, feel free to adjust it to your needs. It is a framework to help you quickly & efficiently bootstrap and design component libraries using industry best practices.
|
|
58
|
-
|
|
59
|
-
## 🧪 Testing
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
bun test
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## 📈 Changelog
|
|
66
|
-
|
|
67
|
-
Please see our [releases](https://github.com/stacksjs/stacks/releases) page for more information on what has changed recently.
|
|
68
|
-
|
|
69
|
-
## 🚜 Contributing
|
|
70
|
-
|
|
71
|
-
Please review the [Contributing Guide](https://github.com/stacksjs/contributing) for details.
|
|
72
|
-
|
|
73
|
-
## 🏝 Community
|
|
74
|
-
|
|
75
|
-
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
|
76
|
-
|
|
77
|
-
[Discussions on GitHub](https://github.com/stacksjs/stacks/discussions)
|
|
78
|
-
|
|
79
|
-
For casual chit-chat with others using this package:
|
|
80
|
-
|
|
81
|
-
[Join the Stacks Discord Server](https://discord.gg/stacksjs)
|
|
82
|
-
|
|
83
|
-
## 📄 License
|
|
84
|
-
|
|
85
|
-
The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/stacks/tree/main/LICENSE.md) for more information.
|
|
86
|
-
|
|
87
|
-
Made with 💙
|
|
16
|
+
Run `buddy dev:components` while developing the component library and
|
|
17
|
+
`buddy build:components` to produce its distributable output.
|
|
@@ -4,10 +4,5 @@
|
|
|
4
4
|
* Complete dashboard components for building modern admin interfaces.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// Layout Components
|
|
8
|
-
export { default as DashboardLayout } from './DashboardLayout.vue'
|
|
9
|
-
export { default as SidebarModern } from './SidebarModern.vue'
|
|
10
|
-
export { default as NavbarModern } from './NavbarModern.vue'
|
|
11
|
-
|
|
12
7
|
// UI Components (re-export from UI folder)
|
|
13
8
|
export * from './UI'
|
|
@@ -35,7 +35,7 @@ export interface ComponentEntry {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Scan the dashboard component directory plus any user component
|
|
38
|
-
* directories. Returns one entry per `.stx`
|
|
38
|
+
* directories. Returns one entry per `.stx` file with the file
|
|
39
39
|
* size (so the UI can show "small", "medium", "large" hints).
|
|
40
40
|
*/
|
|
41
41
|
export function listComponents(): ComponentEntry[] {
|
|
@@ -49,7 +49,7 @@ export function listComponents(): ComponentEntry[] {
|
|
|
49
49
|
const abs = resolve(root, dir)
|
|
50
50
|
if (!existsSync(abs)) continue
|
|
51
51
|
walkSync(abs, (file) => {
|
|
52
|
-
if (
|
|
52
|
+
if (!file.endsWith('.stx')) return
|
|
53
53
|
try {
|
|
54
54
|
const stat = statSync(file)
|
|
55
55
|
out.push({
|
|
@@ -525,7 +525,7 @@ export function buildNavSections(
|
|
|
525
525
|
{ to: '/buddy', icon: 'terminal', text: 'Buddy CLI' },
|
|
526
526
|
{ to: '/environment', icon: 'settings', text: 'Environment' },
|
|
527
527
|
{ to: '/access-tokens', icon: 'lock', text: 'Access Tokens' },
|
|
528
|
-
{ to: '/settings
|
|
528
|
+
{ to: '/settings', icon: 'settings', text: 'Settings' },
|
|
529
529
|
{ to: '/settings/mail', icon: 'mail', text: 'Mail Settings' },
|
|
530
530
|
]])
|
|
531
531
|
}
|
|
@@ -652,7 +652,7 @@ export function buildWebSidebarSections(): WebSidebarSection[] {
|
|
|
652
652
|
{
|
|
653
653
|
id: 'system',
|
|
654
654
|
label: '',
|
|
655
|
-
items: [{ id: 'settings', label: 'Settings', icon: NAV_ICON_CLASSES.settings, iconColor: 'blue', href: '/settings
|
|
655
|
+
items: [{ id: 'settings', label: 'Settings', icon: NAV_ICON_CLASSES.settings, iconColor: 'blue', href: '/settings' }],
|
|
656
656
|
},
|
|
657
657
|
]
|
|
658
658
|
}
|
|
@@ -671,6 +671,6 @@ export function buildSidebarChunks(): { top: string, nav: string, bottom: string
|
|
|
671
671
|
return {
|
|
672
672
|
top: `<a href="/" class="sidebar-link sidebar-link-home"><span class="sidebar-icon">${svg('home')}</span><span>Home</span></a>`,
|
|
673
673
|
nav: buildSidebarNavHtml(manifest.models, manifest.sections),
|
|
674
|
-
bottom: `<a href="/settings
|
|
674
|
+
bottom: `<a href="/settings" class="sidebar-link"><span class="sidebar-icon">${svg('settings')}</span><span>Settings</span></a>`,
|
|
675
675
|
}
|
|
676
676
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
interface TrayMenuItem {
|
|
2
|
+
label?: string
|
|
3
|
+
type?: 'normal' | 'separator' | 'submenu'
|
|
4
|
+
action?: string
|
|
5
|
+
submenu?: TrayMenuItem[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface CraftTrayBridge {
|
|
9
|
+
setTitle(title: string): Promise<void>
|
|
10
|
+
setTooltip(tooltip: string): Promise<void>
|
|
11
|
+
setMenu(items: TrayMenuItem[]): Promise<void>
|
|
12
|
+
onClickToggleWindow(): () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CraftBridge {
|
|
16
|
+
tray?: CraftTrayBridge
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type TrayActionHandler = (action: string) => void | Promise<void>
|
|
20
|
+
|
|
21
|
+
function bridge(): CraftBridge | undefined {
|
|
22
|
+
return (globalThis as typeof globalThis & { craft?: CraftBridge }).craft
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildTrayMenu(projects: string[]): TrayMenuItem[] {
|
|
26
|
+
const projectItems = projects.map((project) => {
|
|
27
|
+
const name = project.split('/').filter(Boolean).pop() || project
|
|
28
|
+
return {
|
|
29
|
+
label: name,
|
|
30
|
+
type: 'submenu' as const,
|
|
31
|
+
submenu: [
|
|
32
|
+
{ label: 'Open Terminal', action: `open-terminal::${project}` },
|
|
33
|
+
{ label: 'Copy IP Address', action: `copy-ip::${project}` },
|
|
34
|
+
{ label: 'Open Dashboard', action: `open-dashboard::${project}` },
|
|
35
|
+
{ label: 'Buddy Commands', action: `buddy-commands::${project}` },
|
|
36
|
+
{ label: 'Deploy', action: `deploy::${project}` },
|
|
37
|
+
{ type: 'separator' as const },
|
|
38
|
+
{ label: 'Deploy Logs', action: `deploy-logs::${project}` },
|
|
39
|
+
{ label: 'Site Logs', action: `site-logs::${project}` },
|
|
40
|
+
{ label: 'Error Logs', action: `error-logs::${project}` },
|
|
41
|
+
{ type: 'separator' as const },
|
|
42
|
+
{ label: 'Edit .env', action: `edit-env::${project}` },
|
|
43
|
+
{ label: 'Edit DNS', action: `edit-dns::${project}` },
|
|
44
|
+
{ label: 'Edit Email Addresses', action: `edit-email::${project}` },
|
|
45
|
+
{ label: 'Ask Buddy', action: `ask-buddy::${project}` },
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
{ label: 'Refresh', action: 'refresh' },
|
|
52
|
+
{ label: 'Open Terminal', action: 'open-terminal' },
|
|
53
|
+
{ label: 'Environment Check', action: 'env-check' },
|
|
54
|
+
{ label: 'Settings', action: 'settings' },
|
|
55
|
+
{ label: 'Check for Updates', action: 'check-updates' },
|
|
56
|
+
{ type: 'separator' },
|
|
57
|
+
{ label: 'Projects', type: 'submenu', submenu: projectItems },
|
|
58
|
+
{ type: 'separator' },
|
|
59
|
+
{ label: 'Quit', action: 'quit' },
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function installTrayMenu(projects: string[], onAction: TrayActionHandler): Promise<() => void> {
|
|
64
|
+
const craft = bridge()
|
|
65
|
+
if (!craft?.tray)
|
|
66
|
+
return () => {}
|
|
67
|
+
|
|
68
|
+
await craft.tray.setTitle('Stacks')
|
|
69
|
+
await craft.tray.setTooltip('Stacks project shortcuts')
|
|
70
|
+
await craft.tray.setMenu(buildTrayMenu(projects))
|
|
71
|
+
const removeClick = craft.tray.onClickToggleWindow()
|
|
72
|
+
const listener = (event: Event) => {
|
|
73
|
+
const action = (event as CustomEvent<{ action?: string }>).detail?.action
|
|
74
|
+
if (action && action !== 'quit') void onAction(action)
|
|
75
|
+
}
|
|
76
|
+
globalThis.addEventListener('craft:tray:menuAction', listener)
|
|
77
|
+
return () => {
|
|
78
|
+
removeClick()
|
|
79
|
+
globalThis.removeEventListener('craft:tray:menuAction', listener)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
# Layouts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
STX layouts wrap pages with shared navigation, metadata, and structure.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
With [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) and [`vite-plugin-layouts`](https://github.com/stacksjs/vite-plugin-layouts), you can specify the layout in the page's SFCs like this:
|
|
5
|
+
`default.stx` is used when a page does not select another layout. A page can
|
|
6
|
+
choose a named layout with the STX layout directive:
|
|
8
7
|
|
|
9
8
|
```html
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</
|
|
9
|
+
@layout('home')
|
|
10
|
+
|
|
11
|
+
<main>
|
|
12
|
+
<h1>{{ title }}</h1>
|
|
13
|
+
</main>
|
|
14
14
|
```
|
|
15
|
+
|
|
16
|
+
Layouts live in this directory and use the same STX template syntax as pages
|
|
17
|
+
and components.
|
|
@@ -550,12 +550,6 @@ Sponsor Here</div>
|
|
|
550
550
|
<div class="package-desc">WebSocket & real-time features</div>
|
|
551
551
|
</div>
|
|
552
552
|
|
|
553
|
-
<div class="package-item">
|
|
554
|
-
<div class="package-emoji">🔬</div>
|
|
555
|
-
<div class="package-title">X-Ray</div>
|
|
556
|
-
<div class="package-desc">Debugging & monitoring tools</div>
|
|
557
|
-
</div>
|
|
558
|
-
|
|
559
553
|
<div class="package-item">
|
|
560
554
|
<div class="package-emoji">💳</div>
|
|
561
555
|
<div class="package-title">Payments</div>
|
|
@@ -613,7 +607,7 @@ Sponsor Here</div>
|
|
|
613
607
|
<div class="folder-item-icon">🏗️</div>
|
|
614
608
|
<div class="folder-item-content">
|
|
615
609
|
<div class="folder-item-name">stx</div>
|
|
616
|
-
<div class="folder-item-desc">A fast & powerful UI framework. Templating inspired by Laravel Blade
|
|
610
|
+
<div class="folder-item-desc">A fast & powerful UI framework. Templating inspired by Laravel Blade and modern component systems.</div>
|
|
617
611
|
</div>
|
|
618
612
|
</a>
|
|
619
613
|
</div>
|