noph-ui 0.44.0 → 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 (135) 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 +6 -1
  14. package/dist/badge/types.d.ts +0 -1
  15. package/dist/button/Button.svelte +5 -15
  16. package/dist/button/IconButton.svelte +5 -15
  17. package/dist/button/SegmentedButton.svelte +15 -1
  18. package/dist/button/SplitButton.svelte +92 -0
  19. package/dist/button/SplitButton.svelte.d.ts +4 -0
  20. package/dist/button/index.d.ts +1 -0
  21. package/dist/button/index.js +1 -0
  22. package/dist/button/types.d.ts +23 -4
  23. package/dist/card/types.d.ts +1 -1
  24. package/dist/checkbox/Checkbox.svelte +49 -58
  25. package/dist/chip/AssistChip.svelte +5 -5
  26. package/dist/chip/ChipSet.svelte +3 -11
  27. package/dist/chip/FilterChip.svelte +35 -27
  28. package/dist/chip/InputChip.svelte +13 -2
  29. package/dist/chip/SuggestionChip.svelte +29 -0
  30. package/dist/chip/SuggestionChip.svelte.d.ts +4 -0
  31. package/dist/chip/index.d.ts +1 -0
  32. package/dist/chip/index.js +1 -0
  33. package/dist/chip/types.d.ts +24 -9
  34. package/dist/date-picker/Calendar.svelte +1 -1
  35. package/dist/date-picker/DatePickerDialog.svelte +30 -8
  36. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -1
  37. package/dist/date-picker/DateRangePicker.svelte +45 -10
  38. package/dist/date-picker/DateRangePicker.svelte.d.ts +4 -1
  39. package/dist/date-picker/DockedDatePicker.svelte +18 -4
  40. package/dist/date-picker/DockedDatePicker.svelte.d.ts +4 -1
  41. package/dist/date-picker/DockedDateTimePicker.svelte +18 -4
  42. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +4 -1
  43. package/dist/date-picker/SelectionList.svelte +6 -32
  44. package/dist/date-picker/SelectionList.svelte.d.ts +2 -12
  45. package/dist/date-picker/TimeColumn.svelte +4 -29
  46. package/dist/date-picker/exitVisibility.svelte.d.ts +10 -0
  47. package/dist/date-picker/exitVisibility.svelte.js +30 -0
  48. package/dist/date-picker/optionList.d.ts +16 -0
  49. package/dist/date-picker/optionList.js +43 -0
  50. package/dist/date-picker/types.d.ts +11 -4
  51. package/dist/dialog/Dialog.svelte +77 -68
  52. package/dist/dialog/Dialog.svelte.d.ts +3 -3
  53. package/dist/dialog/types.d.ts +5 -3
  54. package/dist/fab/ExtendedFab.svelte +281 -0
  55. package/dist/fab/ExtendedFab.svelte.d.ts +5 -0
  56. package/dist/fab/Fab.svelte +239 -0
  57. package/dist/fab/Fab.svelte.d.ts +5 -0
  58. package/dist/fab/FabMenu.svelte +255 -0
  59. package/dist/fab/FabMenu.svelte.d.ts +7 -0
  60. package/dist/fab/index.d.ts +3 -0
  61. package/dist/fab/index.js +3 -0
  62. package/dist/fab/types.d.ts +58 -0
  63. package/dist/fab/types.js +1 -0
  64. package/dist/icons/ArrowBackIcon.svelte +13 -0
  65. package/dist/icons/ArrowBackIcon.svelte.d.ts +7 -0
  66. package/dist/icons/ChevronDownIcon.svelte +3 -0
  67. package/dist/icons/ChevronDownIcon.svelte.d.ts +26 -0
  68. package/dist/icons/SearchIcon.svelte +15 -0
  69. package/dist/icons/SearchIcon.svelte.d.ts +7 -0
  70. package/dist/icons/index.d.ts +3 -0
  71. package/dist/icons/index.js +3 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.js +6 -0
  74. package/dist/internal/focus-ring.css +12 -0
  75. package/dist/menu/Menu.svelte +12 -7
  76. package/dist/menu/Menu.svelte.d.ts +2 -2
  77. package/dist/menu/types.d.ts +0 -2
  78. package/dist/navigation-bar/NavigationBar.svelte +66 -0
  79. package/dist/navigation-bar/NavigationBar.svelte.d.ts +4 -0
  80. package/dist/navigation-bar/NavigationBarItem.svelte +261 -0
  81. package/dist/navigation-bar/NavigationBarItem.svelte.d.ts +5 -0
  82. package/dist/navigation-bar/index.d.ts +2 -0
  83. package/dist/navigation-bar/index.js +2 -0
  84. package/dist/navigation-bar/types.d.ts +29 -0
  85. package/dist/navigation-bar/types.js +1 -0
  86. package/dist/navigation-drawer/NavigationDrawer.svelte +128 -76
  87. package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +4 -1
  88. package/dist/navigation-drawer/NavigationDrawerItem.svelte +10 -3
  89. package/dist/navigation-drawer/types.d.ts +6 -1
  90. package/dist/navigation-rail/NavigationRailItem.svelte +16 -0
  91. package/dist/navigation-rail/types.d.ts +3 -0
  92. package/dist/popover.svelte.d.ts +14 -0
  93. package/dist/popover.svelte.js +31 -0
  94. package/dist/press.svelte.d.ts +19 -0
  95. package/dist/press.svelte.js +45 -0
  96. package/dist/radio/Radio.svelte +26 -7
  97. package/dist/radio/types.d.ts +3 -0
  98. package/dist/search/Search.svelte +472 -0
  99. package/dist/search/Search.svelte.d.ts +5 -0
  100. package/dist/search/index.d.ts +1 -0
  101. package/dist/search/index.js +1 -0
  102. package/dist/search/types.d.ts +39 -0
  103. package/dist/search/types.js +1 -0
  104. package/dist/select/Select.svelte +48 -87
  105. package/dist/sheet/Sheet.svelte +260 -0
  106. package/dist/sheet/Sheet.svelte.d.ts +7 -0
  107. package/dist/sheet/index.d.ts +1 -0
  108. package/dist/sheet/index.js +1 -0
  109. package/dist/sheet/types.d.ts +19 -0
  110. package/dist/sheet/types.js +1 -0
  111. package/dist/slider/Slider.svelte +154 -30
  112. package/dist/slider/types.d.ts +3 -0
  113. package/dist/snackbar/Snackbar.svelte +48 -37
  114. package/dist/snackbar/Snackbar.svelte.d.ts +2 -2
  115. package/dist/snackbar/types.d.ts +3 -2
  116. package/dist/switch/Switch.svelte +51 -20
  117. package/dist/switch/types.d.ts +3 -0
  118. package/dist/tabs/Tab.svelte +8 -12
  119. package/dist/tabs/Tabs.svelte +3 -3
  120. package/dist/text-field/TextField.svelte +2 -2
  121. package/dist/toolbar/Toolbar.svelte +133 -0
  122. package/dist/toolbar/Toolbar.svelte.d.ts +4 -0
  123. package/dist/toolbar/index.d.ts +1 -0
  124. package/dist/toolbar/index.js +1 -0
  125. package/dist/toolbar/types.d.ts +15 -0
  126. package/dist/toolbar/types.js +1 -0
  127. package/dist/tooltip/RichTooltip.svelte +25 -12
  128. package/dist/tooltip/RichTooltip.svelte.d.ts +2 -2
  129. package/dist/tooltip/Tooltip.svelte +22 -11
  130. package/dist/tooltip/Tooltip.svelte.d.ts +4 -1
  131. package/dist/types.d.ts +6 -0
  132. package/dist/types.js +6 -0
  133. package/package.json +13 -13
  134. package/dist/date-picker/enterAnimation.d.ts +0 -1
  135. package/dist/date-picker/enterAnimation.js +0 -10
