@noxlovette/material 0.1.18 → 0.1.20

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.
@@ -1,4 +1,5 @@
1
1
  export * from './clickOutside.js';
2
2
  export * from './floating.js';
3
3
  export * from './keyboard.js';
4
+ export * from './portal.js';
4
5
  export * from './positionFloating.js';
@@ -1,4 +1,5 @@
1
1
  export * from './clickOutside.js';
2
2
  export * from './floating.js';
3
3
  export * from './keyboard.js';
4
+ export * from './portal.js';
4
5
  export * from './positionFloating.js';
@@ -0,0 +1,3 @@
1
+ export declare function portal(node: HTMLElement, target?: HTMLElement): {
2
+ destroy(): void;
3
+ };
@@ -0,0 +1,8 @@
1
+ export function portal(node, target = document.body) {
2
+ target.appendChild(node);
3
+ return {
4
+ destroy() {
5
+ node.remove();
6
+ }
7
+ };
8
+ }
@@ -14,7 +14,6 @@ Floating action buttons (FABs) help people take primary actions.
14
14
  import { fab } from './theme.js';
15
15
  import type { FABProps } from './types.js';
16
16
  import FABMenu from './FABMenu.svelte';
17
- import ButtonIcon from './ButtonIcon.svelte';
18
17
  import { Layer, Icon } from '../../utils/index.js';
19
18
  import { clickOutside } from '../../actions/index.js';
20
19
  import { Button, type ButtonRootProps } from 'bits-ui';
@@ -34,9 +33,19 @@ Floating action buttons (FABs) help people take primary actions.
34
33
  }: FABProps = $props();
35
34
 
36
35
  const { base, icon, label: labelClass } = $derived(fab({ size, config, expanded }));
37
- const btnCls = $derived(base({ class: clsx(className) }));
38
36
 
39
37
  let showMenu = $state(false);
38
+ let wrapperEl = $state<HTMLElement | null>(null);
39
+
40
+ const btnCls = $derived(
41
+ base({
42
+ class: clsx(
43
+ className,
44
+ 'transition-[border-radius] duration-300',
45
+ withMenu && showMenu && '!rounded-full'
46
+ )
47
+ })
48
+ );
40
49
 
41
50
  function handleClick(e: MouseEvent) {
42
51
  if (disabled) {
@@ -45,24 +54,12 @@ Floating action buttons (FABs) help people take primary actions.
45
54
  }
46
55
  if (withMenu) {
47
56
  e.preventDefault();
48
- showMenu = true;
57
+ showMenu = !showMenu;
49
58
  }
50
59
  }
51
60
  </script>
52
61
 
