@interstellarforge/matter-svelte 0.0.0-stage → 0.2.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Avatar.svelte +25 -0
  3. package/dist/Avatar.svelte.d.ts +10 -0
  4. package/dist/Badge.svelte +18 -0
  5. package/dist/Badge.svelte.d.ts +10 -0
  6. package/dist/Banner.svelte +25 -0
  7. package/dist/Banner.svelte.d.ts +11 -0
  8. package/dist/Button.svelte +44 -0
  9. package/dist/Button.svelte.d.ts +13 -0
  10. package/dist/Card.svelte +28 -0
  11. package/dist/Card.svelte.d.ts +11 -0
  12. package/dist/Checkbox.svelte +30 -0
  13. package/dist/Checkbox.svelte.d.ts +10 -0
  14. package/dist/Dialog.svelte +75 -0
  15. package/dist/Dialog.svelte.d.ts +14 -0
  16. package/dist/EmptyState.svelte +19 -0
  17. package/dist/EmptyState.svelte.d.ts +11 -0
  18. package/dist/Icon.svelte +35 -0
  19. package/dist/Icon.svelte.d.ts +12 -0
  20. package/dist/IconButton.svelte +22 -0
  21. package/dist/IconButton.svelte.d.ts +14 -0
  22. package/dist/Input.svelte +40 -0
  23. package/dist/Input.svelte.d.ts +12 -0
  24. package/dist/Menu.svelte +127 -0
  25. package/dist/Menu.svelte.d.ts +10 -0
  26. package/dist/ProgressBar.svelte +30 -0
  27. package/dist/ProgressBar.svelte.d.ts +11 -0
  28. package/dist/Select.svelte +27 -0
  29. package/dist/Select.svelte.d.ts +10 -0
  30. package/dist/Switch.svelte +33 -0
  31. package/dist/Switch.svelte.d.ts +8 -0
  32. package/dist/Tabs.svelte +78 -0
  33. package/dist/Tabs.svelte.d.ts +14 -0
  34. package/dist/Textarea.svelte +21 -0
  35. package/dist/Textarea.svelte.d.ts +7 -0
  36. package/dist/Tooltip.svelte +32 -0
  37. package/dist/Tooltip.svelte.d.ts +11 -0
  38. package/dist/index.d.ts +19 -0
  39. package/dist/index.js +18 -0
  40. package/dist/types.d.ts +14 -0
  41. package/dist/types.js +1 -0
  42. package/package.json +52 -3
  43. package/README.md +0 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Interstellarforge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+
