noph-ui 0.40.3 → 0.42.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.
@@ -26,6 +26,8 @@
26
26
 
27
27
  const uid = $props.id()
28
28
 
29
+ const tooltipId = $derived(title && !disabled && !loading ? uid : undefined)
30
+
29
31
  let pressed = $state(false)
30
32
  let pressedTimeout: ReturnType<typeof setTimeout>
31
33
 
@@ -79,7 +81,8 @@
79
81
  }
80
82
  attributes.onclick?.(event)
81
83
  }}
82
- aria-describedby={title ? uid : attributes['aria-describedby']}
84
+ aria-describedby={tooltipId ?? attributes['aria-describedby']}
85
+ interestfor={tooltipId ?? attributes['interestfor']}
83
86
  aria-label={title || attributes['aria-label']}
84
87
  bind:this={element}
85
88
  class={[
@@ -99,7 +102,8 @@
99
102
  {:else}
100
103
  <button
101
104
  {...attributes as HTMLButtonAttributes}
102
- aria-describedby={title ? uid : attributes['aria-describedby']}
105
+ aria-describedby={tooltipId ?? attributes['aria-describedby']}
106
+ interestfor={tooltipId ?? attributes['interestfor']}
103
107
  aria-label={title || attributes['aria-label']}
104
108
  disabled={disabled || loading}
105
109
  aria-pressed={toggle ? selected : undefined}
@@ -130,8 +134,8 @@
130
134
  </button>
131
135
  {/if}
132
136
 
133
- {#if title && !disabled && !loading}
134
- <Tooltip id={uid}>{title}</Tooltip>
137
+ {#if tooltipId}
138
+ <Tooltip id={tooltipId}>{title}</Tooltip>
135
139
  {/if}
136
140
 
137
141
  <style>
@@ -170,9 +174,16 @@
170
174
  --np-icon-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
171
175
  transition:
172
176
  background-color var(--np-motion-expressive-default-effects),
173
- border-radius var(--np-motion-expressive-default-effects),
174
177
  box-shadow var(--np-motion-expressive-default-effects);
175
178
  }
179
+ @media (prefers-reduced-motion: no-preference) {
180
+ .np-button {
181
+ transition:
182
+ background-color var(--np-motion-expressive-default-effects),
183
+ border-radius var(--np-motion-expressive-default-effects),
184
+ box-shadow var(--np-motion-expressive-default-effects);
185
+ }
186
+ }
176
187
  .xs {
177
188
  font-size: 0.875rem;
178
189
  height: 2rem;
@@ -286,7 +297,11 @@
286
297
  outline-color: var(--np-color-secondary);
287
298
  outline-width: 3px;
288
299
  outline-offset: 2px;
289
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
300
+ }
301
+ @media (prefers-reduced-motion: no-preference) {
302
+ .enabled:focus-visible {
303
+ animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
304
+ }
290
305
  }
291
306
  .text {
292
307
  --np-ripple-hover-color: var(--np-text-button-label-text-color, var(--np-color-primary));
@@ -25,6 +25,7 @@
25
25
  }: IconButtonProps = $props()
26
26
 
27
27
  const uid = $props.id()
28
+ const tooltipId = $derived(title && !disabled && !loading ? uid : undefined)
28
29
 
29
30
  let pressed = $state(false)
30
31
  let pressedTimeout: ReturnType<typeof setTimeout>
@@ -70,7 +71,8 @@
70
71
  }
71
72
  attributes.onclick?.(event)
72
73
  }}
73
- aria-describedby={title ? uid : attributes['aria-describedby']}
74
+ aria-describedby={tooltipId ?? attributes['aria-describedby']}
75
+ interestfor={tooltipId ?? attributes['interestfor']}
74
76
  aria-label={title || attributes['aria-label']}
75
77
  bind:this={element}
