noph-ui 0.41.0 → 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,10 +29,6 @@
29
29
  element?.hidePopover()
30
30
  }
31
31
 
32
- // CSS anchor positioning has no way to read which position-try fallback won, so the
33
- // scale pivot can't be expressed declaratively (see open spec request for @position-try
34
- // to set transform-origin: https://github.com/w3c/csswg-drafts/issues/11666). Until that
35
- // lands, mirror Compose's calculateTransformOrigin (Menu.kt) in JS instead.
36
32
  const calculateTransformOrigin = (anchorRect: DOMRect, menuRect: DOMRect) => {
37
33
  const pivot = (anchorStart: number, anchorEnd: number, menuStart: number, menuEnd: number) => {
38
34
  if (menuStart >= anchorEnd) return 0
@@ -136,9 +132,13 @@
136
132
  .np-menu-container:popover-open {
137
133
  opacity: 1;
138
134
  scale: 1;
139
- animation:
140
- fadeIn var(--np-motion-expressive-fast-effects),
141
- scaleIn var(--np-motion-expressive-fast-spatial);
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
+ }
142
142
  }
143
143
  @keyframes fadeIn {
144
144
  from {
@@ -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
 
@@ -135,7 +138,6 @@
135
138
  transform-origin: top center;
136
139
  transition:
137
140
  opacity var(--np-motion-expressive-fast-effects),
138
- scale var(--np-motion-expressive-fast-spatial),
139
141
  display 150ms allow-discrete,
140
142
  overlay 150ms allow-discrete;
141
143
  }
@@ -145,7 +147,6 @@
145
147
  scale: 1;
146
148
  @starting-style {
147
149
  opacity: 0;
148
- scale: 0.8;
149
150
  }
150
151
  }
151
152
 
@@ -181,7 +182,8 @@
181
182
 
182
183
  .np-select-outline-start,
183
184
  .np-select-outline-notch,
184
- .np-select-outline-end {
185
+ .np-select-outline-end,
186
+ .np-select-filled {
185
187
  transition:
186
188
  border-color var(--easing-fast),
187
189
  border-width var(--easing-fast);
@@ -210,7 +212,6 @@
210
212
  pointer-events: none;
211
213
  inset-inline-end: 0.75rem;
212
214
  inset-block-start: 1rem;
213
- transition: rotate var(--easing-fast);
214
215
  }
215
216
 
216
217
  .np-select-container:has(select:open) .arrow {
@@ -258,9 +259,6 @@
258
259
  border-start-start-radius: var(--np-shape-corner-extra-small);
259
260
  border-start-end-radius: var(--np-shape-corner-extra-small);
260
261
  border-block-end: 1px solid var(--np-color-on-surface-variant);
261
- transition:
262
- border-color var(--easing-fast),
263
- border-width var(--easing-fast);
264
262
  }
265
263
 
266
264
  .np-select-container:focus-within .np-select-filled {
@@ -306,20 +304,10 @@
306
304
  transform: translateY(0.5rem);
307
305
  }
308
306
 
309
- .animate-label label {
310
- transition-property: font-size;
311
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
312
- transition-duration: 150ms;
313
- }
314
-
315
307
  .np-select-container:focus-within label {
316
308
  color: var(--np-color-primary);
317
309
  }
318
310
 
319
- .animate-label:not(.is-empty) .np-select-outline label {
320
- animation: slideUpOutline 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
321
- }
322
-
323
311
  @keyframes slideUpOutline {
324
312
  from {
325
313
  transform: translateY(1rem);
@@ -329,10 +317,6 @@
329
317
  }
330
318
  }
331
319
 
332
- .animate-label:not(.is-empty) .np-select-filled label {
333
- animation: slideUpFilled 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
334
- }
335
-
336
320
  @keyframes slideUpFilled {
337
321
  from {
338
322
  transform: translateY(1.25rem);
@@ -349,10 +333,6 @@
349
333
  position: absolute;
350
334
  }
351
335
 
352
- .is-empty.animate-label .np-select-outline label {
353
- animation: slideDownOutline 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
354
- }
355
-
356
336
  @keyframes slideDownOutline {
357
337
  from {
358
338
  transform: translateY(-1.5rem);
@@ -367,10 +347,6 @@
367
347
  transform: translateY(1.25rem);
368
348
  }
369
349
 
370
- .is-empty.animate-label .np-select-filled label {
371
- animation: slideDownFilled 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
372
- }
373
-
374
350
  @keyframes slideDownFilled {
375
351
  from {
376
352
  transform: translateY(0.5rem);
@@ -433,4 +409,47 @@
433
409
  .disabled .np-select-outline {
434
410
  border-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
435
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
+ }
436
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 {
@@ -216,7 +216,6 @@
216
216
  inset-inline: 0;
217
217
  bottom: var(--_focus-bottom, 0);
218
218
  border-radius: var(--np-shape-corner-medium);
219
- transition: height 0.3s ease;
220
219
  pointer-events: none;
221
220
  }
222
221
  .np-tab:focus-visible {
@@ -228,6 +227,15 @@
228
227
  outline-color: var(--np-color-secondary);
229
228
  outline-width: 3px;
230
229
  outline-offset: -3px;
231
- 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
+ }
232
240
  }
233
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-default-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>
@@ -588,7 +588,7 @@
588
588
  inset-inline-start: var(--floating-label-inline-start, 0);
589
589
  }
590
590
  .label {
591
- transition: all var(--np-motion-expressive-fast-effects);
591
+ transition: color var(--np-motion-expressive-fast-effects);
592
592
  box-sizing: border-box;
593
593
  color: var(--np-color-on-surface-variant);
594
594
  overflow: hidden;
@@ -793,4 +793,10 @@
793
793
  .disabled .outline-notch {
794
794
  opacity: 0.12;
795
795
  }
796
+
797
+ @media (prefers-reduced-motion: no-preference) {
798
+ .label {
799
+ transition: all var(--np-motion-expressive-fast-effects);
800
+ }
801
+ }
796
802
  </style>
@@ -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] {
@@ -142,9 +159,13 @@
142
159
  .np-tooltip:popover-open {
143
160
  opacity: 1;
144
161
  scale: 1;
145
- animation:
146
- fadeIn var(--np-motion-expressive-fast-effects),
147
- scaleIn var(--np-motion-expressive-fast-spatial);
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
+ }
148
169
  }
149
170
 
150
171
  @keyframes fadeIn {
@@ -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.41.0",
3
+ "version": "0.42.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {