@hashrytech/quick-components-kit 0.17.2 → 0.17.4

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,17 @@
1
1
  # @hashrytech/quick-components-kit
2
2
 
3
+ ## 0.17.4
4
+
5
+ ### Patch Changes
6
+
7
+ - patch: adding higher z-index for modal and drawer
8
+
9
+ ## 0.17.3
10
+
11
+ ### Patch Changes
12
+
13
+ - refactor: Drawers, Modals, Tables, trap focus etc
14
+
3
15
  ## 0.17.2
4
16
 
5
17
  ### Patch Changes
@@ -1,7 +1,7 @@
1
1
  export type PortalOptions = {
2
2
  target?: HTMLElement;
3
- prepend?: boolean;
3
+ append?: boolean;
4
4
  };
5
- export declare function portalAction(node: HTMLElement, { target, prepend }?: PortalOptions): {
5
+ export declare function portalAction(node: HTMLElement, { target, append }?: PortalOptions): {
6
6
  destroy(): void;
7
7
  };
@@ -21,15 +21,15 @@
21
21
  * @param node - The HTML element to be portaled.
22
22
  * @param options - Optional config:
23
23
  * - `target`: The DOM node to portal into (defaults to `document.body`)
24
- * - `prepend`: Whether to insert at the beginning instead of appending (default: true)
24
+ * - `append`: Whether to insert at the end instead of appending to the begining (default: true)
25
25
  */
26
26
  import { browser } from '$app/environment';
27
- export function portalAction(node, { target = browser ? document.body : undefined, prepend = true } = {}) {
28
- if (prepend) {
29
- target?.prepend(node);
27
+ export function portalAction(node, { target = browser ? document.body : undefined, append = true } = {}) {
28
+ if (append) {
29
+ target?.appendChild(node);
30
30
  }
31
31
  else {
32
- target?.appendChild(node);
32
+ target?.prepend(node);
33
33
  }
34
34
  return {
35
35
  destroy() {
@@ -0,0 +1,25 @@
1
+ export declare const FOCUSABLE_ELEMENTS: string[];
2
+ /**
3
+ * @action trapFocus
4
+ *
5
+ * A Svelte action to trap keyboard focus within a DOM node.
6
+ * Useful for modals, dialogs, and overlays to ensure accessibility and usability.
7
+ *
8
+ * Automatically focuses the first focusable element inside the node when mounted.
9
+ * Prevents `Tab` and `Shift+Tab` from leaving the focusable area.
10
+ * Restores focus to the previously focused element when destroyed.
11
+ *
12
+ * @example
13
+ * ```svelte
14
+ * <div use:trapFocus>
15
+ * <button>First</button>
16
+ * <button>Second</button>
17
+ * </div>
18
+ * ```
19
+ *
20
+ * @param node HTMLElement to trap focus within
21
+ * @returns An object with a `destroy` method to clean up listeners and restore previous focus
22
+ */
23
+ export declare function trapFocus(node: HTMLElement): {
24
+ destroy(): void;
25
+ };
@@ -0,0 +1,91 @@
1
+ export const FOCUSABLE_ELEMENTS = [
2
+ 'a[href]',
3
+ 'area[href]',
4
+ 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
5
+ 'select:not([disabled]):not([aria-hidden])',
6
+ 'textarea:not([disabled]):not([aria-hidden])',
7
+ 'button:not([disabled]):not([aria-hidden])',
8
+ 'iframe',
9
+ 'object',
10
+ 'embed',
11
+ '[contenteditable]',
12
+ '[tabindex]:not([tabindex^="-"])',
13
+ ];
14
+ /**
15
+ * @action trapFocus
16
+ *
17
+ * A Svelte action to trap keyboard focus within a DOM node.
18
+ * Useful for modals, dialogs, and overlays to ensure accessibility and usability.
19
+ *
20
+ * Automatically focuses the first focusable element inside the node when mounted.
21
+ * Prevents `Tab` and `Shift+Tab` from leaving the focusable area.
22
+ * Restores focus to the previously focused element when destroyed.
23
+ *
24
+ * @example
25
+ * ```svelte
26
+ * <div use:trapFocus>
27
+ * <button>First</button>
28
+ * <button>Second</button>
29
+ * </div>
30
+ * ```
31
+ *
32
+ * @param node HTMLElement to trap focus within
33
+ * @returns An object with a `destroy` method to clean up listeners and restore previous focus
34
+ */
35
+ export function trapFocus(node) {
36
+ const previous = document.activeElement;
37
+ function focusable() {
38
+ return Array.from(node.querySelectorAll('button, [href], input, select, textarea, iframe, object, embed, [contenteditable], [tabindex]:not([tabindex="-1"])')).filter(el => {
39
+ // Skip if disabled
40
+ if (el.hasAttribute('disabled'))
41
+ return false;
42
+ // Skip if hidden attribute is present
43
+ if (el.hidden)
44
+ return false;
45
+ // Skip if any parent is aria-hidden
46
+ let current = el;
47
+ while (current) {
48
+ if (current.getAttribute('aria-hidden') === 'true')
49
+ return false;
50
+ current = current.parentElement;
51
+ }
52
+ // Skip if not visible via CSS
53
+ const style = getComputedStyle(el);
54
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
55
+ return false;
56
+ return true;
57
+ });
58
+ }
59
+ function handleKeydown(event) {
60
+ if (event.key !== 'Tab')
61
+ return;
62
+ const elements = focusable();
63
+ if (elements.length === 0)
64
+ return;
65
+ const first = elements[0];
66
+ const last = elements[elements.length - 1];
67
+ const active = document.activeElement;
68
+ if (event.shiftKey) {
69
+ if (active === first || !elements.includes(active)) {
70
+ last.focus();
71
+ event.preventDefault();
72
+ }
73
+ }
74
+ else {
75
+ if (active === last || !elements.includes(active)) {
76
+ first.focus();
77
+ event.preventDefault();
78
+ }
79
+ }
80
+ }
81
+ // Focus the first element and add keydown listener
82
+ const elements = focusable();
83
+ elements[0]?.focus();
84
+ node.addEventListener('keydown', handleKeydown);
85
+ return {
86
+ destroy() {
87
+ node.removeEventListener('keydown', handleKeydown);
88
+ previous?.focus();
89
+ }
90
+ };
91
+ }
@@ -20,7 +20,7 @@
20
20
 
21
21
  </script>
22
22
 
23
- <button {disabled} class={twMerge("flex flex-row items-center gap-2 px-4 py-2 bg-primary-button hover:bg-primary-button-hover rounded-primary w-fit cursor-pointer text-white focus:outline-focus-primary",
23
+ <button {disabled} class={twMerge("flex flex-row items-center gap-2 px-4 py-2 focus:outline-primary-focus bg-primary-button hover:bg-primary-button-hover rounded-primary w-fit cursor-pointer text-white focus:ring-primary-focus focus:ring",
24
24
  "disabled:bg-primary-button/60 disabled:cursor-default", props.class)}
25
25
  {onclick}>
26
26
  {#if icon}<span class="w-10">{@render icon()}</span>{/if}
@@ -1,65 +1,60 @@
1
1
  <!--
2
2
  @component Drawer
3
3
 
4
- A flexible slide-in drawer component built with Svelte 5 and Tailwind CSS. Supports positional rendering (`left`, `right`, `top`, `bottom`), transition control, accessibility, and optional background overlay. Typically used for side menus, filters, or modal-like panels.
4
+ A flexible, accessible slide-in drawer component for Svelte 5 using Tailwind CSS and transitions. Includes focus trapping, overlay support, inert background, and escape key handling.
5
5
 
6
6
  ## Props
7
7
 
8
8
  - `open?`: `boolean` — Whether the drawer is visible. Bindable.
9
- - `escapeKeyClose?`: `boolean` — If true, pressing Escape closes the drawer. Default: `true`.
10
- - `disableBodyScroll?`: `boolean` — Prevents body scrolling when drawer is open. Default: `true`.
11
- - `ariaLabel?`: `string` — Accessibility label for the drawer. Default: `"Drawer"`.
12
- - `position?`: `"left" | "right" | "top" | "bottom"` — Direction the drawer slides from. Default: `"left"`.
13
- - `transitionDuration?`: `number` Duration of fly transition in milliseconds. Defaults to `config.transitionDuration`.
14
- - `transitionDistance?`: `number` — Distance the drawer slides in from. Default: `240`.
15
- - `overlayClasses?`: `string` — Additional classes passed to the background overlay.
16
- - `class?`: `ClassNameValue` — Tailwind classes for the drawer content container.
17
- - `children?`: `Snippet` — Svelte fragment rendered inside the drawer content.
18
-
19
- ## Slots
20
-
21
- This component uses `children` as a rendered snippet via `{@render}`.
22
-
23
- ## Overlay
24
-
25
- The `<Overlay>` component is used to darken the background and optionally block interaction. It is clickable and will trigger the drawer to close unless stopped by event propagation. You can customize its styling using the `overlayClasses` prop.
9
+ - `escapeKeyClose?`: `boolean` — If `true`, allows closing the drawer via the Escape key. Default: `true`.
10
+ - `disableBodyScroll?`: `boolean` — Prevents body scroll while drawer is open. Default: `true`.
11
+ - `inertId?`: `string` — DOM element ID to set `inert` while the drawer is open (for accessibility).
12
+ - `ariaLabel?`: `string` — ARIA label for screen readers. Default: `"Drawer"`.
13
+ - `position?`: `"left" | "right" | "top" | "bottom"` — Drawer slide direction. Default: `"left"`.
14
+ - `transitionDuration?`: `number` — Drawer animation duration in milliseconds.
15
+ - `transitionDistance?`: `number` — Distance the drawer flies in from (px).
16
+ - `overlayClasses?`: `string` — Custom Tailwind classes for the backdrop overlay.
17
+ - `class?`: `ClassNameValue` — Classes passed to the drawer container.
18
+ - `children?`: `Snippet` — Svelte content rendered inside the drawer.
26
19
 
27
20
  ## Usage
28
21
 
29
22
  ```svelte
30
23
  <script>
31
- import Drawer from '@hashrytech/quick-components-kit';
32
- let isOpen = false;
24
+ import { Drawer } from '@your-lib';
25
+ let open = false;
33
26
  </script>
34
27
 
35
- <Drawer bind:open={isOpen} position="right" ariaLabel="Menu" transitionDuration={300}>
28
+ <Drawer bind:open={open} position="right" inertId="main-content">
36
29
  <p class="p-4">Drawer content goes here</p>
37
30
  </Drawer>
38
31
  ```
39
32
  -->
40
33
 
41
34
  <script lang="ts" module>
42
- import { type Snippet } from 'svelte';
43
- import type { ClassNameValue } from 'tailwind-merge';
44
- import { fly } from 'svelte/transition';
45
- import {twMerge} from 'tailwind-merge';
46
- import Overlay from '../overlay/Overlay.svelte';
47
- import { onKeydown } from '../../actions/on-keydown.js';
48
- import { stopInteraction } from '../../actions/stop-interaction.js';
49
- import { config } from '../../configs/config.js';
35
+ import { type Snippet } from 'svelte';
36
+ import type { ClassNameValue } from 'tailwind-merge';
37
+ import { fly } from 'svelte/transition';
38
+ import { twMerge } from 'tailwind-merge';
39
+ import { Overlay } from '../overlay/index.js';
40
+ import { onKeydown } from '../../actions/on-keydown.js';
41
+ import { config } from '../../configs/config.js';
42
+ import { Portal } from "../portal/index.js";
43
+ import { trapFocus } from '../../actions/trap-focus.js';
50
44
 
51
- export type DrawerProps = {
52
- open?: boolean;
53
- escapeKeyClose?: boolean;
54
- disableBodyScroll?: boolean;
55
- ariaLabel?: string;
56
- transitionDuration?: number;
57
- transitionDistance?: number;
58
- position?: "left" | "right" | "top" | "bottom";
59
- overlayClasses?: string;
60
- children?: Snippet;
61
- class?: ClassNameValue;
62
- };
45
+ export type DrawerProps = {
46
+ open?: boolean;
47
+ escapeKeyClose?: boolean;
48
+ disableBodyScroll?: boolean;
49
+ inertId?: string;
50
+ ariaLabel?: string;
51
+ transitionDuration?: number;
52
+ transitionDistance?: number;
53
+ position?: "left" | "right" | "top" | "bottom";
54
+ overlayClasses?: string;
55
+ children?: Snippet;
56
+ class?: ClassNameValue;
57
+ };
63
58
 
64
59
  </script>
65
60
 
@@ -68,6 +63,7 @@ The `<Overlay>` component is used to darken the background and optionally block
68
63
  open=$bindable(false),
69
64
  escapeKeyClose=true,
70
65
  disableBodyScroll=true,
66
+ inertId,
71
67
  ariaLabel="Drawer",
72
68
  position="left",
73
69
  transitionDuration=config.transitionDuration,
@@ -77,6 +73,8 @@ The `<Overlay>` component is used to darken the background and optionally block
77
73
  ...props
78
74
  }: DrawerProps = $props();
79
75
 
76
+ let drawerElement: HTMLDivElement | undefined = $state();
77
+
80
78
  const transitionProperties = {
81
79
  x: position == "left" ? -transitionDistance : position == "right" ? transitionDistance : 0,
82
80
  y: position == "top" ? -transitionDistance : position == "bottom" ? transitionDistance : 0,
@@ -90,6 +88,15 @@ The `<Overlay>` component is used to darken the background and optionally block
90
88
  bottom: "bottom-0 left-0 right-0 h-60",
91
89
  };
92
90
 
91
+ $effect(() => {
92
+ if (open && drawerElement) {
93
+ if(inertId)
94
+ document.getElementById(inertId)?.setAttribute("inert", "true");
95
+ drawerElement?.focus();
96
+
97
+ }
98
+ });
99
+
93
100
  function handleKeydown() {
94
101
  if(open && escapeKeyClose) {
95
102
  closeDrawer();
@@ -97,21 +104,23 @@ The `<Overlay>` component is used to darken the background and optionally block
97
104
  };
98
105
 
99
106
  export function closeDrawer() {
100
- open = false;
107
+ if(inertId)
108
+ document.getElementById(inertId)?.removeAttribute("inert");
109
+ open = false;
101
110
  };
102
111
 
103
112
  </script>
104
113
 
105
114
  {#if open}
106
- <div class="fixed inset-0">
107
- <Overlay {transitionDuration} {disableBodyScroll} class={overlayClasses} onclick={() => open = false} />
108
- <div role="dialog" aria-modal="true" aria-label={ariaLabel} tabindex="{open ? 0 : -1}" aria-hidden="{!open}"
109
- class={twMerge("fixed flex flex-col items-center gap-2 bg-white outline-0 focus:outline-0 active:outline-focus-primary focus:outline-focus-primary overflow-y-auto z-50", postionClasses[position], props.class)}
115
+ <Portal class="fixed z-50">
116
+ <Overlay {transitionDuration} {disableBodyScroll} class={overlayClasses} onclick={closeDrawer} />
117
+ <div bind:this={drawerElement} use:trapFocus role="dialog" aria-modal="true" aria-label={ariaLabel} tabindex={open ? 0 : -1} aria-hidden={!open}
118
+ class={twMerge("fixed bg-white overflow-y-auto focus:outline-none", postionClasses[position], props.class)}
110
119
  in:fly={transitionProperties}
111
120
  out:fly={transitionProperties}
112
121
  use:onKeydown={{key: "Escape", callback: handleKeydown}}>
113
122
  {@render children?.()}
114
123
  </div>
115
- </div>
124
+ </Portal>
116
125
  {/if}
117
126
 
@@ -4,6 +4,7 @@ export type DrawerProps = {
4
4
  open?: boolean;
5
5
  escapeKeyClose?: boolean;
6
6
  disableBodyScroll?: boolean;
7
+ inertId?: string;
7
8
  ariaLabel?: string;
8
9
  transitionDuration?: number;
9
10
  transitionDistance?: number;
@@ -15,38 +16,31 @@ export type DrawerProps = {
15
16
  /**
16
17
  * Drawer
17
18
  *
18
- * A flexible slide-in drawer component built with Svelte 5 and Tailwind CSS. Supports positional rendering (`left`, `right`, `top`, `bottom`), transition control, accessibility, and optional background overlay. Typically used for side menus, filters, or modal-like panels.
19
+ * A flexible, accessible slide-in drawer component for Svelte 5 using Tailwind CSS and transitions. Includes focus trapping, overlay support, inert background, and escape key handling.
19
20
  *
20
21
  * ## Props
21
22
  *
22
23
  * - `open?`: `boolean` — Whether the drawer is visible. Bindable.
23
- * - `escapeKeyClose?`: `boolean` — If true, pressing Escape closes the drawer. Default: `true`.
24
- * - `disableBodyScroll?`: `boolean` — Prevents body scrolling when drawer is open. Default: `true`.
25
- * - `ariaLabel?`: `string` — Accessibility label for the drawer. Default: `"Drawer"`.
26
- * - `position?`: `"left" | "right" | "top" | "bottom"` — Direction the drawer slides from. Default: `"left"`.
27
- * - `transitionDuration?`: `number` Duration of fly transition in milliseconds. Defaults to `config.transitionDuration`.
28
- * - `transitionDistance?`: `number` — Distance the drawer slides in from. Default: `240`.
29
- * - `overlayClasses?`: `string` — Additional classes passed to the background overlay.
30
- * - `class?`: `ClassNameValue` — Tailwind classes for the drawer content container.
31
- * - `children?`: `Snippet` — Svelte fragment rendered inside the drawer content.
32
- *
33
- * ## Slots
34
- *
35
- * This component uses `children` as a rendered snippet via `{@render}`.
36
- *
37
- * ## Overlay
38
- *
39
- * The `<Overlay>` component is used to darken the background and optionally block interaction. It is clickable and will trigger the drawer to close unless stopped by event propagation. You can customize its styling using the `overlayClasses` prop.
24
+ * - `escapeKeyClose?`: `boolean` — If `true`, allows closing the drawer via the Escape key. Default: `true`.
25
+ * - `disableBodyScroll?`: `boolean` — Prevents body scroll while drawer is open. Default: `true`.
26
+ * - `inertId?`: `string` — DOM element ID to set `inert` while the drawer is open (for accessibility).
27
+ * - `ariaLabel?`: `string` — ARIA label for screen readers. Default: `"Drawer"`.
28
+ * - `position?`: `"left" | "right" | "top" | "bottom"` — Drawer slide direction. Default: `"left"`.
29
+ * - `transitionDuration?`: `number` — Drawer animation duration in milliseconds.
30
+ * - `transitionDistance?`: `number` — Distance the drawer flies in from (px).
31
+ * - `overlayClasses?`: `string` — Custom Tailwind classes for the backdrop overlay.
32
+ * - `class?`: `ClassNameValue` — Classes passed to the drawer container.
33
+ * - `children?`: `Snippet` — Svelte content rendered inside the drawer.
40
34
  *
41
35
  * ## Usage
42
36
  *
43
37
  * ```svelte
44
38
  * <script>
45
- * import Drawer from '@hashrytech/quick-components-kit';
46
- * let isOpen = false;
39
+ * import { Drawer } from '@your-lib';
40
+ * let open = false;
47
41
  * </script>
48
42
  *
49
- * <Drawer bind:open={isOpen} position="right" ariaLabel="Menu" transitionDuration={300}>
43
+ * <Drawer bind:open={open} position="right" inertId="main-content">
50
44
  * <p class="p-4">Drawer content goes here</p>
51
45
  * </Drawer>
52
46
  * ```
@@ -2,15 +2,20 @@
2
2
  import { type Snippet } from 'svelte';
3
3
  import type { ClassNameValue } from 'tailwind-merge';
4
4
  import {twMerge} from 'tailwind-merge';
5
- import Overlay from '../overlay/Overlay.svelte';
5
+ import { Overlay } from '../overlay/index.js';
6
6
  import { onKeydown } from '../../actions/on-keydown.js';
7
+ import { Portal } from "../portal/index.js";
8
+ import { config } from '../../configs/config.js';
9
+ import { fly } from 'svelte/transition';
10
+ import { trapFocus } from '../../actions/trap-focus.js';
7
11
 
8
12
  export type ModalProps = {
9
13
  open?: boolean;
10
14
  escapeKeyClose?: boolean;
11
15
  disableBodyScroll?: boolean;
12
16
  ariaLabel?: string;
13
- overlayTransitionDuration?: number;
17
+ inertId?: string;
18
+ transitionDuration?: number;
14
19
  overlayClasses?: string;
15
20
  children?: Snippet;
16
21
  class?: ClassNameValue;
@@ -19,10 +24,24 @@
19
24
  </script>
20
25
 
21
26
  <script lang="ts">
22
- let {open=$bindable(false), escapeKeyClose=true, disableBodyScroll=true, overlayTransitionDuration=100, ariaLabel="Modal", overlayClasses="", children, ...props}: ModalProps = $props();
27
+ let {open=$bindable(false), escapeKeyClose=true, disableBodyScroll=true, transitionDuration=config.transitionDuration, inertId, ariaLabel="Modal", overlayClasses="", children, ...props}: ModalProps = $props();
28
+
29
+ let modalElement: HTMLDivElement | undefined = $state();
30
+
31
+ $effect(() => {
32
+ if (open && modalElement) {
33
+ if(inertId)
34
+ document.getElementById(inertId)?.setAttribute("inert", "true");
35
+ modalElement?.focus();
36
+
37
+ }
38
+ });
23
39
 
24
40
  export function closeModal() {
25
41
  open = false;
42
+ if(inertId)
43
+ document.getElementById(inertId)?.removeAttribute("inert");
44
+
26
45
  };
27
46
 
28
47
  function handleKeydown() {
@@ -34,15 +53,18 @@
34
53
  </script>
35
54
 
36
55
  {#if open}
37
- <Overlay transitionDuration={overlayTransitionDuration} {disableBodyScroll} class={overlayClasses} onclick={() => open = false}>
38
-
39
- <div role="dialog" aria-modal="true" aria-label={ariaLabel} tabindex="{open ? 0 : -1}" aria-hidden="{!open}"
40
- class={twMerge("fixed bg-white rounded top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 outline-0 focus:outline-0 active:outline-focus-primary focus:outline-focus-primary overflow-y-auto w-full max-w-md h-96 z-50", props.class)}
41
- use:onKeydown={{key: "Escape", callback: handleKeydown}}>
42
- {@render children?.()}
43
- </div>
56
+ <Portal class="fixed z-50">
57
+ <Overlay {transitionDuration} {disableBodyScroll} class={overlayClasses} onclick={closeModal} />
58
+ <div bind:this={modalElement} use:trapFocus role="dialog" aria-modal="true" aria-label={ariaLabel} tabindex={open ? 0 : -1} aria-hidden={!open}
59
+ class={twMerge("fixed w-full max-w-2xl top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 h-6/12 overflow-y-auto focus:outline-none bg-white rounded-primary", props.class)}
60
+ use:onKeydown={{key: "Escape", callback: handleKeydown}}
61
+ in:fly={{y: 150, duration: transitionDuration}}
62
+ out:fly={{y: 150, duration: transitionDuration}}>
63
+
64
+ {@render children?.()}
44
65
 
45
- </Overlay>
66
+ </div>
67
+ </Portal>
46
68
  {/if}
47
69
 
48
70
 
@@ -5,7 +5,8 @@ export type ModalProps = {
5
5
  escapeKeyClose?: boolean;
6
6
  disableBodyScroll?: boolean;
7
7
  ariaLabel?: string;
8
- overlayTransitionDuration?: number;
8
+ inertId?: string;
9
+ transitionDuration?: number;
9
10
  overlayClasses?: string;
10
11
  children?: Snippet;
11
12
  class?: ClassNameValue;
@@ -11,7 +11,7 @@
11
11
  ariaLabel?: string;
12
12
  transitionDuration?: number;
13
13
  children?: Snippet;
14
- onclick?: (event: MouseEvent) => void;
14
+ onclick?: (event: Event) => void;
15
15
  class?: ClassNameValue;
16
16
  };
17
17
 
@@ -21,7 +21,7 @@
21
21
  let { disableBodyScroll=true, transitionDuration=config.transitionDuration, ariaLabel="Overlay", onclick, children, ...props }: OverlayProps = $props();
22
22
  </script>
23
23
 
24
- <div transition:fade={{duration: transitionDuration}} class={twMerge("fixed top-0 left-0 right-0 bottom-0 bg-primary-overlay z-40", props.class)} role="presentation" {onclick} use:disableScroll={disableBodyScroll}>
24
+ <div transition:fade={{duration: transitionDuration}} class={twMerge("fixed inset-0 bg-primary-overlay", props.class)} role="presentation" {onclick} use:disableScroll={disableBodyScroll} >
25
25
  {@render children?.()}
26
26
  </div>
27
27
 
@@ -5,7 +5,7 @@ export type OverlayProps = {
5
5
  ariaLabel?: string;
6
6
  transitionDuration?: number;
7
7
  children?: Snippet;
8
- onclick?: (event: MouseEvent) => void;
8
+ onclick?: (event: Event) => void;
9
9
  class?: ClassNameValue;
10
10
  };
11
11
  declare const Overlay: import("svelte").Component<OverlayProps, {}, "">;
@@ -1 +1 @@
1
- export { default as Overlay } from './Overlay.svelte';
1
+ export { default as Overlay } from "./Overlay.svelte";
@@ -1 +1 @@
1
- export { default as Overlay } from './Overlay.svelte';
1
+ export { default as Overlay } from "./Overlay.svelte";
@@ -1,12 +1,12 @@
1
1
  <!--
2
2
  @component Portal
3
3
 
4
- A utility component that renders its children outside the current DOM hierarchy by prepending them into a target DOM element (e.g., `document.body`). Useful for modals, drawers, toasts, and overlays to avoid z-index and layout conflicts.
4
+ A utility component that renders its children outside the current DOM hierarchy by appending or prepending them into a target DOM element (e.g., `document.body`). Useful for modals, drawers, toasts, and overlays to avoid z-index and layout conflicts.
5
5
 
6
6
  ## Props
7
7
 
8
8
  - `target?`: `HTMLElement` — The DOM node to render the content into. Defaults to `document.body` if not provided. Useful for directing content to a specific container.
9
- - `prepend?: boolean` — If true (default), the portal will be prepended to the target node. If false, it will be appended. This controls the render order of layered content.
9
+ - `append?: boolean` — If true (default), the portal will be appended to the end of the target node. If false, it will be prepended to the begining. This controls the render order of layered content.
10
10
  - `children?`: `Snippet` — The Svelte children content to render inside the portal. Use `{@render}` to render dynamic fragments.
11
11
  - `class?`: `ClassNameValue` — Tailwind CSS or custom classes applied to the outer container div.
12
12
 
@@ -36,30 +36,30 @@ A utility component that renders its children outside the current DOM hierarchy
36
36
 
37
37
  <script lang="ts" module>
38
38
  import { type Snippet } from 'svelte';
39
- import type { ClassNameValue } from 'tailwind-merge';
40
- import {twMerge} from 'tailwind-merge';
41
- import { onMount, onDestroy } from 'svelte';
42
- import { browser } from '$app/environment';
43
-
44
- export type PortalProps = {
45
- target?: HTMLElement;
46
- prepend?: boolean;
47
- children?: Snippet;
48
- class?: ClassNameValue;
49
- };
39
+ import type { ClassNameValue } from 'tailwind-merge';
40
+ import {twMerge} from 'tailwind-merge';
41
+ import { onMount, onDestroy } from 'svelte';
42
+ import { browser } from '$app/environment';
43
+
44
+ export type PortalProps = {
45
+ target?: HTMLElement;
46
+ append?: boolean;
47
+ children?: Snippet;
48
+ class?: ClassNameValue;
49
+ };
50
50
 
51
51
  </script>
52
52
 
53
53
  <script lang="ts">
54
- let { target = browser ? document.body : undefined, prepend = true, children, ...props }: PortalProps = $props();
54
+ let { target = browser ? document.body : undefined, append = true, children, ...props }: PortalProps = $props();
55
55
  let el: HTMLDivElement;
56
56
 
57
57
  onMount(() => {
58
58
  if (!browser || !el) return;
59
- if(prepend)
60
- target?.prepend(el);
61
- else
59
+ if(append)
62
60
  target?.append(el);
61
+ else
62
+ target?.prepend(el);
63
63
  });
64
64
 
65
65
  onDestroy(() => {
@@ -2,19 +2,19 @@ import { type Snippet } from 'svelte';
2
2
  import type { ClassNameValue } from 'tailwind-merge';
3
3
  export type PortalProps = {
4
4
  target?: HTMLElement;
5
- prepend?: boolean;
5
+ append?: boolean;
6
6
  children?: Snippet;
7
7
  class?: ClassNameValue;
8
8
  };
9
9
  /**
10
10
  * Portal
11
11
  *
12
- * A utility component that renders its children outside the current DOM hierarchy by prepending them into a target DOM element (e.g., `document.body`). Useful for modals, drawers, toasts, and overlays to avoid z-index and layout conflicts.
12
+ * A utility component that renders its children outside the current DOM hierarchy by appending or prepending them into a target DOM element (e.g., `document.body`). Useful for modals, drawers, toasts, and overlays to avoid z-index and layout conflicts.
13
13
  *
14
14
  * ## Props
15
15
  *
16
16
  * - `target?`: `HTMLElement` — The DOM node to render the content into. Defaults to `document.body` if not provided. Useful for directing content to a specific container.
17
- * - `prepend?: boolean` — If true (default), the portal will be prepended to the target node. If false, it will be appended. This controls the render order of layered content.
17
+ * - `append?: boolean` — If true (default), the portal will be appended to the end of the target node. If false, it will be prepended to the begining. This controls the render order of layered content.
18
18
  * - `children?`: `Snippet` — The Svelte children content to render inside the portal. Use `{@render}` to render dynamic fragments.
19
19
  * - `class?`: `ClassNameValue` — Tailwind CSS or custom classes applied to the outer container div.
20
20
  *
@@ -99,7 +99,7 @@ Supports both desktop and mobile rendering, multi-select checkboxes, and custom
99
99
  -->
100
100
 
101
101
  <script lang="ts" module>
102
- import type { Snippet } from 'svelte';
102
+ import { onMount, type Snippet } from 'svelte';
103
103
  import type { ClassNameValue } from 'tailwind-merge';
104
104
 
105
105
  /**
@@ -171,6 +171,8 @@ Supports both desktop and mobile rendering, multi-select checkboxes, and custom
171
171
  let { showMultiSelect=$bindable(false), selected=$bindable([]), rows=$bindable([]), getKey, headings, tableRow, tableRowMobile, multiSelectTh, multiSelectTd,
172
172
  tableMobileTdClass, outerDivClass, headingsRowClass, multiSelectThClass, tableRowClass, multiSelectTdClass, tableRowMobileClass, checkboxClass, ...props }: TableProps<T> = $props();
173
173
 
174
+ let tableElement: HTMLTableElement | undefined = $state();
175
+
174
176
  function addSelected(rowkey: string){
175
177
  if(selected.includes(rowkey)){
176
178
  selected = selected.filter((item) => item !== rowkey);
@@ -181,27 +183,48 @@ Supports both desktop and mobile rendering, multi-select checkboxes, and custom
181
183
 
182
184
  function handleSelectAll(event: any){
183
185
  if (event.target.checked) {
184
- for(let i = 0; i < rows.length; i++){
185
- selected.push(getKey(rows[i]));
186
- }
186
+ for(let i = 0; i < rows.length; i++){
187
+ selected.push(getKey(rows[i]));
188
+ }
187
189
  }
188
190
  else{
189
191
  selected = [];
190
192
  }
191
193
  }
192
194
 
195
+ /*function applyStickyToSecondColumn(table: HTMLTableElement) {
196
+ if (!table) return;
197
+
198
+ const toAdd = showMultiSelect ? 'left-[3rem]' : "left-0"; // 3 rem is the width of the checkbox column
199
+ const toRemove = showMultiSelect ? 'left-0' : "left-[3rem]";
200
+
201
+ // thead > tr > th:nth-child(2)
202
+ const headCells = table.querySelectorAll('thead tr > th:nth-child(2)');
203
+ for (const cell of headCells) {
204
+ cell.classList.remove(toRemove);
205
+ cell.classList.add('sticky', toAdd);
206
+ }
207
+
208
+ // tbody > tr > td:nth-child(2)
209
+ const bodyCells = table.querySelectorAll('tbody tr > td:nth-child(2)');
210
+ for (const cell of bodyCells) {
211
+ cell.classList.remove(toRemove);
212
+ cell.classList.add('sticky', toAdd);
213
+ }
214
+ }*/
215
+
193
216
  </script>
194
217
 
195
218
 
196
219
  <div class={twMerge("rounded-lg border-primary-table-border w-full overflow-x-auto", tableRowMobile ? "border-0 md:border bg-transparent" : "border bg-white", outerDivClass)}>
197
- <table class={twMerge("table-fixed w-full text-sm", tableRowMobile ? "border-separate border-spacing-y-2 md:border-collapse md:border-spacing-y-0": "", props.class)} cellpadding="10px">
220
+ <table bind:this={tableElement} class={twMerge("table-fixed w-full text-sm", tableRowMobile ? "border-separate border-spacing-y-2 md:border-collapse md:border-spacing-y-0": "", props.class)} cellpadding="10px">
198
221
  <thead class={twMerge("bg-primary-table-heading", tableRowMobile ? "hidden md:table-header-group" : "")}>
199
222
  <tr class={twMerge("text-xs text-primary-table-heading-text whitespace-nowrap bg-inherit", headingsRowClass)}>
200
223
  {#if showMultiSelect}
201
224
  {#if multiSelectTh}
202
225
  {@render multiSelectTh()}
203
226
  {:else}
204
- <th class={twMerge("w-12 px-4 py-2 text-center bg-inherit", multiSelectThClass)}>
227
+ <th class={twMerge("w-12 px-4 py-2 text-center bg-inherit sticky left-0", multiSelectThClass)}>
205
228
  <Checkbox id="header-multiselect-checkbox" onchange={handleSelectAll} checked={ rows ? selected.length === rows.length: false} class={checkboxClass} />
206
229
  </th>
207
230
  {/if}
@@ -222,7 +245,7 @@ Supports both desktop and mobile rendering, multi-select checkboxes, and custom
222
245
  {#if multiSelectTd}
223
246
  {@render multiSelectTd(row)}
224
247
  {:else}
225
- <td class={twMerge("text-center px-4 py-2 bg-inherit", multiSelectTdClass )}>
248
+ <td class={twMerge("text-center px-4 py-2 bg-inherit sticky left-0", multiSelectTdClass )}>
226
249
  <Checkbox id={"checkbox-" + rowKey} value={rowKey} checked={isSelected} onchange={()=>{addSelected(rowKey)}} class={checkboxClass} />
227
250
  </td>
228
251
  {/if}
@@ -1,4 +1,4 @@
1
- import type { Snippet } from 'svelte';
1
+ import { type Snippet } from 'svelte';
2
2
  import type { ClassNameValue } from 'tailwind-merge';
3
3
  /**
4
4
  * Props for the generic Table component
@@ -16,4 +16,4 @@
16
16
 
17
17
  </script>
18
18
 
19
- <th scope="col" class={twMerge("p-3 text-left uppercase font-semibold", props.class)}>{@render children?.()}</th>
19
+ <th scope="col" class={twMerge("p-3 text-left uppercase font-semibold bg-inherit", props.class)}>{@render children?.()}</th>
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/hashrytech/quick-components-kit.git"
7
7
  },
8
- "version": "0.17.2",
8
+ "version": "0.17.4",
9
9
  "license": "MIT",
10
10
  "author": "Hashry Tech",
11
11
  "files": [