noph-ui 0.43.1 → 0.45.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 (175) hide show
  1. package/README.md +10 -1
  2. package/dist/animation.d.ts +17 -0
  3. package/dist/animation.js +48 -0
  4. package/dist/app-bar/AppBar.svelte +280 -0
  5. package/dist/app-bar/AppBar.svelte.d.ts +4 -0
  6. package/dist/app-bar/index.d.ts +1 -0
  7. package/dist/app-bar/index.js +1 -0
  8. package/dist/app-bar/types.d.ts +35 -0
  9. package/dist/app-bar/types.js +1 -0
  10. package/dist/autocomplete/AutoComplete.svelte +10 -11
  11. package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
  12. package/dist/autocomplete/types.d.ts +1 -1
  13. package/dist/badge/Badge.svelte +11 -1
  14. package/dist/badge/Badge.svelte.d.ts +2 -5
  15. package/dist/badge/types.d.ts +5 -0
  16. package/dist/badge/types.js +1 -0
  17. package/dist/button/Button.svelte +5 -15
  18. package/dist/button/IconButton.svelte +5 -15
  19. package/dist/button/SegmentedButton.svelte +15 -1
  20. package/dist/button/SplitButton.svelte +92 -0
  21. package/dist/button/SplitButton.svelte.d.ts +4 -0
  22. package/dist/button/index.d.ts +1 -0
  23. package/dist/button/index.js +1 -0
  24. package/dist/button/types.d.ts +23 -4
  25. package/dist/card/Card.svelte +1 -1
  26. package/dist/card/types.d.ts +1 -1
  27. package/dist/checkbox/Checkbox.svelte +50 -58
  28. package/dist/chip/AssistChip.svelte +5 -5
  29. package/dist/chip/ChipSet.svelte +3 -10
  30. package/dist/chip/ChipSet.svelte.d.ts +1 -1
  31. package/dist/chip/FilterChip.svelte +35 -27
  32. package/dist/chip/FilterChip.svelte.d.ts +1 -1
  33. package/dist/chip/InputChip.svelte +13 -2
  34. package/dist/chip/SuggestionChip.svelte +29 -0
  35. package/dist/chip/SuggestionChip.svelte.d.ts +4 -0
  36. package/dist/chip/index.d.ts +1 -0
  37. package/dist/chip/index.js +1 -0
  38. package/dist/chip/types.d.ts +26 -10
  39. package/dist/date-picker/Calendar.svelte +1 -1
  40. package/dist/date-picker/DatePickerDialog.svelte +30 -8
  41. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -1
  42. package/dist/date-picker/DateRangePicker.svelte +45 -10
  43. package/dist/date-picker/DateRangePicker.svelte.d.ts +4 -1
  44. package/dist/date-picker/DockedDatePicker.svelte +18 -4
  45. package/dist/date-picker/DockedDatePicker.svelte.d.ts +4 -1
  46. package/dist/date-picker/DockedDateTimePicker.svelte +18 -4
  47. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +4 -1
  48. package/dist/date-picker/SelectionList.svelte +6 -32
  49. package/dist/date-picker/SelectionList.svelte.d.ts +2 -12
  50. package/dist/date-picker/TimeColumn.svelte +4 -29
  51. package/dist/date-picker/exitVisibility.svelte.d.ts +10 -0
  52. package/dist/date-picker/exitVisibility.svelte.js +30 -0
  53. package/dist/date-picker/optionList.d.ts +16 -0
  54. package/dist/date-picker/optionList.js +43 -0
  55. package/dist/date-picker/types.d.ts +11 -4
  56. package/dist/dialog/Dialog.svelte +77 -68
  57. package/dist/dialog/Dialog.svelte.d.ts +3 -3
  58. package/dist/dialog/types.d.ts +5 -3
  59. package/dist/divider/Divider.svelte +6 -2
  60. package/dist/divider/Divider.svelte.d.ts +1 -1
  61. package/dist/divider/types.d.ts +1 -0
  62. package/dist/fab/ExtendedFab.svelte +281 -0
  63. package/dist/fab/ExtendedFab.svelte.d.ts +5 -0
  64. package/dist/fab/Fab.svelte +239 -0
  65. package/dist/fab/Fab.svelte.d.ts +5 -0
  66. package/dist/fab/FabMenu.svelte +255 -0
  67. package/dist/fab/FabMenu.svelte.d.ts +7 -0
  68. package/dist/fab/index.d.ts +3 -0
  69. package/dist/fab/index.js +3 -0
  70. package/dist/fab/types.d.ts +58 -0
  71. package/dist/fab/types.js +1 -0
  72. package/dist/icons/ArrowBackIcon.svelte +13 -0
  73. package/dist/icons/ArrowBackIcon.svelte.d.ts +7 -0
  74. package/dist/icons/ChevronDownIcon.svelte +3 -0
  75. package/dist/icons/ChevronDownIcon.svelte.d.ts +26 -0
  76. package/dist/icons/Icon.svelte +1 -6
  77. package/dist/icons/Icon.svelte.d.ts +1 -6
  78. package/dist/icons/SearchIcon.svelte +15 -0
  79. package/dist/icons/SearchIcon.svelte.d.ts +7 -0
  80. package/dist/icons/index.d.ts +3 -0
  81. package/dist/icons/index.js +3 -0
  82. package/dist/icons/types.d.ts +6 -0
  83. package/dist/icons/types.js +1 -0
  84. package/dist/index.d.ts +6 -0
  85. package/dist/index.js +6 -0
  86. package/dist/internal/focus-ring.css +12 -0
  87. package/dist/list/Item.svelte +5 -0
  88. package/dist/list/Item.svelte.d.ts +1 -1
  89. package/dist/list/ListItem.svelte +2 -2
  90. package/dist/list/ListItem.svelte.d.ts +1 -1
  91. package/dist/list/types.d.ts +1 -0
  92. package/dist/loading-indicator/LoadingIndicator.svelte +7 -2
  93. package/dist/loading-indicator/LoadingIndicator.svelte.d.ts +1 -1
  94. package/dist/loading-indicator/types.d.ts +3 -2
  95. package/dist/menu/Menu.svelte +13 -8
  96. package/dist/menu/Menu.svelte.d.ts +2 -2
  97. package/dist/menu/MenuItem.svelte +2 -2
  98. package/dist/menu/MenuItem.svelte.d.ts +1 -1
  99. package/dist/menu/types.d.ts +0 -2
  100. package/dist/navigation-bar/NavigationBar.svelte +66 -0
  101. package/dist/navigation-bar/NavigationBar.svelte.d.ts +4 -0
  102. package/dist/navigation-bar/NavigationBarItem.svelte +261 -0
  103. package/dist/navigation-bar/NavigationBarItem.svelte.d.ts +5 -0
  104. package/dist/navigation-bar/index.d.ts +2 -0
  105. package/dist/navigation-bar/index.js +2 -0
  106. package/dist/navigation-bar/types.d.ts +29 -0
  107. package/dist/navigation-bar/types.js +1 -0
  108. package/dist/navigation-drawer/NavigationDrawer.svelte +128 -76
  109. package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +4 -1
  110. package/dist/navigation-drawer/NavigationDrawerItem.svelte +13 -3
  111. package/dist/navigation-drawer/NavigationDrawerItem.svelte.d.ts +1 -1
  112. package/dist/navigation-drawer/types.d.ts +7 -1
  113. package/dist/navigation-rail/NavigationRail.svelte +9 -3
  114. package/dist/navigation-rail/NavigationRail.svelte.d.ts +1 -1
  115. package/dist/navigation-rail/NavigationRailItem.svelte +25 -1
  116. package/dist/navigation-rail/NavigationRailItem.svelte.d.ts +1 -1
  117. package/dist/navigation-rail/types.d.ts +7 -1
  118. package/dist/popover.svelte.d.ts +14 -0
  119. package/dist/popover.svelte.js +31 -0
  120. package/dist/press.svelte.d.ts +19 -0
  121. package/dist/press.svelte.js +45 -0
  122. package/dist/progress/CircularProgress.svelte +4 -1
  123. package/dist/progress/CircularProgress.svelte.d.ts +1 -1
  124. package/dist/progress/LinearProgress.svelte +9 -1
  125. package/dist/progress/LinearProgress.svelte.d.ts +1 -1
  126. package/dist/progress/types.d.ts +3 -2
  127. package/dist/radio/Radio.svelte +26 -7
  128. package/dist/radio/types.d.ts +3 -0
  129. package/dist/search/Search.svelte +472 -0
  130. package/dist/search/Search.svelte.d.ts +5 -0
  131. package/dist/search/index.d.ts +1 -0
  132. package/dist/search/index.js +1 -0
  133. package/dist/search/types.d.ts +39 -0
  134. package/dist/search/types.js +1 -0
  135. package/dist/select/NativeSelect.svelte +13 -16
  136. package/dist/select/NativeSelect.svelte.d.ts +2 -12
  137. package/dist/select/Option.svelte +1 -7
  138. package/dist/select/Option.svelte.d.ts +1 -6
  139. package/dist/select/Select.svelte +48 -87
  140. package/dist/select/Select.svelte.d.ts +1 -1
  141. package/dist/select/types.d.ts +15 -1
  142. package/dist/sheet/Sheet.svelte +260 -0
  143. package/dist/sheet/Sheet.svelte.d.ts +7 -0
  144. package/dist/sheet/index.d.ts +1 -0
  145. package/dist/sheet/index.js +1 -0
  146. package/dist/sheet/types.d.ts +19 -0
  147. package/dist/sheet/types.js +1 -0
  148. package/dist/slider/Slider.svelte +154 -30
  149. package/dist/slider/Slider.svelte.d.ts +1 -1
  150. package/dist/slider/types.d.ts +3 -0
  151. package/dist/snackbar/Snackbar.svelte +48 -37
  152. package/dist/snackbar/Snackbar.svelte.d.ts +2 -2
  153. package/dist/snackbar/types.d.ts +3 -2
  154. package/dist/switch/Switch.svelte +51 -20
  155. package/dist/switch/types.d.ts +3 -0
  156. package/dist/tabs/Tab.svelte +8 -12
  157. package/dist/tabs/Tabs.svelte +3 -3
  158. package/dist/tabs/Tabs.svelte.d.ts +1 -1
  159. package/dist/text-field/TextField.svelte +19 -12
  160. package/dist/text-field/TextField.svelte.d.ts +1 -1
  161. package/dist/toolbar/Toolbar.svelte +133 -0
  162. package/dist/toolbar/Toolbar.svelte.d.ts +4 -0
  163. package/dist/toolbar/index.d.ts +1 -0
  164. package/dist/toolbar/index.js +1 -0
  165. package/dist/toolbar/types.d.ts +15 -0
  166. package/dist/toolbar/types.js +1 -0
  167. package/dist/tooltip/RichTooltip.svelte +25 -12
  168. package/dist/tooltip/RichTooltip.svelte.d.ts +2 -2
  169. package/dist/tooltip/Tooltip.svelte +22 -11
  170. package/dist/tooltip/Tooltip.svelte.d.ts +4 -1
  171. package/dist/types.d.ts +8 -0
  172. package/dist/types.js +8 -0
  173. package/package.json +13 -13
  174. package/dist/date-picker/enterAnimation.d.ts +0 -1
  175. package/dist/date-picker/enterAnimation.js +0 -10
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
2
  import '../internal/focus-ring.css'
