firstly 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # firstly
2
2
 
3
+ ## 0.6.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#298](https://github.com/jycouet/firstly/pull/298) [`626db86`](https://github.com/jycouet/firstly/commit/626db8664ddcd480bc941ba38c51aaee742188c3) Thanks [@jycouet](https://github.com/jycouet)! - fix(deps): declare `@kitql/helpers` as a runtime dependency
8
+
9
+ `esm/index.js` does `import * as h from '@kitql/helpers'` (and re-exports it /
10
+ builds `ff_Log` from it), but the package only listed it under
11
+ `devDependencies`. Consumers that didn't happen to hoist it got
12
+ `Cannot find module '@kitql/helpers'` at runtime. Moved it into `dependencies`.
13
+
14
+ ## 0.6.2
15
+
16
+ ### Patch Changes
17
+
18
+ - [#296](https://github.com/jycouet/firstly/pull/296) [`ae7d8ec`](https://github.com/jycouet/firstly/commit/ae7d8ec532e677d42e1e5730c1ef58a29bb32062) Thanks [@jycouet](https://github.com/jycouet)! - fix(svelte): portal `FF_DialogManager` panels to `<body>`
19
+
20
+ Dialog/confirm/prompt panels rendered wherever `<FF_DialogManager>` sat in the
21
+ layout - inside the app root that the manager marks `inert` to trap focus. When
22
+ the `inert` effect won the race against the panel's autofocus, the whole panel
23
+ stopped receiving pointer events (real clicks died; `elementFromPoint` returned
24
+ `<body>`; AT saw it as "ignored"), while synthetic `.click()` still worked - so
25
+ it looked fine in tests but was dead under a real mouse.
26
+
27
+ Panels are now portaled to `<body>` (true siblings of the app root, matching the
28
+ existing design comment), so inerting the root never touches them. The
29
+ now-obsolete `root.contains(activeElement)` race-guard is dropped, so the
30
+ background is reliably inerted again.
31
+
3
32
  ## 0.6.1
4
33
 
5
34
  ### Patch Changes
@@ -77,19 +77,33 @@
77
77
  }
78
78
  })
79
79
 
80
+ // Move a dialog panel out to `<body>` so it becomes a true sibling of the app
81
+ // root, NOT a descendant of it. The inert effect below marks the app root
82
+ // `inert`; without this portal the panels render inside that root (wherever
83
+ // <FF_DialogManager> is mounted in the layout) and get disabled too - the
84
+ // panel stops receiving pointer events the moment `inert` wins the race
85
+ // against the panel's autofocus, so real clicks (and AT) silently die. The
86
+ // `portal` runs before the inner `ffAutofocus`, so focus still lands correctly.
87
+ function portal(node: HTMLElement) {
88
+ document.body.appendChild(node)
89
+ return {
90
+ destroy() {
91
+ node.remove()
92
+ },
93
+ }
94
+ }
95
+
80
96
  // Mark the app root `inert` (+ aria-hidden) while any dialog is open, so the background can't
