@stacksjs/defaults 0.72.49 → 0.72.50

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 (31) hide show
  1. package/ide/vscode/package.json +1 -1
  2. package/lang/de.yml +0 -1
  3. package/package.json +2 -2
  4. package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +8 -0
  5. package/resources/components/Dashboard/Auth/Login.stx +8 -0
  6. package/resources/components/Dashboard/Auth/LoginDashboard.stx +5 -1
  7. package/resources/components/Dashboard/Auth/MagicLink.stx +94 -0
  8. package/resources/components/Dashboard/Auth/Register.stx +7 -0
  9. package/resources/components/Dashboard/Auth/RegisterDashboard.stx +5 -1
  10. package/resources/components/Dashboard/Auth/ResetPassword.stx +163 -0
  11. package/resources/components/Dashboard/Auth/ResetPasswordDashboard.stx +95 -0
  12. package/resources/layouts/403.stx +15 -148
  13. package/resources/layouts/404.stx +4 -1
  14. package/resources/views/auth/magic/[token].stx +24 -0
  15. package/resources/views/errors/404.stx +36 -0
  16. package/resources/views/forgot-password.stx +13 -0
  17. package/resources/views/login.stx +68 -0
  18. package/resources/views/password/reset/[token].stx +18 -0
  19. package/resources/views/register.stx +64 -0
  20. package/views/[...all].stx +0 -7
  21. package/views/about.md +0 -3
  22. package/views/auth/login.stx +0 -1
  23. package/views/auth/magic/[token].stx +0 -88
  24. package/views/auth/signup.stx +0 -120
  25. package/views/forum/category.stx +0 -92
  26. package/views/forum/index.stx +0 -209
  27. package/views/forum/new-topic.stx +0 -113
  28. package/views/forum/profile.stx +0 -1
  29. package/views/forum/topic.stx +0 -143
  30. package/views/hello-world/[name].stx +0 -47
  31. package/views/index.stx +0 -21
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.72.49",
5
+ "version": "0.72.50",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/lang/de.yml CHANGED
@@ -11,4 +11,3 @@ intro:
11
11
  hi: Hi, {name}!
12
12
  aka: Auch bekannt als
13
13
  whats-your-name: Wie heißt du?
14
- not-found: Nicht gefunden
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.49",
5
+ "version": "0.72.50",
6
6
  "description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@iconify-json/f7": "^1.2.2",
48
48
  "@iconify-json/hugeicons": "^1.2.27",
49
- "@stacksjs/mobile": "^0.72.49",
49
+ "@stacksjs/mobile": "^0.72.50",
50
50
  "@stacksjs/sanitizer": "^0.2.113"
51
51
  },
