adonis-atlas 0.1.0
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/LICENSE +21 -0
- package/README.md +150 -0
- package/build/chunk-7QVYU63E.js +7 -0
- package/build/chunk-7QVYU63E.js.map +1 -0
- package/build/client/app.js +20 -0
- package/build/client/app.js.map +1 -0
- package/build/client/boot.js +23 -0
- package/build/client/boot.js.map +1 -0
- package/build/client/resources/components/DataTable.vue +103 -0
- package/build/client/resources/components/FormField.vue +40 -0
- package/build/client/resources/components/Layout.vue +68 -0
- package/build/client/resources/components/Pagination.vue +60 -0
- package/build/client/resources/components/SearchBar.vue +41 -0
- package/build/client/resources/components/fields/BooleanField.vue +26 -0
- package/build/client/resources/components/fields/DateTimeField.vue +26 -0
- package/build/client/resources/components/fields/EmailField.vue +27 -0
- package/build/client/resources/components/fields/NumberField.vue +27 -0
- package/build/client/resources/components/fields/PasswordField.vue +28 -0
- package/build/client/resources/components/fields/TextField.vue +27 -0
- package/build/client/resources/css/atlas.css +662 -0
- package/build/client/resources/pages/atlas/Create.vue +132 -0
- package/build/client/resources/pages/atlas/Edit.vue +145 -0
- package/build/client/resources/pages/atlas/Index.vue +138 -0
- package/build/commands/main.js +9 -0
- package/build/commands/main.js.map +1 -0
- package/build/commands/make_resource.js +42 -0
- package/build/commands/make_resource.js.map +1 -0
- package/build/configure.js +17 -0
- package/build/configure.js.map +1 -0
- package/build/index.js +29 -0
- package/build/index.js.map +1 -0
- package/build/providers/atlas_provider.js +56 -0
- package/build/providers/atlas_provider.js.map +1 -0
- package/build/stubs/config.stub +18 -0
- package/build/stubs/resource.stub +25 -0
- package/package.json +81 -0
- package/stubs/config.stub +18 -0
- package/stubs/main.ts +3 -0
- package/stubs/resource.stub +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fachri Hawari
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Adonis Atlas
|
|
2
|
+
|
|
3
|
+
A modern, dark-themed Admin Panel for AdonisJS 6. Built with Vue 3, Inertia.js, and Tailwind-free custom CSS.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Zero-Config Frontend**: No boilerplate needed.
|
|
8
|
+
- 🌑 **Dark Mode**: Beautiful dark theme by default.
|
|
9
|
+
- 🛠 **Resource Generator**: Quickly generate CRUD interfaces for your Lucid models.
|
|
10
|
+
- ⚡️ **Powered by Inertia**: SPA-like experience without the complexity.
|
|
11
|
+
- 🔌 **Prefix Agnostic**: Mount it at `/atlas`, `/admin`, or anywhere else.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add adonis-atlas
|
|
17
|
+
pnpm add vue @inertiajs/vue3 # Peer dependencies
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
### 1. Configure Vite (Frontend)
|
|
23
|
+
|
|
24
|
+
Atlas comes with a pre-built frontend entry point so you don't have to write any Vue code.
|
|
25
|
+
Update your `vite.config.ts`:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { defineConfig } from 'vite'
|
|
29
|
+
import adonisjs from '@adonisjs/vite/client'
|
|
30
|
+
|
|
31
|
+
export default defineConfig({
|
|
32
|
+
plugins: [
|
|
33
|
+
adonisjs({
|
|
34
|
+
/**
|
|
35
|
+
* Use the Atlas boot file as your entry point
|
|
36
|
+
*/
|
|
37
|
+
entrypoints: ['adonis-atlas/boot'],
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Reload on Edge template changes
|
|
41
|
+
*/
|
|
42
|
+
reload: ['resources/views/**/*.edge'],
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Register Routes (Backend)
|
|
49
|
+
|
|
50
|
+
By default, Atlas **automatically registers its routes** during the boot process. You don't have to do anything!
|
|
51
|
+
|
|
52
|
+
Default paths:
|
|
53
|
+
- Dashboard: `/atlas/*`
|
|
54
|
+
- API: `/atlas/api/*`
|
|
55
|
+
|
|
56
|
+
#### Customizing the Prefix
|
|
57
|
+
If you want to change the prefix, update your `config/atlas.ts` (created during `node ace configure adonis-atlas`):
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
export default defineConfig({
|
|
61
|
+
prefix: '/admin',
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Manual Registration (Optional)
|
|
66
|
+
If you need full control (e.g. adding custom middleware), disable auto-mounting in `config/atlas.ts`:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
mountRoutes: false,
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then register them manually in `start/routes.ts`:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { registerAtlasRoutes } from 'adonis-atlas'
|
|
78
|
+
|
|
79
|
+
registerAtlasRoutes('/custom-admin')
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Configure Shield (Security)
|
|
83
|
+
|
|
84
|
+
Since Atlas uses its own API endpoints, you need to exclude them from CSRF protection in `config/shield.ts`.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
export const csrf = {
|
|
88
|
+
enabled: true,
|
|
89
|
+
exceptRoutes: (ctx) => {
|
|
90
|
+
// Exclude Atlas API routes
|
|
91
|
+
return ctx.request.url().includes('/api/')
|
|
92
|
+
},
|
|
93
|
+
enableXsrfCookie: true,
|
|
94
|
+
methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
### defining Resources
|
|
101
|
+
|
|
102
|
+
Create a resource class to define how your model should be displayed.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// app/atlas/resources/user_resource.ts
|
|
106
|
+
import { Resource } from 'adonis-atlas'
|
|
107
|
+
import { TextField, EmailField, DateTimeField } from 'adonis-atlas'
|
|
108
|
+
import User from '#models/user'
|
|
109
|
+
|
|
110
|
+
export default class UserResource extends Resource {
|
|
111
|
+
static model = User
|
|
112
|
+
static title = 'Users'
|
|
113
|
+
|
|
114
|
+
fields() {
|
|
115
|
+
return [
|
|
116
|
+
TextField.make('id').sortable(),
|
|
117
|
+
|
|
118
|
+
TextField.make('fullName')
|
|
119
|
+
.label('Full Name')
|
|
120
|
+
.sortable()
|
|
121
|
+
.rules('required', 'max:255'),
|
|
122
|
+
|
|
123
|
+
EmailField.make('email')
|
|
124
|
+
.sortable()
|
|
125
|
+
.rules('required', 'email'),
|
|
126
|
+
|
|
127
|
+
DateTimeField.make('createdAt')
|
|
128
|
+
.label('Joined')
|
|
129
|
+
.sortable(),
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Registering Resources
|
|
136
|
+
|
|
137
|
+
Register your resources in the Atlas registry (usually in a service provider or preload file).
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import app from '@adonisjs/core/services/app'
|
|
141
|
+
import { ResourceRegistry } from 'adonis-atlas'
|
|
142
|
+
import UserResource from '#app/atlas/resources/user_resource'
|
|
143
|
+
|
|
144
|
+
const registry = await app.container.make('atlas.registry')
|
|
145
|
+
registry.register('user', UserResource)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "../chunk-7QVYU63E.js";
|
|
4
|
+
const atlasPages = {
|
|
5
|
+
"atlas/Index": /* @__PURE__ */ __name(() => import("./resources/pages/atlas/Index.vue"), "atlas/Index"),
|
|
6
|
+
"atlas/Create": /* @__PURE__ */ __name(() => import("./resources/pages/atlas/Create.vue"), "atlas/Create"),
|
|
7
|
+
"atlas/Edit": /* @__PURE__ */ __name(() => import("./resources/pages/atlas/Edit.vue"), "atlas/Edit")
|
|
8
|
+
};
|
|
9
|
+
function resolveAtlasPage(name) {
|
|
10
|
+
const loader = atlasPages[name];
|
|
11
|
+
if (!loader) {
|
|
12
|
+
throw new Error(`Atlas page not found: ${name}. Available: ${Object.keys(atlasPages).join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
return loader();
|
|
15
|
+
}
|
|
16
|
+
__name(resolveAtlasPage, "resolveAtlasPage");
|
|
17
|
+
export {
|
|
18
|
+
resolveAtlasPage
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/app.ts"],"sourcesContent":["// Atlas client-side page resolver\n// Used by the host app's Inertia resolve function to load Atlas pages\n// from within the adonis-atlas package.\n//\n// Usage in host app's inertia/app/app.ts:\n// import { resolveAtlasPage } from 'adonis-atlas/client'\n// resolve: (name) => {\n// if (name.startsWith('atlas/')) return resolveAtlasPage(name)\n// return resolvePageComponent(...)\n// }\n\nconst atlasPages: Record<string, () => Promise<unknown>> = {\n 'atlas/Index': () => import('./resources/pages/atlas/Index.vue'),\n 'atlas/Create': () => import('./resources/pages/atlas/Create.vue'),\n 'atlas/Edit': () => import('./resources/pages/atlas/Edit.vue'),\n}\n\nexport function resolveAtlasPage(name: string): Promise<unknown> {\n const loader = atlasPages[name]\n if (!loader) {\n throw new Error(\n `Atlas page not found: ${name}. Available: ${Object.keys(atlasPages).join(', ')}`\n )\n }\n return loader()\n}\n"],"mappings":";;;AAWA,MAAMA,aAAqD;EACzD,eAAe,6BAAM,OAAO,mCAAA,GAAb;EACf,gBAAgB,6BAAM,OAAO,oCAAA,GAAb;EAChB,cAAc,6BAAM,OAAO,kCAAA,GAAb;AAChB;AAEO,SAASC,iBAAiBC,MAAY;AAC3C,QAAMC,SAASH,WAAWE,IAAAA;AAC1B,MAAI,CAACC,QAAQ;AACX,UAAM,IAAIC,MACR,yBAAyBF,IAAAA,gBAAoBG,OAAOC,KAAKN,UAAAA,EAAYO,KAAK,IAAA,CAAA,EAAO;EAErF;AACA,SAAOJ,OAAAA;AACT;AARgBF;","names":["atlasPages","resolveAtlasPage","name","loader","Error","Object","keys","join"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "../chunk-7QVYU63E.js";
|
|
4
|
+
import "./resources/css/atlas.css";
|
|
5
|
+
import { createApp, h } from "vue";
|
|
6
|
+
import { createInertiaApp } from "@inertiajs/vue3";
|
|
7
|
+
import { resolveAtlasPage } from "./app.js";
|
|
8
|
+
const appName = import.meta.env.VITE_APP_NAME || "Adonis Atlas";
|
|
9
|
+
createInertiaApp({
|
|
10
|
+
progress: {
|
|
11
|
+
color: "#5468FF"
|
|
12
|
+
},
|
|
13
|
+
title: /* @__PURE__ */ __name((title) => `${title} - ${appName}`, "title"),
|
|
14
|
+
// Only resolves Atlas pages.
|
|
15
|
+
// If you need custom pages, create your own app.ts and use resolveAtlasPage helper.
|
|
16
|
+
resolve: /* @__PURE__ */ __name((name) => resolveAtlasPage(name), "resolve"),
|
|
17
|
+
setup({ el, App, props, plugin }) {
|
|
18
|
+
createApp({
|
|
19
|
+
render: /* @__PURE__ */ __name(() => h(App, props), "render")
|
|
20
|
+
}).use(plugin).mount(el);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
//# sourceMappingURL=boot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/boot.ts"],"sourcesContent":["/**\n * Default entry point for Adonis Atlas applications.\n * Import this file in your Vite config entrypoints if you don't need custom frontend code.\n */\nimport './resources/css/atlas.css'\nimport { createApp, h } from 'vue'\nimport { createInertiaApp } from '@inertiajs/vue3'\nimport { resolveAtlasPage } from './app.js'\n\nconst appName = import.meta.env.VITE_APP_NAME || 'Adonis Atlas'\n\ncreateInertiaApp({\n progress: { color: '#5468FF' },\n\n title: (title) => `${title} - ${appName}`,\n\n // Only resolves Atlas pages.\n // If you need custom pages, create your own app.ts and use resolveAtlasPage helper.\n resolve: (name) => resolveAtlasPage(name),\n\n setup({ el, App, props, plugin }) {\n createApp({ render: () => h(App, props) })\n .use(plugin)\n .mount(el)\n },\n})\n"],"mappings":";;;AAIA,OAAO;AACP,SAASA,WAAWC,SAAS;AAC7B,SAASC,wBAAwB;AACjC,SAASC,wBAAwB;AAEjC,MAAMC,UAAU,YAAYC,IAAIC,iBAAiB;AAEjDJ,iBAAiB;EACfK,UAAU;IAAEC,OAAO;EAAU;EAE7BC,OAAO,wBAACA,UAAU,GAAGA,KAAAA,MAAWL,OAAAA,IAAzB;;;EAIPM,SAAS,wBAACC,SAASR,iBAAiBQ,IAAAA,GAA3B;EAETC,MAAM,EAAEC,IAAIC,KAAKC,OAAOC,OAAM,GAAE;AAC9BhB,cAAU;MAAEiB,QAAQ,6BAAMhB,EAAEa,KAAKC,KAAAA,GAAb;IAAoB,CAAA,EACrCG,IAAIF,MAAAA,EACJG,MAAMN,EAAAA;EACX;AACF,CAAA;","names":["createApp","h","createInertiaApp","resolveAtlasPage","appName","env","VITE_APP_NAME","progress","color","title","resolve","name","setup","el","App","props","plugin","render","use","mount"]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface FieldDef {
|
|
3
|
+
name: string
|
|
4
|
+
label: string
|
|
5
|
+
component: string
|
|
6
|
+
sortable: boolean
|
|
7
|
+
value?: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Row {
|
|
11
|
+
id: number | string
|
|
12
|
+
fields: FieldDef[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
rows: Row[]
|
|
17
|
+
sort?: string
|
|
18
|
+
order?: string
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{
|
|
22
|
+
sort: [field: string]
|
|
23
|
+
edit: [id: number | string]
|
|
24
|
+
delete: [id: number | string]
|
|
25
|
+
}>()
|
|
26
|
+
|
|
27
|
+
function columns(): FieldDef[] {
|
|
28
|
+
if (props.rows.length === 0) return []
|
|
29
|
+
return props.rows[0].fields.filter((f) => f.component !== 'PasswordField')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getSortDir(fieldName: string): string | null {
|
|
33
|
+
if (props.sort !== fieldName) return null
|
|
34
|
+
return props.order || 'asc'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatValue(value: any, component: string): string {
|
|
38
|
+
if (value === null || value === undefined) return '—'
|
|
39
|
+
if (component === 'BooleanField') return value ? 'Yes' : 'No'
|
|
40
|
+
if (component === 'DateTimeField') {
|
|
41
|
+
try {
|
|
42
|
+
return new Date(value).toLocaleString()
|
|
43
|
+
} catch {
|
|
44
|
+
return String(value)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return String(value)
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="atlas-card">
|
|
53
|
+
<div class="atlas-table-wrapper">
|
|
54
|
+
<table class="atlas-table">
|
|
55
|
+
<thead>
|
|
56
|
+
<tr>
|
|
57
|
+
<th
|
|
58
|
+
v-for="col in columns()"
|
|
59
|
+
:key="col.name"
|
|
60
|
+
:class="{
|
|
61
|
+
sortable: col.sortable,
|
|
62
|
+
sorted: sort === col.name,
|
|
63
|
+
}"
|
|
64
|
+
@click="col.sortable ? emit('sort', col.name) : null"
|
|
65
|
+
>
|
|
66
|
+
{{ col.label }}
|
|
67
|
+
<span v-if="col.sortable" class="sort-indicator">
|
|
68
|
+
{{ getSortDir(col.name) === 'asc' ? '↑' : getSortDir(col.name) === 'desc' ? '↓' : '↕' }}
|
|
69
|
+
</span>
|
|
70
|
+
</th>
|
|
71
|
+
<th style="width: 100px; text-align: right">Actions</th>
|
|
72
|
+
</tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody>
|
|
75
|
+
<tr v-for="row in rows" :key="row.id">
|
|
76
|
+
<td v-for="field in row.fields.filter((f) => f.component !== 'PasswordField')" :key="field.name">
|
|
77
|
+
{{ formatValue(field.value, field.component) }}
|
|
78
|
+
</td>
|
|
79
|
+
<td>
|
|
80
|
+
<div class="atlas-table__actions">
|
|
81
|
+
<button class="atlas-btn atlas-btn--secondary atlas-btn--sm" @click="emit('edit', row.id)">
|
|
82
|
+
Edit
|
|
83
|
+
</button>
|
|
84
|
+
<button class="atlas-btn atlas-btn--danger atlas-btn--sm" @click="emit('delete', row.id)">
|
|
85
|
+
Delete
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|
|
90
|
+
<tr v-if="rows.length === 0">
|
|
91
|
+
<td :colspan="columns().length + 1">
|
|
92
|
+
<div class="atlas-empty">
|
|
93
|
+
<div class="atlas-empty__title">No records found</div>
|
|
94
|
+
<div class="atlas-empty__text">Try adjusting your search or create a new record.</div>
|
|
95
|
+
</div>
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
</div>
|
|
101
|
+
<slot name="footer" />
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { defineAsyncComponent } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface FieldDef {
|
|
5
|
+
name: string
|
|
6
|
+
label: string
|
|
7
|
+
component: string
|
|
8
|
+
rules: string[]
|
|
9
|
+
value?: any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
defineProps<{
|
|
13
|
+
field: FieldDef
|
|
14
|
+
modelValue: any
|
|
15
|
+
error?: string
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
defineEmits<{
|
|
19
|
+
'update:modelValue': [value: any]
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const componentMap: Record<string, any> = {
|
|
23
|
+
TextField: defineAsyncComponent(() => import('./fields/TextField.vue')),
|
|
24
|
+
EmailField: defineAsyncComponent(() => import('./fields/EmailField.vue')),
|
|
25
|
+
NumberField: defineAsyncComponent(() => import('./fields/NumberField.vue')),
|
|
26
|
+
BooleanField: defineAsyncComponent(() => import('./fields/BooleanField.vue')),
|
|
27
|
+
DateTimeField: defineAsyncComponent(() => import('./fields/DateTimeField.vue')),
|
|
28
|
+
PasswordField: defineAsyncComponent(() => import('./fields/PasswordField.vue')),
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<component
|
|
34
|
+
:is="componentMap[field.component] || componentMap.TextField"
|
|
35
|
+
:field="field"
|
|
36
|
+
:modelValue="modelValue"
|
|
37
|
+
:error="error"
|
|
38
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
39
|
+
/>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Link } from '@inertiajs/vue3'
|
|
3
|
+
|
|
4
|
+
interface NavItem {
|
|
5
|
+
label: string
|
|
6
|
+
uriKey: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
defineProps<{
|
|
10
|
+
navigation: NavItem[]
|
|
11
|
+
resourceTitle?: string
|
|
12
|
+
breadcrumbs?: { label: string; href?: string }[]
|
|
13
|
+
rootUrl?: string
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="atlas-app">
|
|
19
|
+
<div class="atlas-layout">
|
|
20
|
+
<!-- Sidebar -->
|
|
21
|
+
<aside class="atlas-sidebar">
|
|
22
|
+
<div class="atlas-sidebar__brand">
|
|
23
|
+
<div class="atlas-sidebar__brand-icon">A</div>
|
|
24
|
+
<span class="atlas-sidebar__brand-text">Atlas</span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="atlas-sidebar__label">Resources</div>
|
|
28
|
+
<nav class="atlas-sidebar__nav">
|
|
29
|
+
<Link
|
|
30
|
+
v-for="item in navigation"
|
|
31
|
+
:key="item.uriKey"
|
|
32
|
+
:href="`${rootUrl || '/atlas'}/${item.uriKey}`"
|
|
33
|
+
class="atlas-sidebar__link"
|
|
34
|
+
:class="{ 'atlas-sidebar__link--active': resourceTitle === item.label }"
|
|
35
|
+
>
|
|
36
|
+
<svg class="atlas-sidebar__link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
37
|
+
<rect x="3" y="3" width="7" height="7" />
|
|
38
|
+
<rect x="14" y="3" width="7" height="7" />
|
|
39
|
+
<rect x="14" y="14" width="7" height="7" />
|
|
40
|
+
<rect x="3" y="14" width="7" height="7" />
|
|
41
|
+
</svg>
|
|
42
|
+
{{ item.label }}
|
|
43
|
+
</Link>
|
|
44
|
+
</nav>
|
|
45
|
+
</aside>
|
|
46
|
+
|
|
47
|
+
<!-- Main -->
|
|
48
|
+
<div class="atlas-main">
|
|
49
|
+
<header class="atlas-topbar">
|
|
50
|
+
<div class="atlas-topbar__breadcrumb">
|
|
51
|
+
<Link :href="rootUrl || '/atlas'">Atlas</Link>
|
|
52
|
+
<template v-if="breadcrumbs">
|
|
53
|
+
<template v-for="(crumb, i) in breadcrumbs" :key="i">
|
|
54
|
+
<span class="atlas-topbar__breadcrumb-sep">›</span>
|
|
55
|
+
<Link v-if="crumb.href" :href="crumb.href">{{ crumb.label }}</Link>
|
|
56
|
+
<span v-else>{{ crumb.label }}</span>
|
|
57
|
+
</template>
|
|
58
|
+
</template>
|
|
59
|
+
</div>
|
|
60
|
+
</header>
|
|
61
|
+
|
|
62
|
+
<div class="atlas-content atlas-fade-in">
|
|
63
|
+
<slot />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface Meta {
|
|
3
|
+
total: number
|
|
4
|
+
perPage: number
|
|
5
|
+
currentPage: number
|
|
6
|
+
lastPage: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
defineProps<{
|
|
10
|
+
meta: Meta
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
page: [page: number]
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
function pages(meta: Meta): number[] {
|
|
18
|
+
const result: number[] = []
|
|
19
|
+
const start = Math.max(1, meta.currentPage - 2)
|
|
20
|
+
const end = Math.min(meta.lastPage, meta.currentPage + 2)
|
|
21
|
+
for (let i = start; i <= end; i++) result.push(i)
|
|
22
|
+
return result
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div class="atlas-pagination" v-if="meta.lastPage > 1">
|
|
28
|
+
<div class="atlas-pagination__info">
|
|
29
|
+
Showing {{ (meta.currentPage - 1) * meta.perPage + 1 }}–{{ Math.min(meta.currentPage * meta.perPage, meta.total) }} of {{ meta.total }}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="atlas-pagination__controls">
|
|
33
|
+
<button
|
|
34
|
+
class="atlas-pagination__btn"
|
|
35
|
+
:disabled="meta.currentPage <= 1"
|
|
36
|
+
@click="emit('page', meta.currentPage - 1)"
|
|
37
|
+
>
|
|
38
|
+
← Prev
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<button
|
|
42
|
+
v-for="p in pages(meta)"
|
|
43
|
+
:key="p"
|
|
44
|
+
class="atlas-pagination__btn"
|
|
45
|
+
:class="{ 'atlas-pagination__btn--active': p === meta.currentPage }"
|
|
46
|
+
@click="emit('page', p)"
|
|
47
|
+
>
|
|
48
|
+
{{ p }}
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<button
|
|
52
|
+
class="atlas-pagination__btn"
|
|
53
|
+
:disabled="meta.currentPage >= meta.lastPage"
|
|
54
|
+
@click="emit('page', meta.currentPage + 1)"
|
|
55
|
+
>
|
|
56
|
+
Next →
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, onMounted } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
modelValue: string
|
|
6
|
+
placeholder?: string
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits<{
|
|
10
|
+
'update:modelValue': [value: string]
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const local = ref(props.modelValue || '')
|
|
14
|
+
let debounceTimer: ReturnType<typeof setTimeout>
|
|
15
|
+
|
|
16
|
+
watch(local, (val) => {
|
|
17
|
+
clearTimeout(debounceTimer)
|
|
18
|
+
debounceTimer = setTimeout(() => {
|
|
19
|
+
emit('update:modelValue', val)
|
|
20
|
+
}, 300)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
onMounted(() => {
|
|
24
|
+
local.value = props.modelValue || ''
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div class="atlas-search">
|
|
30
|
+
<svg class="atlas-search__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
31
|
+
<circle cx="11" cy="11" r="8" />
|
|
32
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
33
|
+
</svg>
|
|
34
|
+
<input
|
|
35
|
+
v-model="local"
|
|
36
|
+
type="text"
|
|
37
|
+
class="atlas-search__input"
|
|
38
|
+
:placeholder="placeholder || 'Search...'"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
modelValue: boolean
|
|
4
|
+
field: { name: string; label: string }
|
|
5
|
+
error?: string
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
defineEmits<{
|
|
9
|
+
'update:modelValue': [value: boolean]
|
|
10
|
+
}>()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="atlas-form__group">
|
|
15
|
+
<label class="atlas-form__label">{{ field.label }}</label>
|
|
16
|
+
<div class="atlas-toggle" @click="$emit('update:modelValue', !modelValue)">
|
|
17
|
+
<div class="atlas-toggle__track" :class="{ 'atlas-toggle__track--active': modelValue }">
|
|
18
|
+
<div class="atlas-toggle__knob" />
|
|
19
|
+
</div>
|
|
20
|
+
<span style="font-size: 13px; color: var(--atlas-text-secondary)">
|
|
21
|
+
{{ modelValue ? 'Yes' : 'No' }}
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div v-if="error" class="atlas-form__error">{{ error }}</div>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
modelValue: string
|
|
4
|
+
field: { name: string; label: string }
|
|
5
|
+
error?: string
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
defineEmits<{
|
|
9
|
+
'update:modelValue': [value: string]
|
|
10
|
+
}>()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="atlas-form__group">
|
|
15
|
+
<label :for="`field-${field.name}`" class="atlas-form__label">{{ field.label }}</label>
|
|
16
|
+
<input
|
|
17
|
+
:id="`field-${field.name}`"
|
|
18
|
+
type="datetime-local"
|
|
19
|
+
class="atlas-form__input"
|
|
20
|
+
:class="{ 'atlas-form__input--error': error }"
|
|
21
|
+
:value="modelValue"
|
|
22
|
+
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
23
|
+
/>
|
|
24
|
+
<div v-if="error" class="atlas-form__error">{{ error }}</div>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
modelValue: string
|
|
4
|
+
field: { name: string; label: string }
|
|
5
|
+
error?: string
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
defineEmits<{
|
|
9
|
+
'update:modelValue': [value: string]
|
|
10
|
+
}>()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="atlas-form__group">
|
|
15
|
+
<label :for="`field-${field.name}`" class="atlas-form__label">{{ field.label }}</label>
|
|
16
|
+
<input
|
|
17
|
+
:id="`field-${field.name}`"
|
|
18
|
+
type="email"
|
|
19
|
+
class="atlas-form__input"
|
|
20
|
+
:class="{ 'atlas-form__input--error': error }"
|
|
21
|
+
:value="modelValue"
|
|
22
|
+
:placeholder="`Enter ${field.label.toLowerCase()}`"
|
|
23
|
+
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
24
|
+
/>
|
|
25
|
+
<div v-if="error" class="atlas-form__error">{{ error }}</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
modelValue: number | string
|
|
4
|
+
field: { name: string; label: string }
|
|
5
|
+
error?: string
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
defineEmits<{
|
|
9
|
+
'update:modelValue': [value: number]
|
|
10
|
+
}>()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="atlas-form__group">
|
|
15
|
+
<label :for="`field-${field.name}`" class="atlas-form__label">{{ field.label }}</label>
|
|
16
|
+
<input
|
|
17
|
+
:id="`field-${field.name}`"
|
|
18
|
+
type="number"
|
|
19
|
+
class="atlas-form__input"
|
|
20
|
+
:class="{ 'atlas-form__input--error': error }"
|
|
21
|
+
:value="modelValue"
|
|
22
|
+
:placeholder="`Enter ${field.label.toLowerCase()}`"
|
|
23
|
+
@input="$emit('update:modelValue', Number(($event.target as HTMLInputElement).value))"
|
|
24
|
+
/>
|
|
25
|
+
<div v-if="error" class="atlas-form__error">{{ error }}</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|