53
- {#if withMenu && showMenu}
54
- <div class="relative z-50" use:clickOutside={() => (showMenu = false)}>
55
- <FABMenu>
56
- {@render children?.()}
57
- </FABMenu>
58
- <ButtonIcon
59
- variant="filled"
60
- class="shadow-elevation-3"
61
- iconProps={{ name: 'close' }}
62
- onclick={() => (showMenu = !showMenu)}
63
- />
64
- </div>
65
- {:else}
62
+ <div class="contents" bind:this={wrapperEl} use:clickOutside={() => (showMenu = false)}>
66
63
  <Button.Root
67
64
  {disabled}
68
65
  {formaction}
@@ -71,12 +68,18 @@ Floating action buttons (FABs) help people take primary actions.
71
68
  data-cy="m3-fab"
72
69
  {...restProps as ButtonRootProps}
73
70
  >
74
- <Icon {...iconProps} class={icon()} />
71
+ <Icon {...withMenu && showMenu ? { name: 'close' } : iconProps} class={icon()} />
75
72
  <Layer />
76
- {#if expanded}
73
+ {#if expanded && !(withMenu && showMenu)}
77
74
  <p class={labelClass()}>
78
75
  {label}
79
76
  </p>
80
77
  {/if}
81
78
  </Button.Root>
82
- {/if}
79
+
80
+ {#if withMenu && showMenu && wrapperEl?.firstElementChild}
81
+ <FABMenu anchorEl={wrapperEl.firstElementChild as HTMLElement}>
82
+ {@render children?.()}
83
+ </FABMenu>
84
+ {/if}
85
+ </div>
@@ -5,76 +5,31 @@ Should be used with `FABMenuItem` as children.
5
5
  -->
6
6
  <script lang="ts">
7
7
  import clsx from 'clsx';
8
+ import { flip } from '@floating-ui/dom';
8
9
  import { fabMenu } from './theme.js';
9
10
  import type { FABMenuProps } from './types.js';
10
11
  import { enterExit } from '../../animation/enterExit.js';
11
- import { easeEmphasizedDecel } from '../../animation/easing.js';
12
+ import { easeEmphasizedAccel, easeEmphasizedDecel } from '../../animation/easing.js';
13
+ import { floating, portal } from '../../actions/index.js';
12
14
 
13
- let { children, class: className, position = 'top', ...restProps }: FABMenuProps = $props();
15
+ let { children, class: className, anchorEl, ...restProps }: FABMenuProps = $props();
14
16
 
15
- const { base } = $derived(fabMenu({ position }));
16
-
17
- const margin = 8;
18
- const gap = 8;
19
- let coords = $state({
20
- x: 0,
21
- y: 0,
22
- originX: 'left',
23
- originY: 'top',
24
- maxHeight: 0
25
- });
26
-
27
- // TODO: out:enterExit={{ duration: 200, easing: easeEmphasizedAccel, mode: 'scale' }} – if you can solve how to eliminate another button from appearing here, use this too
17
+ const { base } = fabMenu();
28
18
  </script>
29
19
 
30
20
  <ul
31
21
  data-cy="m3-fab-menu"
32
22
  {...restProps}
33
- class={base({
34
- class: clsx(className, 'fab-menu overflow-y-auto')
35
- })}
23
+ class={base({ class: clsx(className, 'fab-menu overflow-y-auto') })}
36
24
  in:enterExit={{ duration: 300, easing: easeEmphasizedDecel, mode: 'scale' }}
37
- style:translate={`${coords.x}px ${coords.y}px`}
38
- style:max-height={coords.maxHeight ? `${coords.maxHeight}px` : undefined}
39
- style:--enter-exit-origin={`${coords.originY} ${coords.originX}`}
40
- {@attach (menu) => {
41
- const rect = menu.getBoundingClientRect();
42
- const anchorRect = menu.parentElement?.getBoundingClientRect();
43
- if (!anchorRect) return;
44
-
45
- const spaceAbove = anchorRect.top - gap - margin;
46
- const spaceBelow = window.innerHeight - margin - (anchorRect.bottom + gap);
47
- const placeAbove = spaceAbove >= rect.height || spaceAbove >= Math.max(0, spaceBelow);
48
-
49
- let maxHeight = placeAbove ? spaceAbove : spaceBelow;
50
- maxHeight = Math.max(0, Math.min(rect.height, maxHeight));
51
-
52
- const targetTop = placeAbove ? anchorRect.top - gap - maxHeight : anchorRect.bottom + gap;
53
-
54
- let targetLeft = anchorRect.right - rect.width;
55
-
56
- if (targetLeft + rect.width > window.innerWidth - margin) {
57
- targetLeft = window.innerWidth - margin - rect.width;
58
- }
59
- if (targetLeft < margin) {
60
- targetLeft = margin;
61
- }
62
-
63
- let clampedTop = targetTop;
64
- if (clampedTop < margin) {
65
- clampedTop = margin;
66
- }
67
- if (clampedTop + maxHeight > window.innerHeight - margin) {
68
- clampedTop = window.innerHeight - margin - maxHeight;
69
- }
70
-
71
- const x = targetLeft - rect.left;
72
- const y = clampedTop - rect.top;
73
- const originY = placeAbove ? 'bottom' : 'top';
74
- const originX =
75
- anchorRect.left + anchorRect.width / 2 < targetLeft + rect.width / 2 ? 'left' : 'right';
76
-
77
- coords = { x, y, originX, originY, maxHeight };
25
+ out:enterExit={{ duration: 200, easing: easeEmphasizedAccel, mode: 'scale' }}
26
+ use:portal
27
+ use:floating={{
28
+ reference: anchorEl,
29
+ strategy: 'fixed',
30
+ placement: 'top-end',
31
+ offsetPx: 8,
32
+ middleware: [flip()]
78
33
  }}
79
34
  >
80
35
  {@render children()}
@@ -82,6 +37,6 @@ Should be used with `FABMenuItem` as children.
82
37
 
83
38
  <style>
84
39
  .fab-menu {
85
- transform-origin: var(--enter-exit-origin, bottom right);
40
+ transform-origin: bottom right;
86
41
  }
87
42
  </style>
@@ -27,8 +27,6 @@ Uses Material 3 selection button styling.
27
27
 
28
28
  const { base, icon } = $derived(
29
29
  button({
30
- variant: 'filled',
31
- color: 'default',
32
30
  usage: 'selection',
33
31
  shape,
34
32
  size,
@@ -491,22 +491,28 @@ export declare const fab: import("tailwind-variants").TVReturnType<{
491
491
  label: string;
492
492
  }, undefined, unknown, unknown, undefined>>;
493
493
  export declare const fabMenu: import("tailwind-variants").TVReturnType<{
494
- position: {
495
- top: string;
494
+ [key: string]: {
495
+ [key: string]: import("tailwind-merge").ClassNameValue | {
496
+ base?: import("tailwind-merge").ClassNameValue;
497
+ };
496
498
  };
497
- }, {
499
+ } | {
500
+ [x: string]: {
501
+ [x: string]: import("tailwind-merge").ClassNameValue | {
502
+ base?: import("tailwind-merge").ClassNameValue;
503
+ };
504
+ };
505
+ } | {}, {
498
506
  base: string;
499
507
  }, undefined, {
500
- position: {
501
- top: string;
508
+ [key: string]: {
509
+ [key: string]: import("tailwind-merge").ClassNameValue | {
510
+ base?: import("tailwind-merge").ClassNameValue;
511
+ };
502
512
  };
503
- }, {
513
+ } | {}, {
504
514
  base: string;
505
- }, import("tailwind-variants").TVReturnType<{
506
- position: {
507
- top: string;
508
- };
509
- }, {
515
+ }, import("tailwind-variants").TVReturnType<unknown, {
510
516
  base: string;
511
517
  }, undefined, unknown, unknown, undefined>>;
512
518
  export declare const fabMenuItem: import("tailwind-variants").TVReturnType<{
@@ -66,7 +66,7 @@ export const button = tv({
66
66
  {
67
67
  usage: 'selection',
68
68
  selected: false,
69
- class: { base: 'bg-md-sys-color-surface-container' }
69
+ class: { base: 'bg-md-sys-color-surface-container-high text-md-sys-color-on-surface-variant' }
70
70
  },
71
71
  {
72
72
  variant: 'text',
@@ -275,7 +275,7 @@ export const buttonIcon = tv({
275
275
  {
276
276
  variation: 'toggle',
277
277
  selected: false,
278
- class: { base: 'bg-md-sys-color-surface-container' }
278
+ class: { base: 'bg-md-sys-color-surface-container-high text-md-sys-color-on-surface-variant' }
279
279
  },
280
280
  {
281
281
  variant: 'text',
@@ -474,10 +474,7 @@ export const fab = tv({
474
474
  });
475
475
  export const fabMenu = tv({
476
476
  slots: {
477
- base: 'gap-2 pb-2 flex flex-col absolute z-[100] overflow-visible p-1'
478
- },
479
- variants: {
480
- position: { top: 'top-16' }
477
+ base: 'gap-2 pb-2 flex flex-col z-[9999] overflow-visible p-1'
481
478
  }
482
479
  });
483
480
  export const fabMenuItem = tv({
@@ -1,7 +1,7 @@
1
1
  import type { AnchorButtonAttributes, IconProps } from '../../utils/index.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
4
- import type { ButtonGroupVariants, ButtonIconVariants, ButtonMDVariants, FABMenuItemVariants, FABMenuVariants, FABVariants } from './theme.js';
4
+ import type { ButtonGroupVariants, ButtonIconVariants, ButtonMDVariants, FABMenuItemVariants, FABVariants } from './theme.js';
5
5
  export interface ButtonGroupProps extends ButtonGroupVariants, HTMLAttributes<HTMLDivElement> {
6
6
  /**
7
7
  * The content of the button group.
@@ -157,11 +157,9 @@ export type FABProps = FABVariants & AnchorButtonAttributes & {
157
157
  */
158
158
  children?: Snippet;
159
159
  };
160
- export type FABMenuProps = FABMenuVariants & HTMLAttributes<HTMLUListElement> & {
161
- /**
162
- * The vertical position of the menu relative to the anchor.
163
- */
164
- position?: FABMenuVariants['position'];
160
+ export type FABMenuProps = HTMLAttributes<HTMLUListElement> & {
161
+ /** The FAB button element to position the menu relative to. */
162
+ anchorEl: HTMLElement;
165
163
  /**
166
164
  * The menu items. Use `FABMenuItem` components.
167
165
  */
@@ -26,7 +26,6 @@ Context menus appear on right-click and provide actions related to the clicked e
26
26
  import Divider from '../divider/Divider.svelte';
27
27
  import { enterExit } from '../../../animation/enterExit.js';
28
28
  import { easeEmphasizedDecel } from '../../../animation/easing.js';
29
- import clsx from 'clsx';
30
29
 
31
30
  let {
32
31
  items = [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noxlovette/material",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {