@lapismd/design-core 0.3.0 → 0.3.1

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,15 @@
1
1
  # @lapismd/design-core
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#16](https://github.com/lapismd/design-core/pull/16) [`f94fb98`](https://github.com/lapismd/design-core/commit/f94fb9852f6c3e54ea57f89c2f185b6f00c68e35) Thanks [@stevejuma](https://github.com/stevejuma)! - Release the Composer Input handle correctly through reactive consumer bindings during teardown while preserving replacement handles. Keep Svelte runtime diagnostics fatal even when component cleanup catches the immediate error.
8
+
9
+ - [#13](https://github.com/lapismd/design-core/pull/13) [`5f533e6`](https://github.com/lapismd/design-core/commit/5f533e656601015b5a5dd3a680f5e97fd13cd1d2) Thanks [@stevejuma](https://github.com/stevejuma)! - Preserve emoji-grid keyboard focus when delayed popover opening frames complete.
10
+
11
+ - [#15](https://github.com/lapismd/design-core/pull/15) [`02756cb`](https://github.com/lapismd/design-core/commit/02756cb32a5405db45cb47cae9e28d448d20651f) Thanks [@stevejuma](https://github.com/stevejuma)! - Require Bits UI 2.19.5 to prevent Scroll Area resize callbacks and text-selection pointer handlers from reading destroyed Svelte state. Make Svelte runtime diagnostics fatal in Storybook tests and preserve the editing Dialog Frame example's initial field focus.
12
+
3
13
  ## 0.3.0
4
14
 
5
15
  ### Minor Changes
@@ -5,6 +5,7 @@
5
5
  <script lang="ts">
6
6
  import * as Command from "@lapismd/design-core/shadcn/command";
7
7
  import type { HTMLAttributes } from "svelte/elements";
8
+ import { untrack } from "svelte";
8
9
  import {
9
10
  createComposerTokens,
10
11
  createPasteAsToken,
@@ -149,12 +150,15 @@
149
150
 
150
151
  $effect(() => {
151
152
  handle = inputHandle;
153
+ // A consumer's binding may proxy the plain handle. Retain the identity
154
+ // returned by that binding without making it an effect dependency.
155
+ const assignedHandle = untrack(() => handle);
152
156
  context?.setInputHandle(inputHandle);
153
157
  return () => {
154
158
  triggerSearch.cancel();
155
159
  tokens.cleanup();
156
160
  context?.setInputHandle(null);
157
- if (handle === inputHandle) handle = null;
161
+ if (handle === assignedHandle) handle = null;
158
162
  };
159
163
  });
160
164
 
@@ -55,6 +55,7 @@
55
55
  } = $props();
56
56
 
57
57
  let query = $state("");
58
+ let searchRef = $state<HTMLInputElement | null>(null);
58
59
  let gridRef = $state<HTMLDivElement | null>(null);
59
60
 
60
61
  const visible = $derived.by(() => {
@@ -77,6 +78,12 @@
77
78
  setOpen(false);
78
79
  }
79
80
 
81
+ function focusSearchOnOpen(event: Event): void {
82
+ // Own initial focus so a deferred popover callback cannot undo navigation.
83
+ event.preventDefault();
84
+ searchRef?.focus();
85
+ }
86
+
80
87
  function gridButtons(): HTMLButtonElement[] {
81
88
  const root = gridRef ?? ref;
82
89
  return Array.from(
@@ -155,9 +162,11 @@
155
162
  align="start"
156
163
  aria-label={label}
157
164
  data-ai-chat-part="emoji-content"
165
+ onOpenAutoFocus={focusSearchOnOpen}
158
166
  >
159
167
  <div bind:this={ref} {...restProps} data-ui-part="emoji-panel">
160
168
  <Input
169
+ bind:ref={searchRef}
161
170
  bind:value={query}
162
171
  aria-label={searchLabel}
163
172
  placeholder={searchLabel}
@@ -5,6 +5,7 @@
5
5
  import { Input } from "@lapismd/design-core/shadcn/input";
6
6
  let open = $state(false);
7
7
  let name = $state("");
8
+ let firstField = $state<HTMLInputElement | null>(null);
8
9
  </script>
9
10
 
10
11
  <Dialog.Root bind:open>
@@ -20,6 +21,11 @@
20
21
  size="large"
21
22
  surface="inset"
22
23
  class={name ? "record-filled" : "record-empty"}
24
+ onOpenAutoFocus={(event) => {
25
+ if (!firstField) return;
26
+ firstField.focus();
27
+ event.preventDefault();
28
+ }}
23
29
  >
24
30
  {#snippet sidebar()}<nav aria-label="Sections">
25
31
  <Button variant="secondary">Details</Button>
@@ -27,6 +33,7 @@
27
33
  {#each Array.from({ length: 24 }) as _, index}<label
28
34
  style="display: grid; gap: 0.5rem; margin-block: 1rem;"
29
35
  >Field {index + 1}{#if index === 0}<Input
36
+ bind:ref={firstField}
30
37
  bind:value={name}
31
38
  />{:else}<Input />{/if}</label
32
39
  >{/each}
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { Button } from "../button/index.js";
3
+ import { ScrollArea } from "./index.js";
4
+
5
+ let { type = "always" }: { type?: "always" | "auto" } = $props();
6
+ let mounted = $state(false);
7
+ let clicks = $state(0);
8
+ </script>
9
+
10
+ <Button onclick={() => (mounted = !mounted)}>Toggle styled viewport</Button>
11
+ <Button onclick={() => clicks++}>Remaining control</Button>
12
+ <p role="status">Remaining clicks: {clicks}</p>
13
+ {#if mounted}
14
+ <ScrollArea
15
+ aria-label="Styled lifecycle"
16
+ {type}
17
+ orientation="both"
18
+ scrollStrategy="styled"
19
+ class="styled-lifecycle-area"
20
+ >
21
+ <div class="styled-lifecycle-content">
22
+ {#each Array.from({ length: 40 }) as _, index}
23
+ <p>Entry {index + 1}</p>
24
+ {/each}
25
+ </div>
26
+ </ScrollArea>
27
+ {/if}
28
+
29
+ <style>
30
+ :global(.styled-lifecycle-area) {
31
+ height: 10rem;
32
+ width: 16rem;
33
+ }
34
+
35
+ .styled-lifecycle-content {
36
+ width: 30rem;
37
+ }
38
+ </style>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ type?: "always" | "auto";
3
+ };
4
+ declare const ScrollAreaStyledLifecycleFixture: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type ScrollAreaStyledLifecycleFixture = ReturnType<typeof ScrollAreaStyledLifecycleFixture>;
6
+ export default ScrollAreaStyledLifecycleFixture;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lapismd/design-core",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Shared Lapis design system, forms, shell, and workspace primitives",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -58,7 +58,7 @@
58
58
  "@lucide/svelte": "^1.21.0",
59
59
  "@tanstack/svelte-virtual": "^3.13.39",
60
60
  "@types/luxon": "^3.4.2",
61
- "bits-ui": "^2.18.1",
61
+ "bits-ui": "^2.19.5",
62
62
  "chrono-node": "^2.10.1",
63
63
  "clsx": "^2.1.1",
64
64
  "codemirror-lang-mermaid": "^0.5.0",
@@ -5,6 +5,7 @@
5
5
  <script lang="ts">
6
6
  import * as Command from "@lapismd/design-core/shadcn/command";
7
7
  import type { HTMLAttributes } from "svelte/elements";
8
+ import { untrack } from "svelte";
8
9
  import {
9
10
  createComposerTokens,
10
11
  createPasteAsToken,
@@ -149,12 +150,15 @@
149
150
 
150
151
  $effect(() => {
151
152
  handle = inputHandle;
153
+ // A consumer's binding may proxy the plain handle. Retain the identity
154
+ // returned by that binding without making it an effect dependency.
155
+ const assignedHandle = untrack(() => handle);
152
156
  context?.setInputHandle(inputHandle);
153
157
  return () => {
154
158
  triggerSearch.cancel();
155
159
  tokens.cleanup();
156
160
  context?.setInputHandle(null);
157
- if (handle === inputHandle) handle = null;
161
+ if (handle === assignedHandle) handle = null;
158
162
  };
159
163
  });
160
164
 
@@ -55,6 +55,7 @@
55
55
  } = $props();
56
56
 
57
57
  let query = $state("");
58
+ let searchRef = $state<HTMLInputElement | null>(null);
58
59
  let gridRef = $state<HTMLDivElement | null>(null);
59
60
 
60
61
  const visible = $derived.by(() => {
@@ -77,6 +78,12 @@
77
78
  setOpen(false);
78
79
  }
79
80
 
81
+ function focusSearchOnOpen(event: Event): void {
82
+ // Own initial focus so a deferred popover callback cannot undo navigation.
83
+ event.preventDefault();
84
+ searchRef?.focus();
85
+ }
86
+
80
87
  function gridButtons(): HTMLButtonElement[] {
81
88
  const root = gridRef ?? ref;
82
89
  return Array.from(
@@ -155,9 +162,11 @@
155
162
  align="start"
156
163
  aria-label={label}
157
164
  data-ai-chat-part="emoji-content"
165
+ onOpenAutoFocus={focusSearchOnOpen}
158
166
  >
159
167
  <div bind:this={ref} {...restProps} data-ui-part="emoji-panel">
160
168
  <Input
169
+ bind:ref={searchRef}
161
170
  bind:value={query}
162
171
  aria-label={searchLabel}
163
172
  placeholder={searchLabel}
@@ -5,6 +5,7 @@
5
5
  import { Input } from "@lapismd/design-core/shadcn/input";
6
6
  let open = $state(false);
7
7
  let name = $state("");
8
+ let firstField = $state<HTMLInputElement | null>(null);
8
9
  </script>
9
10
 
10
11
  <Dialog.Root bind:open>
@@ -20,6 +21,11 @@
20
21
  size="large"
21
22
  surface="inset"
22
23
  class={name ? "record-filled" : "record-empty"}
24
+ onOpenAutoFocus={(event) => {
25
+ if (!firstField) return;
26
+ firstField.focus();
27
+ event.preventDefault();
28
+ }}
23
29
  >
24
30
  {#snippet sidebar()}<nav aria-label="Sections">
25
31
  <Button variant="secondary">Details</Button>
@@ -27,6 +33,7 @@
27
33
  {#each Array.from({ length: 24 }) as _, index}<label
28
34
  style="display: grid; gap: 0.5rem; margin-block: 1rem;"
29
35
  >Field {index + 1}{#if index === 0}<Input
36
+ bind:ref={firstField}
30
37
  bind:value={name}
31
38
  />{:else}<Input />{/if}</label
32
39
  >{/each}
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { Button } from "../button/index.js";
3
+ import { ScrollArea } from "./index.js";
4
+
5
+ let { type = "always" }: { type?: "always" | "auto" } = $props();
6
+ let mounted = $state(false);
7
+ let clicks = $state(0);
8
+ </script>
9
+
10
+ <Button onclick={() => (mounted = !mounted)}>Toggle styled viewport</Button>
11
+ <Button onclick={() => clicks++}>Remaining control</Button>
12
+ <p role="status">Remaining clicks: {clicks}</p>
13
+ {#if mounted}
14
+ <ScrollArea
15
+ aria-label="Styled lifecycle"
16
+ {type}
17
+ orientation="both"
18
+ scrollStrategy="styled"
19
+ class="styled-lifecycle-area"
20
+ >
21
+ <div class="styled-lifecycle-content">
22
+ {#each Array.from({ length: 40 }) as _, index}
23
+ <p>Entry {index + 1}</p>
24
+ {/each}
25
+ </div>
26
+ </ScrollArea>
27
+ {/if}
28
+
29
+ <style>
30
+ :global(.styled-lifecycle-area) {
31
+ height: 10rem;
32
+ width: 16rem;
33
+ }
34
+
35
+ .styled-lifecycle-content {
36
+ width: 30rem;
37
+ }
38
+ </style>