@@ -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 {};
@@ -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;
@@ -21,9 +21,10 @@
21
21
  let groupValue = $derived(
22
22
  typeof value === 'string' || typeof value === 'number' ? value : undefined,
23
23
  )
24
- let grouped = $derived(Array.isArray(group) && groupValue !== undefined)
25
- let groupChecked = $derived(
26
- 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,
27
28
  )
28
29
 
29
30
  const onchange = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
@@ -42,7 +43,11 @@
42
43
 
43
44
  <div
44
45
  {style}
45
- 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
+ ]}
46
51
  bind:this={element}
47
52
  >
48
53
  <div class="np-outline"></div>
@@ -55,37 +60,23 @@
55
60
  {#if !attributes.disabled}
56
61
  <Ripple forElement={inputEl} />
57
62
  {/if}
58
- {#if grouped}
59
- <input
60
- {...attributes}
61
- {value}
62
- class="np-input"
63
- type="checkbox"
64
- bind:indeterminate
65
- bind:this={inputEl}
66
- checked={groupChecked}
67
- {onchange}
68
- aria-invalid={hasError ? 'true' : ariaInvalid}
69
- aria-checked={indeterminate ? 'mixed' : undefined}
70
- />
71
- {:else}
72
- <input
73
- {...attributes}
74
- {value}
75
- class="np-input"
76
- type="checkbox"
77
- bind:indeterminate
78
- bind:checked
79
- bind:this={inputEl}
80
- aria-invalid={hasError ? 'true' : ariaInvalid}
81
- aria-checked={indeterminate ? 'mixed' : undefined}
82
- />
83
- {/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
+ />
84
75
  </div>
85
76
  </div>
86
77
 
87
78
  <style>
88
- .np-container {
79
+ .np-checkbox-container {
89
80
  border-radius: var(--np-checkbox-container-shape, 2px);
90
81
  display: inline-flex;
91
82
  place-content: center;
@@ -102,7 +93,7 @@
102
93
  :global(label) {
103
94
  -webkit-tap-highlight-color: transparent;
104
95
  }
105
- .np-container:has(input:disabled) {
96
+ .np-checkbox-container:has(input:disabled) {
106
97
  cursor: default;
107
98
  }
108
99
  .np-input {
@@ -148,18 +139,18 @@
148
139
  border-width: 2px;
149
140
  box-sizing: border-box;
150
141
  }
151
- .np-container:focus-within .np-outline,
152
- .np-container:hover .np-outline {
142
+ .np-checkbox-container:focus-within .np-outline,
143
+ .np-checkbox-container:hover .np-outline {
153
144
  border-color: var(--np-color-on-surface);
154
145
  border-width: 2px;
155
146
  }
156
- .np-container:has(input:disabled) .np-outline {
147
+ .np-checkbox-container:has(input:disabled) .np-outline {
157
148
  border-color: var(--np-color-on-surface);
158
149
  border-width: 2px;
159
150
  opacity: 0.38;
160
151
  }
161
- .np-container:has(input:disabled:checked) .np-outline,
162
- .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 {
163
154
  visibility: hidden;
164
155
  }
165
156
  .np-background {
@@ -173,17 +164,17 @@
173
164
  transition-timing-function: cubic-bezier(0.3, 0, 0.8, 0.15), linear;
174
165
  transform: scale(0.6);
175
166
  }
176
- .np-container:has(input:indeterminate) .np-background,
177
- .np-container:has(input:checked) .np-background,
178
- .np-container:has(input:indeterminate) .np-icon,
179
- .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 {
180
171
  opacity: 1;
181
172
  transition-duration: 350ms, 50ms;
182
173
  transition-timing-function: cubic-bezier(0.05, 0.7, 0.1, 1), linear;
183
174
  transform: scale(1);
184
175
  }
185
- .np-container:has(input:disabled:checked) .np-background,
186
- .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 {
187
178
  background: var(--np-color-on-surface);
188
179
  opacity: 0.38;
189
180
  }
@@ -208,23 +199,23 @@
208
199
  height: 18px;
209
200
  width: 18px;
210
201
  }
211
- .np-container:has(input:disabled) .np-icon {
202
+ .np-checkbox-container:has(input:disabled) .np-icon {
212
203
  fill: var(--np-color-surface);
213
204
  }
214
- .np-container:has(input:checked) .mark.short,
215
- .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 {
216
207
  height: 5.6568542495px;
217
208
  }
218
- .np-container:has(input:indeterminate) .mark,
219
- .np-container:has(input:checked) .mark {
209
+ .np-checkbox-container:has(input:indeterminate) .mark,
210
+ .np-checkbox-container:has(input:checked) .mark {
220
211
  transition-property: none;
221
212
  }
222
- .np-container:has(input:checked) .mark,
223
- .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 {
224
215
  transform: scaleY(-1) translate(7px, -14px) rotate(45deg);
225
216
  }
226
- .np-container:has(input:indeterminate) .mark,
227
- .np-container:has(input:checked) .mark {
217
+ .np-checkbox-container:has(input:indeterminate) .mark,
218
+ .np-checkbox-container:has(input:checked) .mark {
228
219
  animation-duration: 350ms;
229
220
  animation-timing-function: cubic-bezier(0.05, 0.7, 0.1, 1);
230
221
  transition-duration: 350ms;
@@ -241,11 +232,11 @@
241
232
  transition-duration: 150ms;
242
233
  transition-timing-function: cubic-bezier(0.3, 0, 0.8, 0.15);
243
234
  }
244
- .np-container:has(input:checked) .mark.long {
235
+ .np-checkbox-container:has(input:checked) .mark.long {
245
236
  animation-name: prev-unselected-to-checked;
246
237
  }
247
- .np-container:has(input:checked) .mark.long,
248
- .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 {
249
240
  width: 11.313708499px;
250
241
  }
251
242
  .mark.long {
@@ -253,7 +244,7 @@
253
244
  transition-property: transform, width;
254
245
  width: 10px;
255
246
  }
256
- .np-container:has(input:indeterminate) .mark {
247
+ .np-checkbox-container:has(input:indeterminate) .mark {
257
248
  transform: scaleY(-1) translate(4px, -10px) rotate(0deg);
258
249
  }
259
250
  @keyframes prev-unselected-to-checked {
@@ -266,7 +257,7 @@
266
257
  background-color: CanvasText;
267
258
  }
268
259
 
269
- .np-container:has(input:disabled:checked) .np-background {
260
+ .np-checkbox-container:has(input:disabled:checked) .np-background {
270
261
  background-color: GrayText;
271
262
  opacity: 1;
272
263
  }
@@ -275,7 +266,7 @@
275
266
  border-color: CanvasText;
276
267
  }
277
268
 
278
- .np-container:has(input:disabled) .np-outline {
269
+ .np-checkbox-container:has(input:disabled) .np-outline {
279
270
  border-color: GrayText;
280
271
  opacity: 1;
281
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, element = $bindable(), ...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,15 +12,7 @@
12
12
  const onkeydown = arrowKeyNav(CHIP_SELECTOR, 'horizontal')
13
13
  </script>
14
14
 
15
- <div
16
- {...attributes}
17
- bind:this={element}
18
- class={[
19
- 'np-chip-set-wrapper',
20
- (chipsCount ?? 0) > 0 && 'np-chip-set-has-chips',
21
- attributes.class,
22
- ]}
23
- >
15
+ <div {...attributes} bind:this={element} class={['np-chip-set-wrapper', attributes.class]}>
24
16
  {#if children}
25
17
  <div {@attach attach} class={['np-chip-set']} role="toolbar" tabindex="-1" {onkeydown}>
26
18
  {@render children()}
@@ -34,7 +26,7 @@
34
26
  padding: 0.5rem;
35
27
  margin: -0.5rem;
36
28
  }
37
- .np-chip-set-has-chips {
29
+ .np-chip-set-wrapper:has(.np-chip-set > :global(*)) {
38
30
  margin-inline-end: 0;
39
31
  }
40
32
  .np-chip-set {
@@ -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);
@@ -12,13 +12,16 @@
12
12
  icon,
13
13
  element = $bindable(),
14
14
  actionElement = $bindable(),
15
- ariaLabelRemove = 'Remove',
15
+ removeAriaLabel = 'Remove',
16
16
  onclick,
17
17
  onremove,
18
18
  name,
19
19
  value,
20
+ issues,
20
21
  ...attributes
21
22
  }: InputChipProps = $props()
23
+
24
+ let hasError = $derived(!!issues?.length)
22
25
  </script>
23
26
 
24
27
  <div
@@ -30,6 +33,7 @@
30
33
  icon ? 'np-input-chip-icon' : '',
31
34
  disabled ? 'np-input-chip-disabled' : '',
32
35
  selected ? 'np-input-chip-selected' : '',
36
+ hasError && !disabled && 'np-chip-error',
33
37
  attributes.class,
34
38
  ]}
35
39
  >
@@ -57,7 +61,7 @@
57
61
  type="button"
58
62
  size="xs"
59
63
  --np-icon-button-icon-size="1.125rem"
60
- aria-label={ariaLabelRemove}
64
+ aria-label={removeAriaLabel}
61
65
  onclick={onremove}
62
66
  >
63
67
  <CloseIcon />
@@ -77,6 +81,13 @@
77
81
  padding-inline-end: 1px;
78
82
  min-width: 0;
79
83
  }
84
+ .np-chip-error {
85
+ --np-input-chip-outline-color: var(--np-color-error);
86
+ --np-icon-button-icon-color: var(--np-color-error);
87
+ --np-ripple-hover-color: var(--np-color-error);
88
+ --np-ripple-pressed-color: var(--np-color-error);
89
+ color: var(--np-color-error);
90
+ }
80
91
  .np-input-chip-label {
81
92
  appearance: none;
82
93
  background: none;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import Button from '../button/Button.svelte'
3
+ import type { SuggestionChipProps } from './types.js'
4
+
5
+ let {
6
+ variant = 'outlined',
7
+ disabled = false,
8
+ label = '',
9
+ element = $bindable(),
10
+ ...attributes
11
+ }: SuggestionChipProps = $props()
12
+ </script>
13
+
14
+ <Button
15
+ --np-elevated-button-label-text-color="var(--np-suggestion-chip-label-text-color, var(--np-color-on-surface))"
16
+ --np-outlined-button-label-text-color="var(--np-suggestion-chip-label-text-color, var(--np-color-on-surface))"
17
+ --np-outlined-button-outline-color="var(--np-suggestion-chip-outline-color, var(--np-color-outline-variant))"
18
+ --np-button-padding="1rem"
19
+ --np-button-shape="0.5rem"
20
+ style="letter-spacing: 0.006rem"
21
+ bind:element
22
+ {...attributes}
23
+ size="xs"
24
+ shape="square"
25
+ {variant}
26
+ {disabled}
27
+ >
28
+ {label}
29
+ </Button>
@@ -0,0 +1,4 @@
1
+ import type { SuggestionChipProps } from './types.ts';
2
+ declare const SuggestionChip: import("svelte").Component<SuggestionChipProps, {}, "element">;
3
+ type SuggestionChip = ReturnType<typeof SuggestionChip>;
4
+ export default SuggestionChip;
@@ -1,4 +1,5 @@
1
1
  export { default as FilterChip } from './FilterChip.svelte';
2
2
  export { default as AssistChip } from './AssistChip.svelte';
3
3
  export { default as InputChip } from './InputChip.svelte';
4
+ export { default as SuggestionChip } from './SuggestionChip.svelte';
4
5
  export { default as ChipSet } from './ChipSet.svelte';