@stacksjs/defaults 0.70.358 → 0.70.362

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.
@@ -114,10 +114,10 @@ Use `{"action":"assert","selector":"button","text":"Save","focused":true}` to ve
114
114
 
115
115
  ### Crawl (whole-site browser audit)
116
116
  ```bash
117
- bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts crawl <url> [--viewport 1280x900] [--max 500] [--path /extra-route] [--settle 350] [--progress]
117
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts crawl <url> [--viewport 1280x900] [--max 500] [--path /extra-route] [--settle 350] [--progress] [--summary]
118
118
  ```
119
- Uses a fresh isolated page target per route, relaunching Chromium if its DevTools
120
- session exits during a long audit, while
119
+ Uses a fresh isolated page target per route, waits for each target to fully close,
120
+ and relaunches Chromium if its DevTools session or target lifecycle fails during a long audit, while
121
121
  following every same-origin link it discovers.
122
122
  The viewport is deterministic and defaults to `1280x900`; pass `--viewport 768x1024`
123
123
  to repeat the same route audit at a tablet breakpoint.
@@ -125,7 +125,8 @@ Each page is checked for a non-200 document, console exceptions, failed
125
125
  requests, and horizontal overflow. Repeat `--path` to seed routes that are not
126
126
  linked from the starting page. The command prints every crawled path and exits
127
127
  nonzero when any page fails. Add `--progress` to stream each completed page
128
- during a long audit.
128
+ during a long audit. Add `--summary` to report totals and compact failure
129
+ diagnostics without printing every visited path or overflowing element.
129
130
 
130
131
  ## Stacks-Specific QA
131
132
 
@@ -264,12 +264,38 @@ async function createPage(port: number): Promise<BrowserPage> {
264
264
  }
265
265
  }
266
266
 
267
- async function closePage(port: number, page: BrowserPage): Promise<void> {
268
- page.cdp.close()
267
+ async function closePage(port: number, page: BrowserPage): Promise<boolean> {
269
268
  try {
270
- await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(page.targetId)}`)
269
+ const response = await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(page.targetId)}`)
270
+ if (!response.ok) {
271
+ page.cdp.close()
272
+ return false
273
+ }
274
+ }
275
+ catch {
276
+ page.cdp.close()
277
+ return false
278
+ }
279
+ page.cdp.close()
280
+
281
+ // Chromium acknowledges target closure before every renderer/helper has
282
+ // actually left the target list. Creating the next target immediately can
283
+ // accumulate closing renderers across a large crawl, eventually dropping
284
+ // Network and Runtime events. Wait for the target to disappear so each route
285
+ // starts with the isolation the command promises.
286
+ for (let attempt = 0; attempt < 40; attempt++) {
287
+ try {
288
+ const targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json() as any[]
289
+ if (!targets.some(target => target.id === page.targetId))
290
+ return true
291
+ }
292
+ catch {
293
+ return false
294
+ }
295
+ await Bun.sleep(25)
271
296
  }
272
- catch { /* the target or browser may already have closed */ }
297
+
298
+ return false
273
299
  }
274
300
 
275
301
  function kill(s: Session): void {
@@ -864,7 +890,11 @@ async function main() {
864
890
  }
865
891
  }
866
892
  const replacePage = async (): Promise<void> => {
867
- await closePage(session.port, browserPage)
893
+ const closed = await closePage(session.port, browserPage)
894
+ if (!closed) {
895
+ kill(session)
896
+ session = await launch()
897
+ }
868
898
  browserPage = await createReplacementPage()
869
899
  cdp = browserPage.cdp
870
900
  }
@@ -1019,6 +1049,16 @@ async function main() {
1019
1049
  || page.consoleErrors.length > 0
1020
1050
  || page.failedRequests.length > 0
1021
1051
  || page.horizontalOverflowPx > 0)