4
+ interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
5
+ /** Full name: used as the accessible name and for the initials fallback. */
6
+ name: string;
7
+ src?: string;
8
+ size?: "sm" | "md" | "lg";
9
+ }
10
+
11
+ let { name, src, size = "md", class: className, ...rest }: Props = $props();
12
+
13
+ const initials = $derived(
14
+ name
15
+ .split(/\s+/)
16
+ .filter(Boolean)
17
+ .slice(0, 2)
18
+ .map((part) => part.charAt(0))
19
+ .join(""),
20
+ );
21
+ </script>
22
+
23
+ <span {...rest} class={["m-avatar", className]} data-size={size} role="img" aria-label={name}>
24
+ {#if src}<img {src} alt="" />{:else}{initials}{/if}
25
+ </span>
@@ -0,0 +1,10 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
3
+ /** Full name: used as the accessible name and for the initials fallback. */
4
+ name: string;
5
+ src?: string;
6
+ size?: "sm" | "md" | "lg";
7
+ }
8
+ declare const Avatar: import("svelte").Component<Props, {}, "">;
9
+ type Avatar = ReturnType<typeof Avatar>;
10
+ export default Avatar;
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ import type { BadgeTone } from "./types.ts";
6
+
7
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
8
+ tone?: BadgeTone;
9
+ children?: Snippet;
10
+ }
11
+
12
+ /** Status must also be readable as text; never rely on the tone colour alone. */
13
+ let { tone = "neutral", class: className, children, ...rest }: Props = $props();
14
+ </script>
15
+
16
+ <span {...rest} class={["m-badge", className]} data-tone={tone}>
17
+ {@render children?.()}
18
+ </span>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { BadgeTone } from "./types.ts";
4
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
5
+ tone?: BadgeTone;
6
+ children?: Snippet;
7
+ }
8
+ declare const Badge: import("svelte").Component<Props, {}, "">;
9
+ type Badge = ReturnType<typeof Badge>;
10
+ export default Badge;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ import type { BannerTone } from "./types.ts";
6
+
7
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
8
+ tone?: BannerTone;
9
+ title?: string;
10
+ children?: Snippet;
11
+ }
12
+
13
+ let { tone = "info", title, class: className, children, ...rest }: Props = $props();
14
+ </script>
15
+
16
+ <!-- Warnings and errors are announced immediately (alert); the rest politely (status). -->
17
+ <div
18
+ role={tone === "danger" || tone === "warning" ? "alert" : "status"}
19
+ {...rest}
20
+ class={["m-banner", className]}
21
+ data-tone={tone}
22
+ >
23
+ {#if title}<div class="m-banner__title">{title}</div>{/if}
24
+ {@render children?.()}
25
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { BannerTone } from "./types.ts";
4
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
5
+ tone?: BannerTone;
6
+ title?: string;
7
+ children?: Snippet;
8
+ }
9
+ declare const Banner: import("svelte").Component<Props, {}, "">;
10
+ type Banner = ReturnType<typeof Banner>;
11
+ export default Banner;
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLButtonAttributes } from "svelte/elements";
4
+
5
+ import type { ButtonVariant, ControlSize } from "./types.ts";
6
+
7
+ interface Props extends HTMLButtonAttributes {
8
+ variant?: ButtonVariant;
9
+ size?: ControlSize;
10
+ /** Keeps the button focusable but blocks clicks while pending. */
11
+ loading?: boolean;
12
+ children?: Snippet;
13
+ }
14
+
15
+ let {
16
+ variant = "secondary",
17
+ size = "md",
18
+ type = "button",
19
+ loading = false,
20
+ class: className,
21
+ onclick,
22
+ children,
23
+ ...rest
24
+ }: Props = $props();
25
+ </script>
26
+
27
+ <button
28
+ {...rest}
29
+ {type}
30
+ class={["m-button", className]}
31
+ data-variant={variant}
32
+ data-size={size}
33
+ aria-busy={loading ? true : undefined}
34
+ aria-disabled={loading ? true : undefined}
35
+ onclick={(event) => {
36
+ if (loading) {
37
+ event.preventDefault();
38
+ } else {
39
+ onclick?.(event);
40
+ }
41
+ }}
42
+ >
43
+ {@render children?.()}
44
+ </button>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLButtonAttributes } from "svelte/elements";
3
+ import type { ButtonVariant, ControlSize } from "./types.ts";
4
+ interface Props extends HTMLButtonAttributes {
5
+ variant?: ButtonVariant;
6
+ size?: ControlSize;
7
+ /** Keeps the button focusable but blocks clicks while pending. */
8
+ loading?: boolean;
9
+ children?: Snippet;
10
+ }
11
+ declare const Button: import("svelte").Component<Props, {}, "">;
12
+ type Button = ReturnType<typeof Button>;
13
+ export default Button;
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ interface Props extends HTMLAttributes<HTMLElement> {
6
+ /** Element to render; default `div`. */
7
+ as?: string;
8
+ padded?: boolean;
9
+ children?: Snippet;
10
+ }
11
+
12
+ let {
13
+ as: tag = "div",
14
+ padded = false,
15
+ class: className,
16
+ children,
17
+ ...rest
18
+ }: Props = $props();
19
+ </script>
20
+
21
+ <svelte:element
22
+ this={tag}
23
+ {...rest}
24
+ class={["m-card", className]}
25
+ data-padded={padded ? "" : undefined}
26
+ >
27
+ {@render children?.()}
28
+ </svelte:element>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ interface Props extends HTMLAttributes<HTMLElement> {
4
+ /** Element to render; default `div`. */
5
+ as?: string;
6
+ padded?: boolean;
7
+ children?: Snippet;
8
+ }
9
+ declare const Card: import("svelte").Component<Props, {}, "">;
10
+ type Card = ReturnType<typeof Card>;
11
+ export default Card;
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLInputAttributes } from "svelte/elements";
4
+
5
+ interface Props extends Omit<HTMLInputAttributes, "type" | "value"> {
6
+ invalid?: boolean;
7
+ indeterminate?: boolean;
8
+ children?: Snippet;
9
+ }
10
+
11
+ let {
12
+ invalid = false,
13
+ indeterminate = false,
14
+ checked = $bindable(false),
15
+ class: className,
16
+ children,
17
+ ...rest
18
+ }: Props = $props();
19
+ </script>
20
+
21
+ <label class={["m-checkbox", className]}>
22
+ <input
23
+ {...rest}
24
+ type="checkbox"
25
+ bind:checked
26
+ {indeterminate}
27
+ aria-invalid={invalid ? true : undefined}
28
+ />
29
+ <span>{@render children?.()}</span>
30
+ </label>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLInputAttributes } from "svelte/elements";
3
+ interface Props extends Omit<HTMLInputAttributes, "type" | "value"> {
4
+ invalid?: boolean;
5
+ indeterminate?: boolean;
6
+ children?: Snippet;
7
+ }
8
+ declare const Checkbox: import("svelte").Component<Props, {}, "checked">;
9
+ type Checkbox = ReturnType<typeof Checkbox>;
10
+ export default Checkbox;
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ interface Props {
5
+ open?: boolean;
6
+ onOpenChange?: (open: boolean) => void;
7
+ /** Visible heading; also the accessible name. */
8
+ title: string;
9
+ /** Accessible name of the close button. */
10
+ closeLabel: string;
11
+ children?: Snippet;
12
+ footer?: Snippet;
13
+ }
14
+
15
+ let {
16
+ open = $bindable(false),
17
+ onOpenChange,
18
+ title,
19
+ closeLabel,
20
+ children,
21
+ footer,
22
+ }: Props = $props();
23
+
24
+ const titleId = $props.id();
25
+ let dialog: HTMLDialogElement | undefined = $state();
26
+
27
+ /** Modal dialog on the native `<dialog>`: focus trap, Esc and inert background come from the platform. */
28
+ $effect(() => {
29
+ if (!dialog) {
30
+ return;
31
+ }
32
+ if (open && !dialog.open) {
33
+ dialog.showModal();
34
+ } else if (!open && dialog.open) {
35
+ dialog.close();
36
+ }
37
+ });
38
+
39
+ function setOpen(next: boolean) {
40
+ open = next;
41
+ onOpenChange?.(next);
42
+ }
43
+ </script>
44
+
45
+ <dialog
46
+ bind:this={dialog}
47
+ class="m-dialog"
48
+ aria-labelledby={titleId}
49
+ onclose={() => {
50
+ setOpen(false);
51
+ }}
52
+ onclick={(event) => {
53
+ if (event.target === event.currentTarget) {
54
+ setOpen(false);
55
+ }
56
+ }}
57
+ >
58
+ <div class="m-dialog__header">
59
+ <h2 id={titleId} class="m-dialog__title">{title}</h2>
60
+ <button
61
+ type="button"
62
+ class="m-button"
63
+ data-variant="ghost"
64
+ data-icon-only=""
65
+ aria-label={closeLabel}
66
+ onclick={() => {
67
+ setOpen(false);
68
+ }}
69
+ >
70
+ <span aria-hidden="true">×</span>
71
+ </button>
72
+ </div>
73
+ <div class="m-dialog__body">{@render children?.()}</div>
74
+ {#if footer}<div class="m-dialog__footer">{@render footer()}</div>{/if}
75
+ </dialog>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ open?: boolean;
4
+ onOpenChange?: (open: boolean) => void;
5
+ /** Visible heading; also the accessible name. */
6
+ title: string;
7
+ /** Accessible name of the close button. */
8
+ closeLabel: string;
9
+ children?: Snippet;
10
+ footer?: Snippet;
11
+ }
12
+ declare const Dialog: import("svelte").Component<Props, {}, "open">;
13
+ type Dialog = ReturnType<typeof Dialog>;
14
+ export default Dialog;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
6
+ title: string;
7
+ children?: Snippet;
8
+ /** Optional call to action, e.g. a `Button`. */
9
+ action?: Snippet;
10
+ }
11
+
12
+ let { title, class: className, children, action, ...rest }: Props = $props();
13
+ </script>
14
+
15
+ <div {...rest} class={["m-empty", className]}>
16
+ <div class="m-empty__title">{title}</div>
17
+ {#if children}<div>{@render children()}</div>{/if}
18
+ {@render action?.()}
19
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
4
+ title: string;
5
+ children?: Snippet;
6
+ /** Optional call to action, e.g. a `Button`. */
7
+ action?: Snippet;
8
+ }
9
+ declare const EmptyState: import("svelte").Component<Props, {}, "">;
10
+ type EmptyState = ReturnType<typeof EmptyState>;
11
+ export default EmptyState;
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import type { IconDefinition } from "@interstellarforge/matter-icons";
3
+ import type { SVGAttributes } from "svelte/elements";
4
+
5
+ interface Props extends SVGAttributes<SVGSVGElement> {
6
+ /** A named export of `@interstellarforge/matter-icons`, e.g. `search` (tree-shakeable). */
7
+ icon: IconDefinition;
8
+ size?: number;
9
+ /** Accessible name. Without it the icon is decorative and hidden from assistive technology. */
10
+ label?: string;
11
+ }
12
+
13
+ let { icon, size = 18, label, class: className, ...rest }: Props = $props();
14
+ </script>
15
+
16
+ <svg
17
+ {...rest}
18
+ class={["m-icon", className]}
19
+ width={size}
20
+ height={size}
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ stroke-width="1.8"
25
+ stroke-linecap="round"
26
+ stroke-linejoin="round"
27
+ focusable="false"
28
+ role={label ? "img" : undefined}
29
+ aria-label={label}
30
+ aria-hidden={label ? undefined : true}
31
+ >
32
+ <!-- Static markup from our own, SVGO-optimised icon sources. -->
33
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted static icon markup -->
34
+ {@html icon.body}
35
+ </svg>
@@ -0,0 +1,12 @@
1
+ import type { IconDefinition } from "@interstellarforge/matter-icons";
2
+ import type { SVGAttributes } from "svelte/elements";
3
+ interface Props extends SVGAttributes<SVGSVGElement> {
4
+ /** A named export of `@interstellarforge/matter-icons`, e.g. `search` (tree-shakeable). */
5
+ icon: IconDefinition;
6
+ size?: number;
7
+ /** Accessible name. Without it the icon is decorative and hidden from assistive technology. */
8
+ label?: string;
9
+ }
10
+ declare const Icon: import("svelte").Component<Props, {}, "">;
11
+ type Icon = ReturnType<typeof Icon>;
12
+ export default Icon;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLButtonAttributes } from "svelte/elements";
4
+
5
+ import Button from "./Button.svelte";
6
+ import type { ButtonVariant, ControlSize } from "./types.ts";
7
+
8
+ interface Props extends Omit<HTMLButtonAttributes, "aria-label"> {
9
+ /** Accessible name; required because the button has no visible text. */
10
+ label: string;
11
+ variant?: ButtonVariant;
12
+ size?: ControlSize;
13
+ loading?: boolean;
14
+ children?: Snippet;
15
+ }
16
+
17
+ let { label, children, ...rest }: Props = $props();
18
+ </script>
19
+
20
+ <Button {...rest} aria-label={label} data-icon-only="">
21
+ {@render children?.()}
22
+ </Button>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLButtonAttributes } from "svelte/elements";
3
+ import type { ButtonVariant, ControlSize } from "./types.ts";
4
+ interface Props extends Omit<HTMLButtonAttributes, "aria-label"> {
5
+ /** Accessible name; required because the button has no visible text. */
6
+ label: string;
7
+ variant?: ButtonVariant;
8
+ size?: ControlSize;
9
+ loading?: boolean;
10
+ children?: Snippet;
11
+ }
12
+ declare const IconButton: import("svelte").Component<Props, {}, "">;
13
+ type IconButton = ReturnType<typeof IconButton>;
14
+ export default IconButton;
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import type { IconDefinition } from "@interstellarforge/matter-icons";
3
+ import type { HTMLInputAttributes } from "svelte/elements";
4
+
5
+ import Icon from "./Icon.svelte";
6
+
7
+ interface Props extends Omit<HTMLInputAttributes, "value"> {
8
+ value?: string;
9
+ /** Sets `aria-invalid` and the error border. */
10
+ invalid?: boolean;
11
+ /** Decorative leading icon, e.g. `search`. The input still needs its own accessible name. */
12
+ icon?: IconDefinition;
13
+ }
14
+
15
+ let {
16
+ invalid = false,
17
+ icon,
18
+ value = $bindable(),
19
+ class: className,
20
+ ...rest
21
+ }: Props = $props();
22
+ </script>
23
+
24
+ {#snippet field(cls: unknown)}
25
+ <input
26
+ {...rest}
27
+ bind:value
28
+ class={["m-input", cls]}
29
+ aria-invalid={invalid ? true : undefined}
30
+ />
31
+ {/snippet}
32
+
33
+ {#if icon}
34
+ <span class={["m-input-icon", className]}>
35
+ <Icon {icon} />
36
+ {@render field(undefined)}
37
+ </span>
38
+ {:else}
39
+ {@render field(className)}
40
+ {/if}
@@ -0,0 +1,12 @@
1
+ import type { IconDefinition } from "@interstellarforge/matter-icons";
2
+ import type { HTMLInputAttributes } from "svelte/elements";
3
+ interface Props extends Omit<HTMLInputAttributes, "value"> {
4
+ value?: string;
5
+ /** Sets `aria-invalid` and the error border. */
6
+ invalid?: boolean;
7
+ /** Decorative leading icon, e.g. `search`. The input still needs its own accessible name. */
8
+ icon?: IconDefinition;
9
+ }
10
+ declare const Input: import("svelte").Component<Props, {}, "value">;
11
+ type Input = ReturnType<typeof Input>;
12
+ export default Input;
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ import { tick } from "svelte";
3
+
4
+ import type { MenuItem } from "./types.ts";
5
+
6
+ interface Props {
7
+ /** Visible text of the trigger button. */
8
+ triggerLabel: string;
9
+ items: MenuItem[];
10
+ onSelect: (id: string) => void;
11
+ }
12
+
13
+ let { triggerLabel, items, onSelect }: Props = $props();
14
+
15
+ const menuId = $props.id();
16
+ let root: HTMLDivElement | undefined = $state();
17
+ let trigger: HTMLButtonElement | undefined = $state();
18
+ const itemEls: (HTMLButtonElement | undefined)[] = [];
19
+ let open = $state(false);
20
+ const enabled = $derived(items.flatMap((item, index) => (item.disabled ? [] : [index])));
21
+
22
+ $effect(() => {
23
+ if (!open) {
24
+ return;
25
+ }
26
+ function onPointerDown(event: PointerEvent) {
27
+ if (!root?.contains(event.target as Node)) {
28
+ open = false;
29
+ }
30
+ }
31
+ document.addEventListener("pointerdown", onPointerDown);
32
+ return () => {
33
+ document.removeEventListener("pointerdown", onPointerDown);
34
+ };
35
+ });
36
+
37
+ function focusItem(index: number | undefined) {
38
+ if (index !== undefined) {
39
+ itemEls[index]?.focus();
40
+ }
41
+ }
42
+
43
+ function close() {
44
+ open = false;
45
+ trigger?.focus();
46
+ }
47
+
48
+ async function openAt(position: "first" | "last") {
49
+ open = true;
50
+ await tick();
51
+ focusItem(position === "first" ? enabled[0] : enabled.at(-1));
52
+ }
53
+
54
+ function onMenuKeyDown(event: KeyboardEvent) {
55
+ const current = itemEls.findIndex((element) => element === document.activeElement);
56
+ const position = enabled.indexOf(current);
57
+ if (event.key === "Escape") {
58
+ event.preventDefault();
59
+ close();
60
+ return;
61
+ }
62
+ const next = {
63
+ ArrowDown: enabled[(position + 1) % enabled.length],
64
+ ArrowUp: enabled[(position - 1 + enabled.length) % enabled.length],
65
+ Home: enabled[0],
66
+ End: enabled.at(-1),
67
+ }[event.key];
68
+ if (next !== undefined) {
69
+ event.preventDefault();
70
+ focusItem(next);
71
+ return;
72
+ }
73
+ if (event.key.length === 1) {
74
+ const typed = event.key.toLowerCase();
75
+ focusItem(
76
+ enabled.find(
77
+ (index) => index > current && items[index]?.label.toLowerCase().startsWith(typed),
78
+ ) ?? enabled.find((index) => items[index]?.label.toLowerCase().startsWith(typed)),
79
+ );
80
+ }
81
+ }
82
+ </script>
83
+
84
+ <div bind:this={root} class="m-menu-wrap">
85
+ <button
86
+ bind:this={trigger}
87
+ type="button"
88
+ class="m-button"
89
+ aria-haspopup="menu"
90
+ aria-expanded={open}
91
+ aria-controls={menuId}
92
+ onclick={() => {
93
+ if (open) {
94
+ open = false;
95
+ } else {
96
+ void openAt("first");
97
+ }
98
+ }}
99
+ onkeydown={(event) => {
100
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
101
+ event.preventDefault();
102
+ void openAt(event.key === "ArrowDown" ? "first" : "last");
103
+ }
104
+ }}
105
+ >
106
+ {triggerLabel}
107
+ </button>
108
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
109
+ <div id={menuId} role="menu" class="m-menu" hidden={!open} onkeydown={onMenuKeyDown}>
110
+ {#each items as item, index (item.id)}
111
+ <button
112
+ bind:this={itemEls[index]}
113
+ type="button"
114
+ role="menuitem"
115
+ class="m-menu__item"
116
+ tabindex={-1}
117
+ disabled={item.disabled}
118
+ onclick={() => {
119
+ onSelect(item.id);
120
+ close();
121
+ }}
122
+ >
123
+ {item.label}
124
+ </button>
125
+ {/each}
126
+ </div>
127
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { MenuItem } from "./types.ts";
2
+ interface Props {
3
+ /** Visible text of the trigger button. */
4
+ triggerLabel: string;
5
+ items: MenuItem[];
6
+ onSelect: (id: string) => void;
7
+ }
8
+ declare const Menu: import("svelte").Component<Props, {}, "">;
9
+ type Menu = ReturnType<typeof Menu>;
10
+ export default Menu;
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+
4
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
5
+ /** Accessible name (required). */
6
+ label: string;
7
+ /** Omit for an indeterminate bar. */
8
+ value?: number;
9
+ max?: number;
10
+ }
11
+
12
+ let { label, value, max = 100, class: className, style, ...rest }: Props = $props();
13
+
14
+ const percent = $derived(
15
+ value === undefined ? undefined : Math.min(100, Math.max(0, (value / max) * 100)),
16
+ );
17
+ </script>
18
+
19
+ <div
20
+ {...rest}
21
+ class={["m-progress", className]}
22
+ role="progressbar"
23
+ aria-label={label}
24
+ aria-valuemin={0}
25
+ aria-valuemax={max}
26
+ aria-valuenow={value}
27
+ style={percent === undefined ? style : `${style ?? ""};--m-progress:${String(percent)}%`}
28
+ >
29
+ <div class="m-progress__bar"></div>
30
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
3
+ /** Accessible name (required). */
4
+ label: string;
5
+ /** Omit for an indeterminate bar. */
6
+ value?: number;
7
+ max?: number;
8
+ }
9
+ declare const ProgressBar: import("svelte").Component<Props, {}, "">;
10
+ type ProgressBar = ReturnType<typeof ProgressBar>;
11
+ export default ProgressBar;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLSelectAttributes } from "svelte/elements";
4
+
5
+ interface Props extends Omit<HTMLSelectAttributes, "value"> {
6
+ value?: string;
7
+ invalid?: boolean;
8
+ children?: Snippet;
9
+ }
10
+
11
+ let {
12
+ invalid = false,
13
+ value = $bindable(),
14
+ class: className,
15
+ children,
16
+ ...rest
17
+ }: Props = $props();
18
+ </script>
19
+
20
+ <select
21
+ {...rest}
22
+ bind:value
23
+ class={["m-input m-select", className]}
24
+ aria-invalid={invalid ? true : undefined}
25
+ >
26
+ {@render children?.()}
27
+ </select>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLSelectAttributes } from "svelte/elements";
3
+ interface Props extends Omit<HTMLSelectAttributes, "value"> {
4
+ value?: string;
5
+ invalid?: boolean;
6
+ children?: Snippet;
7
+ }
8
+ declare const Select: import("svelte").Component<Props, {}, "value">;
9
+ type Select = ReturnType<typeof Select>;
10
+ export default Select;
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import type { HTMLButtonAttributes } from "svelte/elements";
3
+
4
+ interface Props extends Omit<HTMLButtonAttributes, "role" | "value"> {
5
+ checked?: boolean;
6
+ onCheckedChange?: (checked: boolean) => void;
7
+ }
8
+
9
+ /** Needs an accessible name via `aria-label` or `aria-labelledby`. */
10
+ let {
11
+ checked = $bindable(false),
12
+ onCheckedChange,
13
+ class: className,
14
+ onclick,
15
+ ...rest
16
+ }: Props = $props();
17
+ </script>
18
+
19
+ <button
20
+ {...rest}
21
+ type="button"
22
+ role="switch"
23
+ aria-checked={checked}
24
+ class={["m-switch", className]}
25
+ onclick={(event) => {
26
+ onclick?.(event);
27
+ if (event.defaultPrevented) {
28
+ return;
29
+ }
30
+ checked = !checked;
31
+ onCheckedChange?.(checked);
32
+ }}
33
+ ></button>
@@ -0,0 +1,8 @@
1
+ import type { HTMLButtonAttributes } from "svelte/elements";
2
+ interface Props extends Omit<HTMLButtonAttributes, "role" | "value"> {
3
+ checked?: boolean;
4
+ onCheckedChange?: (checked: boolean) => void;
5
+ }
6
+ declare const Switch: import("svelte").Component<Props, {}, "checked">;
7
+ type Switch = ReturnType<typeof Switch>;
8
+ export default Switch;
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ import type { TabItem } from "./types.ts";
5
+
6
+ interface Props {
7
+ items: TabItem[];
8
+ /** Accessible name of the tab list. */
9
+ label: string;
10
+ value?: string;
11
+ onValueChange?: (value: string) => void;
12
+ /** Renders the panel content for the selected tab. */
13
+ panel: Snippet<[string]>;
14
+ }
15
+
16
+ let { items, label, value = $bindable(), onValueChange, panel }: Props = $props();
17
+
18
+ const base = $props.id();
19
+ const tabs: Record<string, HTMLButtonElement | undefined> = {};
20
+ const enabled = $derived(items.filter((item) => !item.disabled));
21
+ const selected = $derived(value ?? enabled[0]?.id);
22
+
23
+ function select(id: string) {
24
+ value = id;
25
+ onValueChange?.(id);
26
+ tabs[id]?.focus();
27
+ }
28
+
29
+ function onkeydown(event: KeyboardEvent) {
30
+ const index = enabled.findIndex((item) => item.id === selected);
31
+ const last = enabled.length - 1;
32
+ const target = {
33
+ ArrowRight: enabled[index === last ? 0 : index + 1],
34
+ ArrowLeft: enabled[index <= 0 ? last : index - 1],
35
+ Home: enabled[0],
36
+ End: enabled[last],
37
+ }[event.key];
38
+ if (target) {
39
+ event.preventDefault();
40
+ select(target.id);
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <div class="m-tabs">
46
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
47
+ <div role="tablist" aria-label={label} class="m-tablist" {onkeydown}>
48
+ {#each items as item (item.id)}
49
+ <button
50
+ bind:this={tabs[item.id]}
51
+ type="button"
52
+ role="tab"
53
+ id="{base}-tab-{item.id}"
54
+ class="m-tab"
55
+ aria-selected={item.id === selected}
56
+ aria-controls="{base}-panel-{item.id}"
57
+ tabindex={item.id === selected ? 0 : -1}
58
+ disabled={item.disabled}
59
+ onclick={() => {
60
+ select(item.id);
61
+ }}
62
+ >
63
+ {item.label}
64
+ </button>
65
+ {/each}
66
+ </div>
67
+ {#each items as item (item.id)}
68
+ <div
69
+ role="tabpanel"
70
+ id="{base}-panel-{item.id}"
71
+ class="m-tabpanel"
72
+ aria-labelledby="{base}-tab-{item.id}"
73
+ hidden={item.id !== selected}
74
+ >
75
+ {#if item.id === selected}{@render panel(item.id)}{/if}
76
+ </div>
77
+ {/each}
78
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { TabItem } from "./types.ts";
3
+ interface Props {
4
+ items: TabItem[];
5
+ /** Accessible name of the tab list. */
6
+ label: string;
7
+ value?: string;
8
+ onValueChange?: (value: string) => void;
9
+ /** Renders the panel content for the selected tab. */
10
+ panel: Snippet<[string]>;
11
+ }
12
+ declare const Tabs: import("svelte").Component<Props, {}, "value">;
13
+ type Tabs = ReturnType<typeof Tabs>;
14
+ export default Tabs;
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import type { HTMLTextareaAttributes } from "svelte/elements";
3
+
4
+ interface Props extends HTMLTextareaAttributes {
5
+ invalid?: boolean;
6
+ }
7
+
8
+ let {
9
+ invalid = false,
10
+ value = $bindable(),
11
+ class: className,
12
+ ...rest
13
+ }: Props = $props();
14
+ </script>
15
+
16
+ <textarea
17
+ {...rest}
18
+ bind:value
19
+ class={["m-input m-textarea", className]}
20
+ aria-invalid={invalid ? true : undefined}
21
+ ></textarea>
@@ -0,0 +1,7 @@
1
+ import type { HTMLTextareaAttributes } from "svelte/elements";
2
+ interface Props extends HTMLTextareaAttributes {
3
+ invalid?: boolean;
4
+ }
5
+ declare const Textarea: import("svelte").Component<Props, {}, "value">;
6
+ type Textarea = ReturnType<typeof Textarea>;
7
+ export default Textarea;
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ interface Props {
5
+ content: string;
6
+ /** Receives the props the trigger must spread (`aria-describedby`). */
7
+ children: Snippet<[{ "aria-describedby": string }]>;
8
+ }
9
+
10
+ /** Opens on hover and focus; Esc dismisses. Placement is fixed to the top in the MVP (no collision handling). */
11
+ let { content, children }: Props = $props();
12
+
13
+ const id = $props.id();
14
+ let open = $state(false);
15
+ </script>
16
+
17
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
18
+ <span
19
+ class="m-tooltip-wrap"
20
+ onmouseenter={() => (open = true)}
21
+ onmouseleave={() => (open = false)}
22
+ onfocusin={() => (open = true)}
23
+ onfocusout={() => (open = false)}
24
+ onkeydown={(event) => {
25
+ if (event.key === "Escape") {
26
+ open = false;
27
+ }
28
+ }}
29
+ >
30
+ {@render children({ "aria-describedby": id })}
31
+ <span {id} role="tooltip" class="m-tooltip" hidden={!open}>{content}</span>
32
+ </span>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ content: string;
4
+ /** Receives the props the trigger must spread (`aria-describedby`). */
5
+ children: Snippet<[{
6
+ "aria-describedby": string;
7
+ }]>;
8
+ }
9
+ declare const Tooltip: import("svelte").Component<Props, {}, "">;
10
+ type Tooltip = ReturnType<typeof Tooltip>;
11
+ export default Tooltip;
@@ -0,0 +1,19 @@
1
+ export { default as Avatar } from "./Avatar.svelte";
2
+ export { default as Badge } from "./Badge.svelte";
3
+ export { default as Banner } from "./Banner.svelte";
4
+ export { default as Button } from "./Button.svelte";
5
+ export { default as Card } from "./Card.svelte";
6
+ export { default as Checkbox } from "./Checkbox.svelte";
7
+ export { default as Dialog } from "./Dialog.svelte";
8
+ export { default as EmptyState } from "./EmptyState.svelte";
9
+ export { default as Icon } from "./Icon.svelte";
10
+ export { default as IconButton } from "./IconButton.svelte";
11
+ export { default as Input } from "./Input.svelte";
12
+ export { default as Menu } from "./Menu.svelte";
13
+ export { default as ProgressBar } from "./ProgressBar.svelte";
14
+ export { default as Select } from "./Select.svelte";
15
+ export { default as Switch } from "./Switch.svelte";
16
+ export { default as Tabs } from "./Tabs.svelte";
17
+ export { default as Textarea } from "./Textarea.svelte";
18
+ export { default as Tooltip } from "./Tooltip.svelte";
19
+ export type { BadgeTone, BannerTone, ButtonVariant, ControlSize, MenuItem, TabItem, } from "./types.ts";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ export { default as Avatar } from "./Avatar.svelte";
2
+ export { default as Badge } from "./Badge.svelte";
3
+ export { default as Banner } from "./Banner.svelte";
4
+ export { default as Button } from "./Button.svelte";
5
+ export { default as Card } from "./Card.svelte";
6
+ export { default as Checkbox } from "./Checkbox.svelte";
7
+ export { default as Dialog } from "./Dialog.svelte";
8
+ export { default as EmptyState } from "./EmptyState.svelte";
9
+ export { default as Icon } from "./Icon.svelte";
10
+ export { default as IconButton } from "./IconButton.svelte";
11
+ export { default as Input } from "./Input.svelte";
12
+ export { default as Menu } from "./Menu.svelte";
13
+ export { default as ProgressBar } from "./ProgressBar.svelte";
14
+ export { default as Select } from "./Select.svelte";
15
+ export { default as Switch } from "./Switch.svelte";
16
+ export { default as Tabs } from "./Tabs.svelte";
17
+ export { default as Textarea } from "./Textarea.svelte";
18
+ export { default as Tooltip } from "./Tooltip.svelte";
@@ -0,0 +1,14 @@
1
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
2
+ export type ControlSize = "sm" | "md" | "lg";
3
+ export type BadgeTone = "neutral" | "accent" | "info" | "success" | "warning" | "danger";
4
+ export interface TabItem {
5
+ id: string;
6
+ label: string;
7
+ disabled?: boolean;
8
+ }
9
+ export interface MenuItem {
10
+ id: string;
11
+ label: string;
12
+ disabled?: boolean;
13
+ }
14
+ export type BannerTone = "info" | "success" | "warning" | "danger";
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@interstellarforge/matter-svelte",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "description": "Matter components for Svelte 5 (Vite SPA and SvelteKit). Styling comes from @interstellarforge/matter-css.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/interstellarforge/matter.git",
9
+ "directory": "packages/svelte"
10
+ },
11
+ "type": "module",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "sideEffects": [
16
+ "**/*.css"
17
+ ],
18
+ "svelte": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "svelte": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "publishConfig": {
28
+ "registry": "https://registry.npmjs.org/",
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "peerDependencies": {
33
+ "svelte": "^5.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@sveltejs/package": "2.5.8",
37
+ "@sveltejs/vite-plugin-svelte": "7.3.1",
38
+ "@testing-library/dom": "10.4.2",
39
+ "@testing-library/svelte": "5.4.2",
40
+ "@testing-library/user-event": "14.6.8",
41
+ "axe-core": "4.14.0",
42
+ "jsdom": "30.1.3",
43
+ "svelte": "5.57.3",
44
+ "svelte-check": "4.7.7",
45
+ "vitest": "4.1.6"
46
+ },
47
+ "dependencies": {
48
+ "@interstellarforge/matter-icons": "0.2.0"
49
+ },
50
+ "scripts": {
51
+ "build": "svelte-package -i src -o dist",
52
+ "typecheck": "svelte-check --tsconfig ./tsconfig.json",
53
+ "test": "vitest run"
54
+ }
6
55
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.