noph-ui 0.40.0 → 0.40.2

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.
@@ -193,6 +193,7 @@
193
193
  display: grid;
194
194
  position: relative;
195
195
  align-self: start;
196
+ flex: 1 0 auto;
196
197
  min-width: calc(100% - 32px);
197
198
  margin: 1rem;
198
199
  grid-gap: 0.5rem;
@@ -215,7 +216,8 @@
215
216
 
216
217
  .np-card-action {
217
218
  display: flex;
218
- margin-top: 1.5rem;
219
+ margin-top: auto;
220
+ padding-top: 1.5rem;
219
221
  justify-content: flex-end;
220
222
  }
221
223
 
@@ -37,6 +37,7 @@
37
37
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- absorbed on purpose
38
38
  type,
39
39
  'aria-invalid': ariaInvalid,
40
+ style,
40
41
  displayMonth = $bindable(),
41
42
  open = $bindable(false),
42
43
  element = $bindable(),
@@ -70,6 +71,8 @@
70
71
  invalidDateMessage = 'Enter a valid date.',
71
72
  selectedDateLabel = 'selected',
72
73
  onchange,
74
+ oninput,
75
+ onblur,
73
76
  ...attributes
74
77
  }: DockedDatePickerProps = $props()
75
78
 
@@ -77,7 +80,6 @@
77
80
 
78
81
  const uid = $props.id()
79
82
 
80
- let anchorElement = $state<HTMLDivElement>()
81
83
  let menuElement = $state<HTMLDivElement>()
82
84
  let mode = $state<'days' | 'months' | 'years'>('days')
83
85
  let listMode = $state<'months' | 'years' | undefined>(undefined)
@@ -224,7 +226,7 @@
224
226
  const closePicker = (restoreFocus = true) => {
225
227
  menuElement?.hidePopover()
226
228
  if (restoreFocus) {
227
- anchorElement?.querySelector<HTMLElement>('input')?.focus()
229
+ element?.querySelector<HTMLElement>('input')?.focus()
228
230
  }
229
231
  }
230
232
 
@@ -290,185 +292,181 @@
290
292
  }
291
293
  </script>
292
294
 
