noph-ui 0.37.0 → 0.39.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.
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Noph UI
2
2
 
3
- Noph UI is a modern, powerful UI library for Svelte 5, fully aligned with the Material 3
4
- guidelines. Build stunning, consistent user interfaces with the efficiency and flexibility of
5
- Svelte and Google’s Material Design framework.
3
+ Noph UI is a modern, powerful UI library for Svelte, fully aligned with the Material 3 Expressive guidelines. Build stunning, consistent user interfaces with the efficiency and flexibility of Svelte and Google’s Material Design framework.
6
4
 
7
5
  ## Install
8
6
 
@@ -50,33 +48,43 @@ body {
50
48
  }
51
49
  ```
52
50
 
53
- ## Roadmap
54
-
55
- Beta
56
-
57
- - Auto complete
58
- - Badges
59
- - Buttons
60
- - Cards
61
- - Checkbox
62
- - Chips
63
- - DatePicker
64
- - Dialogs
65
- - Divider
66
- - Icon buttons
67
- - Lists
68
- - Loading indicator
69
- - Menus
70
- - Navigation Drawer
71
- - Navigation Rail
72
- - Progress indicators
73
- - Radio
74
- - Ripple
75
- - Segmented buttons
76
- - Select
77
- - Slider
78
- - Snackbar
79
- - Switch
80
- - Tabs
81
- - Text fields
82
- - Tooltips
51
+ ## Components
52
+
53
+ Live demos and usage examples for every component are available on [noph.dev](https://noph.dev).
54
+
55
+ **Actions**
56
+ [Buttons](https://noph.dev/components/button) ·
57
+ [Icon buttons](https://noph.dev/components/icon-button) ·
58
+ [Segmented buttons](https://noph.dev/components/segmented-button) ·
59
+ [Ripple](https://noph.dev/components/ripple)
60
+
61
+ **Inputs & selection**
62
+ [Autocomplete](https://noph.dev/components/autocomplete) ·
63
+ [Checkbox](https://noph.dev/components/checkbox) ·
64
+ [Chips](https://noph.dev/components/chip) ·
65
+ [DatePicker](https://noph.dev/components/date-picker) ·
66
+ [DateTimePicker](https://noph.dev/components/date-time-picker) ·
67
+ [Radio](https://noph.dev/components/radio) ·
68
+ [Select](https://noph.dev/components/select) ·
69
+ [Slider](https://noph.dev/components/slider) ·
70
+ [Switch](https://noph.dev/components/switch) ·
71
+ [Text fields](https://noph.dev/components/text-field)
72
+
73
+ **Navigation**
74
+ [Menus](https://noph.dev/components/menu) ·
75
+ [Navigation Drawer](https://noph.dev/components/navigation-drawer) ·
76
+ [Navigation Rail](https://noph.dev/components/navigation-rail) ·
77
+ [Tabs](https://noph.dev/components/tabs)
78
+
79
+ **Communication**
80
+ [Badges](https://noph.dev/components/badge) ·
81
+ [Dialogs](https://noph.dev/components/dialog) ·
82
+ [Loading indicator](https://noph.dev/components/loading-indicator) ·
83
+ [Progress indicators](https://noph.dev/components/progress) ·
84
+ [Snackbar](https://noph.dev/components/snackbar) ·
85
+ [Tooltips](https://noph.dev/components/tooltip)
86
+
87
+ **Containment**
88
+ [Cards](https://noph.dev/components/card) ·
89
+ [Divider](https://noph.dev/components/divider) ·
90
+ [Lists](https://noph.dev/components/list)
@@ -206,6 +206,7 @@
206
206
  style="position-anchor:--{uid};{widthProp}:{clientWidth}px"
207
207
  role="listbox"
208
208
  class={[!displayOptions.length && 'np-auto-complete-empty']}
209
+ coverAnchor={false}
209
210
  --np-menu-justify-self="none"
210
211
  --np-menu-position-area="bottom span-right"
211
212
  --np-menu-margin="2px 0"
@@ -475,13 +475,9 @@
475
475
 
476
476
  .np-calendar-cell.range-start::before {
477
477
  inset-inline-start: 50%;
478
- border-start-start-radius: var(--np-shape-corner-full);
479
- border-end-start-radius: var(--np-shape-corner-full);
480
478
  }
481
479
 
482
480
  .np-calendar-cell.range-end::before {
483
481
  inset-inline-end: 50%;
484
- border-start-end-radius: var(--np-shape-corner-full);
485
- border-end-end-radius: var(--np-shape-corner-full);
486
482
  }
487
483
  </style>
@@ -173,6 +173,15 @@
173
173
 
174
174
  .np-date-picker-stepper.hidden {
175
175
  visibility: hidden;
176
+ opacity: 0;
177
+ }
178
+
179
+ @media (prefers-reduced-motion: no-preference) {
180
+ .np-date-picker-stepper {
181
+ transition:
182
+ opacity var(--np-motion-expressive-default-effects),
183
+ visibility var(--np-motion-expressive-default-effects);
184
+ }
176
185
  }
177
186
 
178
187
  .np-date-picker-stepper {
@@ -172,14 +172,6 @@
172
172
  bind:element
173
173
  aria-label={title}
174
174
  class={['np-date-range-picker', attributes.class]}
175
- --np-dialog-container-width="100%"
176
- --np-dialog-container-min-width="0"
177
- --np-dialog-inset="0"
178
- --np-dialog-padding="0"
179
- --np-dialog-max-height="100dvh"
180
- --np-dialog-elevation="none"
181
- --np-dialog-container-color="var(--np-date-range-picker-container-color, var(--np-color-surface))"
182
- --np-dialog-container-shape="var(--np-date-range-picker-container-shape, var(--np-shape-corner-none))"
183
175
  ontoggle={(event) => {
184
176
  const nowOpen = event.newState === 'open'
185
177
  open = nowOpen
@@ -198,7 +190,7 @@
198
190
  {#if open}
199
191
  <div class="np-date-range-picker-content">
200
192
  <div class="np-date-range-picker-header">
201
- <div class="np-date-range-picker-actions">
193
+ <div class="np-date-range-picker-top-bar">
202
194
  <IconButton type="button" aria-label={cancelLabel} onclick={cancel}>
203
195
  <CloseIcon />
204
196
  </IconButton>
@@ -206,11 +198,14 @@
206
198
  {confirmLabel}
207
199
  </Button>
208
200
  </div>
209
- <h2 class="np-date-range-picker-headline">
210
- <span class={[!startDate && 'placeholder']}>{headlineStart}</span>
211
- <span aria-hidden="true">–</span>
212
- <span class={[!endDate && 'placeholder']}>{headlineEnd}</span>
213
- </h2>
201
+ <div class="np-date-range-picker-header-text">
202
+ <span class="np-date-range-picker-title">{title}</span>
203
+ <h2 class="np-date-range-picker-headline">
204
+ <span class={[!startDate && 'placeholder']}>{headlineStart}</span>
205
+ <span aria-hidden="true">–</span>
206
+ <span class={[!endDate && 'placeholder']}>{headlineEnd}</span>
207
+ </h2>
208
+ </div>
214
209
  </div>
215
210
  <Divider />
216
211
 
@@ -251,9 +246,35 @@
251
246
  </div>
252
247
  </div>
253
248
  {/if}
249
+
250
+ {#snippet actions()}
251
+ {#if open}
252
+ <Button type="button" variant="text" onclick={cancel}>{cancelLabel}</Button>
253
+ <Button type="button" variant="text" disabled={!canConfirm} onclick={confirm}>
254
+ {confirmLabel}
255
+ </Button>
256
+ {/if}
257
+ {/snippet}
254
258
  </Dialog>
255
259
 
256
260
  <style>
261
+ :global(.np-date-range-picker) {
262
+ --np-dialog-container-width: 100%;
263
+ --np-dialog-container-min-width: 0;
264
+ --np-dialog-inset: 0;
265
+ --np-dialog-padding: 0;
266
+ --np-dialog-max-height: 100dvh;
267
+ --np-dialog-elevation: none;
268
+ --np-dialog-container-color: var(
269
+ --np-date-range-picker-container-color,
270
+ var(--np-color-surface)
271
+ );
272
+ --np-dialog-container-shape: var(
273
+ --np-date-range-picker-container-shape,
274
+ var(--np-shape-corner-none)
275
+ );
276
+ }
277
+
257
278
  :global(.np-date-range-picker .np-dialog) {
258
279
  height: 100dvh;
259
280
  }
@@ -262,6 +283,10 @@
262
283
  overflow: hidden;
263
284
  }
264
285
 
286
+ :global(.np-date-range-picker .np-dialog .np-dialog-actions) {
287
+ display: none;
288
+ }
289
+
265
290
  .np-date-range-picker-content {
266
291
  display: flex;
267
292
  flex-direction: column;
@@ -272,13 +297,18 @@
272
297
  .np-date-range-picker-header,
273
298
  .np-date-range-picker-weekdays,
274
299
  .np-date-range-picker-months {
300
+ box-sizing: border-box;
275
301
  max-width: var(--np-date-range-picker-content-width, 25.5rem);
276
302
  margin-inline: auto;
277
303
  width: 100%;
278
304
  }
279
305
 
306
+ .np-date-range-picker-header,
307
+ .np-date-range-picker-weekdays {
308
+ flex: none;
309
+ }
310
+
280
311
  .np-date-range-picker-header {
281
- box-sizing: border-box;
282
312
  min-height: 8rem;
283
313
  display: flex;
284
314
  flex-direction: column;
@@ -288,12 +318,24 @@
288
318
  color: var(--np-color-on-surface-variant);
289
319
  }
290
320
 
291
- .np-date-range-picker-actions {
321
+ .np-date-range-picker-top-bar {
292
322
  display: flex;
293
323
  align-items: center;
294
324
  justify-content: space-between;
295
325
  }
296
326
 
327
+ .np-date-range-picker-header-text {
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: 0.25rem;
331
+ }
332
+
333
+ .np-date-range-picker-title {
334
+ font-size: 0.875rem;
335
+ line-height: 1.25rem;
336
+ font-weight: 500;
337
+ }
338
+
297
339
  .np-date-range-picker-headline {
298
340
  margin: 0;
299
341
  display: flex;
@@ -314,10 +356,6 @@
314
356
  align-items: center;
315
357
  height: 3rem;
316
358
  padding-inline: 0.75rem;
317
- max-width: 25.5rem;
318
- margin-inline: auto;
319
- width: 100%;
320
- box-sizing: border-box;
321
359
  font-size: 1rem;
322
360
  line-height: 1.5rem;
323
361
  color: var(--np-color-on-surface);
@@ -330,10 +368,6 @@
330
368
  overflow-y: auto;
331
369
  overscroll-behavior: contain;
332
370
  padding-inline: 0.75rem;
333
- max-width: 25.5rem;
334
- margin-inline: auto;
335
- width: 100%;
336
- box-sizing: border-box;
337
371
  scrollbar-color: var(--np-color-on-surface-variant) transparent;
338
372
  scrollbar-width: thin;
339
373
  }
@@ -348,4 +382,50 @@
348
382
  font-weight: 500;
349
383
  color: var(--np-date-picker-range-month-subhead-color, var(--np-color-on-surface-variant));
350
384
  }
385
+
386
+ @media (min-width: 37.5rem) {
387
+ :global(.np-date-range-picker) {
388
+ --np-dialog-container-width: fit-content;
389
+ --np-dialog-inset: 2rem 1rem;
390
+ --np-dialog-max-height: calc(100dvh - 3rem);
391
+ --np-dialog-elevation: var(--np-elevation-3);
392
+ --np-dialog-container-color: var(
393
+ --np-date-range-picker-container-color,
394
+ var(--np-color-surface-container-high)
395
+ );
396
+ --np-dialog-container-shape: var(
397
+ --np-date-range-picker-container-shape,
398
+ var(--np-shape-corner-extra-large)
399
+ );
400
+ }
401
+
402
+ :global(.np-date-range-picker .np-dialog) {
403
+ height: auto;
404
+ }
405
+
406
+ :global(.np-date-range-picker .np-dialog .np-dialog-actions) {
407
+ display: flex;
408
+ margin-top: 0;
409
+ padding: 0.5rem 0.75rem 0.75rem;
410
+ }
411
+
412
+ .np-date-range-picker-content {
413
+ width: var(--np-date-range-picker-content-width, 22.5rem);
414
+ max-width: 100%;
415
+ }
416
+
417
+ .np-date-range-picker-top-bar {
418
+ display: none;
419
+ }
420
+
421
+ .np-date-range-picker-header {
422
+ min-height: 7.5rem;
423
+ justify-content: flex-end;
424
+ padding: 1rem 0.75rem 0.75rem 1.5rem;
425
+ }
426
+
427
+ .np-date-range-picker-months {
428
+ max-height: var(--np-date-range-picker-months-max-height, 20rem);
429
+ }
430
+ }
351
431
  </style>
@@ -28,7 +28,6 @@
28
28
  toISODate,
29
29
  today as getToday,
30
30
  } from './dateUtils.js'
31
- import { afterTwoFrames } from './enterAnimation.js'
32
31
  import type { DockedDatePickerProps, ISODate } from './types.js'
33
32
 
34
33
  let {
@@ -76,6 +75,7 @@
76
75
  let anchorElement = $state<HTMLDivElement>()
77
76
  let menuElement = $state<HTMLDivElement>()
78
77
  let mode = $state<'days' | 'months' | 'years'>('days')
78
+ let listMode = $state<'months' | 'years' | undefined>(undefined)
79
79
  let pending = $state<ISODate | undefined>(undefined)
80
80
  let focusedDay = $state<Date | undefined>(undefined)
81
81
  let inputElement = $state<HTMLInputElement | HTMLTextAreaElement>()
@@ -105,15 +105,16 @@
105
105
  let shortMonthNames = $derived(getMonthNames(locale, 'short'))
106
106
  let rowCount = $derived(getWeekRowCount(monthDate, week))
107
107
 
108
- let viewEnter = $state(false)
109
- let cancelViewEnter: (() => void) | undefined
110
-
111
108
  const setMode = (next: 'days' | 'months' | 'years') => {
112
109
  if (mode === next) return
113
110
  mode = next
114
- cancelViewEnter?.()
115
- viewEnter = true
116
- cancelViewEnter = afterTwoFrames(() => (viewEnter = false))
111
+ if (next !== 'days') listMode = next
112
+ }
113
+
114
+ const leaveList = async () => {
115
+ setMode('days')
116
+ await tick()
117
+ focusCalendar()
117
118
  }
118
119
 
119
120
  const formatValue = () => {
@@ -254,14 +255,14 @@
254
255
 
255
256
  const chooseMonth = (month: number) => {
256
257
  setMonth(new Date(monthDate.getFullYear(), month, 1))
257
- setMode('days')
258
+ leaveList()
258
259
  }
259
260
 
260
261
  const chooseYear = (year: number) => {
261
262
  // A year is offered as soon as any of its days is in range, so keeping the month the list was
262
263
  // opened from can land on a month where nothing at all is selectable.
263
264
  setMonth(new Date(year, clampMonthInYear(year, monthDate.getMonth(), minDate, maxDate), 1))
264
- setMode('days')
265
+ leaveList()
265
266
  }
266
267
  </script>
267
268
 
@@ -319,6 +320,7 @@
319
320
  style={`position-anchor:--${uid};`}
320
321
  --np-menu-justify-self="none"
321
322
  --np-menu-position-area="bottom span-right"
323
+ --np-menu-over-anchor-position-area="span-all span-right"
322
324
  --np-menu-margin="0"
323
325
  --np-menu-container-color="var(--np-docked-date-picker-container-color, var(--np-color-surface-container-high))"
324
326
  --np-menu-container-shape="var(--np-docked-date-picker-container-shape, var(--np-shape-corner-large))"
@@ -333,16 +335,14 @@
333
335
  }
334
336
  if (newState === 'closed') {
335
337
  setMode('days')
338
+ listMode = undefined
336
339
  focusedDay = undefined
337
340
  displayMonth = monthBeforeOpen
338
341
  }
339
342
  }}
340
343
  >
341
344
  {#if open}
342
- <div
343
- class={['np-docked-date-picker-container', mode !== 'days' && 'menu']}
344
- style:--np-calendar-rows={rowCount}
345
- >
345
+ <div class="np-docked-date-picker-container" style:--np-calendar-rows={rowCount}>
346
346
  <CalendarHeader
347
347
  {mode}
348
348
  monthLabel={shortMonthNames[monthDate.getMonth()]}
@@ -367,8 +367,8 @@
367
367
  ontoggleyears={() => setMode(mode === 'years' ? 'days' : 'years')}
368
368
  />
369
369
 
370
- {#if mode === 'days'}
371
- <div class={['np-docked-date-picker-body', viewEnter && 'entering']}>
370
+ <div class="np-docked-date-picker-views">
371
+ <div class="np-docked-date-picker-body" inert={mode !== 'days'}>
372
372
  <Calendar
373
373
  month={monthDate}
374
374
  selected={pendingDate}
@@ -395,33 +395,37 @@
395
395
  }}
396
396
  />
397
397
  </div>
398
- <div class="np-docked-date-picker-actions">
399
- <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button
400
- >
401
- <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
402
- </div>
403
- {:else}
404
- <div class={['np-docked-date-picker-menu-view', viewEnter && 'entering']}>
405
- <Divider --np-divider-color="var(--np-color-outline-variant)" />
406
- {#if mode === 'months'}
407
- <SelectionList
408
- id="{uid}-months"
409
- aria-label={selectMonthLabel}
410
- options={monthOptions}
411
- value={monthDate.getMonth()}
412
- onselect={chooseMonth}
413
- />
414
- {:else}
415
- <SelectionList
416
- id="{uid}-years"
417
- aria-label={selectYearLabel}
418
- options={yearOptions}
419
- value={monthDate.getFullYear()}
420
- onselect={chooseYear}
421
- />
422
- {/if}
423
- </div>
424
- {/if}
398
+
399
+ {#if listMode}
400
+ <div class={['np-docked-date-picker-menu-view', mode !== 'days' && 'open']}>
401
+ <div class="np-docked-date-picker-menu-shade">
402
+ <Divider --np-divider-color="var(--np-color-outline-variant)" />
403
+ {#if listMode === 'months'}
404
+ <SelectionList
405
+ id="{uid}-months"
406
+ aria-label={selectMonthLabel}
407
+ options={monthOptions}
408
+ value={monthDate.getMonth()}
409
+ onselect={chooseMonth}
410
+ />
411
+ {:else}
412
+ <SelectionList
413
+ id="{uid}-years"
414
+ aria-label={selectYearLabel}
415
+ options={yearOptions}
416
+ value={monthDate.getFullYear()}
417
+ onselect={chooseYear}
418
+ />
419
+ {/if}
420
+ </div>
421
+ </div>
422
+ {/if}
423
+ </div>
424
+
425
+ <div class="np-docked-date-picker-actions">
426
+ <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button>
427
+ <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
428
+ </div>
425
429
  </div>
426
430
  {/if}
427
431
  </Menu>
@@ -456,8 +460,12 @@
456
460
  margin-inline: 0.75rem;
457
461
  }
458
462
 
463
+ .np-docked-date-picker-views {
464
+ position: relative;
465
+ margin-top: 1.875rem;
466
+ }
467
+
459
468
  .np-docked-date-picker-body {
460
- padding-top: 1.875rem;
461
469
  padding-inline: 0.75rem;
462
470
  }
463
471
 
@@ -476,45 +484,61 @@
476
484
  height: 2.25rem;
477
485
  }
478
486
 
479
- .np-docked-date-picker-container.menu {
480
- padding-bottom: 1.25rem;
481
- }
482
-
483
487
  .np-docked-date-picker-menu-view {
484
- display: flex;
485
- flex-direction: column;
488
+ position: absolute;
489
+ inset: 0;
486
490
  overflow: hidden;
487
- margin-top: 1.875rem;
488
- height: calc(2.5rem + var(--np-calendar-rows) * 3rem);
491
+ visibility: hidden;
489
492
  }
490
493
 
491
- .np-docked-date-picker-menu-view :global(.np-date-picker-selection-list) {
492
- flex: 1;
493
- min-height: 0;
494
+ .np-docked-date-picker-menu-view.open {
495
+ visibility: visible;
494
496
  }
495
497
 
496
- .np-docked-date-picker-menu-view.entering {
497
- height: 0;
498
- margin-top: 0;
498
+ .np-docked-date-picker-menu-shade {
499
+ display: flex;
500
+ flex-direction: column;
501
+ height: 100%;
502
+ translate: 0 -100%;
499
503
  opacity: 0.6;
500
- transition: none;
504
+ background-color: var(
505
+ --np-docked-date-picker-container-color,
506
+ var(--np-color-surface-container-high)
507
+ );
501
508
  }
502
509
 
503
- .np-docked-date-picker-body.entering {
504
- opacity: 0.6;
505
- transition: none;
510
+ .np-docked-date-picker-menu-view.open .np-docked-date-picker-menu-shade {
511
+ translate: 0 0;
512
+ opacity: 1;
513
+ }
514
+
515
+ .np-docked-date-picker-menu-view :global(.np-date-picker-selection-list) {
516
+ flex: 1;
517
+ min-height: 0;
506
518
  }
507
519
 
508
520
  @media (prefers-reduced-motion: no-preference) {
509
521
  .np-docked-date-picker-menu-view {
522
+ transition: visibility var(--np-motion-expressive-default-effects);
523
+ }
524
+
525
+ .np-docked-date-picker-menu-shade {
510
526
  transition:
511
- height var(--np-motion-expressive-default-effects),
512
- margin-top var(--np-motion-expressive-default-effects),
527
+ translate var(--np-motion-expressive-default-effects),
528
+ opacity var(--np-motion-expressive-fast-effects);
529
+ }
530
+
531
+ .np-docked-date-picker-menu-view.open .np-docked-date-picker-menu-shade {
532
+ transition:
533
+ translate var(--np-motion-expressive-default-effects),
513
534
  opacity var(--np-motion-expressive-default-effects);
514
535
  }
515
536
 
516
- .np-docked-date-picker-body {
517
- transition: opacity var(--np-motion-expressive-default-effects);
537
+ @starting-style {
538
+ .np-docked-date-picker-menu-view.open .np-docked-date-picker-menu-shade {
539
+ translate: 0 -100%;
540
+ opacity: 0.6;
541
+ }
518
542
  }
519
543
  }
520
544