1052
+ const summaryOnly = Boolean(flags.summary)
1053
+ const reportedFailures = summaryOnly
1054
+ ? failures.map(page => ({
1055
+ path: page.path,
1056
+ status: page.status,
1057
+ consoleErrorCount: page.consoleErrors.length,
1058
+ failedRequestCount: page.failedRequests.length,
1059
+ horizontalOverflowPx: page.horizontalOverflowPx,
1060
+ }))
1061
+ : failures
1022
1062
  console.log(JSON.stringify({
1023
1063
  start: start.href,
1024
1064
  browser: session.browser,
@@ -1026,8 +1066,9 @@ async function main() {
1026
1066
  crawled: pages.length,
1027
1067
  remaining: queue.length,
1028
1068
  max,
1029
- paths: pages.map(page => page.path),
1030
- failures,
1069
+ failureCount: failures.length,
1070
+ ...(!summaryOnly ? { paths: pages.map(page => page.path) } : {}),
1071
+ failures: reportedFailures,
1031
1072
  }, null, 2))
1032
1073
  if (failures.length > 0)
1033
1074
  process.exitCode = 1
@@ -1,16 +1,15 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { ask } from '@stacksjs/ai'
4
+ import { log } from '@stacksjs/logging'
5
+ import { response } from '@stacksjs/router'
3
6
  import { schema } from '@stacksjs/validation'
4
7
 
5
- // TODO: this should have been auto-generated
6
- interface Request {
7
- question: string
8
- }
9
-
10
8
  export default new Action({
11
9
  name: 'AiAskAction',
12
10
  description: 'Ask AI',
13
11
  method: 'POST',
12
+ apiResponse: true,
14
13
 
15
14
  validations: {
16
15
  question: {
@@ -19,22 +18,18 @@ export default new Action({
19
18
  },
20
19
  },
21
20
 
22
- async handle(request: Request) {
23
- try {
24
- const question = request.question
25
-
26
- console.log(`Question received: ${question}`)
21
+ async handle(request: RequestInstance) {
22
+ await request.validate()
23
+ const question = String(request.get('question') || '').trim()
27
24
 
28
- return {
25
+ try {
26
+ return response.json({
29
27
  data: await ask(question),
30
- }
28
+ })
31
29
  }
32
30
  catch (error) {
33
- console.error('Error:', error)
34
-
35
- return {
36
- error: 'An error occurred while processing your request.',
37
- }
31
+ log.error('[ai] Ask request failed', { error })
32
+ return response.json({ message: 'The AI provider could not answer the question.' }, 502)
38
33
  }
39
34
  },
40
35
  })
@@ -1,16 +1,15 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { summarize } from '@stacksjs/ai'
4
+ import { log } from '@stacksjs/logging'
5
+ import { response } from '@stacksjs/router'
3
6
  import { schema } from '@stacksjs/validation'
4
7
 
5
- // TODO: this should have been auto-generated
6
- interface Request {
7
- text: string
8
- }
9
-
10
8
  export default new Action({
11
9
  name: 'AiSummaryAction',
12
10
  description: 'Summary AI',
13
11
  method: 'POST',
12
+ apiResponse: true,
14
13
 
15
14
  validations: {
16
15
  text: {
@@ -19,22 +18,18 @@ export default new Action({
19
18
  },
20
19
  },
21
20
 
22
- async handle(request: Request) {
23
- try {
24
- const text = request.text
25
-
26
- console.log(`Text received: ${text}`)
21
+ async handle(request: RequestInstance) {
22
+ await request.validate()
23
+ const text = String(request.get('text') || '').trim()
27
24
 
28
- return {
25
+ try {
26
+ return response.json({
29
27
  data: await summarize(text),
30
- }
28
+ })
31
29
  }
32
30
  catch (error) {
33
- console.error('Error:', error)
34
-
35
- return {
36
- error: 'An error occurred while processing your request.',
37
- }
31
+ log.error('[ai] Summary request failed', { error })
32
+ return response.json({ message: 'The AI provider could not summarize the text.' }, 502)
38
33
  }
39
34
  },
40
35
  })
@@ -1,15 +1,16 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { log } from '@stacksjs/logging'
4
+ import { response } from '@stacksjs/router'
3
5
  import { schema } from '@stacksjs/validation'
4
6
 
5
- interface Request {
6
- message: string
7
- level: 'info' | 'warn' | 'error'
8
- }
7
+ type LogLevel = 'info' | 'warn' | 'error'
9
8
 
10
9
  export default new Action({
11
10
  name: 'Dummy Logger',
12
11
  description: 'This action is used to demo how to POST to a server and upon success, log a message.',
12
+ method: 'POST',
13
+ apiResponse: true,
13
14
 
14
15
  // the request object is optional, but if it is provided, it will be used for validation
15
16
  validations: {
@@ -29,14 +30,13 @@ export default new Action({
29
30
  },
30
31
  },
31
32
 
32
- // handle(request: { message: string, level: 'info' | 'warn' | 'error' }) {
33
- handle(request?: Request) {
34
- if (!request)
35
- return 'No request was provided.'
33
+ async handle(request: RequestInstance) {
34
+ await request.validate()
36
35
 
37
- // TODO: need to validate with ts-validation
38
- log[request.level](request.message)
36
+ const message = String(request.get('message') || '').trim()
37
+ const level = String(request.get('level')) as LogLevel
38
+ log[level](message)
39
39
 
40
- return `Logged "${request.message}" at "${request.level}" level`
40
+ return response.json({ level, message: `Logged at ${level} level.` })
41
41
  },
42
42
  })
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.358",
5
+ "version": "0.70.362",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.358",
5
+ "version": "0.70.362",
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",
@@ -6,17 +6,16 @@ const record = useReactiveProp('record', null as CampaignRecord | null)
6
6
  const busy = useReactiveProp('busy', false)
7
7
  const error = useReactiveProp('error', '')
8
8
  const emit = defineEmits()
9
+ const description = derived(() => `The persisted campaign ${record()?.name || ''} will be permanently removed. Campaigns with delivery history are retained.`)
9
10
  </script>
10
11
 
11
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
12
- <button type="button" aria-label="Close delete campaign dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
13
- <section role="dialog" aria-modal="true" aria-labelledby="campaign-delete-title" class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
14
- <h2 id="campaign-delete-title" class="font-semibold text-gray-900 text-lg dark:text-white">Delete campaign?</h2>
15
- <p class="mt-2 text-gray-500 text-sm dark:text-neutral-400">The persisted campaign <strong>{{ record()?.name || '' }}</strong> will be permanently removed. Campaigns with delivery history are retained.</p>
16
- <div :if="error()" role="alert" class="mt-4 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">{{ error() }}</div>
17
- <div class="flex gap-2 justify-end mt-6">
18
- <Button :disabled="busy()" variant="secondary" @click="emit('close')">Cancel</Button>
19
- <Button :loading="busy()" variant="danger" @click="emit('confirm')">Delete campaign</Button>
20
- </div>
21
- </section>
22
- </div>
12
+ <ConfirmDialog
13
+ :isOpen="open()"
14
+ title="Delete campaign?"
15
+ :description="description()"
16
+ confirmLabel="Delete campaign"
17
+ :busy="busy()"
18
+ :error="error()"
19
+ @close="emit('close')"
20
+ @confirm="emit('confirm')"
21
+ />
@@ -6,20 +6,20 @@ const record = useReactiveProp('record', null as MarketingListRecord | null)
6
6
  const busy = useReactiveProp('busy', false)
7
7
  const error = useReactiveProp('error', '')
8
8
  const emit = defineEmits()
9
+ const protectedRecord = derived(() => Boolean(record()?.subscriberCount) || Boolean(record()?.campaignCount))
10
+ const description = derived(() => protectedRecord()
11
+ ? `${record()?.name || 'This email list'} has membership or campaign history. Archive it instead to preserve relationships.`
12
+ : `${record()?.name || 'This email list'} will be permanently removed.`)
9
13
  </script>
10
14
 
11
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
12
- <button type="button" aria-label="Close delete email list dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
13
- <section role="dialog" aria-modal="true" aria-labelledby="marketing-list-delete-title" class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
14
- <h2 id="marketing-list-delete-title" class="font-semibold text-gray-900 text-lg dark:text-white">Delete email list?</h2>
15
- <p class="mt-2 text-gray-600 text-sm dark:text-neutral-300"><span class="font-medium">{{ record()?.name }}</span> will be permanently removed.</p>
16
- <p :if="record()?.subscriberCount || record()?.campaignCount" class="mt-3 p-3 text-amber-800 text-sm dark:text-amber-200 bg-amber-50 dark:bg-amber-950/30 rounded-lg">This list has membership or campaign history. Archive it instead to preserve relationships.</p>
17
- <p :if="error()" role="alert" class="mt-3 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 rounded-lg">{{ error() }}</p>
18
- <footer class="flex gap-2 justify-end mt-6">
19
- <Button :disabled="busy()" variant="secondary" @click="emit('close')">Cancel</Button>
20
- <Button :disabled="busy() || Boolean(record()?.subscriberCount) || Boolean(record()?.campaignCount)" :loading="busy()" variant="danger" @click="emit('confirm')">
21
- Delete email list
22
- </Button>
23
- </footer>
24
- </section>
25
- </div>
15
+ <ConfirmDialog
16
+ :isOpen="open()"
17
+ title="Delete email list?"
18
+ :description="description()"
19
+ confirmLabel="Delete email list"
20
+ :busy="busy()"
21
+ :disabled="protectedRecord()"
22
+ :error="error()"
23
+ @close="emit('close')"
24
+ @confirm="emit('confirm')"
25
+ />
@@ -6,17 +6,16 @@ const record = useReactiveProp('record', null as SocialPostRecord | null)
6
6
  const busy = useReactiveProp('busy', false)
7
7
  const error = useReactiveProp('error', '')
8
8
  const emit = defineEmits()
9
+ const description = derived(() => `The persisted ${record()?.platform || 'social'} post will be permanently removed. This does not remove a post already published on the external platform.`)
9
10
  </script>
10
11
 
11
- <div :if="open()" class="flex fixed inset-0 z-50 items-center justify-center p-4 dashboard-modal-layer">
12
- <button type="button" aria-label="Close delete social post dialog" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
13
- <section role="dialog" aria-modal="true" aria-labelledby="social-post-delete-title" class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700 shadow-xl">
14
- <h2 id="social-post-delete-title" class="font-semibold text-gray-900 text-lg dark:text-white">Delete social post?</h2>
15
- <p class="mt-2 text-gray-600 text-sm dark:text-neutral-300">The persisted {{ record()?.platform }} post will be permanently removed. This does not remove a post already published on the external platform.</p>
16
- <p :if="error()" role="alert" class="mt-3 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 rounded-lg">{{ error() }}</p>
17
- <footer class="flex gap-2 justify-end mt-6">
18
- <Button :disabled="busy()" variant="secondary" @click="emit('close')">Cancel</Button>
19
- <Button :loading="busy()" variant="danger" @click="emit('confirm')">Delete social post</Button>
20
- </footer>
21
- </section>
22
- </div>
12
+ <ConfirmDialog
13
+ :isOpen="open()"
14
+ title="Delete social post?"
15
+ :description="description()"
16
+ confirmLabel="Delete social post"
17
+ :busy="busy()"
18
+ :error="error()"
19
+ @close="emit('close')"
20
+ @confirm="emit('confirm')"
21
+ />
@@ -8,6 +8,8 @@ const confirmLabel = useReactiveProp('confirmLabel', 'Delete')
8
8
  const cancelLabel = useReactiveProp('cancelLabel', 'Cancel')
9
9
  const variant = useReactiveProp('variant', 'danger' as ConfirmVariant)
10
10
  const busy = useReactiveProp('busy', false)
11
+ const disabled = useReactiveProp('disabled', false)
12
+ const error = useReactiveProp('error', '')
11
13
  const emit = defineEmits()
12
14
 
13
15
  function close(): void {
@@ -16,7 +18,7 @@ function close(): void {
16
18
  }
17
19
 
18
20
  function confirm(): void {
19
- if (!busy())
21
+ if (!busy() && !disabled())
20
22
  emit('confirm')
21
23
  }
22
24
 
@@ -32,57 +34,53 @@ function iconName(): string {
32
34
 
33
35
  </script>
34
36
 
35
- <template :if="open()">
36
- <div class="fixed inset-0 overflow-y-auto z-[55] dashboard-modal-layer" @keydown.escape="close">
37
- <button type="button" class="fixed inset-0 w-full bg-black/50 backdrop-blur-sm cursor-default" aria-label="Close confirmation dialog" @click="close"></button>
38
-
39
- <div class="flex relative items-center justify-center p-4 min-h-full">
40
- <section
41
- role="alertdialog"
42
- aria-modal="true"
43
- :aria-label="title()"
44
- class="relative p-6 max-w-md w-full bg-white dark:bg-neutral-900 rounded-xl shadow-2xl"
45
- >
46
- <div class="flex gap-4 items-start">
47
- <div class="shrink-0 p-2.5 rounded-lg" :class="iconClass()">
48
- <span class="block h-5 w-5" :class="iconName()"></span>
49
- </div>
50
- <div class="flex-1 min-w-0">
51
- <h3 class="font-semibold text-lg text-neutral-900 dark:text-white">
52
- {{ title() }}
53
- </h3>
54
- <p class="mt-1 text-neutral-500 text-sm dark:text-neutral-400">
55
- {{ description() }}
56
- </p>
57
- </div>
58
- </div>
37
+ <Modal
38
+ :isOpen="open()"
39
+ :title="title()"
40
+ :description="description()"
41
+ :closable="!busy()"
42
+ role="alertdialog"
43
+ size="sm"
44
+ @close="close"
45
+ >
46
+ <template #header-leading>
47
+ <div class="shrink-0 p-2.5 rounded-lg" :class="iconClass()">
48
+ <span class="block h-5 w-5" :class="iconName()"></span>
49
+ </div>
50
+ </template>
59
51
 
60
- <div class="flex gap-2 items-center justify-end mt-6">
61
- <Button
62
- variant="secondary"
63
- :disabled="busy()"
64
- @click="close"
65
- >
66
- {{ cancelLabel() }}
67
- </Button>
68
- <Button
69
- :if="variant() === 'warning'"
70
- variant="warning"
71
- :loading="busy()"
72
- @click="confirm"
73
- >
74
- {{ confirmLabel() }}
75
- </Button>
76
- <Button
77
- :if="variant() !== 'warning'"
78
- variant="danger"
79
- :loading="busy()"
80
- @click="confirm"
81
- >
82
- {{ confirmLabel() }}
83
- </Button>
84
- </div>
85
- </section>
52
+ <template #footer>
53
+ <div class="w-full">
54
+ <div :if="error()" role="alert" class="mb-4 p-3 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900">
55
+ {{ error() }}
56
+ </div>
57
+ <div class="flex gap-2 items-center justify-end">
58
+ <Button
59
+ variant="secondary"
60
+ :disabled="busy()"
61
+ @click="close"
62
+ >
63
+ {{ cancelLabel() }}
64
+ </Button>
65
+ <Button
66
+ :if="variant() === 'warning'"
67
+ variant="warning"
68
+ :loading="busy()"
69
+ :disabled="disabled()"
70
+ @click="confirm"
71
+ >
72
+ {{ confirmLabel() }}
73
+ </Button>
74
+ <Button
75
+ :if="variant() !== 'warning'"
76
+ variant="danger"
77
+ :loading="busy()"
78
+ :disabled="disabled()"
79
+ @click="confirm"
80
+ >
81
+ {{ confirmLabel() }}
82
+ </Button>
83
+ </div>
86
84
  </div>
87
- </div>
88
- </template>
85
+ </template>
86
+ </Modal>
@@ -5,9 +5,10 @@ interface ModalProps {
5
5
  description?: string
6
6
  size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
7
7
  closable?: boolean
8
+ role?: 'dialog' | 'alertdialog'
8
9
  }
9
10
 
10
- const { isOpen = false, title = '', description = '', size = 'md', closable = true } = defineProps<ModalProps>()
11
+ const { isOpen = false, title = '', description = '', size = 'md', closable = true, role = 'dialog' } = defineProps<ModalProps>()
11
12
 
12
13
  const sizeClasses = {
13
14
  sm: 'max-w-sm',
@@ -22,6 +23,7 @@ const open = useReactiveProp('isOpen', false)
22
23
  const liveClosable = useReactiveProp('closable', true)
23
24
  const liveTitle = useReactiveProp('title', '')
24
25
  const liveDescription = useReactiveProp('description', '')
26
+ const liveRole = useReactiveProp('role', 'dialog')
25
27
  const dialog = useRef<HTMLDialogElement>('dashboardModalDialog')
26
28
  const activeElement = useActiveElement()
27
29
  const scrollLocked = useScrollLock()
@@ -72,7 +74,7 @@ effect(() => {
72
74
  <dialog
73
75
  :if="open()"
74
76
  ref="dashboardModalDialog"
75
- role="dialog"
77
+ :role="liveRole()"
76
78
  aria-modal="true"
77
79
  :aria-label="liveTitle() || 'Dialog'"
78
80
  class="fixed inset-0 m-0 p-0 h-dvh max-h-none w-screen max-w-none overflow-y-auto z-[55] bg-transparent dashboard-modal-layer dashboard-modal-dialog"
@@ -84,8 +86,9 @@ effect(() => {
84
86
  class="relative w-full rounded-xl bg-white dark:bg-neutral-900 shadow-2xl {{ sizeClasses[size] }}"
85
87
  >
86
88
  <!-- Header -->
87
- @if(title || closable)
88
- <div class="flex gap-4 items-start justify-between p-6 border-b border-neutral-200 dark:border-neutral-700">
89
+ <div :if="liveTitle() || liveClosable()" class="flex gap-4 items-start justify-between p-6 border-b border-neutral-200 dark:border-neutral-700">
90
+ <div class="flex gap-3 items-start min-w-0">
91
+ <slot name="header-leading" />
89
92
  <div class="min-w-0">
90
93
  <h3 :if="liveTitle()" class="font-semibold text-lg text-neutral-900 dark:text-white">
91
94
  {{ liveTitle() }}
@@ -94,26 +97,27 @@ effect(() => {
94
97
  {{ liveDescription() }}
95
98
  </p>
96
99
  </div>
97
-
98
- @if(closable)
99
- <Button
100
- variant="ghost"
101
- size="sm"
102
- iconOnly
103
- ariaLabel="Close modal"
104
- @click="closeModal"
105
- >
106
- <div class="h-5 w-5 i-hugeicons-cancel-01"></div>
107
- </Button>
108
- @endif
109
100
  </div>
110
- @endif
111
101
 
112
- <!-- Body -->
113
- <div class="p-6">
114
- <slot />
102
+ <Button
103
+ :if="liveClosable()"
104
+ variant="ghost"
105
+ size="sm"
106
+ iconOnly
107
+ ariaLabel="Close modal"
108
+ @click="closeModal"
109
+ >
110
+ <div class="h-5 w-5 i-hugeicons-cancel-01"></div>
111
+ </Button>
115
112
  </div>
116
113
 
114
+ <!-- Body -->
115
+ @if($slots.default)
116
+ <div class="p-6">
117
+ <slot />
118
+ </div>
119
+ @endif
120
+
117
121
  @if($slots.footer)
118
122
  <div class="flex items-center justify-end px-6 py-4 border-neutral-200 border-t dark:border-neutral-700">
119
123
  <slot name="footer" />