noph-ui 0.34.4 → 0.36.1

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 (55) hide show
  1. package/README.md +13 -13
  2. package/dist/badge/Badge.svelte +4 -2
  3. package/dist/badge/Badge.svelte.d.ts +1 -0
  4. package/dist/button/Button.svelte +3 -5
  5. package/dist/button/IconButton.svelte +7 -7
  6. package/dist/button/SegmentedButton.svelte +22 -25
  7. package/dist/card/Card.svelte +11 -35
  8. package/dist/checkbox/Checkbox.svelte +68 -32
  9. package/dist/checkbox/types.d.ts +1 -0
  10. package/dist/chip/ChipSet.svelte +10 -2
  11. package/dist/chip/FilterChip.svelte +48 -52
  12. package/dist/chip/InputChip.svelte +32 -31
  13. package/dist/chip/InputChip.svelte.d.ts +1 -1
  14. package/dist/chip/types.d.ts +3 -1
  15. package/dist/dialog/Dialog.svelte +53 -10
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +2 -0
  18. package/dist/keyboard-nav.js +12 -4
  19. package/dist/list/Item.svelte +39 -66
  20. package/dist/loading-indicator/LoadingIndicator.svelte +135 -0
  21. package/dist/loading-indicator/LoadingIndicator.svelte.d.ts +4 -0
  22. package/dist/loading-indicator/index.d.ts +1 -0
  23. package/dist/loading-indicator/index.js +1 -0
  24. package/dist/loading-indicator/shapes.d.ts +6 -0
  25. package/dist/loading-indicator/shapes.js +49 -0
  26. package/dist/loading-indicator/types.d.ts +7 -0
  27. package/dist/loading-indicator/types.js +1 -0
  28. package/dist/menu/Menu.svelte +10 -1
  29. package/dist/menu/MenuItem.svelte +4 -4
  30. package/dist/navigation-drawer/NavigationDrawer.svelte +35 -1
  31. package/dist/navigation-drawer/NavigationDrawerItem.svelte +1 -1
  32. package/dist/navigation-rail/NavigationRailItem.svelte +2 -2
  33. package/dist/progress/CircularProgress.svelte +214 -117
  34. package/dist/progress/LinearProgress.svelte +402 -102
  35. package/dist/progress/types.d.ts +2 -0
  36. package/dist/ripple/Ripple.svelte +6 -7
  37. package/dist/select/Option.svelte +0 -1
  38. package/dist/select/Select.svelte +41 -20
  39. package/dist/slider/Slider.svelte +623 -0
  40. package/dist/slider/Slider.svelte.d.ts +4 -0
  41. package/dist/slider/index.d.ts +1 -0
  42. package/dist/slider/index.js +1 -0
  43. package/dist/slider/types.d.ts +23 -0
  44. package/dist/slider/types.js +1 -0
  45. package/dist/snackbar/Snackbar.svelte +21 -10
  46. package/dist/switch/Switch.svelte +1 -1
  47. package/dist/tabs/Tab.svelte +18 -16
  48. package/dist/tabs/Tabs.svelte +4 -2
  49. package/dist/tabs/types.d.ts +2 -0
  50. package/dist/text-field/TextField.svelte +2 -0
  51. package/dist/themes/defaultTheme.css +3 -9
  52. package/dist/tooltip/Tooltip.svelte +53 -10
  53. package/dist/types.d.ts +2 -0
  54. package/dist/types.js +2 -0
  55. package/package.json +11 -11
package/README.md CHANGED
@@ -52,30 +52,30 @@ body {
52
52
 
53
53
  ## Roadmap
54
54
 
55
- Beta (No breaking changes expected)
55
+ Beta
56
56
 
57
+ - Auto complete
58
+ - Badges
57
59
  - Buttons
58
60
  - Cards
59
61
  - Checkbox
62
+ - Chips
63
+ - Dialogs
60
64
  - Divider
61
65
  - Icon buttons
62
- - Lists (Docs missing)
66
+ - Lists
67
+ - Loading indicator
68
+ - Menus
69
+ - Navigation Drawer
70
+ - Navigation Rail
63
71
  - Progress indicators
64
72
  - Radio
65
73
  - Ripple
66
74
  - Segmented buttons
67
75
  - Select
76
+ - Slider
68
77
  - Snackbar
69
78
  - Switch
70
- - Text fields
71
79
  - Tabs
72
-
73
- In progress (Breaking changes expected)
74
-
75
- - Auto complete
76
- - Chips (Docs missing)
77
- - Dialogs (Fullscreen + Docs missing)
78
- - Menus (Positioning missing + Docs missing)
79
- - Navigation Drawer (Docs missing)
80
- - Navigation Rail (Badge is missing + Docs missing)
81
- - Tooltips (Positioning missing)
80
+ - Text fields
81
+ - Tooltips
@@ -1,9 +1,11 @@
1
1
  <script lang="ts">
2
- let { label }: { label?: string | number } = $props()
2
+ let { label, ariaLabel }: { label?: string | number; ariaLabel?: string } = $props()
3
3
  </script>
4
4
 
5
5
  <div
6
- aria-hidden="true"
6
+ aria-hidden={ariaLabel ? undefined : 'true'}
7
+ role={ariaLabel ? 'status' : undefined}
8
+ aria-label={ariaLabel}
7
9
  class={[
8
10
  'np-badge-container',
9
11
  label === undefined ? 'np-badge-container-no-label' : 'np-badge-container-label',
@@ -1,5 +1,6 @@
1
1
  type $$ComponentProps = {
2
2
  label?: string | number;
3
+ ariaLabel?: string;
3
4
  };
4
5
  declare const Badge: import("svelte").Component<$$ComponentProps, {}, "">;
5
6
  type Badge = ReturnType<typeof Badge>;
@@ -49,7 +49,7 @@
49
49
  {/if}
50
50
  {#if loading}
51
51
  <div class="circular-progress">
52
- <CircularProgress aria-label={loadingAriaLabel} indeterminate />
52
+ <CircularProgress aria-label={loadingAriaLabel} indeterminate track={false} />
53
53
  </div>
54
54
  {/if}
55
55
  {#if start}
@@ -417,10 +417,8 @@
417
417
  :global(.np-button .button-icon svg) {
418
418
  fill: currentColor;
419
419
  display: block;
420
- }
421
- :global(.np-button .button-icon svg) {
422
- width: calc((var(--button-height) - 0.375rem) / 2);
423
- height: calc((var(--button-height) - 0.375rem) / 2);
420
+ width: var(--_icon-size);
421
+ height: var(--_icon-size);
424
422
  }
425
423
 
426
424
  @media (forced-colors: active) {
@@ -50,7 +50,7 @@
50
50
  {/if}
51
51
  {#if loading}
52
52
  <div class="np-icon-button-circular-progress">
53
- <CircularProgress aria-label={loadingAriaLabel} indeterminate />
53
+ <CircularProgress aria-label={loadingAriaLabel} indeterminate track={false} />
54
54
  </div>
55
55
  {/if}
56
56
  {#if selectedIcon && selected}
@@ -69,8 +69,8 @@
69
69
  }
70
70
  attributes.onclick?.(event)
71
71
  }}
72
- aria-describedby={title ? uid : undefined}
73
- aria-label={title}
72
+ aria-describedby={title ? uid : attributes['aria-describedby']}
73
+ aria-label={title || attributes['aria-label']}
74
74
  bind:this={element}
75
75
  class={[
76
76
  'np-icon-button',
@@ -184,7 +184,7 @@
184
184
  border-radius: 1.25rem;
185
185
  }
186
186
  .s.square {
187
- border-radius: var(--np-button-shape, 0.75rem);
187
+ border-radius: var(--np-icon-button-shape, 0.75rem);
188
188
  }
189
189
  .s.narrow {
190
190
  width: 2rem;
@@ -206,7 +206,7 @@
206
206
  border-radius: 1.75rem;
207
207
  }
208
208
  .m.square {
209
- border-radius: var(--np-button-shape, 1rem);
209
+ border-radius: var(--np-icon-button-shape, 1rem);
210
210
  }
211
211
  .m.narrow {
212
212
  width: 3rem;
@@ -228,7 +228,7 @@
228
228
  border-radius: 3rem;
229
229
  }
230
230
  .l.square {
231
- border-radius: var(--np-button-shape, 1.75rem);
231
+ border-radius: var(--np-icon-button-shape, 1.75rem);
232
232
  }
233
233
  .l.narrow {
234
234
  width: 4rem;
@@ -250,7 +250,7 @@
250
250
  border-radius: 4.25rem;
251
251
  }
252
252
  .xl.square {
253
- border-radius: var(--np-button-shape, 1.75rem);
253
+ border-radius: var(--np-icon-button-shape, 1.75rem);
254
254
  }
255
255
  .xl.narrow {
256
256
  width: 6.5rem;
@@ -11,7 +11,6 @@
11
11
  group = $bindable(),
12
12
  ...attributes
13
13
  }: SegmentedButtonProps = $props()
14
- let hoverState = $state(-1)
15
14
  </script>
16
15
 
17
16
  <div
@@ -21,6 +20,7 @@
21
20
  style="{attributes.style};grid-template-columns: repeat({options.length}, minmax(max-content, 1fr));"
22
21
  >
23
22
  {#each options as option, i (i)}
23
+ {@const optionLabel = option.label ?? `${name}-${i}`}
24
24
  <label class={['np-segmented-button', option.icon && 'width-icon']}>
25
25
  <div class="check-icon-wrapper">
26
26
  <div class="check-icon">
@@ -42,7 +42,7 @@
42
42
  {option.label}
43
43
  {/if}
44
44
  {#if !option.disabled}
45
- <Ripple forceHover={i === hoverState} />
45
+ <Ripple />
46
46
  {/if}
47
47
  {#if group !== undefined}
48
48
  {#if multiSelect}
@@ -50,13 +50,9 @@
50
50
  type="checkbox"
51
51
  onclick={option.onclick}
52
52
  bind:group
53
- onfocus={(event) => {
54
- if (event) hoverState = i
55
- }}
56
- onblur={() => (hoverState = -1)}
57
53
  {name}
58
- aria-label={typeof option.label === 'function' ? `${name}-${i}` : option.label}
59
- value={option.label}
54
+ aria-label={optionLabel}
55
+ value={optionLabel}
60
56
  disabled={option.disabled}
61
57
  checked={option.selected}
62
58
  />
@@ -65,13 +61,9 @@
65
61
  type="radio"
66
62
  onclick={option.onclick}
67
63
  bind:group
68
- onfocus={(event) => {
69
- if (event) hoverState = i
70
- }}
71
- onblur={() => (hoverState = -1)}
72
64
  {name}
73
- aria-label={typeof option.label === 'function' ? `${name}-${i}` : option.label}
74
- value={option.label}
65
+ aria-label={optionLabel}
66
+ value={optionLabel}
75
67
  disabled={option.disabled}
76
68
  checked={option.selected}
77
69
  />
@@ -80,13 +72,9 @@
80
72
  <input
81
73
  type={multiSelect ? 'checkbox' : 'radio'}
82
74
  onclick={option.onclick}
83
- onfocus={(event) => {
84
- if (event) hoverState = i
85
- }}
86
- onblur={() => (hoverState = -1)}
87
75
  {name}
88
- aria-label={option.label ?? `${name}-${i}`}
89
- value={option.label}
76
+ aria-label={optionLabel}
77
+ value={optionLabel}
90
78
  disabled={option.disabled}
91
79
  checked={option.selected}
92
80
  />
@@ -108,7 +96,7 @@
108
96
 
109
97
  .np-segmented-button {
110
98
  flex: 1;
111
- padding: 0.5rem 2rem;
99
+ padding: 0.5rem 1.75rem;
112
100
  text-align: center;
113
101
  cursor: pointer;
114
102
  font-size: 0.875rem;
@@ -136,7 +124,7 @@
136
124
 
137
125
  .np-segmented-button:has(input:disabled) {
138
126
  cursor: unset;
139
- opacity: 0.5;
127
+ opacity: 0.38;
140
128
  }
141
129
 
142
130
  .np-segmented-button:last-child {
@@ -159,7 +147,7 @@
159
147
  }
160
148
  .width-icon,
161
149
  .np-segmented-button:has(input:checked) {
162
- padding: 0.5rem 1rem;
150
+ padding: 0.5rem 0.75rem;
163
151
  }
164
152
  .np-segmented-button:has(input:checked) {
165
153
  color: var(--np-color-on-secondary-container);
@@ -168,6 +156,7 @@
168
156
  opacity: 1;
169
157
  }
170
158
  .check-icon {
159
+ display: flex;
171
160
  width: 0;
172
161
  overflow: hidden;
173
162
  fill: currentColor;
@@ -182,6 +171,7 @@
182
171
  transition: width 0.15s linear;
183
172
  }
184
173
  .alternate-icon {
174
+ display: flex;
185
175
  width: 1.5rem;
186
176
  overflow: hidden;
187
177
  fill: currentColor;
@@ -213,21 +203,28 @@
213
203
  width: 1.5rem;
214
204
  }
215
205
 
216
- .np-segmented-buttons:has(input:focus-visible) {
206
+ .np-segmented-button:has(input:focus-visible) :global(.np-ripple-surface)::before {
207
+ opacity: var(--np-ripple-focus-opacity, 0.1);
208
+ }
209
+
210
+ .np-segmented-button:has(input:focus-visible) {
217
211
  outline-style: solid;
218
212
  outline-color: var(--np-color-secondary);
219
213
  outline-width: 3px;
220
- outline-offset: 2px;
214
+ outline-offset: -3px;
221
215
  animation: focusAnimation 0.3s ease forwards;
222
216
  }
223
217
  @keyframes focusAnimation {
224
218
  0% {
219
+ outline-offset: -3px;
225
220
  outline-width: 3px;
226
221
  }
227
222
  50% {
223
+ outline-offset: -6px;
228
224
  outline-width: 6px;
229
225
  }
230
226
  100% {
227
+ outline-offset: -3px;
231
228
  outline-width: 3px;
232
229
  }
233
230
  }
@@ -12,25 +12,9 @@
12
12
  supportingText,
13
13
  action,
14
14
  children,
15
- onfocus,
16
- onblur,
17
15
  type,
18
16
  ...attributes
19
17
  }: CardProps = $props()
20
-
21
- let focused = $state(false)
22
-
23
- $effect(() => {
24
- if (!element) return
25
- // eslint-disable-next-line no-undef
26
- const formElements: NodeListOf<
27
- HTMLButtonElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
28
- > = element.querySelectorAll('input, button, select, textarea')
29
-
30
- formElements.forEach((el) => {
31
- el.disabled = disabled
32
- })
33
- })
34
18
  </script>
35
19
 
36
20
  {#snippet content()}
@@ -61,7 +45,7 @@
61
45
  {/if}
62
46
  </div>
63
47
  {#if !disabled && type !== 'text'}
64
- <Ripple forceHover={focused} />
48
+ <Ripple />
65
49
  {/if}
66
50
  {/snippet}
67
51
 
@@ -85,14 +69,6 @@
85
69
  {...attributes}
86
70
  type="button"
87
71
  bind:this={element}
88
- onfocus={(event) => {
89
- focused = true
90
- onfocus?.(event)
91
- }}
92
- onblur={(event) => {
93
- focused = false
94
- onblur?.(event)
95
- }}
96
72
  {disabled}
97
73
  class={[
98
74
  'np-card-container',
@@ -110,14 +86,6 @@
110
86
  aria-disabled={disabled}
111
87
  tabindex={disabled ? -1 : undefined}
112
88
  role="link"
113
- onfocus={(event) => {
114
- focused = true
115
- onfocus?.(event)
116
- }}
117
- onblur={(event) => {
118
- focused = false
119
- onblur?.(event)
120
- }}
121
89
  class={[
122
90
  'np-card-container',
123
91
  `np-card-${variant}`,
@@ -162,8 +130,8 @@
162
130
  --border-radius: var(--np-outlined-card-container-shape, var(--np-shape-corner-medium));
163
131
  }
164
132
  .np-card-outlined.np-card-disabled {
165
- border-color: color-mix(in srgb, var(--np-color-outline) 32%, transparent);
166
- --background-color: none;
133
+ border-color: color-mix(in srgb, var(--np-color-outline) 12%, transparent);
134
+ --background-color: transparent;
167
135
  }
168
136
  .np-card-container {
169
137
  font: inherit;
@@ -181,6 +149,10 @@
181
149
  cursor: pointer;
182
150
  }
183
151
 
152
+ .np-card-container:focus-visible :global(.np-ripple-surface)::before {
153
+ opacity: var(--np-ripple-focus-opacity, 0.1);
154
+ }
155
+
184
156
  .np-card-container:focus-visible {
185
157
  outline-style: solid;
186
158
  outline-color: var(--np-color-secondary);
@@ -211,6 +183,10 @@
211
183
  overflow: hidden;
212
184
  background-size: cover;
213
185
  background-position: 50%;
186
+ border-start-start-radius: var(--border-radius);
187
+ border-start-end-radius: var(--border-radius);
188
+ }
189
+ .np-card-container:not(:has(.np-card-content:not(:empty))) .np-card-image {
214
190
  border-radius: var(--border-radius);
215
191
  }
216
192
  .np-card-content {
@@ -7,37 +7,41 @@
7
7
  checked = $bindable(),
8
8
  element = $bindable(),
9
9
  group = $bindable(),
10
+ error = false,
10
11
  style,
11
12
  value,
12
13
  ...attributes
13
14
  }: CheckboxProps = $props()
14
15
 
15
- $effect(() => {
16
- if (group && value) {
17
- checked = group.includes(value)
18
- }
19
- })
16
+ let inputEl: HTMLInputElement | undefined = $state()
17
+
18
+ let groupValue = $derived(
19
+ typeof value === 'string' || typeof value === 'number' ? value : undefined,
20
+ )
21
+ let grouped = $derived(Array.isArray(group) && groupValue !== undefined)
22
+ let groupChecked = $derived(
23
+ Array.isArray(group) && groupValue !== undefined ? group.includes(groupValue) : false,
24
+ )
20
25
 
21
- $effect(() => {
22
- if (value && group) {
23
- const index = group.indexOf(value)
24
- if (checked) {
25
- if (index < 0) {
26
- group?.push(value)
27
- group = group
28
- }
29
- } else {
30
- if (index >= 0) {
31
- group.splice(index, 1)
32
- group = group
33
- }
34
- }
26
+ const onchange = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
27
+ const next = event.currentTarget.checked
28
+ if (Array.isArray(group) && groupValue !== undefined) {
29
+ group = next
30
+ ? group.includes(groupValue)
31
+ ? group
32
+ : [...group, groupValue]
33
+ : group.filter((entry) => entry !== groupValue)
35
34
  }
36
- })
37
- let inputEl: HTMLInputElement | undefined = $state()
35
+ checked = next
36
+ attributes.onchange?.(event)
37
+ }
38
38
  </script>
39
39
 
40
- <div {style} class={['np-container', attributes.class]} bind:this={element}>
40
+ <div
41
+ {style}
42
+ class={['np-container', error && !attributes.disabled && 'np-error', attributes.class]}
43
+ bind:this={element}
44
+ >
41
45
  <div class="np-outline"></div>
42
46
  <div class="np-background"></div>
43
47
  <svg class="np-icon" viewBox="0 0 18 18" aria-hidden="true">
@@ -48,16 +52,32 @@
48
52
  {#if !attributes.disabled}
49
53
  <Ripple forElement={inputEl} />
50
54
  {/if}
51
- <input
52
- {...attributes}
53
- {value}
54
- class="np-input"
55
- type="checkbox"
56
- bind:indeterminate
57
- bind:checked
58
- bind:this={inputEl}
59
- aria-checked={indeterminate ? 'mixed' : undefined}
60
- />
55
+ {#if grouped}
56
+ <input
57
+ {...attributes}
58
+ {value}
59
+ class="np-input"
60
+ type="checkbox"
61
+ bind:indeterminate
62
+ bind:this={inputEl}
63
+ checked={groupChecked}
64
+ {onchange}
65
+ aria-invalid={error ? 'true' : undefined}
66
+ aria-checked={indeterminate ? 'mixed' : undefined}
67
+ />
68
+ {:else}
69
+ <input
70
+ {...attributes}
71
+ {value}
72
+ class="np-input"
73
+ type="checkbox"
74
+ bind:indeterminate
75
+ bind:checked
76
+ bind:this={inputEl}
77
+ aria-invalid={error ? 'true' : undefined}
78
+ aria-checked={indeterminate ? 'mixed' : undefined}
79
+ />
80
+ {/if}
61
81
  </div>
62
82
  </div>
63
83
 
@@ -172,6 +192,22 @@
172
192
  background: var(--np-color-on-surface);
173
193
  opacity: 0.38;
174
194
  }
195
+
196
+ .np-error .np-outline,
197
+ .np-error:hover .np-outline,
198
+ .np-error:focus-within .np-outline {
199
+ border-color: var(--np-color-error);
200
+ }
201
+ .np-error .np-background {
202
+ background-color: var(--np-color-error);
203
+ }
204
+ .np-error .np-icon {
205
+ fill: var(--np-color-on-error);
206
+ }
207
+ .np-error {
208
+ --np-ripple-hover-color: var(--np-color-error);
209
+ --np-ripple-pressed-color: var(--np-color-error);
210
+ }
175
211
  .np-icon {
176
212
  fill: var(--np-checkbox-selected-icon-color, var(--np-color-on-primary));
177
213
  height: 18px;
@@ -2,4 +2,5 @@ import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  export interface CheckboxProps extends Omit<HTMLInputAttributes, 'type'> {
3
3
  element?: HTMLElement;
4
4
  group?: (string | number)[] | null;
5
+ error?: boolean;
5
6
  }
@@ -1,7 +1,15 @@
1
1
  <script lang="ts">
2
+ import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
2
3
  import type { ChipSetProps } from './types.ts'
3
4
 
4
5
  let { children, chipsCount, ...attributes }: ChipSetProps = $props()
6
+
7
+ const CHIP_SELECTOR = "input:not([type='hidden']), button, a[href]"
8
+ const attach = rovingTabindex(CHIP_SELECTOR, {
9
+ currentAttr: 'aria-current',
10
+ currentValue: 'true',
11
+ })
12
+ const onkeydown = arrowKeyNav(CHIP_SELECTOR, 'horizontal')
5
13
  </script>
6
14
 
7
15
  <div
@@ -13,7 +21,7 @@
13
21
  style={attributes.style}
14
22
  >
15
23
  {#if children}
16
- <div class={['np-chip-set']} role="toolbar">
24
+ <div {@attach attach} class={['np-chip-set']} role="toolbar" tabindex="-1" {onkeydown}>
17
25
  {@render children()}
18
26
  </div>
19
27
  {/if}
@@ -26,7 +34,7 @@
26
34
  margin: -0.5rem;
27
35
  }
28
36
  .np-chip-set-has-chips {
29
- margin-right: 0;
37
+ margin-inline-end: 0;
30
38
  }
31
39
  .np-chip-set {
32
40
  display: flex;