81
- // be tabbed into or read by AT. The dialog panels render at the document body level (a sibling
82
- // of the app root), so inerting the root never touches the panels. SSR-safe.
97
+ // be tabbed into or read by AT. The dialog panels are portaled to <body> (a sibling of the app
98
+ // root), so inerting the root never touches them - we can apply it unconditionally (no
99
+ // activeElement race-guard, which previously let the background stay live). SSR-safe.
83
100
  $effect(() => {
84
101
  if (typeof document === 'undefined' || total === 0) return
85
102
  const root = document.querySelector<HTMLElement>(
86
103
  '[data-sveltekit-root], #svelte, body > div:first-child',
87
104
  )
88
- if (!root || root.contains(document.activeElement)) {
89
- // Fallback: no identifiable single root, or the dialog itself lives under it - skip
90
- // inert (the per-panel focus trap still contains keyboard navigation).
91
- return
92
- }
105
+ // No identifiable single root: skip (the per-panel focus trap still contains keyboard nav).
106
+ if (!root) return
93
107
  root.setAttribute('inert', '')
94
108
  root.setAttribute('aria-hidden', 'true')
95
109
  return () => {
@@ -112,52 +126,58 @@
112
126
  {@render d.render.body(close)}
113
127
  {/if}
114
128
  {/snippet}
115
- {@render (shell ?? cfg.dialog.shell ?? defaultShell)({
116
- id: d.id,
117
- body: itemBody,
118
- close: (r) => dialog._close(d.id, r),
119
- dismiss: () => dialog.requestClose(d.id),
120
- dismissible: d.options.dismissible,
121
- width: d.options.width,
122
- isTop: d.id === topId,
123
- })}
129
+ <div use:portal>
130
+ {@render (shell ?? cfg.dialog.shell ?? defaultShell)({
131
+ id: d.id,
132
+ body: itemBody,
133
+ close: (r) => dialog._close(d.id, r),
134
+ dismiss: () => dialog.requestClose(d.id),
135
+ dismissible: d.options.dismissible,
136
+ width: d.options.width,
137
+ isTop: d.id === topId,
138
+ })}
139
+ </div>
124
140
  {/each}
125
141
 
126
142
  {#each dialog.confirmList as c (c.id)}
127
- {@render (confirm ?? cfg.dialog.confirm ?? defaultConfirm)({
128
- id: c.id,
129
- message: resolveMessage(c.message),
130
- title: c.title === undefined ? undefined : resolveMessage(c.title),
131
- confirmLabel: resolveMessage(c.confirmLabel ?? cfg.messages.confirm),
132
- cancelLabel: resolveMessage(c.cancelLabel ?? cfg.messages.cancel),
133
- danger: c.danger,
134
- confirm: () => dialog._resolveConfirm(c.id, true),
135
- cancel: () => dialog._resolveConfirm(c.id, false),
136
- isTop: c.id === topId,
137
- })}
143
+ <div use:portal>
144
+ {@render (confirm ?? cfg.dialog.confirm ?? defaultConfirm)({
145
+ id: c.id,
146
+ message: resolveMessage(c.message),
147
+ title: c.title === undefined ? undefined : resolveMessage(c.title),
148
+ confirmLabel: resolveMessage(c.confirmLabel ?? cfg.messages.confirm),
149
+ cancelLabel: resolveMessage(c.cancelLabel ?? cfg.messages.cancel),
150
+ danger: c.danger,
151
+ confirm: () => dialog._resolveConfirm(c.id, true),
152
+ cancel: () => dialog._resolveConfirm(c.id, false),
153
+ isTop: c.id === topId,
154
+ })}
155
+ </div>
138
156
  {/each}
139
157
 
140
158
  {#each dialog.promptList as p (p.id)}
141
159
  {@const promptUi = prompt ?? cfg.dialog.prompt}
142
- {#if promptUi}
143
- {@render promptUi({
144
- id: p.id,
145
- title: p.title === undefined ? undefined : resolveMessage(p.title),
146
- label: p.label === undefined ? undefined : resolveMessage(p.label),
147
- placeholder: p.placeholder,
148
- initial: p.initial,
149
- confirmLabel: resolveMessage(p.confirmLabel ?? cfg.messages.ok),
150
- cancelLabel: resolveMessage(p.cancelLabel ?? cfg.messages.cancel),
151
- submit: (value) => dialog._resolvePrompt(p.id, value),
152
- cancel: () => dialog._resolvePrompt(p.id, null),
153
- })}
154
- {:else}
155
- <FF_PromptDefault
156
- item={p}
157
- onsubmit={(value) => dialog._resolvePrompt(p.id, value)}
158
- oncancel={() => dialog._resolvePrompt(p.id, null)}
159
- />
160
- {/if}
160
+ <div use:portal>
161
+ {#if promptUi}
162
+ {@render promptUi({
163
+ id: p.id,
164
+ title: p.title === undefined ? undefined : resolveMessage(p.title),
165
+ label: p.label === undefined ? undefined : resolveMessage(p.label),
166
+ placeholder: p.placeholder,
167
+ initial: p.initial,
168
+ confirmLabel: resolveMessage(p.confirmLabel ?? cfg.messages.ok),
169
+ cancelLabel: resolveMessage(p.cancelLabel ?? cfg.messages.cancel),
170
+ submit: (value) => dialog._resolvePrompt(p.id, value),
171
+ cancel: () => dialog._resolvePrompt(p.id, null),
172
+ })}
173
+ {:else}
174
+ <FF_PromptDefault
175
+ item={p}
176
+ onsubmit={(value) => dialog._resolvePrompt(p.id, value)}
177
+ oncancel={() => dialog._resolvePrompt(p.id, null)}
178
+ />
179
+ {/if}
180
+ </div>
161
181
  {/each}
162
182
 
163
183
  <!-- Built-in defaults: usable with zero config and theme-adaptive via semantic tokens
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firstly",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "funding": "https://github.com/sponsors/jycouet",
@@ -31,6 +31,7 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
+ "@kitql/helpers": "0.8.15",
34
35
  "@layerstack/utils": "1.0.0",
35
36
  "@mdi/js": "7.4.47",
36
37
  "@types/nodemailer": "8.0.0",
@@ -39,7 +40,7 @@
39
40
  "esm-env": "1.2.2",
40
41
  "nodemailer": "8.0.5",
41
42
  "svelte-sonner": "1.1.1",
42
- "tailwind-merge": "3.5.0",
43
+ "tailwind-merge": "3.6.0",
43
44
  "tailwindcss": "4.2.2",
44
45
  "vite-plugin-kit-routes": "1.0.6",
45
46
  "vite-plugin-stripper": "0.10.4"