3
+ import { pressMorph } from '../press.svelte.js'
3
4
  import CircularProgress from '../progress/CircularProgress.svelte'
4
5
  import Ripple from '../ripple/Ripple.svelte'
5
6
  import Tooltip from '../tooltip/Tooltip.svelte'
6
- import { onMount } from 'svelte'
7
7
  import type { HTMLButtonAttributes } from 'svelte/elements'
8
8
  import type { IconButtonProps } from './types.js'
9
9
 
@@ -29,22 +29,12 @@
29
29
  const isLink = $derived(attributes.href != null && !disabled && !loading)
30
30
  const tooltipId = $derived(title && !disabled && !loading ? uid : undefined)
31
31
 
32
- let pressed = $state(false)
33
- let pressedTimeout: ReturnType<typeof setTimeout>
32
+ const morph = pressMorph()
34
33
 
35
34
  const handlePress = () => {
36
35
  if (disabled || loading) return
37
- pressed = true
38
- pressedTimeout = setTimeout(() => {
39
- pressed = false
40
- }, 100)
36
+ morph.press()
41
37
  }
42
-
43
- onMount(() => {
44
- return () => {
45
- clearTimeout(pressedTimeout)
46
- }
47
- })
48
38
  </script>
49
39
 
50
40
  {#snippet content()}
@@ -86,7 +76,7 @@
86
76
  'enabled',
87
77
  toggle && 'toggle',
88
78
  selected && 'selected',
89
- pressed && 'pressed',
79
+ morph.pressed && 'pressed',
90
80
  attributes.class,
91
81
  ].filter(Boolean)}