76
78
  class={[
@@ -91,7 +93,8 @@
91
93
  {:else}
92
94
  <button
93
95
  {...attributes as HTMLButtonAttributes}
94
- aria-describedby={title ? uid : attributes['aria-describedby']}
96
+ aria-describedby={tooltipId ?? attributes['aria-describedby']}
97
+ interestfor={tooltipId ?? attributes['interestfor']}
95
98
  aria-label={title || attributes['aria-label']}
96
99
  aria-pressed={toggle ? selected : undefined}
97
100
  aria-busy={loading}
@@ -122,8 +125,8 @@
122
125
  </button>
123
126
  {/if}
124
127
 
125
- {#if title && !disabled && !loading}
126
- <Tooltip id={uid}>{title}</Tooltip>
128
+ {#if tooltipId}
129
+ <Tooltip id={tooltipId}>{title}</Tooltip>
127
130
  {/if}
128
131
 
129
132
  <style>
@@ -142,9 +145,14 @@
142
145
  align-items: center;
143
146
  justify-content: center;
144
147
  --np-icon-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
145
- transition:
146
- background-color var(--np-motion-expressive-default-effects),
147
- border-radius var(--np-motion-expressive-default-effects);
148
+ transition: background-color var(--np-motion-expressive-default-effects);
149
+ }
150
+ @media (prefers-reduced-motion: no-preference) {
151
+ .np-icon-button {
152
+ transition:
153
+ background-color var(--np-motion-expressive-default-effects),
154
+ border-radius var(--np-motion-expressive-default-effects);
155
+ }
148
156
  }
149
157
 
150
158
  :global(.np-icon-button svg) {
@@ -291,7 +299,11 @@
291
299
  outline-color: var(--np-color-secondary);
292
300
  outline-width: 3px;
293
301
  outline-offset: 2px;
294
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
302
+ }
303
+ @media (prefers-reduced-motion: no-preference) {
304
+ .enabled:focus-visible {
305
+ animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
306
+ }
295
307
  }
296
308
  .text {
297
309
  --np-ripple-hover-color: var(--np-icon-button-icon-color, var(--np-color-on-surface-variant));
@@ -110,7 +110,7 @@
110
110
  align-items: center;
111
111
  border-inline-end: 1px solid var(--np-color-outline);
112
112
  position: relative;
113
- transition: all var(--np-motion-expressive-fast-effects);
113
+ transition: color var(--np-motion-expressive-fast-effects);
114
114
  }
115
115
  .np-segmented-button-icon-label {
116
116
  display: inline-flex;
@@ -161,7 +161,6 @@
161
161
  width: 0;
162
162
  overflow: hidden;
163
163
  fill: currentColor;
164
- transition: width var(--np-motion-expressive-slow-effects);
165
164
  }
166
165
  :global(.check-icon svg) {
167
166
  display: block;
@@ -169,14 +168,12 @@
169
168
  .check-icon-wrapper {
170
169
  width: 0;
171
170
  overflow: hidden;
172
- transition: width var(--np-motion-expressive-fast-effects);
173
171
  }
174
172
  .alternate-icon {
175
173
  display: flex;
176
174
  width: 1.5rem;
177
175
  overflow: hidden;
178
176
  fill: currentColor;
179
- transition: width var(--np-motion-expressive-slow-effects);
180
177
  }
181
178
  :global(.alternate-icon svg) {
182
179
  display: block;
@@ -186,9 +183,7 @@
186
183
  .alternate-icon-wrapper {
187
184
  width: 2rem;
188
185
  overflow: hidden;
189
- transition:
190
- width var(--np-motion-expressive-fast-effects),
191
- opacity var(--np-motion-expressive-fast-effects);
186
+ transition: opacity var(--np-motion-expressive-fast-effects);
192
187
  }
193
188
  .np-segmented-button:has(input:checked) :global(.alternate-icon-wrapper) {
194
189
  width: 0;
@@ -213,6 +208,28 @@
213
208
  outline-color: var(--np-color-secondary);
214
209
  outline-width: 3px;
215
210
  outline-offset: -3px;
216
- animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
211
+ }
212
+
213
+ @media (prefers-reduced-motion: no-preference) {
214
+ .np-segmented-button {
215
+ transition: all var(--np-motion-expressive-fast-effects);
216
+ }
217
+ .check-icon {
218
+ transition: width var(--np-motion-expressive-slow-effects);
219
+ }
220
+ .check-icon-wrapper {
221
+ transition: width var(--np-motion-expressive-fast-effects);
222
+ }
223
+ .alternate-icon {
224
+ transition: width var(--np-motion-expressive-slow-effects);
225
+ }
226
+ .alternate-icon-wrapper {
227
+ transition:
228
+ width var(--np-motion-expressive-fast-effects),
229
+ opacity var(--np-motion-expressive-fast-effects);
230
+ }
231
+ .np-segmented-button:has(input:focus-visible) {
232
+ animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
233
+ }
217
234
  }
218
235
  </style>
@@ -159,7 +159,11 @@
159
159
  outline-color: var(--np-color-secondary);
160
160
  outline-width: 3px;
161
161
  outline-offset: 2px;
162
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
162
+ }
163
+ @media (prefers-reduced-motion: no-preference) {
164
+ .np-card-container:focus-visible {
165
+ animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
166
+ }
163
167
  }
164
168
  .np-card-disabled {
165
169
  opacity: 0.38;
@@ -133,7 +133,6 @@
133
133
  z-index: 1;
134
134
  padding-inline: 1rem;
135
135
  overflow: hidden;
136
- transition: padding var(--np-motion-expressive-default-effects);
137
136
  }
138
137
  .np-chip-icon-checked {
139
138
  display: flex;
@@ -141,17 +140,11 @@
141
140
  min-width: 0;
142
141
  margin-inline-end: -0.5rem;
143
142
  overflow: hidden;
144
- transition:
145
- width var(--np-motion-expressive-default-effects),
146
- margin var(--np-motion-expressive-default-effects);
147
143
  }
148
144
  .np-filter-chip:has(input:checked) .np-chip-icon-checked {
149
145
  width: 18px;
150
146
  margin-inline-end: 0;
151
147
  }
152
- .np-filter-chip-icon .np-chip-icon-checked {
153
- transition: none;
154
- }
155
148
  .np-filter-chip:has(input:checked) .np-chip-icon {
156
149
  display: none;
157
150
  }
@@ -238,7 +231,6 @@
238
231
  outline-color: var(--np-color-secondary);
239
232
  outline-width: 3px;
240
233
  outline-offset: 2px;
241
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
242
234
  }
243
235
 
244
236
  .np-filter-chip-disabled .np-filter-chip-label {
@@ -259,4 +251,21 @@
259
251
  outline-color: transparent;
260
252
  background-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
261
253
  }
254
+
255
+ @media (prefers-reduced-motion: no-preference) {
256
+ .np-filter-chip-label {
257
+ transition: padding var(--np-motion-expressive-default-effects);
258
+ }
259
+ .np-chip-icon-checked {
260
+ transition:
261
+ width var(--np-motion-expressive-default-effects),
262
+ margin var(--np-motion-expressive-default-effects);
263
+ }
264
+ .np-filter-chip-icon .np-chip-icon-checked {
265
+ transition: none;
266
+ }
267
+ .np-filter-chip:has(input:focus-visible) {
268
+ animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
269
+ }
270
+ }
262
271
  </style>
@@ -150,7 +150,6 @@
150
150
  outline-color: var(--np-color-secondary);
151
151
  outline-width: 3px;
152
152
  outline-offset: 2px;
153
- animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
154
153
  }
155
154
 
156
155
  .np-input-chip-disabled .np-input-chip-label {
@@ -166,4 +165,10 @@
166
165
  outline-color: transparent;
167
166
  background-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
168
167
  }
168
+
169
+ @media (prefers-reduced-motion: no-preference) {
170
+ .np-input-chip:has(:focus-visible) {
171
+ animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
172
+ }
173
+ }
169
174
  </style>
@@ -0,0 +1,4 @@
1
+ [interestfor][aria-describedby] {
2
+ interest-delay-start: 0s;
3
+ interest-delay-end: 500ms;
4
+ }
@@ -151,7 +151,11 @@
151
151
  outline-width: 3px;
152
152
  outline-offset: -3px;
153
153
  border-radius: var(--np-shape-corner-extra-small);
154
- animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
154
+ }
155
+ @media (prefers-reduced-motion: no-preference) {
156
+ .np-item:focus-visible {
157
+ animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
158
+ }
155
159
  }
156
160
 
157
161
  div.np-item {
@@ -29,6 +29,20 @@
29
29
  element?.hidePopover()
30
30
  }
31
31
 
32
+ const calculateTransformOrigin = (anchorRect: DOMRect, menuRect: DOMRect) => {
33
+ const pivot = (anchorStart: number, anchorEnd: number, menuStart: number, menuEnd: number) => {
34
+ if (menuStart >= anchorEnd) return 0
35
+ if (menuEnd <= anchorStart) return 1
36
+ if (menuEnd === menuStart) return 0
37
+ const intersectionCenter =
38
+ (Math.max(anchorStart, menuStart) + Math.min(anchorEnd, menuEnd)) / 2
39
+ return (intersectionCenter - menuStart) / (menuEnd - menuStart)
40
+ }
41
+ const x = pivot(anchorRect.left, anchorRect.right, menuRect.left, menuRect.right)
42
+ const y = pivot(anchorRect.top, anchorRect.bottom, menuRect.top, menuRect.bottom)
43
+ return `${x * 100}% ${y * 100}%`
44
+ }
45
+
32
46
  const refreshValues = () => {
33
47
  if (element && anchor && open) {
34
48
  const anchorRect = anchor.getBoundingClientRect()
@@ -40,6 +54,10 @@
40
54
  const above = Math.max(anchorRect.top - margin, 0)
41
55
  const overAnchor = coverAnchor && wanted > below && wanted > above && wanted <= room
42
56
  element.style.maxHeight = `${Math.floor(overAnchor ? room : Math.max(below, above))}px`
57
+ element.style.transformOrigin = calculateTransformOrigin(
58
+ anchorRect,
59
+ element.getBoundingClientRect(),
60
+ )
43
61
  }
44
62
  }
45
63
  $effect(refreshValues)
@@ -90,6 +108,7 @@
90
108
  scrollbar-width: thin;
91
109
  justify-self: var(--np-menu-justify-self, anchor-center);
92
110
  position-area: var(--np-menu-position-area, bottom);
111
+ transform-origin: var(--np-menu-transform-origin, top center);
93
112
  position-try-fallbacks:
94
113
  flip-block,
95
114
  flip-inline,
@@ -112,14 +131,23 @@
112
131
 
113
132
  .np-menu-container:popover-open {
114
133
  opacity: 1;
115
- animation: fadeIn var(--np-motion-expressive-default-effects);
134
+ scale: 1;
135
+ }
136
+ @media (prefers-reduced-motion: no-preference) {
137
+ .np-menu-container:popover-open {
138
+ animation:
139
+ fadeIn var(--np-motion-expressive-fast-effects),
140
+ scaleIn var(--np-motion-expressive-fast-spatial);
141
+ }
116
142
  }
117
143
  @keyframes fadeIn {
118
- 0% {
144
+ from {
119
145
  opacity: 0;
120
146
  }
121
- 100% {
122
- opacity: 1;
147
+ }
148
+ @keyframes scaleIn {
149
+ from {
150
+ scale: 0.8;
123
151
  }
124
152
  }
125
153
  </style>
@@ -110,19 +110,27 @@
110
110
 
111
111
  .np-navigation-drawer-container[popover] .np-navigation-wrapper {
112
112
  transform: var(--np-navigation-drawer-start, translateX(-100%));
113
- transition: transform var(--np-motion-standard-slow-spatial);
114
- }
115
-
116
- .np-navigation-drawer-container[popover] {
117
- transition:
118
- overlay var(--np-motion-standard-slow-spatial) allow-discrete,
119
- display var(--np-motion-standard-slow-spatial) allow-discrete;
120
113
  }
121
114
 
122
115
  .np-navigation-drawer-container:popover-open .np-navigation-wrapper {
123
116
  transform: translateX(0);
124
- @starting-style {
125
- transform: var(--np-navigation-drawer-start, translateX(-100%));
117
+ }
118
+
119
+ @media (prefers-reduced-motion: no-preference) {
120
+ .np-navigation-drawer-container[popover] .np-navigation-wrapper {
121
+ transition: transform var(--np-motion-standard-slow-spatial);
122
+ }
123
+
124
+ .np-navigation-drawer-container[popover] {
125
+ transition:
126
+ overlay var(--np-motion-standard-slow-spatial) allow-discrete,
127
+ display var(--np-motion-standard-slow-spatial) allow-discrete;
128
+ }
129
+
130
+ .np-navigation-drawer-container:popover-open .np-navigation-wrapper {
131
+ @starting-style {
132
+ transform: var(--np-navigation-drawer-start, translateX(-100%));
133
+ }
126
134
  }
127
135
  }
128
136
  .np-navigation-drawer {
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { on } from 'svelte/events'
3
+ import { MediaQuery } from 'svelte/reactivity'
3
4
  import type { RippleProps } from './types.js'
4
5
 
5
6
  let {
@@ -22,6 +23,8 @@
22
23
  const EASING_STANDARD = 'cubic-bezier(0.2, 0, 0, 1)'
23
24
  const TOUCH_DELAY_MS = 150
24
25
 
26
+ const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)', false)
27
+
25
28
  let rippleSize = $state('')
26
29
  let rippleScale = $state('')
27
30
  let initialSize = $state(0)
@@ -128,7 +131,7 @@
128
131
  },
129
132
  {
130
133
  pseudoElement: PRESS_PSEUDO,
131
- duration: PRESS_GROW_MS,
134
+ duration: reducedMotion.current ? 0 : PRESS_GROW_MS,
132
135
  easing: EASING_STANDARD,
133
136
  fill: ANIMATION_FILL,
134
137
  },
@@ -104,6 +104,9 @@
104
104
  position: relative;
105
105
  font-size: 1rem;
106
106
  display: inline-block;
107
+ box-sizing: border-box;
108
+ min-width: var(--np-select-min-width, 210px);
109
+ max-width: var(--np-select-max-width, 100%);
107
110
  color: var(--np-color-on-surface-variant);
108
111
  }
109
112
 
@@ -131,14 +134,17 @@
131
134
  box-shadow: var(--np-elevation-2);
132
135
  border: none;
133
136
  opacity: 0;
137
+ scale: 0.8;
138
+ transform-origin: top center;
134
139
  transition:
135
- opacity var(--easing-fast),
140
+ opacity var(--np-motion-expressive-fast-effects),
136
141
  display 150ms allow-discrete,
137
142
  overlay 150ms allow-discrete;
138
143
  }
139
144
 
140
145
  select:open::picker(select) {
141
146
  opacity: 1;
147
+ scale: 1;
142
148
  @starting-style {
143
149
  opacity: 0;
144
150
  }
@@ -176,7 +182,8 @@
176
182
 
177
183
  .np-select-outline-start,
178
184
  .np-select-outline-notch,
179
- .np-select-outline-end {
185
+ .np-select-outline-end,
186
+ .np-select-filled {
180
187
  transition:
181
188
  border-color var(--easing-fast),
182
189
  border-width var(--easing-fast);
@@ -205,7 +212,6 @@
205
212
  pointer-events: none;
206
213
  inset-inline-end: 0.75rem;
207
214
  inset-block-start: 1rem;
208
- transition: rotate var(--easing-fast);
209
215
  }
210
216
 
211
217
  .np-select-container:has(select:open) .arrow {
@@ -253,9 +259,6 @@
253
259
  border-start-start-radius: var(--np-shape-corner-extra-small);
254
260
  border-start-end-radius: var(--np-shape-corner-extra-small);
255
261
  border-block-end: 1px solid var(--np-color-on-surface-variant);
256
- transition:
257
- border-color var(--easing-fast),
258
- border-width var(--easing-fast);
259
262
  }
260
263
 
261
264
  .np-select-container:focus-within .np-select-filled {
@@ -301,20 +304,10 @@
301
304
  transform: translateY(0.5rem);
302
305
  }
303
306
 
304
- .animate-label label {
305
- transition-property: font-size;
306
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
307
- transition-duration: 150ms;
308
- }
309
-
310
307
  .np-select-container:focus-within label {
311
308
  color: var(--np-color-primary);
312
309
  }
313
310
 
314
- .animate-label:not(.is-empty) .np-select-outline label {
315
- animation: slideUpOutline 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
316
- }
317
-
318
311
  @keyframes slideUpOutline {
319
312
  from {
320
313
  transform: translateY(1rem);
@@ -324,10 +317,6 @@
324
317
  }
325
318
  }
326
319
 
327
- .animate-label:not(.is-empty) .np-select-filled label {
328
- animation: slideUpFilled 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
329
- }
330
-
331
320
  @keyframes slideUpFilled {
332
321
  from {
333
322
  transform: translateY(1.25rem);
@@ -344,10 +333,6 @@
344
333
  position: absolute;
345
334
  }
346
335
 
347
- .is-empty.animate-label .np-select-outline label {
348
- animation: slideDownOutline 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
349
- }
350
-
351
336
  @keyframes slideDownOutline {
352
337
  from {
353
338
  transform: translateY(-1.5rem);
@@ -362,10 +347,6 @@
362
347
  transform: translateY(1.25rem);
363
348
  }
364
349
 
365
- .is-empty.animate-label .np-select-filled label {
366
- animation: slideDownFilled 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
367
- }
368
-
369
350
  @keyframes slideDownFilled {
370
351
  from {
371
352
  transform: translateY(0.5rem);
@@ -428,4 +409,47 @@
428
409
  .disabled .np-select-outline {
429
410
  border-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
430
411
  }
412
+
413
+ @media (prefers-reduced-motion: no-preference) {
414
+ ::picker(select) {
415
+ transition:
416
+ opacity var(--np-motion-expressive-fast-effects),
417
+ scale var(--np-motion-expressive-fast-spatial),
418
+ display 150ms allow-discrete,
419
+ overlay 150ms allow-discrete;
420
+ }
421
+
422
+ select:open::picker(select) {
423
+ @starting-style {
424
+ opacity: 0;
425
+ scale: 0.8;
426
+ }
427
+ }
428
+
429
+ .arrow {
430
+ transition: rotate var(--easing-fast);
431
+ }
432
+
433
+ .animate-label label {
434
+ transition-property: font-size;
435
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
436
+ transition-duration: 150ms;
437
+ }
438
+
439
+ .animate-label:not(.is-empty) .np-select-outline label {
440
+ animation: slideUpOutline 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
441
+ }
442
+
443
+ .animate-label:not(.is-empty) .np-select-filled label {
444
+ animation: slideUpFilled 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
445
+ }
446
+
447
+ .is-empty.animate-label .np-select-outline label {
448
+ animation: slideDownOutline 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
449
+ }
450
+
451
+ .is-empty.animate-label .np-select-filled label {
452
+ animation: slideDownFilled 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
453
+ }
454
+ }
431
455
  </style>
@@ -59,7 +59,10 @@
59
59
  outline-width: 3px;
60
60
  outline-offset: -3px;
61
61
  border-radius: var(--np-shape-corner-extra-small);
62
- animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
62
+
63
+ @media (prefers-reduced-motion: no-preference) {
64
+ animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
65
+ }
63
66
  }
64
67
  }
65
68
  </style>
@@ -808,7 +808,6 @@
808
808
 
809
809
  .icon .arrow {
810
810
  display: flex;
811
- transition: rotate 150ms cubic-bezier(0.2, 0, 0, 1);
812
811
  }
813
812
  .field.menu-open .icon .arrow {
814
813
  rotate: 180deg;
@@ -949,9 +948,7 @@
949
948
  inset-inline-start: var(--floating-label-left, 0);
950
949
  }
951
950
  .label {
952
- transition-property: all;
953
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
954
- transition-duration: 150ms;
951
+ transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
955
952
  box-sizing: border-box;
956
953
  color: var(--np-color-on-surface-variant);
957
954
  overflow: hidden;
@@ -1138,4 +1135,16 @@
1138
1135
  .disabled .outline-notch {
1139
1136
  opacity: 0.12;
1140
1137
  }
1138
+
1139
+ @media (prefers-reduced-motion: no-preference) {
1140
+ .icon .arrow {
1141
+ transition: rotate 150ms cubic-bezier(0.2, 0, 0, 1);
1142
+ }
1143
+
1144
+ .label {
1145
+ transition-property: all;
1146
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1147
+ transition-duration: 150ms;
1148
+ }
1149
+ }
1141
1150
  </style>
@@ -138,7 +138,12 @@
138
138
 
139
139
  .np-snackbar:popover-open {
140
140
  opacity: 1;
141
- animation: slideIn var(--np-motion-expressive-default-spatial);
141
+ }
142
+
143
+ @media (prefers-reduced-motion: no-preference) {
144
+ .np-snackbar:popover-open {
145
+ animation: slideIn var(--np-motion-expressive-default-spatial);
146
+ }
142
147
  }
143
148
 
144
149
  @keyframes slideIn {
@@ -148,10 +148,11 @@
148
148
  padding: 0 1rem;
149
149
  color: var(--np-color-on-surface-variant);
150
150
  height: 3rem;
151
- transition: color 0.3s cubic-bezier(0.33, 1, 0.68, 1);
151
+ transition: color var(--np-motion-expressive-fast-effects);
152
152
  }
153
153
  .np-tab-content-active {
154
154
  --_focus-bottom: 4px;
155
+ transition: color var(--np-motion-expressive-default-effects);
155
156
  }
156
157
  .np-tab-content-active.primary {
157
158
  color: var(--np-color-primary);
@@ -215,7 +216,6 @@
215
216
  inset-inline: 0;
216
217
  bottom: var(--_focus-bottom, 0);
217
218
  border-radius: var(--np-shape-corner-medium);
218
- transition: height 0.3s ease;
219
219
  pointer-events: none;
220
220
  }
221
221
  .np-tab:focus-visible {
@@ -227,6 +227,15 @@
227
227
  outline-color: var(--np-color-secondary);
228
228
  outline-width: 3px;
229
229
  outline-offset: -3px;
230
- animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
230
+ }
231
+
232
+ @media (prefers-reduced-motion: no-preference) {
233
+ .focus-area {
234
+ transition: height 0.3s ease;
235
+ }
236
+
237
+ .np-tab:focus-visible .focus-area {
238
+ animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
239
+ }
231
240
  }
232
241
  </style>
@@ -80,7 +80,10 @@
80
80
  border-start-start-radius: var(--np-indicator-radius, var(--np-shape-corner-full));
81
81
  border-start-end-radius: var(--np-indicator-radius, var(--np-shape-corner-full));
82
82
  position-anchor: --np-tab-indicator;
83
- transition: var(--np-motion-expressive-fast-spatial);
83
+
84
+ @media (prefers-reduced-motion: no-preference) {
85
+ transition: var(--np-motion-expressive-default-spatial);
86
+ }
84
87
  }
85
88
  }
86
89
  </style>
@@ -18,6 +18,8 @@
18
18
  populated = false,
19
19
  inputElement = $bindable(),
20
20
  placeholder = ' ',
21
+ minLines = 1,
22
+ maxLines = 4,
21
23
  children,
22
24
  focused = $bindable(false),
23
25
  clientWidth = $bindable(),
@@ -31,11 +33,13 @@
31
33
 
32
34
  <label
33
35
  style={(variant === 'outlined'
34
- ? '--_label-text-color:var(--np-outlined-text-field-label-text-color);--top-space:1rem;--bottom-space:1rem;--floating-label-top:-0.5rem;--floating-label-inline-start:-2.25rem;--_focus-outline-width:3px;'
36
+ ? '--_label-text-color:var(--np-outlined-text-field-label-text-color);--top-space:1rem;--bottom-space:1rem;--floating-label-top:-0.5rem;--floating-label-inline-start:-2.25rem;'
35
37
  : !label?.length
36
38
  ? '--_label-text-color:var(--np-filled-text-field-label-text-color);--np-input-chip-outline-color:var(--np-color-outline);--top-space:1rem;--bottom-space:1rem; '
37
39
  : '--_label-text-color:var(--np-filled-text-field-label-text-color);--np-input-chip-outline-color:var(--np-color-outline); ' +
38
- (children ? '--top-space:2rem;--bottom-space:1rem;' : '')) + style}
40
+ (children ? '--top-space:2rem;--bottom-space:1rem;' : '')) +
41
+ `--_min-height:calc(${minLines} * 1lh);--_max-height:${`calc(${maxLines} * 1lh)`};` +
42
+ style}
39
43
  class={['text-field', attributes.class]}
40
44
  bind:this={element}
41
45
  bind:clientWidth
@@ -43,7 +47,6 @@
43
47
  >
44
48
  <div
45
49
  class="field"
46
- class:resizable={attributes.type === 'textarea'}
47
50
  class:no-label={!label?.length}
48
51
  class:with-start={start}
49
52
  class:with-end={end}
@@ -109,8 +112,7 @@
109
112
  bind:focused
110
113
  bind:value
111
114
  bind:this={inputElement}
112
- class="input"
113
- rows={attributes.rows || 2}></textarea>
115
+ class="input"></textarea>
114
116
  {:else}
115
117
  <div class="input-wrapper">
116
118
  {#if suffixText}
@@ -272,7 +274,6 @@
272
274
  }
273
275
  .text-field {
274
276
  display: inline-flex;
275
- resize: both;
276
277
  text-align: start;
277
278
  }
278
279
  :global(label) {
@@ -324,11 +325,6 @@
324
325
  background: var(--np-color-on-surface);
325
326
  opacity: 0.08;
326
327
  }
327
- .resizable .np-container > * {
328
- top: var(--_focus-outline-width, 3px);
329
- inset-inline-start: var(--_focus-outline-width, 0);
330
- }
331
-
332
328
  .content * {
333
329
  all: unset;
334
330
  color: currentColor;
@@ -418,6 +414,11 @@
418
414
  .content textarea {
419
415
  margin-top: var(--top-space, 1.5rem);
420
416
  margin-bottom: var(--bottom-space, 0.5rem);
417
+ field-sizing: content;
418
+ min-inline-size: 20ch;
419
+ max-inline-size: 100%;
420
+ min-height: var(--_min-height, 1lh);
421
+ max-height: var(--_max-height, 4lh);
421
422
  }
422
423
 
423
424
  .content .input-wrapper .input,
@@ -587,7 +588,7 @@
587
588
  inset-inline-start: var(--floating-label-inline-start, 0);
588
589
  }
589
590
  .label {
590
- transition: all var(--np-motion-expressive-fast-effects);
591
+ transition: color var(--np-motion-expressive-fast-effects);
591
592
  box-sizing: border-box;
592
593
  color: var(--np-color-on-surface-variant);
593
594
  overflow: hidden;
@@ -627,10 +628,6 @@
627
628
  .disabled .label:not(.np-hidden) {
628
629
  opacity: 0.38;
629
630
  }
630
- .resizable:not(.disabled) .np-container {
631
- resize: inherit;
632
- overflow: hidden;
633
- }
634
631
  .disabled.no-label .content,
635
632
  .disabled:has(input:-webkit-autofill) .content,
636
633
  .disabled:has(input:not(:placeholder-shown)) .content,
@@ -638,15 +635,6 @@
638
635
  .disabled.populated .content {
639
636
  opacity: 0.38;
640
637
  }
641
- .field,
642
- .container-overflow {
643
- resize: inherit;
644
- }
645
- .resizable .np-container {
646
- bottom: 3px;
647
- inset-inline-end: var(--_focus-outline-width, 0);
648
- clip-path: inset(3px 0 0 var(--_focus-outline-width));
649
- }
650
638
  .outline-start,
651
639
  .outline-end {
652
640
  border: inherit;
@@ -805,4 +793,10 @@
805
793
  .disabled .outline-notch {
806
794
  opacity: 0.12;
807
795
  }
796
+
797
+ @media (prefers-reduced-motion: no-preference) {
798
+ .label {
799
+ transition: all var(--np-motion-expressive-fast-effects);
800
+ }
801
+ }
808
802
  </style>
@@ -26,10 +26,14 @@ export interface InputFieldProps extends HTMLInputAttributes, FieldProps {
26
26
  }
27
27
  export interface TextAreaFieldProps extends HTMLTextareaAttributes, FieldProps {
28
28
  type: 'textarea';
29
+ minLines?: number;
30
+ maxLines?: number;
29
31
  }
30
32
  export interface TextFieldProps extends HTMLAttributes<TextFieldElement>, Omit<HTMLInputAttributes, keyof HTMLAttributes<HTMLInputElement> | 'type' | 'value' | 'defaultValue' | 'defaultvalue'>, Omit<HTMLTextareaAttributes, keyof HTMLAttributes<HTMLTextAreaElement> | 'value' | 'defaultValue' | 'defaultvalue'>, FieldProps {
31
33
  type?: TextFieldType | 'textarea';
32
34
  value?: string | number | null;
33
35
  defaultValue?: string | number | null;
36
+ minLines?: number;
37
+ maxLines?: number;
34
38
  }
35
39
  export {};
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ import type { RichTooltipProps } from './types.js'
3
+
4
+ let {
5
+ children,
6
+ subhead,
7
+ actions,
8
+ open = $bindable(),
9
+ element = $bindable(),
10
+ id,
11
+ ontoggle,
12
+ ...attributes
13
+ }: RichTooltipProps = $props()
14
+
15
+ const findControl = () =>
16
+ id ? (document.querySelector<HTMLElement>(`[popovertarget="${id}"]`) ?? undefined) : undefined
17
+
18
+ export const showPopover = () => {
19
+ if (!element || element.matches(':popover-open')) return
20
+ const source = findControl()
21
+ element.showPopover(source ? { source } : undefined)
22
+ }
23
+
24
+ export const hidePopover = () => {
25
+ element?.hidePopover()
26
+ }
27
+
28
+ $effect(() => {
29
+ if (!element) return
30
+ if (open === true) showPopover()
31
+ else if (open === false) hidePopover()
32
+ })
33
+ </script>
34
+
35
+ <div
36
+ {...attributes}
37
+ {id}
38
+ class={[
39
+ 'np-rich-tooltip',
40
+ subhead && 'np-rich-tooltip-with-subhead',
41
+ actions && 'np-rich-tooltip-with-actions',
42
+ attributes.class,
43
+ ]}
44
+ role="tooltip"
45
+ popover="auto"
46
+ bind:this={element}
47
+ ontoggle={(event) => {
48
+ let { newState } = event
49
+ open = newState === 'open'
50
+ ontoggle?.(event)
51
+ }}
52
+ >
53
+ {#if subhead}
54
+ <div class="np-rich-tooltip-subhead">{subhead}</div>
55
+ {/if}
56
+ {#if children}
57
+ <div class="np-rich-tooltip-text">{@render children()}</div>
58
+ {/if}
59
+ {#if actions}
60
+ <div class="np-rich-tooltip-actions">{@render actions()}</div>
61
+ {/if}
62
+ </div>
63
+
64
+ <style>
65
+ .np-rich-tooltip[popover] {
66
+ box-sizing: border-box;
67
+ width: max-content;
68
+ max-width: 20rem;
69
+ word-wrap: break-word;
70
+ overflow-wrap: break-word;
71
+ margin: 4px 0;
72
+ padding: 0.25rem 1rem;
73
+ border: none;
74
+ border-radius: var(--np-shape-corner-medium);
75
+ background: var(--np-color-surface-container);
76
+ color: var(--np-color-on-surface-variant);
77
+ box-shadow: var(--np-elevation-2);
78
+ font-size: 0.875rem;
79
+ line-height: 1.25rem;
80
+ justify-self: var(--np-rich-tooltip-justify-self, anchor-center);
81
+ position-area: var(--np-rich-tooltip-position-area, bottom);
82
+ position-try-fallbacks: flip-block;
83
+ }
84
+ .np-rich-tooltip-with-subhead[popover] {
85
+ padding-block: 0.75rem 1rem;
86
+ }
87
+ .np-rich-tooltip-with-actions[popover] {
88
+ padding-bottom: 0.5rem;
89
+ }
90
+ .np-rich-tooltip-subhead {
91
+ margin-bottom: 0.25rem;
92
+ font-weight: 500;
93
+ }
94
+
95
+ .np-rich-tooltip-actions {
96
+ display: flex;
97
+ flex-wrap: wrap;
98
+ align-items: center;
99
+ gap: 0.5rem;
100
+ min-height: 2.25rem;
101
+ margin-top: 1rem;
102
+ margin-inline-start: calc(-1 * var(--np-rich-tooltip-action-inset, 1rem));
103
+ padding-inline-end: var(--np-rich-tooltip-action-inset, 1rem);
104
+ }
105
+ .np-rich-tooltip:popover-open {
106
+ opacity: 1;
107
+ scale: 1;
108
+ }
109
+ @media (prefers-reduced-motion: no-preference) {
110
+ .np-rich-tooltip:popover-open {
111
+ animation:
112
+ fadeIn var(--np-motion-expressive-fast-effects),
113
+ scaleIn var(--np-motion-expressive-fast-spatial);
114
+ }
115
+ }
116
+
117
+ @keyframes fadeIn {
118
+ from {
119
+ opacity: 0;
120
+ }
121
+ }
122
+ @keyframes scaleIn {
123
+ from {
124
+ scale: 0.8;
125
+ }
126
+ }
127
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { RichTooltipProps } from './types.ts';
2
+ declare const RichTooltip: import("svelte").Component<RichTooltipProps, {
3
+ showPopover: () => void;
4
+ hidePopover: () => void;
5
+ }, "element" | "open">;
6
+ type RichTooltip = ReturnType<typeof RichTooltip>;
7
+ export default RichTooltip;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import '../internal/interest.css'
2
3
  import { on } from 'svelte/events'
3
4
  import { MediaQuery } from 'svelte/reactivity'
4
5
  import type { TooltipProps } from './types.js'
@@ -14,16 +15,33 @@
14
15
 
15
16
  let isTouch = new MediaQuery('(hover: none) and (pointer: coarse)', false)
16
17
 
17
- let attachAnchor = (el: HTMLDivElement) => {
18
- const anchor =
19
- (document.querySelector(`[aria-describedby="${id}"]`) as HTMLElement) ?? undefined
18
+ const isInterestInvoker = (anchor: Element) =>
19
+ 'interestForElement' in anchor &&
20
+ (!(anchor instanceof HTMLAnchorElement || anchor instanceof HTMLAreaElement) ||
21
+ anchor.hasAttribute('href'))
22
+
23
+ let native = $state(false)
24
+
25
+ const attachAnchor = (el: HTMLDivElement) => {
26
+ if (!id) return
27
+ const anchor = document.querySelector<HTMLElement>(`[aria-describedby="${id}"]`)
20
28
  if (!anchor) return
29
+ if (isInterestInvoker(anchor)) {
30
+ native = true
31
+ const wired = anchor.getAttribute('interestfor') === id
32
+ if (!wired) anchor.setAttribute('interestfor', id)
33
+ return () => {
34
+ native = false
35
+ if (!wired) anchor.removeAttribute('interestfor')
36
+ }
37
+ }
21
38
  const anchorName = anchor.style.getPropertyValue('anchor-name')
22
39
  const generatedId = anchorName || `--${uid}`
23
40
  el.style.setProperty('position-anchor', generatedId)
24
41
  if (!anchorName) {
25
42
  anchor.style.setProperty('anchor-name', generatedId)
26
43
  }
44
+ if (isTouch.current) return
27
45
  const mouseEnter = on(anchor, 'mouseenter', onAnchorEnter)
28
46
  const mouseLeave = on(anchor, 'mouseleave', onAnchorLeave)
29
47
  const focus = on(anchor, 'focus', onAnchorFocus)
@@ -93,6 +111,7 @@
93
111
  }
94
112
 
95
113
  $effect(() => {
114
+ if (native) return
96
115
  const off = on(document, 'keydown', onEscape)
97
116
  return () => {
98
117
  off()
@@ -101,25 +120,23 @@
101
120
  })
102
121
  </script>
103
122
 
104
- {#if !isTouch.current}
105
- <div
106
- {...attributes}
107
- {id}
108
- {@attach attachAnchor}
109
- class={['np-tooltip', attributes.class]}
110
- role="tooltip"
111
- popover="hint"
112
- bind:this={element}
113
- onmouseenter={onTooltipEnter}
114
- onmouseleave={onTooltipLeave}
115
- ontoggle={(event) => {
116
- let { newState } = event
117
- open = newState === 'open'
118
- }}
119
- >
120
- {#if children}{@render children()}{/if}
121
- </div>
122
- {/if}
123
+ <div
124
+ {...attributes}
125
+ {id}
126
+ {@attach attachAnchor}
127
+ class={['np-tooltip', attributes.class]}
128
+ role="tooltip"
129
+ popover="hint"
130
+ bind:this={element}
131
+ onmouseenter={native ? undefined : onTooltipEnter}
132
+ onmouseleave={native ? undefined : onTooltipLeave}
133
+ ontoggle={(event) => {
134
+ let { newState } = event
135
+ open = newState === 'open'
136
+ }}
137
+ >
138
+ {#if children}{@render children()}{/if}
139
+ </div>
123
140
 
124
141
  <style>
125
142
  .np-tooltip[popover] {
@@ -141,15 +158,24 @@
141
158
  }
142
159
  .np-tooltip:popover-open {
143
160
  opacity: 1;
144
- animation: scaleIn var(--np-motion-expressive-fast-spatial);
161
+ scale: 1;
162
+ }
163
+ @media (prefers-reduced-motion: no-preference) {
164
+ .np-tooltip:popover-open {
165
+ animation:
166
+ fadeIn var(--np-motion-expressive-fast-effects),
167
+ scaleIn var(--np-motion-expressive-fast-spatial);
168
+ }
145
169
  }
146
170
 
147
- @keyframes scaleIn {
171
+ @keyframes fadeIn {
148
172
  from {
149
- scale: 0;
173
+ opacity: 0;
150
174
  }
151
- to {
152
- scale: 1;
175
+ }
176
+ @keyframes scaleIn {
177
+ from {
178
+ scale: 0.8;
153
179
  }
154
180
  }
155
181
  </style>
@@ -1,3 +1,4 @@
1
+ import '../internal/interest.css';
1
2
  import type { TooltipProps } from './types.ts';
2
3
  declare const Tooltip: import("svelte").Component<TooltipProps, {}, "element" | "open">;
3
4
  type Tooltip = ReturnType<typeof Tooltip>;
@@ -1 +1,2 @@
1
+ export { default as RichTooltip } from './RichTooltip.svelte';
1
2
  export { default as Tooltip } from './Tooltip.svelte';
@@ -1 +1,2 @@
1
+ export { default as RichTooltip } from './RichTooltip.svelte';
1
2
  export { default as Tooltip } from './Tooltip.svelte';
@@ -1,5 +1,20 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLAttributes } from 'svelte/elements';
3
+ declare module 'svelte/elements' {
4
+ interface HTMLAnchorAttributes {
5
+ interestfor?: string | undefined | null;
6
+ }
7
+ interface HTMLButtonAttributes {
8
+ interestfor?: string | undefined | null;
9
+ }
10
+ }
2
11
  export interface TooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'role'> {
3
12
  element?: HTMLDivElement;
4
13
  open?: boolean;
5
14
  }
15
+ export interface RichTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'role' | 'popover'> {
16
+ subhead?: string;
17
+ actions?: Snippet;
18
+ element?: HTMLDivElement;
19
+ open?: boolean;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.40.3",
3
+ "version": "0.42.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {