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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/build/chunk-7QVYU63E.js +7 -0
  4. package/build/chunk-7QVYU63E.js.map +1 -0
  5. package/build/client/app.js +20 -0
  6. package/build/client/app.js.map +1 -0
  7. package/build/client/boot.js +23 -0
  8. package/build/client/boot.js.map +1 -0
  9. package/build/client/resources/components/DataTable.vue +103 -0
  10. package/build/client/resources/components/FormField.vue +40 -0
  11. package/build/client/resources/components/Layout.vue +68 -0
  12. package/build/client/resources/components/Pagination.vue +60 -0
  13. package/build/client/resources/components/SearchBar.vue +41 -0
  14. package/build/client/resources/components/fields/BooleanField.vue +26 -0
  15. package/build/client/resources/components/fields/DateTimeField.vue +26 -0
  16. package/build/client/resources/components/fields/EmailField.vue +27 -0
  17. package/build/client/resources/components/fields/NumberField.vue +27 -0
  18. package/build/client/resources/components/fields/PasswordField.vue +28 -0
  19. package/build/client/resources/components/fields/TextField.vue +27 -0
  20. package/build/client/resources/css/atlas.css +662 -0
  21. package/build/client/resources/pages/atlas/Create.vue +132 -0
  22. package/build/client/resources/pages/atlas/Edit.vue +145 -0
  23. package/build/client/resources/pages/atlas/Index.vue +138 -0
  24. package/build/commands/main.js +9 -0
  25. package/build/commands/main.js.map +1 -0
  26. package/build/commands/make_resource.js +42 -0
  27. package/build/commands/make_resource.js.map +1 -0
  28. package/build/configure.js +17 -0
  29. package/build/configure.js.map +1 -0
  30. package/build/index.js +29 -0
  31. package/build/index.js.map +1 -0
  32. package/build/providers/atlas_provider.js +56 -0
  33. package/build/providers/atlas_provider.js.map +1 -0
  34. package/build/stubs/config.stub +18 -0
  35. package/build/stubs/resource.stub +25 -0
  36. package/package.json +81 -0
  37. package/stubs/config.stub +18 -0
  38. package/stubs/main.ts +3 -0
  39. 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,7 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ export {
5
+ __name
6
+ };
7
+ //# sourceMappingURL=chunk-7QVYU63E.js.map
@@ -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>