52
52
  "scripts": {
@@ -1,4 +1,12 @@
1
1
  <script client>
2
+ // Imported rather than taken as a global, the way LoginDashboard and
3
+ // RegisterDashboard already import useAuth. The bare name only resolves on the
4
+ // dashboard server, so mounting this container anywhere else - such as the
5
+ // site's /forgot-password page - reached the submit handler and threw
6
+ // "dashboardApi is not defined", which the catch below then showed to the
7
+ // visitor as the reason their reset link could not be sent.
8
+ import { dashboardApi } from '../../../../functions/dashboard-api'
9
+
2
10
  const isLoading = state(false)
3
11
  const error = state('')
4
12
  const success = state(false)
@@ -129,6 +129,14 @@ function handleSubmit(): void {
129
129
  <span :if="!isLoading()">Sign in</span>
130
130
  </Button>
131
131
  </form>
132
+
133
+ {{--
134
+ Anything that belongs with the credentials, inside the same card:
135
+ the social sign-in buttons the /login page fills this with. It is a
136
+ slot because the provider list has to be computed server-side by
137
+ `configuredSocialProviders()`, and this component is client-only.
138
+ --}}
139
+ <slot name="social" />
132
140
  </div>
133
141
 
134
142
  <p :if="showSignUp()" class="mt-6 text-center text-neutral-500 text-sm dark:text-neutral-400">
@@ -41,5 +41,9 @@ async function handleLogin(credentials) {
41
41
  :isLoading="isLoading()"
42
42
  :error="error()"
43
43
  :data-auth-status="isLoading() ? 'loading' : 'ready'"
44
- />
44
+ >
45
+ <!-- Forwarded so the page mounting this container can put social
46
+ sign-in inside the card. -->
47
+ <template #social><slot name="social" /></template>
48
+ </Login>
45
49
  </template>
@@ -0,0 +1,94 @@
1
+ <script client>
2
+ /**
3
+ * The magic-link interstitial.
4
+ *
5
+ * The email's GET lands on the page that mounts this, and this posts the token
6
+ * to the consume endpoint - a GET must never consume, because mail-security
7
+ * scanners prefetch links and would burn the single-use token before the
8
+ * person ever taps it. When `autoConsume` is off it waits for a tap instead,
9
+ * for environments whose scanners execute page scripts too.
10
+ *
11
+ * `autoConsume` arrives as a string because it is decided server-side, from
12
+ * `config.auth.magicLink.confirmInteraction`, and a client script cannot read
13
+ * config directly.
14
+ */
15
+ const autoConsume = useReactiveProp<string>('autoConsume', 'true')
16
+ const route = useRoute()
17
+ const status = state('ready') // ready | working | done | failed
18
+ const message = state('')
19
+
20
+ async function consume() {
21
+ if (status() === 'working' || status() === 'done')
22
+ return
23
+
24
+ status.set('working')
25
+ try {
26
+ const reply = await fetch('/auth/magic-link/consume', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ credentials: 'same-origin',
30
+ body: JSON.stringify({ token: String(route.params.token || '') }),
31
+ })
32
+ const body = await reply.json().catch(() => ({}))
33
+
34
+ if (!reply.ok) {
35
+ status.set('failed')
36
+ message.set(body.message || body.error || 'That sign-in link is not valid.')
37
+ return
38
+ }
39
+
40
+ status.set('done')
41
+ const redirect = String(body.redirect_to || '/')
42
+ navigate(redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/')
43
+ }
44
+ catch {
45
+ status.set('failed')
46
+ message.set('Something went wrong. Request a new link and try again.')
47
+ }
48
+ }
49
+
50
+ // `consume` reads `status()`, so calling it from inside the effect makes the
51
+ // effect depend on `status` - and every set inside `consume` schedules another
52
+ // run. Its own early return covers 'working' and 'done', but not 'failed', so a
53
+ // link that fails to consume would be retried forever. A plain flag, which is
54
+ // not a signal and so not a dependency, is what makes this fire once.
55
+ let autoStarted = false
56
+
57
+ effect(() => {
58
+ if (autoStarted || autoConsume() !== 'true')
59
+ return
60
+
61
+ autoStarted = true
62
+ consume()
63
+ })
64
+ </script>
65
+
66
+ <template>
67
+ <main class="flex items-center justify-center px-6 min-h-[100dvh]">
68
+ <div class="max-w-sm w-full text-center">
69
+ <div :if="status() === 'failed'">
70
+ <div class="flex items-center justify-center mx-auto h-12 w-12 bg-red-50 rounded-full">
71
+ <span class="h-6 w-6 text-red-500 i-hugeicons-alert-02"></span>
72
+ </div>
73
+ <h1 class="mt-5 font-semibold text-xl tracking-tight">Sign-in link problem</h1>
74
+ <p class="mt-2 text-[15px] text-gray-600">{{ message() }}</p>
75
+ <a href="/login" class="inline-block mt-6 px-5 py-2.5 font-medium text-white bg-gray-900 rounded-lg">Back to sign in</a>
76
+ </div>
77
+ <div :else-if="status() === 'working' || status() === 'done'">
78
+ <div class="flex items-center justify-center mx-auto h-12 w-12 bg-gray-100 rounded-full">
79
+ <span class="h-6 w-6 text-gray-500 animate-spin i-hugeicons-loading-03"></span>
80
+ </div>
81
+ <h1 class="mt-5 font-semibold text-xl tracking-tight">Signing you in</h1>
82
+ <p class="mt-2 text-[15px] text-gray-600">One moment.</p>
83
+ </div>
84
+ <div :else>
85
+ <div class="flex items-center justify-center mx-auto h-12 w-12 bg-gray-100 rounded-full">
86
+ <span class="h-6 w-6 text-gray-600 i-hugeicons-mail-open-01"></span>
87
+ </div>
88
+ <h1 class="mt-5 font-semibold text-xl tracking-tight">Continue to sign in</h1>
89
+ <p class="mt-2 text-[15px] text-gray-600">Tap below to finish signing in with your emailed link.</p>
90
+ <button type="button" @click="consume" class="mt-6 px-6 py-3 font-medium text-white bg-gray-900 rounded-lg transition-transform active:scale-[0.98]">Continue</button>
91
+ </div>
92
+ </div>
93
+ </main>
94
+ </template>
@@ -225,6 +225,13 @@ function handleSubmit(): void {
225
225
  </Button>
226
226
  </form>
227
227
 
228
+ {{--
229
+ Anything that belongs with the credentials, inside the same card:
230
+ the social sign-in buttons the /register page fills this with. It is
231
+ a slot because the provider list has to be computed server-side by
232
+ `configuredSocialProviders()`, and this component is client-only.
233
+ --}}
234
+ <slot name="social" />
228
235
  </div>
229
236
 
230
237
  <!-- Login Link -->
@@ -41,5 +41,9 @@ async function handleRegister(credentials) {
41
41
  :isLoading="isLoading()"
42
42
  :error="error()"
43
43
  :data-auth-status="isLoading() ? 'loading' : 'ready'"
44
- />
44
+ >
45
+ <!-- Forwarded so the page mounting this container can put social
46
+ sign-in inside the card. -->
47
+ <template #social><slot name="social" /></template>
48
+ </Register>
45
49
  </template>
@@ -0,0 +1,163 @@
1
+ @import('../UI/Input', '../UI/Button')
2
+
3
+ {{--
4
+ The page a mailed reset link lands on. Presentational only: it renders the
5
+ four states the flow has and emits `submit`; ResetPasswordDashboard decides
6
+ which state applies and talks to the API.
7
+
8
+ States, in the order a visitor meets them:
9
+ checking - the token is being verified, before any field is shown
10
+ invalid - the token is expired, already used, or was never real
11
+ success - the password is changed
12
+ (default) - the form
13
+ --}}
14
+
15
+ <script client>
16
+ const emit = defineEmits()
17
+ const title = useReactiveProp<string>('title', 'Choose a new password')
18
+ const subtitle = useReactiveProp<string>('subtitle', 'Set the password you will use to sign in')
19
+ const isLoading = useReactiveProp<boolean>('isLoading', false)
20
+ const error = useReactiveProp<string>('error', '')
21
+ const success = useReactiveProp<boolean>('success', false)
22
+ const checking = useReactiveProp<boolean>('checking', false)
23
+ const invalid = useReactiveProp<boolean>('invalid', false)
24
+ const password = state('')
25
+ const confirmation = state('')
26
+
27
+ function mismatch(): boolean {
28
+ return confirmation().length > 0 && password() !== confirmation()
29
+ }
30
+
31
+ function canSubmit(): boolean {
32
+ return !isLoading() && password().length > 0 && confirmation().length > 0 && !mismatch()
33
+ }
34
+
35
+ function handleSubmit(): void {
36
+ if (!canSubmit())
37
+ return
38
+
39
+ emit('submit', {
40
+ password: password(),
41
+ password_confirmation: confirmation(),
42
+ })
43
+ }
44
+ </script>
45
+
46
+ <div class="flex items-center justify-center px-4 py-12 min-h-screen">
47
+ <div class="max-w-sm w-full">
48
+ <!-- Logo/Brand -->
49
+ <div class="mb-8 text-center">
50
+ <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl shadow-blue-500/25 shadow-lg">
51
+ <span class="h-7 w-7 text-white i-hugeicons-cube"></span>
52
+ </div>
53
+ <h1 class="font-semibold text-2xl text-neutral-900 dark:text-white">
54
+ {{ title() }}
55
+ </h1>
56
+ <p class="mt-2 text-neutral-500 text-sm dark:text-neutral-400">
57
+ {{ subtitle() }}
58
+ </p>
59
+ </div>
60
+
61
+ <!-- Form Card -->
62
+ <div class="p-6 bg-white dark:bg-neutral-900 border border-neutral-200/60 rounded-2xl dark:border-neutral-800 shadow-black/5 shadow-xl dark:shadow-black/20">
63
+ <div :if="checking()">
64
+ <div class="py-4 text-center">
65
+ <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-neutral-100 dark:bg-neutral-800 rounded-full">
66
+ <div class="h-6 w-6 text-neutral-500 animate-spin i-hugeicons-loading-03"></div>
67
+ </div>
68
+ <p class="text-neutral-500 text-sm dark:text-neutral-400">Checking your link</p>
69
+ </div>
70
+ </div>
71
+
72
+ <div :else-if="invalid()">
73
+ <div class="py-4 text-center">
74
+ <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-red-100 dark:bg-red-900/30 rounded-full">
75
+ <div class="h-6 w-6 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></div>
76
+ </div>
77
+ <h2 class="mb-2 font-medium text-lg text-neutral-900 dark:text-white">
78
+ This link no longer works
79
+ </h2>
80
+ <p class="mb-6 text-neutral-500 text-sm dark:text-neutral-400">
81
+ Reset links expire, and each one can be used only once. Ask for a fresh link and it will arrive in a moment.
82
+ </p>
83
+ <Button href="/forgot-password">
84
+ Send a new link
85
+ </Button>
86
+ </div>
87
+ </div>
88
+
89
+ <div :else-if="success()">
90
+ <div class="py-4 text-center">
91
+ <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-green-100 dark:bg-green-900/30 rounded-full">
92
+ <div class="h-6 w-6 text-green-600 dark:text-green-400 i-hugeicons-checkmark-circle-02"></div>
93
+ </div>
94
+ <h2 class="mb-2 font-medium text-lg text-neutral-900 dark:text-white">
95
+ Password changed
96
+ </h2>
97
+ <p class="mb-6 text-neutral-500 text-sm dark:text-neutral-400">
98
+ You can sign in with your new password now.
99
+ </p>
100
+ <Button href="/login">
101
+ Go to sign in
102
+ </Button>
103
+ </div>
104
+ </div>
105
+
106
+ <div :else>
107
+ <div :if="error()" role="alert" class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 rounded-lg dark:border-red-800">
108
+ <div class="flex gap-2 items-center">
109
+ <div class="h-4 w-4 text-red-600 dark:text-red-400 i-hugeicons-alert-circle"></div>
110
+ <p class="text-red-600 text-sm dark:text-red-400">{{ error() }}</p>
111
+ </div>
112
+ </div>
113
+
114
+ <form @submit.prevent="handleSubmit">
115
+ <div class="mb-4">
116
+ <Input
117
+ type="password"
118
+ label="New password"
119
+ placeholder="At least 8 characters"
120
+ autocomplete="new-password"
121
+ v-model:value="password"
122
+ required
123
+ :disabled="isLoading()"
124
+ />
125
+ </div>
126
+
127
+ <div class="mb-6">
128
+ <Input
129
+ type="password"
130
+ label="Confirm new password"
131
+ placeholder="Repeat your new password"
132
+ autocomplete="new-password"
133
+ v-model:value="confirmation"
134
+ :error="mismatch() ? 'Both passwords must match' : ''"
135
+ required
136
+ :disabled="isLoading()"
137
+ />
138
+ </div>
139
+
140
+ <Button
141
+ type="submit"
142
+ variant="primary"
143
+ size="lg"
144
+ fullWidth
145
+ :loading="isLoading()"
146
+ :disabled="!canSubmit()"
147
+ >
148
+ <span :if="isLoading()">Saving...</span>
149
+ <span :if="!isLoading()">Change password</span>
150
+ </Button>
151
+ </form>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Back to Login -->
156
+ <p class="mt-6 text-center text-neutral-500 text-sm dark:text-neutral-400">
157
+ <a href="/login" class="inline-flex gap-1 items-center font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300">
158
+ <div class="h-4 w-4 i-hugeicons-arrow-left-01"></div>
159
+ Back to sign in
160
+ </a>
161
+ </p>
162
+ </div>
163
+ </div>
@@ -0,0 +1,95 @@
1
+ <script client>
2
+ /**
3
+ * Container for the mailed reset link.
4
+ *
5
+ * The link the framework sends is `/password/reset/{token}?email={email}`
6
+ * (core/auth/src/password/reset.ts, overridable via
7
+ * `config.auth.passwordReset.url`), so both halves of the credential come off
8
+ * the URL: the token from the route, the address from the query string.
9
+ *
10
+ * The token is verified before any field is shown. `/password/verify-token`
11
+ * exists for exactly this, and it means an expired link says so immediately
12
+ * instead of after someone has chosen and typed a new password twice.
13
+ */
14
+ import { dashboardApi } from '../../../../functions/dashboard-api'
15
+
16
+ const route = useRoute()
17
+
18
+ const checking = state(true)
19
+ const invalid = state(false)
20
+ const isLoading = state(false)
21
+ const error = state('')
22
+ const success = state(false)
23
+
24
+ function token(): string {
25
+ return String(route.params.token || '')
26
+ }
27
+
28
+ function email(): string {
29
+ return String(route.query.email || '')
30
+ }
31
+
32
+ async function verify(): Promise<void> {
33
+ if (!token() || !email()) {
34
+ checking.set(false)
35
+ invalid.set(true)
36
+ return
37
+ }
38
+
39
+ try {
40
+ await dashboardApi('/password/verify-token', {
41
+ method: 'POST',
42
+ auth: false,
43
+ body: { token: token(), email: email() },
44
+ })
45
+ }
46
+ catch {
47
+ // Any failure here is the link, not the network: a token that verifies
48
+ // is the only thing that lets the form be worth showing.
49
+ invalid.set(true)
50
+ }
51
+ finally {
52
+ checking.set(false)
53
+ }
54
+ }
55
+
56
+ async function handleReset(data): Promise<void> {
57
+ isLoading.set(true)
58
+ error.set('')
59
+
60
+ try {
61
+ await dashboardApi('/password/reset', {
62
+ method: 'POST',
63
+ auth: false,
64
+ body: {
65
+ token: token(),
66
+ email: email(),
67
+ password: data.password,
68
+ password_confirmation: data.password_confirmation,
69
+ },
70
+ })
71
+ success.set(true)
72
+ }
73
+ catch (err) {
74
+ error.set(err instanceof Error ? err.message : 'Could not change your password')
75
+ }
76
+ finally {
77
+ isLoading.set(false)
78
+ }
79
+ }
80
+
81
+ effect(() => {
82
+ verify()
83
+ })
84
+ </script>
85
+
86
+ <template>
87
+ <ResetPassword
88
+ @submit="handleReset"
89
+ :checking="checking()"
90
+ :invalid="invalid()"
91
+ :isLoading="isLoading()"
92
+ :error="error()"
93
+ :success="success()"
94
+ />
95
+ </template>
@@ -1,4 +1,8 @@
1
1
  <script server>
2
+ // Laravel 5.7's 403 illustration (MIT), the same scene the framework's own
3
+ // error pages use, so an app that opts into this layout matches them. Pass
4
+ // `backgroundImage` to swap it for anything else.
5
+ const backgroundImage = props.backgroundImage || '/svgs/403.svg'
2
6
  const appName = props.appName || 'Your App'
3
7
  const supportEmail = props.supportEmail || 'support@example.com'
4
8
  </script>
@@ -17,30 +21,20 @@ const supportEmail = props.supportEmail || 'support@example.com'
17
21
  <h2 class="mb-4 font-bold text-6xl text-slate-900">
18
22
  Access Denied
19
23
  </h2>
20
- <p class="mb-8 font-bold text-8xl bounce" style="animation-duration: 1s; animation-iteration-count: 2;">
24
+ <p class="mb-8 font-bold text-8xl">
21
25
  403
22
26
  </p>
23
27
  <p class="mb-8 text-slate-600 text-xl">
24
28
  Sorry, you don't have permission to access this page.
25
29
  </p>
26
30
 
27
- <div class="flex gap-4 sm:flex-row">
28
- <a
29
- href="/"
30
- class="flex items-center justify-center px-6 py-3 w-full sm:w-auto text-white bg-slate-500 hover:bg-slate-600 rounded-lg duration-200 transition-colors"
31
- >
32
- <i class="mr-2 h-5 w-5 i-hugeicons-home-03"></i>
33
- Back to Home
34
- </a>
35
-
36
- <a
37
- href="javascript:history.back()"
38
- class="flex items-center justify-center px-6 py-3 w-full sm:w-auto text-slate-700 hover:bg-slate-50 border border-slate-300 rounded-lg duration-200 transition-colors"
39
- >
40
- <i class="mr-2 h-5 w-5 i-hugeicons-arrow-left-02"></i>
41
- Go Back
42
- </a>
43
- </div>
31
+ <a
32
+ href="/"
33
+ class="inline-flex items-center justify-center px-6 py-3 text-white bg-slate-500 hover:bg-slate-600 rounded-lg duration-200 transition-colors"
34
+ >
35
+ <i class="mr-2 h-5 w-5 i-hugeicons-home-03"></i>
36
+ Back to Home
37
+ </a>
44
38
  </div>
45
39
 
46
40
  <div class="mt-16 pt-8 border-slate-200 border-t">
@@ -54,135 +48,8 @@ const supportEmail = props.supportEmail || 'support@example.com'
54
48
  </div>
55
49
  </div>
56
50
 
57
- <!-- Right side SVG -->
58
- <div class="flex overflow-hidden relative items-center justify-center h-[100vh] w-full lg:w-1/2">
59
- <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
60
- <defs>
61
- <linearGradient id="a" x1="50%" x2="50%" y1="100%" y2="0%">
62
- <stop offset="0" stop-color="#76c3c3"/>
63
- <stop offset="1" stop-color="#183468"/>
64
- </linearGradient>
65
- <linearGradient id="b" x1="100%" x2="0%" y1="50%" y2="50%">
66
- <stop offset="0" stop-color="#486587"/>
67
- <stop offset=".3323" stop-color="#183352"/>
68
- <stop offset=".6667" stop-color="#264a6e"/>
69
- <stop offset="1" stop-color="#183352"/>
70
- </linearGradient>
71
- <linearGradient id="c" x1="49.87%" x2="48.5%" y1="3.62%" y2="100%">
72
- <stop offset="0" stop-color="#e0f2fa"/>
73
- <stop offset=".0898" stop-color="#89bed6"/>
74
- <stop offset=".3298" stop-color="#1e3c6e"/>
75
- <stop offset="1" stop-color="#1b376b"/>
76
- </linearGradient>
77
- <linearGradient id="d">
78
- <stop offset="0" stop-color="#b0ddf1"/>
79
- <stop offset="1" stop-color="#325c82"/>
80
- </linearGradient>
81
- <linearGradient id="e" x1="49.87%" x2="49.87%" xlink:href="#d" y1="3.62%" y2="77.75%"/>
82
- <linearGradient id="f">
83
- <stop offset="0" stop-color="#1d3a6d"/>
84
- <stop offset="1" stop-color="#467994"/>
85
- </linearGradient>
86
- <linearGradient id="g" x1="91.59%" x2="66.97%" xlink:href="#f" y1="5.89%" y2="100%"/>
87
- <linearGradient id="h" x1="97.27%" x2="52.53%" xlink:href="#f" y1="6.88%" y2="100%"/>
88
- <linearGradient id="i" x1="82.73%" x2="41.46%" xlink:href="#f" y1="41.06%" y2="167.23%"/>
89
- <linearGradient id="j" x1="49.87%" x2="49.87%" xlink:href="#d" y1="3.62%" y2="100.77%"/>
90
- <linearGradient id="k" x1="100%" x2="72.45%" xlink:href="#f" y1="0%" y2="85.2%"/>
91
- <linearGradient id="l" x1="100%" x2="62.1%" y1="0%" y2="68.86%">
92
- <stop offset="0" stop-color="#163055"/>
93
- <stop offset="1" stop-color="#2f587f"/>
94
- </linearGradient>
95
- <circle id="m" cx="180" cy="102" r="40"/>
96
- <filter id="n" height="340%" width="340%" x="-120%" y="-120%">
97
- <feOffset in="SourceAlpha" result="shadowOffsetOuter1"/>
98
- <feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="32"/>
99
- <feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.696473053 0"/>
100
- </filter>
101
- <linearGradient id="o" x1="0%" y1="50%" y2="50%">
102
- <stop offset="0" stop-color="#fff" stop-opacity="0"/>
103
- <stop offset="1" stop-color="#fff"/>
104
- </linearGradient>
105
- </defs>
106
- <g fill="none" fill-rule="evenodd">
107
- <path d="m0 0h1024v1024h-1024z" fill="url(#a)"/>
108
- <g transform="translate(761 481)">
109
- <path d="m96 27 4-1v11h-4z" fill="#8dbcd2"/>
110
- <path d="m76 23 4-1v15h-4z" fill="#8dbcd2"/>
111
- <g fill="#183352">
112
- <path d="m40 22 4 1v14h-4z"/>
113
- <path d="m20 26 4 1v14h-4z"/>
114
- <path d="m59 0h2v20h-2z" opacity=".5"/>
115
- </g>
116
- <path d="m61 0c3 0 3 2 6 2s3-2 6-2 3 2 6 2v8c-3 0-3-2-6-2s-3 2-6 2-3-2-6-2z" fill="url(#b)"/>
117
- <path d="m50 20 10-2v110h-60l10-100 10-2v10.92l10-.98v-11.94l10-2v12.96l10-.98z" fill="#8dbcd2"/>
118
- <path d="m100 26 10 2 10 100h-60v-110l10 2v13.98l10 .98v-12.96l10 2v11.94l10 .98z" fill="#183352"/>
119
- </g>
120
- <g transform="translate(0 565)">
121
- <path d="m1024 385h-1024v-278.14c118.4 21.09 185.14 57.03 327.4 48.14 198.54-12.4 250-125 500-125 90.18 0 147.92 16.3 196.6 37.12z" fill="url(#c)"/>
122
- <path d="m1024 355h-1024v-275.44c76.46-35.75 137.14-79.56 285-79.56 250 0 301.46 112.6 500 125 103.24 6.45 166.7-10.7 239-28.66z" fill="url(#e)"/>
123
- <path d="m344.12 130.57c23.1 13.47-25.27 81.95-145.12 205.43h649c-344.06-141.7-512.02-210.17-503.88-205.43z" fill="url(#e)"/>
124
- <path d="m0 336v-256.44c76.46-35.75 137.14-79.56 285-79.56 71.14 0 86.22 26.04 32.5 82-48 50 147.33 58.02 36 136.5-40.67 28.67 21.17 67.83 185.5 117.5z" fill="url(#g)"/>
125
- <path d="m317.5 82c-48 50 147.33 58.02 36 136.5-40.67 28.67 21.17 67.83 185.5 117.5h-484z" fill="url(#h)"/>
126
- <path d="m353.5 218.5c-40.67 28.67 21.17 67.83 185.5 117.5h-364z" fill="url(#i)"/>
127
- <path d="m0 459v-194.46c100.25 21.2 167.18 50.29 296.67 42.19 198.57-12.43 250.04-125.15 500.07-125.15 109.75 0 171.47 24.16 227.26 51.25v226.17z" fill="url(#j)"/>
128
- <path d="m1024 459h-177.84c51.95-58.9 48.86-97.16-9.28-114.78-186.64-56.58-101.76-162.64-39.97-162.64 109.64 0 171.34 24.12 227.09 51.19z" fill="url(#k)"/>
129
- <path d="m1024 459h-177.81c52.01-59.01 48.94-97.34-9.22-115l187.03 53.48z" fill="url(#l)"/>
130
- </g>
131
- <g id="moonGroup">
132
- <g transform="translate(94 23)">
133
- <use fill="#000" filter="url(#n)" xlink:href="#m"/>
134
- <use fill="#d2f1fe" xlink:href="#m"/>
135
- </g>
136
- </g>
137
- <g transform="translate(94 23)">
138
- <use fill="#000" filter="url(#n)" xlink:href="#m"/>
139
- <g fill="#fff">
140
- <circle cx="123" cy="255" fill-opacity=".4" r="3"/>
141
- <circle cx="2" cy="234" r="2"/>
142
- <circle cx="33" cy="65" r="3"/>
143
- <circle cx="122" cy="2" r="2"/>
144
- <circle cx="72" cy="144" r="2"/>
145
- <circle cx="282" cy="224" r="2"/>
146
- <circle cx="373" cy="65" opacity=".4" r="3"/>
147
- <circle cx="433" cy="255" r="3"/>
148
- </g>
149
- <path d="m373.25 325.25a5 5 0 0 0 0-10h-75v10z" fill="url(#o)" opacity=".4" transform="matrix(.70710678 .70710678 -.70710678 .70710678 325.523078 -145.38023)"/>
150
- <g fill="#fff">
151
- <circle cx="363" cy="345" r="3"/>
152
- <circle cx="513" cy="115" r="3"/>
153
- <circle cx="723" cy="5" opacity=".4" r="3"/>
154
- <circle cx="422" cy="134" r="2"/>
155
- <circle cx="752" cy="204" r="2"/>
156
- <circle cx="672" cy="114" r="2"/>
157
- <circle cx="853" cy="255" opacity=".4" r="3"/>
158
- <circle cx="623" cy="225" r="3"/>
159
- <circle cx="823" cy="55" r="3"/>
160
- <circle cx="902" cy="144" r="2"/>
161
- <circle cx="552" cy="14" r="2"/>
162
- </g>
163
- </g>
164
- <path d="m796 535a4 4 0 0 1 4 4v20h-8v-20a4 4 0 0 1 4-4z" fill="#486587"/>
165
- <path d="m798 535.54a4 4 0 0 0 -2 3.46v20h-4v-20a4 4 0 0 1 6-3.46zm48-.54a4 4 0 0 1 4 4v20h-8v-20a4 4 0 0 1 4-4z" fill="#071423"/>
166
- <path d="m846 559v-20a4 4 0 0 0 -2-3.46 4 4 0 0 1 6 3.46v20z" fill="#8dbcd2"/>
167
- <path d="m554.67 131.48a9.46 9.46 0 0 1 13.33 0 9.46 9.46 0 0 0 13.33 0l13.33-13.24a28.39 28.39 0 0 1 40 0l10 9.93a14.2 14.2 0 0 0 20 0 14.2 14.2 0 0 1 20 0l.6.6a31.8 31.8 0 0 1 9.4 22.56h-146.66v-3.84c0-6.01 2.4-11.78 6.67-16.01zm196.33-123.23c11.07-11 28.93-11 40 0l10 9.94a14.19 14.19 0 0 0 20 0 14.19 14.19 0 0 1 20 0 16.36 16.36 0 0 0 21.3 1.5l8.7-6.47a33.47 33.47 0 0 1 40 0l4.06 3.03a39.6 39.6 0 0 1 15.94 31.75h-200c0-12.72 8.93-28.75 20-39.75zm-736.9 66.89.9-.9a21.29 21.29 0 0 1 30 0 21.29 21.29 0 0 0 30 0l10-9.93a35.48 35.48 0 0 1 50 0l15 14.9a14.2 14.2 0 0 0 20 0 14.2 14.2 0 0 1 20 0c6.4 6.35 10 15 10 24.02v5.77h-200c0-12.71 5.07-24.9 14.1-33.86z" fill="#fff" opacity=".07" transform="translate(54 301)"/>
168
- </g>
169
- </svg>
51
+ <!-- Right side illustration -->
52
+ <div class="overflow-hidden relative h-64 w-full sm:h-80 lg:h-auto lg:w-1/2">
53
+ <img class="absolute inset-0 object-cover h-full w-full" src="{{ backgroundImage }}" alt="">
170
54
  </div>
171
55
  </div>
172
-
173
- <style>
174
- @keyframes floatMoon {
175
- 0% { transform: translateX(0); }
176
- 50% { transform: translateX(20px); }
177
- 100% { transform: translateX(0); }
178
- }
179
- #moonGroup {
180
- animation: floatMoon 4s ease-in-out infinite;
181
- }
182
-
183
- @keyframes waveTop {
184
- 0% { transform: translateY(0); }
185
- 50% { transform: translateY(-2px); }
186
- 100% { transform: translateY(0); }
187
- }
188
- </style>
@@ -1,5 +1,8 @@
1
1
  <script server>
2
- const backgroundImage = props.backgroundImage || '/images/background-auth.jpg'
2
+ // Laravel 5.7's 404 illustration (MIT), the same scene the framework's own
3
+ // error pages use, so an app that opts into this layout matches them. Pass
4
+ // `backgroundImage` to swap it for anything else.
5
+ const backgroundImage = props.backgroundImage || '/svgs/404.svg'
3
6
  </script>
4
7
 
5
8
  <div class="flex relative justify-center shrink-0 md:px-12 lg:px-0 min-h-full">