create-nara 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 (50) hide show
  1. package/README.md +17 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +50 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +3 -0
  6. package/dist/template.d.ts +8 -0
  7. package/dist/template.js +68 -0
  8. package/package.json +28 -0
  9. package/templates/base/.env.example +3 -0
  10. package/templates/base/tsconfig.json +14 -0
  11. package/templates/minimal/routes/web.ts +11 -0
  12. package/templates/minimal/server.ts +10 -0
  13. package/templates/svelte/resources/js/app.ts +12 -0
  14. package/templates/svelte/resources/js/components/DarkModeToggle.svelte +67 -0
  15. package/templates/svelte/resources/js/components/Header.svelte +240 -0
  16. package/templates/svelte/resources/js/components/NaraIcon.svelte +3 -0
  17. package/templates/svelte/resources/js/components/Pagination.svelte +55 -0
  18. package/templates/svelte/resources/js/components/UserModal.svelte +234 -0
  19. package/templates/svelte/resources/js/components/helper.ts +300 -0
  20. package/templates/svelte/resources/js/pages/auth/forgot-password.svelte +97 -0
  21. package/templates/svelte/resources/js/pages/auth/login.svelte +138 -0
  22. package/templates/svelte/resources/js/pages/auth/register.svelte +176 -0
  23. package/templates/svelte/resources/js/pages/auth/reset-password.svelte +106 -0
  24. package/templates/svelte/resources/js/pages/dashboard.svelte +224 -0
  25. package/templates/svelte/resources/js/pages/landing.svelte +446 -0
  26. package/templates/svelte/resources/js/pages/profile.svelte +368 -0
  27. package/templates/svelte/resources/js/pages/users.svelte +260 -0
  28. package/templates/svelte/resources/views/inertia.html +12 -0
  29. package/templates/svelte/routes/web.ts +17 -0
  30. package/templates/svelte/server.ts +12 -0
  31. package/templates/svelte/vite.config.ts +19 -0
  32. package/templates/vue/resources/js/app.ts +14 -0
  33. package/templates/vue/resources/js/components/DarkModeToggle.vue +81 -0
  34. package/templates/vue/resources/js/components/Header.vue +251 -0
  35. package/templates/vue/resources/js/components/NaraIcon.vue +5 -0
  36. package/templates/vue/resources/js/components/Pagination.vue +71 -0
  37. package/templates/vue/resources/js/components/UserModal.vue +276 -0
  38. package/templates/vue/resources/js/components/index.ts +5 -0
  39. package/templates/vue/resources/js/pages/auth/forgot-password.vue +105 -0
  40. package/templates/vue/resources/js/pages/auth/login.vue +142 -0
  41. package/templates/vue/resources/js/pages/auth/register.vue +183 -0
  42. package/templates/vue/resources/js/pages/auth/reset-password.vue +115 -0
  43. package/templates/vue/resources/js/pages/dashboard.vue +233 -0
  44. package/templates/vue/resources/js/pages/landing.vue +358 -0
  45. package/templates/vue/resources/js/pages/profile.vue +370 -0
  46. package/templates/vue/resources/js/pages/users.vue +264 -0
  47. package/templates/vue/resources/views/inertia.html +12 -0
  48. package/templates/vue/routes/web.ts +17 -0
  49. package/templates/vue/server.ts +12 -0
  50. package/templates/vue/vite.config.ts +19 -0
@@ -0,0 +1,12 @@
1
+ import 'dotenv/config';
2
+ import { createApp } from '@nara-web/core';
3
+ import { svelteAdapter } from '@nara-web/inertia-svelte';
4
+ import { registerRoutes } from './routes/web.js';
5
+
6
+ const app = createApp({
7
+ port: Number(process.env.PORT) || 3000,
8
+ adapter: svelteAdapter()
9
+ });
10
+
11
+ registerRoutes(app);
12
+ app.start();
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'vite';
2
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
3
+ import path from 'path';
4
+
5
+ export default defineConfig({
6
+ plugins: [svelte()],
7
+ resolve: {
8
+ alias: {
9
+ '@': path.resolve(__dirname, './resources/js')
10
+ }
11
+ },
12
+ build: {
13
+ manifest: true,
14
+ outDir: 'public/build',
15
+ rollupOptions: {
16
+ input: 'resources/js/app.ts'
17
+ }
18
+ }
19
+ });
@@ -0,0 +1,14 @@
1
+ import { createApp, h } from 'vue';
2
+ import { createInertiaApp } from '@inertiajs/vue3';
3
+
4
+ createInertiaApp({
5
+ resolve: name => {
6
+ const pages = import.meta.glob('./pages/**/*.vue', { eager: true });
7
+ return pages[`./pages/${name}.vue`];
8
+ },
9
+ setup({ el, App, props, plugin }) {
10
+ createApp({ render: () => h(App, props) })
11
+ .use(plugin)
12
+ .mount(el!);
13
+ }
14
+ });
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue';
3
+
4
+ const props = defineProps<{
5
+ onchange?: (mode: boolean) => void;
6
+ }>();
7
+
8
+ const darkMode = ref(false);
9
+ const mounted = ref(false);
10
+
11
+ const toggleDarkMode = () => {
12
+ darkMode.value = !darkMode.value;
13
+
14
+ if (darkMode.value) {
15
+ document.documentElement.classList.add('dark');
16
+ } else {
17
+ document.documentElement.classList.remove('dark');
18
+ }
19
+
20
+ // Save preference to localStorage
21
+ localStorage.setItem('darkMode', String(darkMode.value));
22
+
23
+ if (props.onchange) {
24
+ props.onchange(darkMode.value);
25
+ }
26
+ };
27
+
28
+ onMounted(() => {
29
+ // Check system preference
30
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
31
+ // Check localStorage or fallback to system preference
32
+ const savedMode = localStorage.getItem('darkMode');
33
+ darkMode.value = savedMode === null ? systemPrefersDark : savedMode === 'true';
34
+
35
+ // Apply saved preference
36
+ if (darkMode.value) {
37
+ document.documentElement.classList.add('dark');
38
+ }
39
+
40
+ // Add transition class after initial load to prevent flash
41
+ setTimeout(() => {
42
+ document.documentElement.classList.add('transition-colors');
43
+ mounted.value = true;
44
+ }, 100);
45
+
46
+ // Listen for system preference changes
47
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e: MediaQueryListEvent) => {
48
+ if (localStorage.getItem('darkMode') === null) {
49
+ darkMode.value = e.matches;
50
+ // We don't call toggleDarkMode here to avoid double inversion
51
+ if (darkMode.value) {
52
+ document.documentElement.classList.add('dark');
53
+ } else {
54
+ document.documentElement.classList.remove('dark');
55
+ }
56
+ if (props.onchange) props.onchange(darkMode.value);
57
+ }
58
+ });
59
+ });
60
+ </script>
61
+
62
+ <template>
63
+ <button
64
+ @click="toggleDarkMode"
65
+ class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-slate-800/70 transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-400/50"
66
+ aria-label="Toggle dark mode"
67
+ >
68
+ <template v-if="darkMode">
69
+ <!-- Sun icon for light mode -->
70
+ <svg class="w-5 h-5 text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-slate-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
71
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
72
+ </svg>
73
+ </template>
74
+ <template v-else>
75
+ <!-- Moon icon for dark mode -->
76
+ <svg class="w-5 h-5 text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-slate-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
77
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
78
+ </svg>
79
+ </template>
80
+ </button>
81
+ </template>
@@ -0,0 +1,251 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onUnmounted } from 'vue';
3
+ import { usePage, router, Link } from '@inertiajs/vue3';
4
+ import DarkModeToggle from './DarkModeToggle.vue';
5
+
6
+ interface User {
7
+ id: string;
8
+ name: string;
9
+ email: string;
10
+ is_admin: boolean;
11
+ }
12
+
13
+ interface MenuLink {
14
+ href: string;
15
+ label: string;
16
+ group: string;
17
+ show: boolean;
18
+ }
19
+
20
+ const props = defineProps<{
21
+ group: string;
22
+ }>();
23
+
24
+ const page = usePage();
25
+ const user = computed(() => page.props.user as User | undefined);
26
+
27
+ const scrollY = ref(0);
28
+ const isMenuOpen = ref(false);
29
+
30
+ const scrolled = computed(() => scrollY.value > 50);
31
+
32
+ const menuLinks = computed(() => [
33
+ { href: '/dashboard', label: 'Overview', group: 'dashboard', show: true },
34
+ { href: '/users', label: 'Users', group: 'users', show: !!(user.value?.is_admin) },
35
+ { href: '/profile', label: 'Profile', group: 'profile', show: !!user.value },
36
+ ] as MenuLink[]);
37
+
38
+ const visibleMenuLinks = computed(() => menuLinks.value.filter((item) => item.show));
39
+
40
+ const handleLogout = () => {
41
+ router.post('/logout');
42
+ };
43
+
44
+ const updateScroll = () => {
45
+ scrollY.value = window.scrollY;
46
+ };
47
+
48
+ onMounted(() => {
49
+ window.addEventListener('scroll', updateScroll);
50
+ });
51
+
52
+ onUnmounted(() => {
53
+ window.removeEventListener('scroll', updateScroll);
54
+ });
55
+ </script>
56
+
57
+ <template>
58
+ <header
59
+ class="fixed inset-x-0 top-0 z-50 transition-all duration-500"
60
+ :class="scrolled
61
+ ? 'bg-white/90 dark:bg-surface-dark/95 backdrop-blur-xl border-b border-slate-200/50 dark:border-white/5'
62
+ : 'bg-white/90 dark:bg-surface-dark/95 backdrop-blur-xl'"
63
+ >
64
+ <nav class="px-6 sm:px-12 lg:px-24 py-5 flex items-center justify-between">
65
+
66
+ <!-- Left: Brand + Nav -->
67
+ <div class="flex items-center gap-10">
68
+ <!-- Radical Brand -->
69
+ <Link
70
+ href="/"
71
+ class="group flex items-center gap-3"
72
+ >
73
+ <span class="text-xl font-bold tracking-tighter text-slate-900 dark:text-white group-hover:text-primary-500 transition-colors">
74
+ NARA.
75
+ </span>
76
+ <span class="hidden sm:block text-[9px] uppercase tracking-[0.3em] text-slate-400 dark:text-slate-500 font-medium">
77
+ Dashboard
78
+ </span>
79
+ </Link>
80
+
81
+ <!-- Desktop Navigation - Radical Style -->
82
+ <div class="hidden md:flex items-center gap-1">
83
+ <template v-for="(item, i) in visibleMenuLinks" :key="item.href">
84
+ <Link
85
+ :href="item.href"
86
+ class="relative px-4 py-2 text-xs font-medium uppercase tracking-[0.15em] transition-all duration-300"
87
+ :class="item.group === group
88
+ ? 'text-primary-500'
89
+ : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'"
90
+ >
91
+ {{ item.label }}
92
+ <span v-if="item.group === group" class="absolute bottom-0 left-4 right-4 h-px bg-primary-500"></span>
93
+ </Link>
94
+ <span v-if="i < visibleMenuLinks.length - 1" class="w-px h-3 bg-slate-200 dark:bg-slate-700"></span>
95
+ </template>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Right: Actions -->
100
+ <div class="flex items-center gap-4">
101
+ <!-- Current Page Indicator (Mobile) -->
102
+ <span class="md:hidden text-[10px] uppercase tracking-[0.2em] text-primary-500 font-medium">
103
+ {{ group }}
104
+ </span>
105
+
106
+ <div class="h-4 w-px bg-slate-200 dark:bg-slate-700 hidden sm:block"></div>
107
+
108
+ <DarkModeToggle />
109
+
110
+ <!-- Auth Actions -->
111
+ <div class="hidden sm:flex items-center gap-3">
112
+ <template v-if="user && user.id">
113
+ <div class="flex items-center gap-3">
114
+ <span class="text-xs text-slate-500 dark:text-slate-400">
115
+ {{ user.name }}
116
+ </span>
117
+ <button
118
+ @click="handleLogout"
119
+ class="group relative px-5 py-2 text-xs font-bold uppercase tracking-wider overflow-hidden rounded-full border border-slate-200 dark:border-slate-700 hover:border-red-500/50 dark:hover:border-red-500/50 transition-colors"
120
+ >
121
+ <span class="relative z-10 text-slate-600 dark:text-slate-300 group-hover:text-red-500 transition-colors">
122
+ Logout
123
+ </span>
124
+ </button>
125
+ </div>
126
+ </template>
127
+ <template v-else>
128
+ <Link
129
+ href="/login"
130
+ class="text-xs font-medium text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition-colors"
131
+ >
132
+ Login
133
+ </Link>
134
+ <Link
135
+ href="/register"
136
+ class="group relative px-5 py-2.5 bg-slate-900 dark:bg-white text-white dark:text-black text-xs font-bold uppercase tracking-wider rounded-full overflow-hidden hover:scale-105 transition-transform"
137
+ >
138
+ <span class="relative z-10">Register</span>
139
+ <div class="absolute inset-0 bg-primary-500 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
140
+ </Link>
141
+ </template>
142
+ </div>
143
+
144
+ <!-- Mobile Menu Button -->
145
+ <button
146
+ class="md:hidden relative w-10 h-10 flex items-center justify-center rounded-full border border-slate-200 dark:border-slate-700 hover:border-primary-500/50 transition-colors text-slate-900 dark:text-white"
147
+ @click="isMenuOpen = !isMenuOpen"
148
+ aria-label="Menu"
149
+ >
150
+ <div class="flex flex-col gap-1.5 w-4">
151
+ <span class="block h-px bg-slate-900 dark:bg-white transition-transform duration-300" :class="{ 'rotate-45 translate-y-[3.5px]': isMenuOpen }"></span>
152
+ <span class="block h-px bg-slate-900 dark:bg-white transition-opacity duration-300" :class="{ 'opacity-0': isMenuOpen }"></span>
153
+ <span class="block h-px bg-slate-900 dark:bg-white transition-transform duration-300" :class="{ '-rotate-45 -translate-y-[3.5px]': isMenuOpen }"></span>
154
+ </div>
155
+ </button>
156
+ </div>
157
+ </nav>
158
+ </header>
159
+
160
+ <!-- Mobile Menu -->
161
+ <Teleport to="body">
162
+ <Transition
163
+ enter-active-class="transition duration-200 ease-out"
164
+ enter-from-class="opacity-0"
165
+ enter-to-class="opacity-100"
166
+ leave-active-class="transition duration-200 ease-in"
167
+ leave-from-class="opacity-100"
168
+ leave-to-class="opacity-0"
169
+ >
170
+ <div v-if="isMenuOpen" class="fixed inset-0 bg-white dark:bg-surface-dark z-[9999] md:hidden overflow-y-auto">
171
+ <!-- Close Button -->
172
+ <button
173
+ class="absolute top-6 right-6 w-10 h-10 flex items-center justify-center rounded-full border border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white z-10"
174
+ @click="isMenuOpen = false"
175
+ aria-label="Close menu"
176
+ >
177
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
178
+ <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
179
+ </svg>
180
+ </button>
181
+
182
+ <!-- Brand -->
183
+ <div class="absolute top-6 left-6 z-10">
184
+ <span class="text-xl font-bold tracking-tighter text-slate-900 dark:text-white">NARA.</span>
185
+ </div>
186
+
187
+ <!-- Menu Content -->
188
+ <div class="min-h-screen flex flex-col justify-center px-8 sm:px-12 py-24">
189
+ <!-- Navigation Links -->
190
+ <nav class="space-y-6 mb-12">
191
+ <template v-for="(item, i) in visibleMenuLinks" :key="item.href">
192
+ <Link
193
+ :href="item.href"
194
+ @click="isMenuOpen = false"
195
+ class="block text-4xl sm:text-5xl font-bold tracking-tighter transition-all duration-300"
196
+ :class="item.group === group
197
+ ? 'text-primary-500'
198
+ : 'text-slate-900 dark:text-white hover:text-primary-500 dark:hover:text-primary-400 hover:translate-x-2'"
199
+ :style="{ transitionDelay: `${i * 100}ms` }"
200
+ >
201
+ <span class="inline-flex items-center gap-4">
202
+ <span class="text-xs font-mono text-slate-400 dark:text-slate-500">0{{ i + 1 }}</span>
203
+ {{ item.label }}
204
+ </span>
205
+ </Link>
206
+ </template>
207
+ </nav>
208
+
209
+ <!-- Mobile Auth -->
210
+ <div class="pt-8 border-t border-slate-200 dark:border-slate-800">
211
+ <template v-if="user">
212
+ <p class="text-xs uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500 mb-4">Signed in as</p>
213
+ <p class="text-lg font-medium text-slate-900 dark:text-white mb-6">{{ user.name }}</p>
214
+ <button
215
+ @click="handleLogout"
216
+ class="px-6 py-3 text-sm font-bold uppercase tracking-wider border border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white rounded-full hover:border-red-500 hover:text-red-500 transition-colors"
217
+ >
218
+ Logout
219
+ </button>
220
+ </template>
221
+ <template v-else>
222
+ <div class="flex gap-4">
223
+ <Link
224
+ href="/login"
225
+ class="px-6 py-3 text-sm font-bold uppercase tracking-wider border border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white rounded-full hover:border-primary-500 transition-colors"
226
+ >
227
+ Login
228
+ </Link>
229
+ <Link
230
+ href="/register"
231
+ class="px-6 py-3 text-sm font-bold uppercase tracking-wider bg-slate-900 dark:bg-white text-white dark:text-black rounded-full"
232
+ >
233
+ Register
234
+ </Link>
235
+ </div>
236
+ </template>
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Decorative -->
241
+ <div class="absolute bottom-8 left-8 sm:left-12 text-[10px] uppercase tracking-[0.3em] text-slate-300 dark:text-slate-600">
242
+ NARA Framework
243
+ </div>
244
+
245
+ <!-- Background Decoration -->
246
+ <div class="absolute top-0 right-0 w-64 h-64 bg-primary-500/5 rounded-full blur-3xl pointer-events-none"></div>
247
+ <div class="absolute bottom-0 left-0 w-48 h-48 bg-accent-500/5 rounded-full blur-3xl pointer-events-none"></div>
248
+ </div>
249
+ </Transition>
250
+ </Teleport>
251
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <span class="flex gap-2 items-center">
3
+ <img class="h-12 w-12 rounded-2xl" src="/public/nara.png" alt="Nara logo" />
4
+ </span>
5
+ </template>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ interface PaginationMeta {
6
+ page: number;
7
+ total: number;
8
+ totalPages: number;
9
+ hasNext: boolean;
10
+ hasPrev: boolean;
11
+ }
12
+
13
+ const props = withDefaults(defineProps<{
14
+ meta: PaginationMeta;
15
+ preserveState?: boolean;
16
+ }>(), {
17
+ preserveState: true
18
+ });
19
+
20
+ const goToPage = (page: number) => {
21
+ const url = new URL(window.location.href);
22
+ url.searchParams.set('page', String(page));
23
+ router.visit(url.pathname + url.search, {
24
+ preserveScroll: true,
25
+ preserveState: props.preserveState
26
+ });
27
+ };
28
+
29
+ const pages = computed(() => {
30
+ const length = Math.min(5, props.meta.totalPages);
31
+ const start = Math.max(1, Math.min(props.meta.page - 2, props.meta.totalPages - 4));
32
+ return Array.from({ length }, (_, i) => start + i).filter(p => p <= props.meta.totalPages);
33
+ });
34
+ </script>
35
+
36
+ <template>
37
+ <div v-if="meta.totalPages > 1" class="flex items-center justify-between mt-6 text-xs text-slate-400">
38
+ <div>
39
+ Halaman {{ meta.page }} dari {{ meta.totalPages }} ({{ meta.total }} total)
40
+ </div>
41
+ <div class="flex items-center gap-2">
42
+ <button
43
+ class="px-3 py-1.5 rounded-full bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 text-slate-100 dark:text-slate-800 transition disabled:opacity-50 disabled:cursor-not-allowed"
44
+ @click="goToPage(meta.page - 1)"
45
+ :disabled="!meta.hasPrev"
46
+ >
47
+ ← Prev
48
+ </button>
49
+
50
+ <button
51
+ v-for="pageNum in pages"
52
+ :key="pageNum"
53
+ class="px-3 py-1.5 rounded-full transition"
54
+ :class="pageNum === meta.page
55
+ ? 'bg-primary-500 text-white'
56
+ : 'bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 text-slate-100 dark:text-slate-800'"
57
+ @click="goToPage(pageNum)"
58
+ >
59
+ {{ pageNum }}
60
+ </button>
61
+
62
+ <button
63
+ class="px-3 py-1.5 rounded-full bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 text-slate-100 dark:text-slate-800 transition disabled:opacity-50 disabled:cursor-not-allowed"
64
+ @click="goToPage(meta.page + 1)"
65
+ :disabled="!meta.hasNext"
66
+ >
67
+ Next →
68
+ </button>
69
+ </div>
70
+ </div>
71
+ </template>