svseeds 0.5.3 → 0.6.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.
@@ -25,10 +25,17 @@
25
25
  ```
26
26
  ### Exports
27
27
  ```ts
28
- type RgbColor = [number, number, number];
29
- function getHex(rgb: RgbColor): string
30
- // getHex([255, 123, 34]) => "#ff7b22"
31
- // getHex([255, 255, 255]) => "#ffffff"
28
+ /**
29
+ * Converts RGB color values to hexadecimal color code.
30
+ *
31
+ * @param rgb - Array of RGB values [red, green, blue] where each value is 0-255
32
+ * @returns Hexadecimal color code string starting with '#'
33
+ *
34
+ * @example
35
+ * getHex([255, 123, 34]) // "#ff7b22"
36
+ * getHex([255, 255, 255]) // "#ffffff"
37
+ */
38
+ function getHex(rgb: [number, number, number]): string
32
39
  ```
33
40
  -->
34
41
  <script module lang="ts">
@@ -14,38 +14,4 @@ type RgbColor = [number, number, number];
14
14
  import { type Action } from "svelte/action";
15
15
  import { type HTMLInputAttributes } from "svelte/elements";
16
16
  import { type SVSClass } from "./core";
17
- /**
18
- * ### Types
19
- * default value: *`(value)`*
20
- * ```ts
21
- * interface ColorPickerProps {
22
- * value?: string; // bindable ("#000000")
23
- * alpha?: number; // bindable (1)
24
- * attributes?: HTMLInputAttributes;
25
- * action?: Action;
26
- * element?: HTMLInputElement; // bindable
27
- * styling?: SVSClass;
28
- * variant?: string; // bindable (VARIANT.NEUTRAL)
29
- * }
30
- * ```
31
- * ### Anatomy
32
- * ```svelte
33
- * <label class="whole">
34
- * <div class="middle"> // this middle element is background for transparency color
35
- * <div class="main"> // this main element is color sample
36
- * <input type="color" {...attributes} bind:value bind:this={element} use:action />
37
- * </div>
38
- * </div>
39
- * </label>
40
- * ```
41
- * ### Exports
42
- * ```ts
43
- * type RgbColor = [number, number, number];
44
- * function getHex(rgb: RgbColor): string
45
- * // getHex([255, 123, 34]) => "#ff7b22"
46
- * // getHex([255, 255, 255]) => "#ffffff"
47
- * ```
48
- */
49
- declare const ColorPicker: import("svelte").Component<ColorPickerProps, {}, "variant" | "alpha" | "value" | "element">;
50
- type ColorPicker = ReturnType<typeof ColorPicker>;
51
- export default ColorPicker;
17
+ export {};
@@ -7,7 +7,7 @@
7
7
  label: string | Snippet<[boolean, string]>; // Snippet<[open,variant]>
8
8
  children: Snippet<[string]>; // Snippet<[variant]>
9
9
  open?: boolean; // bindable (false)
10
- duration?: number; // (400)
10
+ duration?: number; // (200)
11
11
  attributes?: HTMLDetailsAttributes;
12
12
  action?: Action;
13
13
  element?: HTMLDetailsElement; // bindable
@@ -32,7 +32,7 @@
32
32
  label: string | Snippet<[boolean, string]>; // Snippet<[open,variant]>
33
33
  children: Snippet<[string]>; // Snippet<[variant]>
34
34
  open?: boolean; // bindable (false)
35
- duration?: number; // (400)
35
+ duration?: number; // (200)
36
36
  attributes?: HTMLDetailsAttributes;
37
37
  action?: Action;
38
38
  element?: HTMLDetailsElement; // bindable
@@ -43,10 +43,13 @@
43
43
  export type DisclosureBindProps = "open" | "variant" | "element";
44
44
 
45
45
  type DisclosureTarget = { currentTarget: EventTarget & HTMLDetailsElement };
46
- const DEFAULT_DURATION = 400;
46
+ const DEFAULT_DURATION = 200;
47
47
  const preset = "svs-disclosure";
48
48
 
49
- const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec));
49
+ function isPrefersReducedMotion(): boolean {
50
+ if (typeof window === "undefined") return false;
51
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
52
+ }
50
53
  class ToggleGurad {
51
54
  #active = false;
52
55
  get active(): boolean {
@@ -54,7 +57,7 @@
54
57
  }
55
58
  activate(duration: number) {
56
59
  this.#active = true;
57
- sleep(duration).then(() => this.#active = false);
60
+ setTimeout(() => this.#active = false, duration);
58
61
  }
59
62
  }
60
63
 
@@ -70,6 +73,7 @@
70
73
 
71
74
  // *** Initialize *** //
72
75
  if (!variant) variant = VARIANT.NEUTRAL;
76
+ if (isPrefersReducedMotion()) duration = 0;
73
77
  if (!isUnsignedInteger(duration)) duration = DEFAULT_DURATION;
74
78
  const cls = fnClass(preset, styling);
75
79
  const attrs = omit(attributes, "class", "open", "ontoggle");
@@ -88,7 +92,7 @@
88
92
  if (open) {
89
93
  toggle(true);
90
94
  } else {
91
- sleep(duration).then(() => toggle(false));
95
+ setTimeout(() => toggle(false), duration);
92
96
  }
93
97
  }
94
98
  function toggle(bool: boolean) {
@@ -23,7 +23,7 @@ import { type SVSClass } from "./core";
23
23
  * label: string | Snippet<[boolean, string]>; // Snippet<[open,variant]>
24
24
  * children: Snippet<[string]>; // Snippet<[variant]>
25
25
  * open?: boolean; // bindable (false)
26
- * duration?: number; // (400)
26
+ * duration?: number; // (200)
27
27
  * attributes?: HTMLDetailsAttributes;
28
28
  * action?: Action;
29
29
  * element?: HTMLDetailsElement; // bindable
@@ -43,9 +43,13 @@
43
43
  export type DrawerReqdProps = "children";
44
44
  export type DrawerBindProps = "open" | "variant" | "element";
45
45
 
46
+ const DEFAULT_DURATION = 200;
46
47
  const preset = "svs-drawer";
47
48
 
48
- const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec));
49
+ function isPrefersReducedMotion(): boolean {
50
+ if (typeof window === "undefined") return false;
51
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
52
+ }
49
53
  function getPositionProp(position: Position): string {
50
54
  switch (position) {
51
55
  case "top":
@@ -63,14 +67,16 @@
63
67
 
64
68
  import { type Snippet, untrack } from "svelte";
65
69
  import { type HTMLAttributes } from "svelte/elements";
66
- import { type SVSClass, VARIANT, PARTS, fnClass, omit } from "./core";
70
+ import { type SVSClass, VARIANT, PARTS, fnClass, isUnsignedInteger, omit } from "./core";
67
71
  </script>
68
72
 
69
73
  <script lang="ts">
70
- let { children, open = $bindable(false), position = "left", size = "auto", duration = 200, closable = true, id, attributes, element = $bindable(), styling, variant = $bindable("") }: DrawerProps = $props();
74
+ let { children, open = $bindable(false), position = "left", size = "auto", duration = -1, closable = true, id, attributes, element = $bindable(), styling, variant = $bindable("") }: DrawerProps = $props();
71
75
 
72
76
  // *** Initialize *** //
73
77
  if (!variant) variant = VARIANT.NEUTRAL;
78
+ if (isPrefersReducedMotion()) duration = 0;
79
+ if (!isUnsignedInteger(duration)) duration = DEFAULT_DURATION;
74
80
  const cls = fnClass(preset, styling);
75
81
  const attrs = omit(attributes, "class", "id", "style", "popover", "ontoggle");
76
82
  const popover = closable ? "auto" : "manual";
@@ -95,7 +101,7 @@
95
101
  attributes?.["ontoggle"]?.(ev as any);
96
102
  open = ev.newState === "open";
97
103
  style = baseStyle + "overflow:hidden;";
98
- sleep(duration).finally(() => style = baseStyle);
104
+ setTimeout(() => style = baseStyle, duration);
99
105
  }
100
106
  $effect(() => untrack(() => { if (open) element?.showPopover(); }));
101
107
  </script>
@@ -0,0 +1,258 @@
1
+ <!--
2
+ @component
3
+ ### Types
4
+ default value: *`(value)`*
5
+ ```ts
6
+ interface ToastProps {
7
+ children: Snippet<[string, string, string, string]>; // Snippet<[message,type,id,variant]>
8
+ name?: string;
9
+ timeout?: number;
10
+ duration?: number; // (200)
11
+ styling?: SVSClass;
12
+ variant?: string; // bindable (VARIANT.NEUTRAL)
13
+ }
14
+ ```
15
+ ### Anatomy
16
+ ```svelte
17
+ <div class="whole">
18
+ {#each}
19
+ <div class="middle" animate:flip={{ duration }}>
20
+ <div class="main">
21
+ {children}
22
+ </div>
23
+ </div>
24
+ {/each}
25
+ </div>
26
+ ```
27
+ ### Exports
28
+ ```ts
29
+ /**
30
+ * Displays a toast notification.
31
+ *
32
+ * @param message - Text message to display in the toast
33
+ * @param type - Optional toast type/category (also used for part of aria-label)
34
+ * @param name - Unique identifier for the toast component
35
+ * @param timeout - Custom auto-dismiss timeout in milliseconds
36
+ * @returns ID of the displayed toast
37
+ */
38
+ function toast(message: string, type?: string, name?: string, timeout?: number): string
39
+ /**
40
+ * Removes a specific toast notification.
41
+ *
42
+ * @param id - ID of the toast to remove
43
+ * @param name - Unique identifier for the toast component
44
+ */
45
+ function removeToast(id: string, name?: string)
46
+ ```
47
+ -->
48
+ <script module lang="ts">
49
+ export interface ToastProps {
50
+ children: Snippet<[string, string, string, string]>; // Snippet<[message,type,id,variant]>
51
+ name?: string;
52
+ timeout?: number;
53
+ duration?: number; // (200)
54
+ styling?: SVSClass;
55
+ variant?: string; // bindable (VARIANT.NEUTRAL)
56
+ }
57
+ export type ToastReqdProps = "children";
58
+ export type ToastBindProps = "variant";
59
+
60
+ export function toast(message: string, type?: string, name?: string, timeout?: number): string {
61
+ return core.add(message, type, name, timeout);
62
+ }
63
+ export function removeToast(id: string, name?: string) {
64
+ core.remove(id, name);
65
+ }
66
+
67
+ const preset = "svs-toast";
68
+ const DEFALT_DURATION = 200;
69
+
70
+ function isPrefersReducedMotion(): boolean {
71
+ if (typeof window === "undefined") return false;
72
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
73
+ }
74
+ function isValidTimeout(timeout?: number): timeout is number {
75
+ return !!timeout && isUnsignedInteger(timeout);
76
+ }
77
+ class ToastCore {
78
+ #containers: Map<string, ToastContainer> = new Map();
79
+ #uniqueId;
80
+
81
+ constructor() {
82
+ this.#uniqueId = new UniqueId();
83
+ }
84
+ register(container: ToastContainer, name?: string) {
85
+ this.#containers.set(this.#uniqueId.register(name), container);
86
+ }
87
+ add(message: string, type?: string, name?: string, timeout?: number): string {
88
+ return this.#containers.get(name ?? this.#uniqueId.default)?.addToast(type ?? "", message, timeout) ?? "";
89
+ }
90
+ remove(id: string, name?: string) {
91
+ this.#containers.get(name ?? this.#uniqueId.default)?.hideToast(id);
92
+ }
93
+ }
94
+ class UniqueId {
95
+ #default = "";
96
+ #ids: Set<string> = new Set();
97
+ get default(): string {
98
+ return this.#default;
99
+ }
100
+ register(name?: string): string {
101
+ const id = name ? name : elemId.id;
102
+ if (this.#ids.has(id)) return "";
103
+ if (!this.#default) this.#default = id;
104
+ this.#ids.add(id);
105
+ return id;
106
+ }
107
+ }
108
+ class ToastContainer {
109
+ #open: boolean = $state(false);
110
+ #style: string;
111
+ #keydownFn = (ev: KeyboardEvent) => {};
112
+ element = $state<HTMLDivElement>();
113
+ toasts: Toast[] = $state([]);
114
+ onkeydown = $derived(this.#open ? this.#keydownFn : undefined);
115
+
116
+ constructor(duration: number, onkeydown: (ev: KeyboardEvent) => void, timeout?: number) {
117
+ this.#style = `position:fixed;background-color:transparent;pointer-events:none;`;
118
+ this.#keydownFn = onkeydown;
119
+ Toast.init(duration, timeout);
120
+ }
121
+ addToast(type: string, message: string, timeout?: number): string {
122
+ if (!this.toasts.length) this.#show();
123
+ const toast = new Toast(type, message, (id) => this.remove(id), timeout);
124
+ this.toasts.push(toast);
125
+ return toast.id;
126
+ }
127
+ hideToast(id: string) {
128
+ this.toasts.find((x) => x.id === id)?.hide();
129
+ }
130
+ remove(id: string) {
131
+ this.toasts = this.toasts.filter((x) => x.id !== id);
132
+ if (!this.toasts.length) this.hide();
133
+ }
134
+ #show() {
135
+ this.element?.showPopover();
136
+ this.#open = true;
137
+ }
138
+ hide() {
139
+ this.element?.hidePopover();
140
+ this.#open = false;
141
+ }
142
+
143
+ get open(): boolean {
144
+ return this.#open;
145
+ }
146
+ get style(): string {
147
+ return this.#style;
148
+ }
149
+ }
150
+ class Toast {
151
+ static #DEFALT_TIMEOUT = Infinity;
152
+ static #DURATION = 0;
153
+ #isVisible = $state(false);
154
+ #id: string;
155
+ #type: string;
156
+ #message: string;
157
+ #remove: (id: string) => void;
158
+ #timeout: number;
159
+ #timeoutId: number = 0;
160
+ constructor(type: string, message: string, remove: (id: string) => void, timeout?: number) {
161
+ this.#id = elemId.id;
162
+ this.#type = type;
163
+ this.#message = message;
164
+ this.#remove = remove;
165
+ this.#timeout = isValidTimeout(timeout) ? timeout : Toast.#DEFALT_TIMEOUT;
166
+ this.#setAutoDismiss();
167
+ }
168
+ show() {
169
+ this.#isVisible = true;
170
+ }
171
+ hide() {
172
+ this.#isVisible = false;
173
+ this.reset();
174
+ setTimeout(() => {
175
+ this.#remove(this.#id);
176
+ }, Toast.#DURATION);
177
+ }
178
+ reset() {
179
+ clearTimeout(this.#timeoutId);
180
+ }
181
+ restart(ev: PointerEvent) {
182
+ this.#setAutoDismiss(ev.pointerType === "touch");
183
+ }
184
+ #setAutoDismiss(extend?: boolean) {
185
+ if (!Number.isFinite(this.#timeout)) return;
186
+ if (typeof window === "undefined") return;
187
+ const timer = extend ? this.#timeout * 1.5 : this.#timeout;
188
+ this.#timeoutId = window.setTimeout(() => this.hide(), timer);
189
+ }
190
+
191
+ static init(duration: number, timeout?: number) {
192
+ Toast.#DEFALT_TIMEOUT = isValidTimeout(timeout) ? timeout : Infinity;
193
+ Toast.#DURATION = duration;
194
+ }
195
+ get label(): string {
196
+ return this.#type ? `${this.#type} message` : "message";
197
+ }
198
+ get isVisible(): boolean {
199
+ return this.#isVisible;
200
+ }
201
+ get id(): string {
202
+ return this.#id;
203
+ }
204
+ get type(): string {
205
+ return this.#type;
206
+ }
207
+ get message(): string {
208
+ return this.#message;
209
+ }
210
+ }
211
+ const core = new ToastCore();
212
+
213
+ import { type Snippet } from "svelte";
214
+ import { flip } from "svelte/animate";
215
+ import { type SVSClass, VARIANT, PARTS, elemId, fnClass, isUnsignedInteger } from "./core";
216
+ </script>
217
+
218
+ <script lang="ts">
219
+ let { children, name, timeout, duration = -1, styling, variant = $bindable("") }: ToastProps = $props();
220
+
221
+ // *** Initialize *** //
222
+ if (!variant) variant = VARIANT.NEUTRAL;
223
+ const cls = fnClass(preset, styling);
224
+ if (isPrefersReducedMotion()) duration = 0;
225
+ if (!isUnsignedInteger(duration)) duration = DEFALT_DURATION;
226
+ const container = new ToastContainer(duration, onkeydown, timeout);
227
+ core.register(container, name);
228
+
229
+ // *** Event Handlers *** //
230
+ function mount(_node: HTMLDivElement, index: number) {
231
+ setTimeout(() => container.toasts[index].show(), 0);
232
+ }
233
+ function enter(index: number): () => void {
234
+ return () => container.toasts[index].reset();
235
+ }
236
+ function leave(index: number): (ev: PointerEvent) => void {
237
+ return (ev) => container.toasts[index].restart(ev);
238
+ }
239
+ function onkeydown(ev: KeyboardEvent) {
240
+ if (ev.key !== "F6") return;
241
+ if (ev.composed) return;
242
+ container.element?.focus();
243
+ }
244
+ </script>
245
+
246
+ <!---------------------------------------->
247
+ <svelte:body onkeydown={container.onkeydown} />
248
+
249
+ <div bind:this={container.element} class={cls(PARTS.WHOLE, variant)} role="region" tabindex="-1" popover="manual" aria-label={`${container.toasts.length} notifications`} style={container.style}>
250
+ {#each container.toasts as toast, i (toast.id)}
251
+ {@const v = toast.isVisible ? variant : VARIANT.INACTIVE}
252
+ <div class={cls(PARTS.MIDDLE, v)} style="width:fit-content;height:fit-content;overflow:hidden;" tabindex="-1" animate:flip={{ duration }}>
253
+ <div class={cls(PARTS.MAIN, v)} role="dialog" aria-modal="false" aria-label={toast.label} tabindex="0" style="pointer-events:auto;" onpointerenter={enter(i)} onpointerleave={leave(i)} onpointercancel={leave(i)} use:mount={i}>
254
+ {@render children(toast.message, toast.type, toast.id, variant)}
255
+ </div>
256
+ </div>
257
+ {/each}
258
+ </div>
@@ -0,0 +1,14 @@
1
+ export interface ToastProps {
2
+ children: Snippet<[string, string, string, string]>;
3
+ name?: string;
4
+ timeout?: number;
5
+ duration?: number;
6
+ styling?: SVSClass;
7
+ variant?: string;
8
+ }
9
+ export type ToastReqdProps = "children";
10
+ export type ToastBindProps = "variant";
11
+ export declare function toast(message: string, type?: string, name?: string, timeout?: number): string;
12
+ export declare function removeToast(id: string, name?: string): void;
13
+ import { type Snippet } from "svelte";
14
+ import { type SVSClass } from "./core";
@@ -27,6 +27,22 @@
27
27
  {/if}
28
28
  </div>
29
29
  ```
30
+ ### Exports
31
+ ```ts
32
+ /**
33
+ * Action function for use with SvSeeds components.
34
+ *
35
+ * @param text - Text content for the tooltip's aria-label
36
+ * @param delay - Delay in milliseconds before the tooltip appears (default: 1000)
37
+ * @param cursor - Whether the tooltip should follow cursor movement
38
+ * @param name - Unique identifier for the tooltip component
39
+ */
40
+ function tooltipAction(text: string, delay?: number, cursor?: boolean, name?: string): Action
41
+ /**
42
+ * Action function for use with standard HTML elements.
43
+ */
44
+ function tooltip(node: HTMLElement, params: { text: string, delay?: number, cursor?: boolean, name?: string }): ActionReturn
45
+ ```
30
46
  -->
31
47
  <script module lang="ts">
32
48
  export interface TooltipProps {
@@ -26,35 +26,3 @@ export type Align = "start" | "center" | "end";
26
26
  import { type Snippet } from "svelte";
27
27
  import { type Action, type ActionReturn } from "svelte/action";
28
28
  import { type SVSClass } from "./core";
29
- /**
30
- * ### Types
31
- * default value: *`(value)`*
32
- * ```ts
33
- * interface TooltipProps {
34
- * children?: Snippet<[string, string, boolean]>; // Snippet<[text,variant,isFlipped]>
35
- * name?: string;
36
- * position?: Position; // ("top")
37
- * align?: Align; // ("center")
38
- * offset?: Vector; // ({ x: 0, y: 0 })
39
- * element?: HTMLDivElement; // bindable
40
- * styling?: SVSClass;
41
- * variant?: string; // bindable (VARIANT.NEUTRAL)
42
- * }
43
- * type Vector = { x: number, y: number };
44
- * type Position = "top" | "right" | "bottom" | "left";
45
- * type Align = "start" | "center" | "end";
46
- * ```
47
- * ### Anatomy
48
- * ```svelte
49
- * <div class="whole" bind:this={element} conditional>
50
- * {#if children}
51
- * {children}
52
- * {:else}
53
- * {text} // `text` is an argument of the tooltip/tooltipAction function
54
- * {/if}
55
- * </div>
56
- * ```
57
- */
58
- declare const Tooltip: import("svelte").Component<TooltipProps, {}, "variant" | "element">;
59
- type Tooltip = ReturnType<typeof Tooltip>;
60
- export default Tooltip;
package/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { default as Sortable, type SortableProps, type SortableReqdProps, type S
15
15
  export { default as Tabs, type TabsProps, type TabsReqdProps, type TabsBindProps } from "./_svseeds/_Tabs.svelte";
16
16
  export { default as TagsInput, type TagsInputProps, type TagsInputReqdProps, type TagsInputBindProps, type TagsInputEvents } from "./_svseeds/_TagsInput.svelte";
17
17
  export { default as TextField, type TextFieldProps, type TextFieldReqdProps, type TextFieldBindProps, type TextFieldValidation } from "./_svseeds/_TextField.svelte";
18
+ export { default as Toast, type ToastProps, type ToastReqdProps, type ToastBindProps, toast, removeToast } from "./_svseeds/_Toast.svelte";
18
19
  export { default as Toggle, type ToggleProps, type ToggleReqdProps, type ToggleBindProps } from "./_svseeds/_Toggle.svelte";
19
20
  export { default as ToggleGroup, type ToggleGroupProps, type ToggleGroupReqdProps, type ToggleGroupBindProps } from "./_svseeds/_ToggleGroup.svelte";
20
21
  export { default as Tooltip, type TooltipProps, type TooltipReqdProps, type TooltipBindProps, type Vector, type Position, type Align, tooltip, tooltipAction } from "./_svseeds/_Tooltip.svelte";
package/index.js CHANGED
@@ -15,6 +15,7 @@ export { default as Sortable, SortableItems } from "./_svseeds/_Sortable.svelte"
15
15
  export { default as Tabs } from "./_svseeds/_Tabs.svelte";
16
16
  export { default as TagsInput } from "./_svseeds/_TagsInput.svelte";
17
17
  export { default as TextField } from "./_svseeds/_TextField.svelte";
18
+ export { default as Toast, toast, removeToast } from "./_svseeds/_Toast.svelte";
18
19
  export { default as Toggle } from "./_svseeds/_Toggle.svelte";
19
20
  export { default as ToggleGroup } from "./_svseeds/_ToggleGroup.svelte";
20
21
  export { default as Tooltip, tooltip, tooltipAction } from "./_svseeds/_Tooltip.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svseeds",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Simple components for Svelte.",
5
5
  "type": "module",
6
6
  "main": "./index.js",