92
82
  >
@@ -119,7 +109,7 @@
119
109
  disabled || loading ? `${variant}-disabled disabled` : `${variant} enabled`,
120
110
  toggle && 'toggle',
121
111
  selected && 'selected',
122
- pressed && 'pressed',
112
+ morph.pressed && 'pressed',
123
113
  attributes.class,
124
114
  ]}
125
115
  >
@@ -10,13 +10,16 @@
10
10
  multiSelect = false,
11
11
  element = $bindable(),
12
12
  group = $bindable(),
13
+ issues,
13
14
  ...attributes
14
15
  }: SegmentedButtonProps = $props()
16
+
17
+ let hasError = $derived(!!issues?.length)
15
18
  </script>
16
19
 
17
20
  <div
18
21
  {...attributes}
19
- class={['np-segmented-buttons', attributes.class]}
22
+ class={['np-segmented-buttons', hasError && 'np-error', attributes.class]}
20
23
  bind:this={element}
21
24
  style="{attributes.style};grid-template-columns: repeat({options.length}, minmax(max-content, 1fr));"
22
25
  >
@@ -95,6 +98,17 @@
95
98
  overflow-x: auto;
96
99
  }
97
100
 
101
+ .np-segmented-buttons.np-error {
102
+ --np-ripple-hover-color: var(--np-color-error);
103
+ --np-ripple-pressed-color: var(--np-color-error);
104
+ border-color: var(--np-color-error);
105
+ color: var(--np-color-error);
106
+ }
107
+
108
+ .np-error .np-segmented-button {
109
+ border-inline-end-color: var(--np-color-error);
110
+ }
111
+
98
112
  .np-segmented-button {
99
113
  flex: 1;
100
114
  padding: 0.5rem 1.75rem;
@@ -0,0 +1,92 @@
1
+ <script lang="ts">
2
+ import ChevronDownIcon from '../icons/ChevronDownIcon.svelte'
3
+ import Menu from '../menu/Menu.svelte'
4
+ import Button from './Button.svelte'
5
+ import ButtonGroup from './ButtonGroup.svelte'
6
+ import type { SplitButtonProps } from './types.js'
7
+
8
+ let {
9
+ label = '',
10
+ icon,
11
+ menu,
12
+ variant = 'filled',
13
+ size = 's',
14
+ disabled = false,
15
+ open = $bindable(false),
16
+ menuLabel = 'More options',
17
+ onclick,
18
+ element = $bindable(),
19
+ ...attributes
20
+ }: SplitButtonProps = $props()
21
+
22
+ const uid = $props.id()
23
+ const menuId = `np-split-button-menu-${uid}`
24
+
25
+ // The trailing half is the menu's invoker, so it is also the popover's implicit anchor.
26
+ let trigger: HTMLElement | undefined = $state()
27
+ </script>
28
+
29
+ {#snippet caret()}
30
+ <ChevronDownIcon />
31
+ {/snippet}
32
+
33
+ <ButtonGroup
34
+ {...attributes}
35
+ bind:element
36
+ variant="connected"
37
+ class={['np-split-button', attributes.class]}
38
+ >
39
+ <Button {variant} {size} {disabled} {onclick} start={icon}>{label}</Button>
40
+ <!--
41
+ A `Button` rather than an `IconButton`, because an icon button has no `elevated` variant and
42
+ a split button's two halves have to match.
43
+ -->
44
+ <Button
45
+ {variant}
46
+ {size}
47
+ {disabled}
48
+ bind:element={trigger}
49
+ class="np-split-button-trigger"
50
+ command="toggle-popover"
51
+ commandfor={menuId}
52
+ aria-haspopup="menu"
53
+ aria-expanded={open}
54
+ aria-label={menuLabel}
55
+ start={caret}
56
+ />
57
+ <Menu id={menuId} anchor={trigger} bind:open coverAnchor={false}>
58
+ {@render menu?.(menuId)}
59
+ </Menu>
60
+ </ButtonGroup>
61
+
62
+ <style>
63
+ /* The caret turns over while the menu is open, the way the select arrow does. */
64
+ :global(.np-split-button-trigger svg) {
65
+ transition: rotate var(--np-motion-expressive-fast-effects);
66
+ }
67
+ :global(.np-split-button-trigger[aria-expanded='true'] svg) {
68
+ rotate: 180deg;
69
+ }
70
+ /*
71
+ The trailing half only holds a caret, so it does not need a full button's width. It is
72
+ square rather than merely narrow, because every size sets its round corner to half its
73
+ height, and only on a square does that corner come out as a true circle.
74
+ */
75
+ :global(.np-split-button > .np-split-button-trigger) {
76
+ --np-button-padding: 0;
77
+ aspect-ratio: 1;
78
+ justify-content: center;
79
+ }
80
+ /*
81
+ While the menu is open the trailing half reads as selected: its inner corner rounds out to
82
+ the outer one, so the half becomes a circle, and it keeps a state layer up. Overriding the
83
+ group's inner corner rather than `border-radius` leaves ButtonGroup's own corner rules in
84
+ charge of which corner is which.
85
+ */
86
+ :global(.np-split-button > .np-split-button-trigger[aria-expanded='true']) {
87
+ --np-button-group-inner-corner: var(--_round-radius, var(--np-shape-corner-full));
88
+ }
89
+ :global(.np-split-button-trigger[aria-expanded='true'] .np-ripple-surface::before) {
90
+ opacity: var(--np-ripple-hover-opacity, 0.08);
91
+ }
92
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { SplitButtonProps } from './types.ts';
2
+ declare const SplitButton: import("svelte").Component<SplitButtonProps, {}, "element" | "open">;
3
+ type SplitButton = ReturnType<typeof SplitButton>;
4
+ export default SplitButton;
@@ -2,3 +2,4 @@ export { default as Button } from './Button.svelte';
2
2
  export { default as ButtonGroup } from './ButtonGroup.svelte';
3
3
  export { default as IconButton } from './IconButton.svelte';
4
4
  export { default as SegmentedButton } from './SegmentedButton.svelte';
5
+ export { default as SplitButton } from './SplitButton.svelte';
@@ -2,3 +2,4 @@ export { default as Button } from './Button.svelte';
2
2
  export { default as ButtonGroup } from './ButtonGroup.svelte';
3
3
  export { default as IconButton } from './IconButton.svelte';
4
4
  export { default as SegmentedButton } from './SegmentedButton.svelte';
5
+ export { default as SplitButton } from './SplitButton.svelte';
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
2
+ import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements';
3
3
  export type ButtonElement = HTMLButtonElement | HTMLAnchorElement;
4
- interface BaseButtonProps extends HTMLAttributes<ButtonElement>, Omit<HTMLButtonAttributes, keyof HTMLAttributes<HTMLButtonElement> | 'type'>, Omit<HTMLAnchorAttributes, keyof HTMLAttributes<HTMLAnchorElement> | 'type'> {
4
+ export interface BaseButtonProps extends HTMLAttributes<ButtonElement>, Omit<HTMLButtonAttributes, keyof HTMLAttributes<HTMLButtonElement> | 'type'>, Omit<HTMLAnchorAttributes, keyof HTMLAttributes<HTMLAnchorElement> | 'type'> {
5
5
  element?: HTMLElement;
6
6
  disabled?: boolean | null;
7
7
  loading?: boolean;
@@ -27,7 +27,24 @@ export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
27
27
  expandedRatio?: number;
28
28
  compressionLimit?: number;
29
29
  element?: HTMLElement;
30
- children?: Snippet;
30
+ }
31
+ export interface SplitButtonProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onclick'> {
32
+ /** Text of the leading action. */
33
+ label?: string;
34
+ /** Leading icon on the action half. */
35
+ icon?: Snippet;
36
+ /** Menu items for the trailing half. Rendered inside a `Menu`. */
37
+ menu?: Snippet<[string]>;
38
+ variant?: Exclude<ButtonProps['variant'], 'text'>;
39
+ size?: BaseButtonProps['size'];
40
+ disabled?: boolean | null;
41
+ /** Bindable. Whether the menu is open. */
42
+ open?: boolean;
43
+ /** Accessible name for the trailing half, which shows only a caret. */
44
+ menuLabel?: string;
45
+ /** The action half's click handler. The trailing half opens the menu instead. */
46
+ onclick?: MouseEventHandler<ButtonElement>;
47
+ element?: HTMLElement;
31
48
  }
32
49
  export interface SegmentedButtonProps extends HTMLAttributes<HTMLDivElement> {
33
50
  name: string;
@@ -42,5 +59,7 @@ export interface SegmentedButtonProps extends HTMLAttributes<HTMLDivElement> {
42
59
  }[];
43
60
  group?: string | number | (string | number)[] | null;
44
61
  element?: HTMLElement;
62
+ issues?: {
63
+ message: string;
64
+ }[];
45
65
  }
46
- export {};
@@ -66,8 +66,8 @@
66
66
  </div>
67
67
  {:else if type === 'button'}
68
68
  <button
69
- aria-disabled={disabled}
70
69
  {...attributes}
70
+ aria-disabled={disabled}
71
71
  type="button"
72
72
  bind:this={element}
73
73
  {disabled}
@@ -3,7 +3,7 @@ import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes } from
3
3
  export type CardElement = HTMLDivElement | HTMLButtonElement | HTMLAnchorElement;
4
4
  export interface CardProps extends HTMLAttributes<CardElement>, Omit<HTMLButtonAttributes, keyof HTMLAttributes<HTMLButtonElement> | 'type'>, Omit<HTMLAnchorAttributes, keyof HTMLAttributes<HTMLAnchorElement> | 'type'> {
5
5
  variant?: 'elevated' | 'filled' | 'outlined';
6
- disabled?: boolean;
6
+ disabled?: boolean | null;
7
7
  image?: string | null;
8
8
  element?: HTMLElement;
9
9
  headline?: string | null;
@@ -11,6 +11,7 @@
11
11
  issues,
12
12
  style,
13
13
  value,
14
+ 'aria-invalid': ariaInvalid,
14
15
  ...attributes
15
16
  }: CheckboxProps = $props()
16
17
 
@@ -20,9 +21,10 @@
20
21
  let groupValue = $derived(
21
22
  typeof value === 'string' || typeof value === 'number' ? value : undefined,
22
23
  )
23
- let grouped = $derived(Array.isArray(group) && groupValue !== undefined)
24
- let groupChecked = $derived(
25
- Array.isArray(group) && groupValue !== undefined ? group.includes(groupValue) : false,
24
+ // A grouped checkbox takes its checked state from the group, so it cannot also `bind:checked`.
25
+ // One controlled input covers both, with the change handler writing back whichever is in play.
26
+ let isChecked = $derived(
27
+ Array.isArray(group) && groupValue !== undefined ? group.includes(groupValue) : checked,
26
28
  )
27
29
 
28
30
  const onchange = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
@@ -41,7 +43,11 @@
41
43
 
42
44
  <div
43
45
  {style}
44
- class={['np-container', hasError && !attributes.disabled && 'np-error', attributes.class]}
46
+ class={[
47
+ 'np-checkbox-container',
48
+ hasError && !attributes.disabled && 'np-error',
49
+ attributes.class,
50
+ ]}
45
51
  bind:this={element}
46
52
  >
47
53
  <div class="np-outline"></div>
@@ -54,37 +60,23 @@
54
60
  {#if !attributes.disabled}
55
61
  <Ripple forElement={inputEl} />
56
62
  {/if}
57
- {#if grouped}
58
- <input
59
- {...attributes}
60
- {value}
61
- class="np-input"
62
- type="checkbox"
63
- bind:indeterminate
64
- bind:this={inputEl}
65
- checked={groupChecked}
66
- {onchange}
67
- aria-invalid={hasError ? 'true' : undefined}
68
- aria-checked={indeterminate ? 'mixed' : undefined}
69
- />
70
- {:else}
71
- <input
72
- {...attributes}
73
- {value}
74
- class="np-input"
75
- type="checkbox"
76
- bind:indeterminate
77
- bind:checked
78
- bind:this={inputEl}
79
- aria-invalid={hasError ? 'true' : undefined}
80
- aria-checked={indeterminate ? 'mixed' : undefined}
81
- />
82
- {/if}
63
+ <input
64
+ {...attributes}
65
+ {value}
66
+ class="np-input"
67
+ type="checkbox"
68
+ bind:indeterminate
69
+ bind:this={inputEl}
70
+ checked={isChecked}
71
+ {onchange}
72
+ aria-invalid={hasError ? 'true' : ariaInvalid}
73
+ aria-checked={indeterminate ? 'mixed' : undefined}
74
+ />
83
75
  </div>
84
76
  </div>
85
77
 
86
78
  <style>
87
- .np-container {
79
+ .np-checkbox-container {
88
80
  border-radius: var(--np-checkbox-container-shape, 2px);
89
81
  display: inline-flex;
90
82
  place-content: center;
@@ -101,7 +93,7 @@
101
93
  :global(label) {
102
94
  -webkit-tap-highlight-color: transparent;
103
95
  }
104
- .np-container:has(input:disabled) {
96
+ .np-checkbox-container:has(input:disabled) {
105
97
  cursor: default;
106
98
  }
107
99
  .np-input {
@@ -147,18 +139,18 @@
147
139
  border-width: 2px;
148
140
  box-sizing: border-box;
149
141
  }
150
- .np-container:focus-within .np-outline,
151
- .np-container:hover .np-outline {
142
+ .np-checkbox-container:focus-within .np-outline,
143
+ .np-checkbox-container:hover .np-outline {
152
144
  border-color: var(--np-color-on-surface);
153
145
  border-width: 2px;
154
146
  }
155
- .np-container:has(input:disabled) .np-outline {
147
+ .np-checkbox-container:has(input:disabled) .np-outline {
156
148
  border-color: var(--np-color-on-surface);
157
149
  border-width: 2px;
158
150
  opacity: 0.38;
159
151
  }
160
- .np-container:has(input:disabled:checked) .np-outline,
161
- .np-container:has(input:disabled:indeterminate) .np-outline {
152
+ .np-checkbox-container:has(input:disabled:checked) .np-outline,
153
+ .np-checkbox-container:has(input:disabled:indeterminate) .np-outline {
162
154
  visibility: hidden;
163
155
  }
164
156
  .np-background {
@@ -172,17 +164,17 @@
172
164
  transition-timing-function: cubic-bezier(0.3, 0, 0.8, 0.15), linear;
173
165
  transform: scale(0.6);
174
166
  }
175
- .np-container:has(input:indeterminate) .np-background,
176
- .np-container:has(input:checked) .np-background,
177
- .np-container:has(input:indeterminate) .np-icon,
178
- .np-container:has(input:checked) .np-icon {
167
+ .np-checkbox-container:has(input:indeterminate) .np-background,
168
+ .np-checkbox-container:has(input:checked) .np-background,
169
+ .np-checkbox-container:has(input:indeterminate) .np-icon,
170
+ .np-checkbox-container:has(input:checked) .np-icon {
179
171
  opacity: 1;
180
172
  transition-duration: 350ms, 50ms;
181
173
  transition-timing-function: cubic-bezier(0.05, 0.7, 0.1, 1), linear;
182
174
  transform: scale(1);
183
175
  }
184
- .np-container:has(input:disabled:checked) .np-background,
185
- .np-container:has(input:disabled:indeterminate) .np-background {
176
+ .np-checkbox-container:has(input:disabled:checked) .np-background,
177
+ .np-checkbox-container:has(input:disabled:indeterminate) .np-background {
186
178
  background: var(--np-color-on-surface);
187
179
  opacity: 0.38;
188
180
  }
@@ -207,23 +199,23 @@
207
199
  height: 18px;
208
200
  width: 18px;
209
201
  }
210
- .np-container:has(input:disabled) .np-icon {
202
+ .np-checkbox-container:has(input:disabled) .np-icon {
211
203
  fill: var(--np-color-surface);
212
204
  }
213
- .np-container:has(input:checked) .mark.short,
214
- .np-container:has(input:not(:checked):not(:indeterminate)) .mark.short {
205
+ .np-checkbox-container:has(input:checked) .mark.short,
206
+ .np-checkbox-container:has(input:not(:checked):not(:indeterminate)) .mark.short {
215
207
  height: 5.6568542495px;
216
208
  }
217
- .np-container:has(input:indeterminate) .mark,
218
- .np-container:has(input:checked) .mark {
209
+ .np-checkbox-container:has(input:indeterminate) .mark,
210
+ .np-checkbox-container:has(input:checked) .mark {
219
211
  transition-property: none;
220
212
  }
221
- .np-container:has(input:checked) .mark,
222
- .np-container:has(input:not(:checked):not(:indeterminate)) .mark {
213
+ .np-checkbox-container:has(input:checked) .mark,
214
+ .np-checkbox-container:has(input:not(:checked):not(:indeterminate)) .mark {
223
215
  transform: scaleY(-1) translate(7px, -14px) rotate(45deg);
224
216
  }
225
- .np-container:has(input:indeterminate) .mark,
226
- .np-container:has(input:checked) .mark {
217
+ .np-checkbox-container:has(input:indeterminate) .mark,
218
+ .np-checkbox-container:has(input:checked) .mark {
227
219
  animation-duration: 350ms;
228
220
  animation-timing-function: cubic-bezier(0.05, 0.7, 0.1, 1);
229
221
  transition-duration: 350ms;
@@ -240,11 +232,11 @@
240
232
  transition-duration: 150ms;
241
233
  transition-timing-function: cubic-bezier(0.3, 0, 0.8, 0.15);
242
234
  }
243
- .np-container:has(input:checked) .mark.long {
235
+ .np-checkbox-container:has(input:checked) .mark.long {
244
236
  animation-name: prev-unselected-to-checked;
245
237
  }
246
- .np-container:has(input:checked) .mark.long,
247
- .np-container:has(input:not(:checked):not(:indeterminate)) .mark.long {
238
+ .np-checkbox-container:has(input:checked) .mark.long,
239
+ .np-checkbox-container:has(input:not(:checked):not(:indeterminate)) .mark.long {
248
240
  width: 11.313708499px;
249
241
  }
250
242
  .mark.long {
@@ -252,7 +244,7 @@
252
244
  transition-property: transform, width;
253
245
  width: 10px;
254
246
  }
255
- .np-container:has(input:indeterminate) .mark {
247
+ .np-checkbox-container:has(input:indeterminate) .mark {
256
248
  transform: scaleY(-1) translate(4px, -10px) rotate(0deg);
257
249
  }
258
250
  @keyframes prev-unselected-to-checked {
@@ -265,7 +257,7 @@
265
257
  background-color: CanvasText;
266
258
  }
267
259
 
268
- .np-container:has(input:disabled:checked) .np-background {
260
+ .np-checkbox-container:has(input:disabled:checked) .np-background {
269
261
  background-color: GrayText;
270
262
  opacity: 1;
271
263
  }
@@ -274,7 +266,7 @@
274
266
  border-color: CanvasText;
275
267
  }
276
268
 
277
- .np-container:has(input:disabled) .np-outline {
269
+ .np-checkbox-container:has(input:disabled) .np-outline {
278
270
  border-color: GrayText;
279
271
  opacity: 1;
280
272
  }
@@ -3,7 +3,7 @@
3
3
  import Button from '../button/Button.svelte'
4
4
 
5
5
  let {
6
- elevated = false,
6
+ variant = 'outlined',
7
7
  disabled = false,
8
8
  label = '',
9
9
  icon,
@@ -13,8 +13,6 @@
13
13
  </script>
14
14
 
15
15
  <Button
16
- size="xs"
17
- shape="square"
18
16
  --np-elevated-button-label-text-color="var(--np-assist-chip-label-text-color, var(--np-color-on-surface))"
19
17
  --np-outlined-button-label-text-color="var(--np-assist-chip-label-text-color, var(--np-color-on-surface))"
20
18
  --np-outlined-button-outline-color="var(--np-assist-chip-outline-color, var(--np-color-outline-variant))"
@@ -27,9 +25,11 @@
27
25
  : 'var(--np-color-primary)'}
28
26
  style="letter-spacing: 0.006rem"
29
27
  bind:element
30
- variant={elevated ? 'elevated' : 'outlined'}
31
- {disabled}
32
28
  {...attributes}
29
+ size="xs"
30
+ shape="square"
31
+ {variant}
32
+ {disabled}
33
33
  start={icon}
34
34
  >
35
35
  {label}
@@ -2,7 +2,7 @@
2
2
  import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
3
3
  import type { ChipSetProps } from './types.js'
4
4
 
5
- let { children, chipsCount, ...attributes }: ChipSetProps = $props()
5
+ let { children, element = $bindable(), ...attributes }: ChipSetProps = $props()
6
6
 
7
7
  const CHIP_SELECTOR = "input:not([type='hidden']), button, a[href]"
8
8
  const attach = rovingTabindex(CHIP_SELECTOR, {
@@ -12,14 +12,7 @@
12
12
  const onkeydown = arrowKeyNav(CHIP_SELECTOR, 'horizontal')
13
13
  </script>
14
14
 
15
- <div
16
- class={[
17
- 'np-chip-set-wrapper',
18
- (chipsCount ?? 0) > 0 && 'np-chip-set-has-chips',
19
- attributes.class,
20
- ]}
21
- style={attributes.style}
22
- >
15
+ <div {...attributes} bind:this={element} class={['np-chip-set-wrapper', attributes.class]}>
23
16
  {#if children}
24
17
  <div {@attach attach} class={['np-chip-set']} role="toolbar" tabindex="-1" {onkeydown}>
25
18
  {@render children()}
@@ -33,7 +26,7 @@
33
26
  padding: 0.5rem;
34
27
  margin: -0.5rem;
35
28
  }
36
- .np-chip-set-has-chips {
29
+ .np-chip-set-wrapper:has(.np-chip-set > :global(*)) {
37
30
  margin-inline-end: 0;
38
31
  }
39
32
  .np-chip-set {
@@ -1,4 +1,4 @@
1
1
  import type { ChipSetProps } from './types.ts';
2
- declare const ChipSet: import("svelte").Component<ChipSetProps, {}, "">;
2
+ declare const ChipSet: import("svelte").Component<ChipSetProps, {}, "element">;
3
3
  type ChipSet = ReturnType<typeof ChipSet>;
4
4
  export default ChipSet;
@@ -9,25 +9,30 @@
9
9
  let {
10
10
  selected = $bindable(),
11
11
  removable = false,
12
- elevated = false,
12
+ variant = 'outlined',
13
13
  disabled = false,
14
14
  label = '',
15
15
  icon,
16
16
  element = $bindable(),
17
- ariaLabelRemove = 'Remove',
17
+ removeAriaLabel = 'Remove',
18
18
  onremove,
19
19
  name,
20
20
  value,
21
21
  group = $bindable(),
22
22
  defaultSelected,
23
+ issues,
24
+ 'aria-invalid': ariaInvalid,
23
25
  ...attributes
24
26
  }: FilterChipProps = $props()
25
27
 
28
+ let hasError = $derived(!!issues?.length)
29
+
26
30
  let chipLabel: HTMLLabelElement | undefined = $state()
27
31
 
28
- let grouped = $derived(Array.isArray(group) && value !== undefined)
29
- let groupChecked = $derived(
30
- Array.isArray(group) && value !== undefined ? group.includes(value) : false,
32
+ // A grouped chip takes its checked state from the group, so it cannot also `bind:checked`. One
33
+ // controlled input covers both, with the change handler writing back whichever is in play.
34
+ let isChecked = $derived(
35
+ Array.isArray(group) && value !== undefined ? group.includes(value) : selected,
31
36
  )
32
37
 
33
38
  const onchange = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
@@ -48,10 +53,11 @@
48
53
  bind:this={element}
49
54
  class={[
50
55
  'np-filter-chip',
51
- elevated ? 'np-filter-chip-elevated' : 'np-filter-chip-default',
56
+ variant === 'elevated' ? 'np-filter-chip-elevated' : 'np-filter-chip-default',
52
57
  icon ? 'np-filter-chip-icon' : '',
53
58
  removable ? 'np-filter-chip-removable' : '',
54
59
  disabled ? 'np-filter-chip-disabled' : '',
60
+ hasError && !disabled && 'np-chip-error',
55
61
  attributes.class,
56
62
  ]}
57
63
  >
@@ -65,26 +71,16 @@
65
71
  <CheckIcon width={18} height={18} />
66
72
  </div>
67
73
  <div class="np-chip-label">{label}</div>
68
- {#if grouped}
69
- <input
70
- type="checkbox"
71
- checked={groupChecked}
72
- {onchange}
73
- {value}
74
- {name}
75
- {disabled}
76
- defaultChecked={defaultSelected}
77
- />
78
- {:else}
79
- <input
80
- type="checkbox"
81
- bind:checked={selected}
82
- {value}
83
- {name}
84
- {disabled}
85
- defaultChecked={defaultSelected}
86
- />
87
- {/if}
74
+ <input
75
+ type="checkbox"
76
+ checked={isChecked}
77
+ {onchange}
78
+ {value}
79
+ {name}
80
+ {disabled}
81
+ defaultChecked={defaultSelected}
82
+ aria-invalid={hasError ? 'true' : ariaInvalid}
83
+ />
88
84
  </label>
89
85
  {#if !disabled}
90
86
  <Ripple forElement={chipLabel} />
@@ -95,7 +91,7 @@
95
91
  type="button"
96
92
  size="xs"
97
93
  --np-icon-button-icon-size="1.125rem"
98
- aria-label={ariaLabelRemove}
94
+ aria-label={removeAriaLabel}
99
95
  onclick={onremove}
100
96
  >
101
97
  <CloseIcon />
@@ -191,6 +187,18 @@
191
187
  outline-width: 1px;
192
188
  outline-offset: -1px;
193
189
  }
190
+ .np-chip-error {
191
+ --np-filter-chip-outline-color: var(--np-color-error);
192
+ --np-ripple-hover-color: var(--np-color-error);
193
+ --np-ripple-pressed-color: var(--np-color-error);
194
+ color: var(--np-color-error);
195
+ }
196
+ .np-chip-error:has(input:checked)::before {
197
+ background-color: var(--np-color-error-container);
198
+ }
199
+ .np-chip-error:has(input:checked) {
200
+ color: var(--np-color-on-error-container);
201
+ }
194
202
  .np-filter-chip:has(input:checked)::before {
195
203
  outline-color: transparent;
196
204
  background-color: var(--np-color-secondary-container);
@@ -1,5 +1,5 @@
1
1
  import '../internal/focus-ring.css';
2
2
  import type { FilterChipProps } from './types.ts';
3
- declare const FilterChip: import("svelte").Component<FilterChipProps, {}, "element" | "selected" | "group">;
3
+ declare const FilterChip: import("svelte").Component<FilterChipProps, {}, "element" | "group" | "selected">;
4
4
  type FilterChip = ReturnType<typeof FilterChip>;
5
5
  export default FilterChip;