293
- <div {...attributes} bind:this={element} class={['np-docked-date-picker', attributes.class]}>
294
- <div class="np-docked-date-picker-anchor" bind:this={anchorElement} style:anchor-name="--{uid}">
295
- <TextField
296
- {label}
297
- {variant}
298
- {issues}
295
+ <TextField
296
+ {...attributes}
297
+ class={['np-docked-date-picker', attributes.class]}
298
+ style={`anchor-name:--${uid};${style ?? ''}`}
299
+ {label}
300
+ {variant}
301
+ {issues}
302
+ {disabled}
303
+ {readonly}
304
+ {required}
305
+ {noAsterisk}
306
+ {autocomplete}
307
+ supportingText={supportingText ?? pattern}
308
+ bind:value={text}
309
+ bind:element
310
+ bind:inputElement
311
+ aria-invalid={inputInvalid || issues?.length || ariaInvalid === true || ariaInvalid === 'true'
312
+ ? 'true'
313
+ : undefined}
314
+ oninput={(event) => {
315
+ handleInput(event)
316
+ oninput?.(event)
317
+ }}
318
+ onblur={(event) => {
319
+ handleBlur()
320
+ onblur?.(event)
321
+ }}
322
+ >
323
+ {#snippet end()}
324
+ <IconButton
325
+ type="button"
326
+ aria-label={openCalendarLabel}
327
+ aria-expanded={open}
328
+ aria-haspopup="dialog"
329
+ aria-controls={open ? `${uid}-calendar` : undefined}
299
330
  {disabled}
300
- {readonly}
301
- {required}
302
- {noAsterisk}
303
- {autocomplete}
304
- supportingText={supportingText ?? pattern}
305
- bind:value={text}
306
- bind:inputElement
307
- aria-invalid={inputInvalid || issues?.length || ariaInvalid === true || ariaInvalid === 'true'
308
- ? 'true'
309
- : undefined}
310
- oninput={handleInput}
311
- onblur={handleBlur}
331
+ onclick={() => {
332
+ if (open) closePicker()
333
+ else openPicker()
334
+ }}
312
335
  >
313
- {#snippet end()}
314
- <IconButton
315
- type="button"
316
- aria-label={openCalendarLabel}
317
- aria-expanded={open}
318
- aria-haspopup="dialog"
319
- aria-controls={open ? `${uid}-calendar` : undefined}
320
- {disabled}
321
- onclick={() => {
322
- if (open) closePicker()
323
- else openPicker()
324
- }}
325
- >
326
- <CalendarToday />
327
- </IconButton>
328
- {/snippet}
329
- </TextField>
330
- <input
331
- bind:this={valueInput}
332
- class="np-docked-date-picker-value"
333
- type="hidden"
334
- value={current ?? ''}
335
- {name}
336
- {form}
337
- {disabled}
338
- />
339
- </div>
340
-
341
- <Menu
342
- id="{uid}-calendar"
343
- role="dialog"
344
- aria-label={label}
345
- aria-modal="false"
346
- class="np-docked-date-picker-menu"
347
- style={`position-anchor:--${uid};`}
348
- --np-menu-justify-self="none"
349
- --np-menu-position-area="bottom span-right"
350
- --np-menu-over-anchor-position-area="span-all span-right"
351
- --np-menu-margin="0"
352
- --np-menu-container-color="var(--np-docked-date-picker-container-color, var(--np-color-surface-container-high))"
353
- --np-menu-container-shape="var(--np-docked-date-picker-container-shape, var(--np-shape-corner-large))"
354
- anchor={anchorElement}
355
- bind:element={menuElement}
356
- bind:open
357
- ontoggle={({ newState }) => {
358
- if (newState === 'open') {
359
- monthBeforeOpen = displayMonth
360
- pending = current
361
- focusCalendarWhenReady()
362
- }
363
- if (newState === 'closed') {
364
- setMode('days')
365
- listMode = undefined
366
- focusedDay = undefined
367
- displayMonth = monthBeforeOpen
368
- }
369
- }}
370
- >
371
- {#if open}
372
- <div class="np-docked-date-picker-container" style:--np-calendar-rows={rowCount}>
373
- <CalendarHeader
374
- {mode}
375
- monthLabel={shortMonthNames[monthDate.getMonth()]}
376
- yearLabel={`${monthDate.getFullYear()}`}
377
- {canPreviousMonth}
378
- {canNextMonth}
379
- {canPreviousYear}
380
- {canNextYear}
381
- {previousMonthLabel}
382
- {nextMonthLabel}
383
- {previousYearLabel}
384
- {nextYearLabel}
385
- {selectMonthLabel}
386
- {selectYearLabel}
387
- monthListId="{uid}-months"
388
- yearListId="{uid}-years"
389
- onpreviousmonth={() => setMonth(addMonths(monthDate, -1))}
390
- onnextmonth={() => setMonth(addMonths(monthDate, 1))}
391
- onpreviousyear={() => setMonth(addMonths(monthDate, -12))}
392
- onnextyear={() => setMonth(addMonths(monthDate, 12))}
393
- ontogglemonths={() => setMode(mode === 'months' ? 'days' : 'months')}
394
- ontoggleyears={() => setMode(mode === 'years' ? 'days' : 'years')}
395
- />
396
-
397
- <div class="np-docked-date-picker-views">
398
- <div class="np-docked-date-picker-body" inert={mode !== 'days'}>
399
- <Calendar
400
- month={monthDate}
401
- selected={pendingDate}
402
- min={minDate}
403
- max={maxDate}
404
- {locale}
405
- firstDayOfWeek={week}
406
- {isDateEnabled}
407
- {adjacentMonthDays}
408
- dynamicRows
409
- todayDate={todayValue}
410
- focusedDate={focusedDay}
411
- selectedLabel={selectedDateLabel}
412
- onselect={selectDay}
413
- onmonthstep={(delta) => setMonth(addMonths(monthDate, delta))}
414
- onfocusday={(date) => {
415
- focusedDay = date
416
- if (
417
- date.getMonth() !== monthDate.getMonth() ||
418
- date.getFullYear() !== monthDate.getFullYear()
419
- ) {
420
- setMonth(date)
421
- }
422
- }}
423
- />
424
- </div>
336
+ <CalendarToday />
337
+ </IconButton>
338
+ {/snippet}
339
+ </TextField>
340
+ <input
341
+ bind:this={valueInput}
342
+ class="np-docked-date-picker-value"
343
+ type="hidden"
344
+ value={current ?? ''}
345
+ {name}
346
+ {form}
347
+ {disabled}
348
+ />
349
+
350
+ <Menu
351
+ id="{uid}-calendar"
352
+ role="dialog"
353
+ aria-label={label}
354
+ aria-modal="false"
355
+ class="np-docked-date-picker-menu"
356
+ style={`position-anchor:--${uid};`}
357
+ --np-menu-justify-self="none"
358
+ --np-menu-position-area="bottom span-right"
359
+ --np-menu-over-anchor-position-area="span-all span-right"
360
+ --np-menu-margin="0"
361
+ --np-menu-container-color="var(--np-docked-date-picker-container-color, var(--np-color-surface-container-high))"
362
+ --np-menu-container-shape="var(--np-docked-date-picker-container-shape, var(--np-shape-corner-large))"
363
+ anchor={element}
364
+ bind:element={menuElement}
365
+ bind:open
366
+ ontoggle={({ newState }) => {
367
+ if (newState === 'open') {
368
+ monthBeforeOpen = displayMonth
369
+ pending = current
370
+ focusCalendarWhenReady()
371
+ }
372
+ if (newState === 'closed') {
373
+ setMode('days')
374
+ listMode = undefined
375
+ focusedDay = undefined
376
+ displayMonth = monthBeforeOpen
377
+ }
378
+ }}
379
+ >
380
+ {#if open}
381
+ <div class="np-docked-date-picker-container" style:--np-calendar-rows={rowCount}>
382
+ <CalendarHeader
383
+ {mode}
384
+ monthLabel={shortMonthNames[monthDate.getMonth()]}
385
+ yearLabel={`${monthDate.getFullYear()}`}
386
+ {canPreviousMonth}
387
+ {canNextMonth}
388
+ {canPreviousYear}
389
+ {canNextYear}
390
+ {previousMonthLabel}
391
+ {nextMonthLabel}
392
+ {previousYearLabel}
393
+ {nextYearLabel}
394
+ {selectMonthLabel}
395
+ {selectYearLabel}
396
+ monthListId="{uid}-months"
397
+ yearListId="{uid}-years"
398
+ onpreviousmonth={() => setMonth(addMonths(monthDate, -1))}
399
+ onnextmonth={() => setMonth(addMonths(monthDate, 1))}
400
+ onpreviousyear={() => setMonth(addMonths(monthDate, -12))}
401
+ onnextyear={() => setMonth(addMonths(monthDate, 12))}
402
+ ontogglemonths={() => setMode(mode === 'months' ? 'days' : 'months')}
403
+ ontoggleyears={() => setMode(mode === 'years' ? 'days' : 'years')}
404
+ />
405
+
406
+ <div class="np-docked-date-picker-views">
407
+ <div class="np-docked-date-picker-body" inert={mode !== 'days'}>
408
+ <Calendar
409
+ month={monthDate}
410
+ selected={pendingDate}
411
+ min={minDate}
412
+ max={maxDate}
413
+ {locale}
414
+ firstDayOfWeek={week}
415
+ {isDateEnabled}
416
+ {adjacentMonthDays}
417
+ dynamicRows
418
+ todayDate={todayValue}
419
+ focusedDate={focusedDay}
420
+ selectedLabel={selectedDateLabel}
421
+ onselect={selectDay}
422
+ onmonthstep={(delta) => setMonth(addMonths(monthDate, delta))}
423
+ onfocusday={(date) => {
424
+ focusedDay = date
425
+ if (
426
+ date.getMonth() !== monthDate.getMonth() ||
427
+ date.getFullYear() !== monthDate.getFullYear()
428
+ ) {
429
+ setMonth(date)
430
+ }
431
+ }}
432
+ />
433
+ </div>
425
434
 
426
- {#if listMode}
427
- <div class={['np-docked-date-picker-menu-view', mode !== 'days' && 'open']}>
428
- <div class="np-docked-date-picker-menu-shade">
429
- <Divider --np-divider-color="var(--np-color-outline-variant)" />
430
- {#if listMode === 'months'}
431
- <SelectionList
432
- id="{uid}-months"
433
- aria-label={selectMonthLabel}
434
- options={monthOptions}
435
- value={monthDate.getMonth()}
436
- onselect={chooseMonth}
437
- />
438
- {:else}
439
- <SelectionList
440
- id="{uid}-years"
441
- aria-label={selectYearLabel}
442
- options={yearOptions}
443
- value={monthDate.getFullYear()}
444
- onselect={chooseYear}
445
- />
446
- {/if}
447
- </div>
435
+ {#if listMode}
436
+ <div class={['np-docked-date-picker-menu-view', mode !== 'days' && 'open']}>
437
+ <div class="np-docked-date-picker-menu-shade">
438
+ <Divider --np-divider-color="var(--np-color-outline-variant)" />
439
+ {#if listMode === 'months'}
440
+ <SelectionList
441
+ id="{uid}-months"
442
+ aria-label={selectMonthLabel}
443
+ options={monthOptions}
444
+ value={monthDate.getMonth()}
445
+ onselect={chooseMonth}
446
+ />
447
+ {:else}
448
+ <SelectionList
449
+ id="{uid}-years"
450
+ aria-label={selectYearLabel}
451
+ options={yearOptions}
452
+ value={monthDate.getFullYear()}
453
+ onselect={chooseYear}
454
+ />
455
+ {/if}
448
456
  </div>
449
- {/if}
450
- </div>
457
+ </div>
458
+ {/if}
459
+ </div>
451
460
 
452
- <div class="np-docked-date-picker-actions">
453
- <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button>
454
- <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
455
- </div>
461
+ <div class="np-docked-date-picker-actions">
462
+ <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button>
463
+ <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
456
464
  </div>
457
- {/if}
458
- </Menu>
459
- </div>
465
+ </div>
466
+ {/if}
467
+ </Menu>
460
468
 
461
469
  <style>
462
- .np-docked-date-picker {
463
- display: inline-flex;
464
- flex-direction: column;
465
- }
466
-
467
- .np-docked-date-picker-anchor {
468
- display: inline-flex;
469
- position: relative;
470
- }
471
-
472
470
  :global(.np-docked-date-picker-menu .np-menu) {
473
471
  padding: 0;
474
472
  }
@@ -51,6 +51,7 @@
51
51
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- absorbed on purpose
52
52
  type,
53
53
  'aria-invalid': ariaInvalid,
54
+ style,
54
55
  displayMonth = $bindable(),
55
56
  open = $bindable(false),
56
57
  element = $bindable(),
@@ -90,6 +91,8 @@
90
91
  minuteLabel = 'Minute',
91
92
  dayPeriodLabel = 'AM or PM',
92
93
  onchange,
94
+ oninput,
95
+ onblur,
93
96
  ...attributes
94
97
  }: DockedDateTimePickerProps = $props()
95
98
 
@@ -98,7 +101,6 @@
98
101
 
99
102
  const uid = $props.id()
100
103
 
101
- let anchorElement = $state<HTMLDivElement>()
102
104
  let menuElement = $state<HTMLDivElement>()
103
105
  let mode = $state<'days' | 'months' | 'years'>('days')
104
106
  let listMode = $state<'months' | 'years' | undefined>(undefined)
@@ -307,7 +309,7 @@
307
309
  const closePicker = (restoreFocus = true) => {
308
310
  menuElement?.hidePopover()
309
311
  if (restoreFocus) {
310
- anchorElement?.querySelector<HTMLElement>('input')?.focus()
312
+ element?.querySelector<HTMLElement>('input')?.focus()
311
313
  }
312
314
  }
313
315
 
@@ -380,225 +382,217 @@
380
382
  }
381
383
  </script>
382
384
 
383
- <div {...attributes} bind:this={element} class={['np-docked-date-time-picker', attributes.class]}>
384
- <div
385
- class="np-docked-date-time-picker-anchor"
386
- bind:this={anchorElement}
387
- style:anchor-name="--{uid}"
388
- >
389
- <TextField
390
- {label}
391
- {variant}
392
- {issues}
385
+ <TextField
386
+ {...attributes}
387
+ class={['np-docked-date-time-picker', attributes.class]}
388
+ style={`anchor-name:--${uid};${style ?? ''}`}
389
+ {label}
390
+ {variant}
391
+ {issues}
392
+ {disabled}
393
+ {readonly}
394
+ {required}
395
+ {noAsterisk}
396
+ {autocomplete}
397
+ supportingText={supportingText ?? pattern}
398
+ bind:value={text}
399
+ bind:element
400
+ bind:inputElement
401
+ aria-invalid={inputInvalid || issues?.length || ariaInvalid === true || ariaInvalid === 'true'
402
+ ? 'true'
403
+ : undefined}
404
+ oninput={(event) => {
405
+ handleInput(event)
406
+ oninput?.(event)
407
+ }}
408
+ onblur={(event) => {
409
+ handleBlur()
410
+ onblur?.(event)
411
+ }}
412
+ >
413
+ {#snippet end()}
414
+ <IconButton
415
+ type="button"
416
+ aria-label={openCalendarLabel}
417
+ aria-expanded={open}
418
+ aria-haspopup="dialog"
419
+ aria-controls={open ? `${uid}-calendar` : undefined}
393
420
  {disabled}
394
- {readonly}
395
- {required}
396
- {noAsterisk}
397
- {autocomplete}
398
- supportingText={supportingText ?? pattern}
399
- bind:value={text}
400
- bind:inputElement
401
- aria-invalid={inputInvalid || issues?.length || ariaInvalid === true || ariaInvalid === 'true'
402
- ? 'true'
403
- : undefined}
404
- oninput={handleInput}
405
- onblur={handleBlur}
421
+ onclick={() => {
422
+ if (open) closePicker()
423
+ else openPicker()
424
+ }}
406
425
  >
407
- {#snippet end()}
408
- <IconButton
409
- type="button"
410
- aria-label={openCalendarLabel}
411
- aria-expanded={open}
412
- aria-haspopup="dialog"
413
- aria-controls={open ? `${uid}-calendar` : undefined}
414
- {disabled}
415
- onclick={() => {
416
- if (open) closePicker()
417
- else openPicker()
418
- }}
419
- >
420
- <CalendarToday />
421
- </IconButton>
422
- {/snippet}
423
- </TextField>
424
- <input
425
- bind:this={valueInput}
426
- class="np-docked-date-time-picker-value"
427
- type="hidden"
428
- value={current ?? ''}
429
- {name}
430
- {form}
431
- {disabled}
432
- />
433
- </div>
434
-
435
- <Menu
436
- id="{uid}-calendar"
437
- role="dialog"
438
- aria-label={label}
439
- aria-modal="false"
440
- class="np-docked-date-time-picker-menu"
441
- style={`position-anchor:--${uid};`}
442
- --np-menu-justify-self="none"
443
- --np-menu-position-area="bottom span-right"
444
- --np-menu-over-anchor-position-area="span-all span-right"
445
- --np-menu-margin="0"
446
- --np-menu-container-color="var(--np-docked-date-time-picker-container-color, var(--np-color-surface-container-high))"
447
- --np-menu-container-shape="var(--np-docked-date-time-picker-container-shape, var(--np-shape-corner-large))"
448
- anchor={anchorElement}
449
- bind:element={menuElement}
450
- bind:open
451
- ontoggle={({ newState }) => {
452
- if (newState === 'open') {
453
- monthBeforeOpen = displayMonth
454
- pendingDay = committed ? toISODate(committed) : undefined
455
- pendingMinutes = committed ? minutesOfDay(committed) : defaultMinutes
456
- focusCalendarWhenReady()
457
- }
458
- if (newState === 'closed') {
459
- setMode('days')
460
- listMode = undefined
461
- focusedDay = undefined
462
- displayMonth = monthBeforeOpen
463
- }
464
- }}
465
- >
466
- {#if open}
467
- <div
468
- class="np-docked-date-time-picker-container"
469
- style:--np-calendar-rows={rowCount}
470
- style:--np-time-columns={hour12 ? 3 : 2}
471
- >
472
- <div class="np-docked-date-time-picker-panels">
473
- <div class="np-docked-date-time-picker-date">
474
- <CalendarHeader
475
- {mode}
476
- monthLabel={shortMonthNames[monthDate.getMonth()]}
477
- yearLabel={`${monthDate.getFullYear()}`}
478
- {canPreviousMonth}
479
- {canNextMonth}
480
- {canPreviousYear}
481
- {canNextYear}
482
- {previousMonthLabel}
483
- {nextMonthLabel}
484
- {previousYearLabel}
485
- {nextYearLabel}
486
- {selectMonthLabel}
487
- {selectYearLabel}
488
- monthListId="{uid}-months"
489
- yearListId="{uid}-years"
490
- onpreviousmonth={() => setMonth(addMonths(monthDate, -1))}
491
- onnextmonth={() => setMonth(addMonths(monthDate, 1))}
492
- onpreviousyear={() => setMonth(addMonths(monthDate, -12))}
493
- onnextyear={() => setMonth(addMonths(monthDate, 12))}
494
- ontogglemonths={() => setMode(mode === 'months' ? 'days' : 'months')}
495
- ontoggleyears={() => setMode(mode === 'years' ? 'days' : 'years')}
496
- />
497
-
498
- <div class="np-docked-date-time-picker-views">
499
- <div class="np-docked-date-time-picker-body" inert={mode !== 'days'}>
500
- <Calendar
501
- month={monthDate}
502
- selected={pendingDate}
503
- min={minDate}
504
- max={maxDate}
505
- {locale}
506
- firstDayOfWeek={week}
507
- {isDateEnabled}
508
- {adjacentMonthDays}
509
- dynamicRows
510
- todayDate={todayValue}
511
- focusedDate={focusedDay}
512
- selectedLabel={selectedDateLabel}
513
- onselect={selectDay}
514
- onmonthstep={(delta) => setMonth(addMonths(monthDate, delta))}
515
- onfocusday={(date) => {
516
- focusedDay = date
517
- if (
518
- date.getMonth() !== monthDate.getMonth() ||
519
- date.getFullYear() !== monthDate.getFullYear()
520
- ) {
521
- setMonth(date)
522
- }
523
- }}
524
- />
525
- </div>
426
+ <CalendarToday />
427
+ </IconButton>
428
+ {/snippet}
429
+ </TextField>
430
+ <input
431
+ bind:this={valueInput}
432
+ class="np-docked-date-time-picker-value"
433
+ type="hidden"
434
+ value={current ?? ''}
435
+ {name}
436
+ {form}
437
+ {disabled}
438
+ />
439
+
440
+ <Menu
441
+ id="{uid}-calendar"
442
+ role="dialog"
443
+ aria-label={label}
444
+ aria-modal="false"
445
+ class="np-docked-date-time-picker-menu"
446
+ style={`position-anchor:--${uid};`}
447
+ --np-menu-justify-self="none"
448
+ --np-menu-position-area="bottom span-right"
449
+ --np-menu-over-anchor-position-area="span-all span-right"
450
+ --np-menu-margin="0"
451
+ --np-menu-container-color="var(--np-docked-date-time-picker-container-color, var(--np-color-surface-container-high))"
452
+ --np-menu-container-shape="var(--np-docked-date-time-picker-container-shape, var(--np-shape-corner-large))"
453
+ anchor={element}
454
+ bind:element={menuElement}
455
+ bind:open
456
+ ontoggle={({ newState }) => {
457
+ if (newState === 'open') {
458
+ monthBeforeOpen = displayMonth
459
+ pendingDay = committed ? toISODate(committed) : undefined
460
+ pendingMinutes = committed ? minutesOfDay(committed) : defaultMinutes
461
+ focusCalendarWhenReady()
462
+ }
463
+ if (newState === 'closed') {
464
+ setMode('days')
465
+ listMode = undefined
466
+ focusedDay = undefined
467
+ displayMonth = monthBeforeOpen
468
+ }
469
+ }}
470
+ >
471
+ {#if open}
472
+ <div
473
+ class="np-docked-date-time-picker-container"
474
+ style:--np-calendar-rows={rowCount}
475
+ style:--np-time-columns={hour12 ? 3 : 2}
476
+ >
477
+ <div class="np-docked-date-time-picker-panels">
478
+ <div class="np-docked-date-time-picker-date">
479
+ <CalendarHeader
480
+ {mode}
481
+ monthLabel={shortMonthNames[monthDate.getMonth()]}
482
+ yearLabel={`${monthDate.getFullYear()}`}
483
+ {canPreviousMonth}
484
+ {canNextMonth}
485
+ {canPreviousYear}
486
+ {canNextYear}
487
+ {previousMonthLabel}
488
+ {nextMonthLabel}
489
+ {previousYearLabel}
490
+ {nextYearLabel}
491
+ {selectMonthLabel}
492
+ {selectYearLabel}
493
+ monthListId="{uid}-months"
494
+ yearListId="{uid}-years"
495
+ onpreviousmonth={() => setMonth(addMonths(monthDate, -1))}
496
+ onnextmonth={() => setMonth(addMonths(monthDate, 1))}
497
+ onpreviousyear={() => setMonth(addMonths(monthDate, -12))}
498
+ onnextyear={() => setMonth(addMonths(monthDate, 12))}
499
+ ontogglemonths={() => setMode(mode === 'months' ? 'days' : 'months')}
500
+ ontoggleyears={() => setMode(mode === 'years' ? 'days' : 'years')}
501
+ />
502
+
503
+ <div class="np-docked-date-time-picker-views">
504
+ <div class="np-docked-date-time-picker-body" inert={mode !== 'days'}>
505
+ <Calendar
506
+ month={monthDate}
507
+ selected={pendingDate}
508
+ min={minDate}
509
+ max={maxDate}
510
+ {locale}
511
+ firstDayOfWeek={week}
512
+ {isDateEnabled}
513
+ {adjacentMonthDays}
514
+ dynamicRows
515
+ todayDate={todayValue}
516
+ focusedDate={focusedDay}
517
+ selectedLabel={selectedDateLabel}
518
+ onselect={selectDay}
519
+ onmonthstep={(delta) => setMonth(addMonths(monthDate, delta))}
520
+ onfocusday={(date) => {
521
+ focusedDay = date
522
+ if (
523
+ date.getMonth() !== monthDate.getMonth() ||
524
+ date.getFullYear() !== monthDate.getFullYear()
525
+ ) {
526
+ setMonth(date)
527
+ }
528
+ }}
529
+ />
530
+ </div>
526
531
 
527
- {#if listMode}
528
- <div class={['np-docked-date-time-picker-menu-view', mode !== 'days' && 'open']}>
529
- <div class="np-docked-date-time-picker-menu-shade">
530
- <Divider --np-divider-color="var(--np-color-outline-variant)" />
531
- {#if listMode === 'months'}
532
- <SelectionList
533
- id="{uid}-months"
534
- aria-label={selectMonthLabel}
535
- options={monthOptions}
536
- value={monthDate.getMonth()}
537
- onselect={chooseMonth}
538
- />
539
- {:else}
540
- <SelectionList
541
- id="{uid}-years"
542
- aria-label={selectYearLabel}
543
- options={yearOptions}
544
- value={monthDate.getFullYear()}
545
- onselect={chooseYear}
546
- />
547
- {/if}
548
- </div>
532
+ {#if listMode}
533
+ <div class={['np-docked-date-time-picker-menu-view', mode !== 'days' && 'open']}>
534
+ <div class="np-docked-date-time-picker-menu-shade">
535
+ <Divider --np-divider-color="var(--np-color-outline-variant)" />
536
+ {#if listMode === 'months'}
537
+ <SelectionList
538
+ id="{uid}-months"
539
+ aria-label={selectMonthLabel}
540
+ options={monthOptions}
541
+ value={monthDate.getMonth()}
542
+ onselect={chooseMonth}
543
+ />
544
+ {:else}
545
+ <SelectionList
546
+ id="{uid}-years"
547
+ aria-label={selectYearLabel}
548
+ options={yearOptions}
549
+ value={monthDate.getFullYear()}
550
+ onselect={chooseYear}
551
+ />
552
+ {/if}
549
553
  </div>
550
- {/if}
551
- </div>
554
+ </div>
555
+ {/if}
552
556
  </div>
557
+ </div>
553
558
 
554
- <div class="np-docked-date-time-picker-time">
555
- <div class="np-docked-date-time-picker-columns">
556
- <TimeColumn
557
- aria-label={hourLabel}
558
- options={hourOptions}
559
- value={pendingHour}
560
- onselect={(hour) =>
561
- setTime(hour * MINUTES_IN_HOUR + (pendingMinutes % MINUTES_IN_HOUR))}
562
- />
559
+ <div class="np-docked-date-time-picker-time">
560
+ <div class="np-docked-date-time-picker-columns">
561
+ <TimeColumn
562
+ aria-label={hourLabel}
563
+ options={hourOptions}
564
+ value={pendingHour}
565
+ onselect={(hour) =>
566
+ setTime(hour * MINUTES_IN_HOUR + (pendingMinutes % MINUTES_IN_HOUR))}
567
+ />
568
+ <TimeColumn
569
+ aria-label={minuteLabel}
570
+ options={minuteOptions}
571
+ value={pendingMinutes % MINUTES_IN_HOUR}
572
+ onselect={(minute) => setTime(pendingHour * MINUTES_IN_HOUR + minute)}
573
+ />
574
+ {#if hour12}
563
575
  <TimeColumn
564
- aria-label={minuteLabel}
565
- options={minuteOptions}
566
- value={pendingMinutes % MINUTES_IN_HOUR}
567
- onselect={(minute) => setTime(pendingHour * MINUTES_IN_HOUR + minute)}
576
+ aria-label={dayPeriodLabel}
577
+ options={periodOptions}
578
+ value={pendingPeriod}
579
+ onselect={(period) => setTime((pendingMinutes % HALF_DAY) + period * HALF_DAY)}
568
580
  />
569
- {#if hour12}
570
- <TimeColumn
571
- aria-label={dayPeriodLabel}
572
- options={periodOptions}
573
- value={pendingPeriod}
574
- onselect={(period) => setTime((pendingMinutes % HALF_DAY) + period * HALF_DAY)}
575
- />
576
- {/if}
577
- </div>
581
+ {/if}
578
582
  </div>
579
583
  </div>
584
+ </div>
580
585
 
581
- <Divider --np-divider-color="var(--np-color-outline-variant)" />
582
- <div class="np-docked-date-time-picker-actions">
583
- <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button>
584
- <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
585
- </div>
586
+ <Divider --np-divider-color="var(--np-color-outline-variant)" />
587
+ <div class="np-docked-date-time-picker-actions">
588
+ <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button>
589
+ <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
586
590
  </div>
587
- {/if}
588
- </Menu>
589
- </div>
591
+ </div>
592
+ {/if}
593
+ </Menu>
590
594
 
591
595
  <style>
592
- .np-docked-date-time-picker {
593
- display: inline-flex;
594
- flex-direction: column;
595
- }
596
-
597
- .np-docked-date-time-picker-anchor {
598
- display: inline-flex;
599
- position: relative;
600
- }
601
-
602
596
  :global(.np-docked-date-time-picker-menu .np-menu) {
603
597
  padding: 0;
604
598
  }
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { TextFieldElement } from '../text-field/types.js';
2
3
  import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
3
4
  export type ISODate = string;
4
5
  export type ISODateTime = string;
@@ -26,11 +27,11 @@ interface MonthStepperLabelProps {
26
27
  nextMonthLabel?: string;
27
28
  previousMonthLabel?: string;
28
29
  }
29
- export interface DockedDatePickerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'>, DatePickerLocaleProps, DatePickerRangeProps, DatePickerCommonLabelProps, MonthStepperLabelProps {
30
+ export interface DockedDatePickerProps extends Omit<HTMLAttributes<TextFieldElement>, 'onchange'>, DatePickerLocaleProps, DatePickerRangeProps, DatePickerCommonLabelProps, MonthStepperLabelProps {
30
31
  value?: ISODate | number | null;
31
32
  displayMonth?: ISODate;
32
33
  open?: boolean;
33
- element?: HTMLDivElement;
34
+ element?: HTMLSpanElement;
34
35
  name?: string;
35
36
  form?: string;
36
37
  required?: boolean;
@@ -54,11 +55,11 @@ export interface DockedDatePickerProps extends Omit<HTMLAttributes<HTMLDivElemen
54
55
  invalidDateMessage?: string;
55
56
  onchange?: (value: ISODate | undefined) => void;
56
57
  }
57
- export interface DockedDateTimePickerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'>, DatePickerLocaleProps, Omit<DatePickerRangeProps, 'min' | 'max'>, DatePickerCommonLabelProps, MonthStepperLabelProps {
58
+ export interface DockedDateTimePickerProps extends Omit<HTMLAttributes<TextFieldElement>, 'onchange'>, DatePickerLocaleProps, Omit<DatePickerRangeProps, 'min' | 'max'>, DatePickerCommonLabelProps, MonthStepperLabelProps {
58
59
  value?: ISODateTime | number | null;
59
60
  displayMonth?: ISODate;
60
61
  open?: boolean;
61
- element?: HTMLDivElement;
62
+ element?: HTMLSpanElement;
62
63
  min?: ISODate | ISODateTime;
63
64
  max?: ISODate | ISODateTime;
64
65
  name?: string;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import ArrowDropDownIcon from '../icons/ArrowDropDownIcon.svelte'
2
3
  import type { HTMLSelectAttributes } from 'svelte/elements'
3
4
  interface SelectProps extends HTMLSelectAttributes {
4
5
  label?: string
@@ -64,9 +65,9 @@
64
65
  </div>
65
66
  <div class="np-select-state-layer"></div>
66
67
  {/if}
67
- <svg class="arrow" height="6" viewBox="7 10 10 5" focusable="false">
68
- <polygon stroke="none" fill-rule="evenodd" points="7 10 12 15 17 10"></polygon>
69
- </svg>
68
+ <span class="arrow">
69
+ <ArrowDropDownIcon />
70
+ </span>
70
71
  <select
71
72
  oninput={(event) => {
72
73
  if (
@@ -198,11 +199,7 @@
198
199
  }
199
200
 
200
201
  .arrow {
201
- padding-inline: 0.375rem;
202
- padding-block: 0.5625rem;
203
- box-sizing: border-box;
204
- width: 24px;
205
- height: 24px;
202
+ display: flex;
206
203
  fill: currentColor;
207
204
  position: absolute;
208
205
  pointer-events: none;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onFormReset } from '../form-reset.js'
3
+ import ArrowDropDownIcon from '../icons/ArrowDropDownIcon.svelte'
3
4
  import Item from '../list/Item.svelte'
4
5
  import Menu from '../menu/Menu.svelte'
5
6
  import Check from './Check.svelte'
@@ -256,10 +257,9 @@
256
257
  </script>
257
258
 
258
259
  {#snippet arrows()}
259
- <svg height="5" viewBox="7 10 10 5" focusable="false">
260
- <polygon class="down" stroke="none" fill-rule="evenodd" points="7 10 12 15 17 10"></polygon>
261
- <polygon class="up" stroke="none" fill-rule="evenodd" points="7 15 12 10 17 15"></polygon>
262
- </svg>
260
+ <span class="arrow">
261
+ <ArrowDropDownIcon />
262
+ </span>
263
263
  {/snippet}
264
264
 
265
265
  <div
@@ -806,16 +806,18 @@
806
806
  opacity: 1;
807
807
  }
808
808
 
809
- .field:not(:has(select:is(:user-invalid, [aria-invalid='true']))).menu-open .down,
810
- .field:not(:has(select:is(:user-invalid, [aria-invalid='true']))):focus .down {
809
+ .field:not(:has(select:is(:user-invalid, [aria-invalid='true']))).menu-open .arrow,
810
+ .field:not(:has(select:is(:user-invalid, [aria-invalid='true']))):focus .arrow {
811
811
  color: var(--np-color-primary);
812
812
  }
813
- .icon .down {
814
- transition: color 75ms linear 75ms;
813
+ .icon .arrow {
814
+ display: flex;
815
+ transition:
816
+ color 75ms linear 75ms,
817
+ rotate 150ms cubic-bezier(0.2, 0, 0, 1);
815
818
  }
816
- .icon .up {
817
- opacity: 0;
818
- transition: color 75ms linear 75ms;
819
+ .field.menu-open .icon .arrow {
820
+ rotate: 180deg;
819
821
  }
820
822
 
821
823
  .content {
@@ -1,4 +1,4 @@
1
1
  import type { SliderProps } from './types.ts';
2
- declare const Slider: import("svelte").Component<SliderProps, {}, "value" | "element" | "endValue" | "inputElement" | "endInputElement">;
2
+ declare const Slider: import("svelte").Component<SliderProps, {}, "value" | "element" | "inputElement" | "endValue" | "endInputElement">;
3
3
  type Slider = ReturnType<typeof Slider>;
4
4
  export default Slider;
@@ -460,6 +460,14 @@
460
460
  margin-inline-start: 1rem;
461
461
  margin-inline-end: 0.75rem;
462
462
  }
463
+ .start:has(:global(.np-icon-button)) {
464
+ margin-inline-start: 0.25rem;
465
+ margin-inline-end: 0.5rem;
466
+ }
467
+ .end:has(:global(.np-icon-button)) {
468
+ margin-inline-start: 0.5rem;
469
+ margin-inline-end: 0.25rem;
470
+ }
463
471
  .field:has(input:is(:user-invalid, [aria-invalid='true'])) .end,
464
472
  .field:has(textarea:is(:user-invalid, [aria-invalid='true'])) .end {
465
473
  color: var(--np-color-error);
package/package.json CHANGED
@@ -1,108 +1,106 @@
1
1
  {
2
- "name": "noph-ui",
3
- "version": "0.40.0",
4
- "license": "MIT",
5
- "homepage": "https://noph.dev",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/cnolte/noph-ui.git"
9
- },
10
- "author": {
11
- "name": "cnolte"
12
- },
13
- "imports": {
14
- "#lib": "./src/lib/index.js",
15
- "#lib/*": "./src/lib/*"
16
- },
17
- "keywords": [
18
- "svelte",
19
- "svelte 5",
20
- "material",
21
- "material 3",
22
- "expressive",
23
- "material you",
24
- "material expressive",
25
- "m3",
26
- "ui",
27
- "frontend",
28
- "design-system",
29
- "ui-library",
30
- "theming"
31
- ],
32
- "scripts": {
33
- "dev": "vite dev",
34
- "build": "vite build && npm run prepack",
35
- "preview": "vite preview",
36
- "prepare": "svelte-kit sync || echo ''",
37
- "prepack": "svelte-kit sync && svelte-package && publint",
38
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
39
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
40
- "lint": "prettier --check . && eslint .",
41
- "format": "prettier --write .",
42
- "docs:toc": "node scripts/build-toc.ts",
43
- "test:unit": "vitest",
44
- "test": "npm run test:unit -- --run && npm run test:e2e",
45
- "test:e2e": "playwright install && playwright test"
46
- },
47
- "exports": {
48
- ".": {
49
- "types": "./dist/index.d.ts",
50
- "svelte": "./dist/index.js"
51
- },
52
- "./icons": {
53
- "types": "./dist/icons/index.d.ts",
54
- "svelte": "./dist/icons/index.js"
55
- },
56
- "./types": {
57
- "types": "./dist/types.d.ts"
58
- },
59
- "./defaultTheme": {
60
- "types": "./dist/themes/defaultTheme.css.d.ts",
61
- "import": "./dist/themes/defaultTheme.css",
62
- "require": "./dist/themes/defaultTheme.css"
63
- }
64
- },
65
- "sideEffects": [
66
- "**/*.css"
67
- ],
68
- "files": [
69
- "dist",
70
- "!dist/**/*.test.*",
71
- "!dist/**/*.spec.*"
72
- ],
73
- "peerDependencies": {
74
- "svelte": "^5.40.0"
75
- },
76
- "devDependencies": {
77
- "@eslint/js": "^10.0.1",
78
- "@materialx/material-color-utilities": "^0.4.8",
79
- "@playwright/test": "^1.62.1",
80
- "@sveltejs/adapter-vercel": "7.0.0-next.6",
81
- "@sveltejs/kit": "3.0.0-next.23",
82
- "@sveltejs/package": "3.0.0-next.7",
83
- "@sveltejs/vite-plugin-svelte": "7.3.0",
84
- "@types/node": "^24",
85
- "@types/prismjs": "^1.26.6",
86
- "@vitest/browser-playwright": "^4.1.10",
87
- "eslint": "^10.8.1",
88
- "eslint-config-prettier": "^10.1.8",
89
- "eslint-plugin-svelte": "^3.22.0",
90
- "globals": "^17.9.0",
91
- "playwright": "^1.62.1",
92
- "prettier": "^3.9.6",
93
- "prettier-plugin-svelte": "^4.1.1",
94
- "prismjs": "^1.30.0",
95
- "publint": "^0.3.23",
96
- "svelte": "^5.56.8",
97
- "svelte-check": "^4.7.5",
98
- "typescript": "^6.0.3",
99
- "typescript-eslint": "^8.66.0",
100
- "vite": "8.2.1",
101
- "vitest": "^4.1.10",
102
- "vitest-browser-svelte": "^2.1.1"
103
- },
104
- "svelte": "./dist/index.js",
105
- "types": "./dist/index.d.ts",
106
- "type": "module",
107
- "packageManager": "pnpm@11.9.0"
108
- }
2
+ "name": "noph-ui",
3
+ "version": "0.40.2",
4
+ "license": "MIT",
5
+ "homepage": "https://noph.dev",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/cnolte/noph-ui.git"
9
+ },
10
+ "author": {
11
+ "name": "cnolte"
12
+ },
13
+ "imports": {
14
+ "#lib": "./src/lib/index.js",
15
+ "#lib/*": "./src/lib/*"
16
+ },
17
+ "keywords": [
18
+ "svelte",
19
+ "svelte 5",
20
+ "material",
21
+ "material 3",
22
+ "expressive",
23
+ "material you",
24
+ "material expressive",
25
+ "m3",
26
+ "ui",
27
+ "frontend",
28
+ "design-system",
29
+ "ui-library",
30
+ "theming"
31
+ ],
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "svelte": "./dist/index.js"
36
+ },
37
+ "./icons": {
38
+ "types": "./dist/icons/index.d.ts",
39
+ "svelte": "./dist/icons/index.js"
40
+ },
41
+ "./types": {
42
+ "types": "./dist/types.d.ts"
43
+ },
44
+ "./defaultTheme": {
45
+ "types": "./dist/themes/defaultTheme.css.d.ts",
46
+ "import": "./dist/themes/defaultTheme.css",
47
+ "require": "./dist/themes/defaultTheme.css"
48
+ }
49
+ },
50
+ "sideEffects": [
51
+ "**/*.css"
52
+ ],
53
+ "files": [
54
+ "dist",
55
+ "!dist/**/*.test.*",
56
+ "!dist/**/*.spec.*"
57
+ ],
58
+ "peerDependencies": {
59
+ "svelte": "^5.40.0"
60
+ },
61
+ "devDependencies": {
62
+ "@eslint/js": "^10.0.1",
63
+ "@materialx/material-color-utilities": "^0.4.8",
64
+ "@playwright/test": "^1.62.1",
65
+ "@sveltejs/adapter-vercel": "7.0.0-next.6",
66
+ "@sveltejs/kit": "3.0.0-next.23",
67
+ "@sveltejs/package": "3.0.0-next.7",
68
+ "@sveltejs/vite-plugin-svelte": "7.3.0",
69
+ "@types/node": "^24",
70
+ "@types/prismjs": "^1.26.6",
71
+ "@vitest/browser-playwright": "^4.1.10",
72
+ "eslint": "^10.8.1",
73
+ "eslint-config-prettier": "^10.1.8",
74
+ "eslint-plugin-svelte": "^3.22.0",
75
+ "globals": "^17.9.0",
76
+ "playwright": "^1.62.1",
77
+ "prettier": "^3.9.6",
78
+ "prettier-plugin-svelte": "^4.1.1",
79
+ "prismjs": "^1.30.0",
80
+ "publint": "^0.3.23",
81
+ "svelte": "^5.56.8",
82
+ "svelte-check": "^4.7.5",
83
+ "typescript": "^6.0.3",
84
+ "typescript-eslint": "^8.66.0",
85
+ "valibot": "^1.4.2",
86
+ "vite": "8.2.1",
87
+ "vitest": "^4.1.10",
88
+ "vitest-browser-svelte": "^2.1.1"
89
+ },
90
+ "svelte": "./dist/index.js",
91
+ "types": "./dist/index.d.ts",
92
+ "type": "module",
93
+ "scripts": {
94
+ "dev": "vite dev",
95
+ "build": "vite build && npm run prepack",
96
+ "preview": "vite preview",
97
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
98
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
99
+ "lint": "prettier --check . && eslint .",
100
+ "format": "prettier --write .",
101
+ "docs:toc": "node scripts/build-toc.ts",
102
+ "test:unit": "vitest",
103
+ "test": "npm run test:unit -- --run && npm run test:e2e",
104
+ "test:e2e": "playwright install && playwright test"
105
+ }
106
+ }