noph-ui 0.7.8 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,7 +65,7 @@ Beta (No breaking changes expected)
65
65
  - Ripple
66
66
  - Segmented buttons
67
67
  - Snackbar
68
- - Text fields (Textarea + Docs missing)
68
+ - Text fields (Theming missing)
69
69
 
70
70
  In progress (Breaking changes expected)
71
71
 
@@ -269,4 +269,10 @@
269
269
  width: calc((var(--button-height) - 0.375rem) / 2);
270
270
  height: calc((var(--button-height) - 0.375rem) / 2);
271
271
  }
272
+
273
+ @media (forced-colors: active) {
274
+ .np-button {
275
+ border: 1px solid CanvasText;
276
+ }
277
+ }
272
278
  </style>
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Ripple from '../ripple/Ripple.svelte'
3
3
  import type { CardProps } from './types.ts'
4
- import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements'
5
4
 
6
5
  let {
7
6
  image,
@@ -214,4 +214,27 @@
214
214
  width: 0;
215
215
  }
216
216
  }
217
+ @media (forced-colors: active) {
218
+ .np-background {
219
+ background-color: CanvasText;
220
+ }
221
+
222
+ .np-container:has(input:disabled:checked) .np-background {
223
+ background-color: GrayText;
224
+ opacity: 1;
225
+ }
226
+
227
+ .np-outline {
228
+ border-color: CanvasText;
229
+ }
230
+
231
+ .np-container:has(input:disabled) .np-outline {
232
+ border-color: GrayText;
233
+ opacity: 1;
234
+ }
235
+
236
+ .np-icon {
237
+ fill: Canvas;
238
+ }
239
+ }
217
240
  </style>
@@ -8,6 +8,7 @@
8
8
  element = $bindable(),
9
9
  showPopover = $bindable(),
10
10
  hidePopover = $bindable(),
11
+ position = 'bottom',
11
12
  ...attributes
12
13
  }: MenuProps = $props()
13
14
 
@@ -83,7 +84,7 @@
83
84
  bind:clientWidth
84
85
  bind:clientHeight
85
86
  popover="auto"
86
- class={['np-menu', attributes.class]}
87
+ class={[position, 'np-menu', attributes.class]}
87
88
  role="menu"
88
89
  >
89
90
  {@render children()}
@@ -101,15 +102,25 @@
101
102
  margin: 0;
102
103
  margin-bottom: 2px;
103
104
  margin-top: 2px;
104
- top: anchor(bottom);
105
- position-try-fallbacks: --menu-top;
106
- justify-self: anchor-center;
107
105
  transition:
108
106
  display 0.2s allow-discrete,
109
107
  opacity 0.2s linear;
110
108
  opacity: 0;
111
109
  z-index: 1;
112
110
  }
111
+ .bottom-left.np-menu[popover] {
112
+ top: anchor(bottom);
113
+ left: anchor(left);
114
+ justify-self: anchor-center;
115
+ position-try-fallbacks: --menu-top-left;
116
+ }
117
+
118
+ .bottom.np-menu[popover] {
119
+ top: anchor(bottom);
120
+ position-try-fallbacks: --menu-top;
121
+ justify-self: anchor-center;
122
+ }
123
+
113
124
  .np-menu:popover-open {
114
125
  opacity: 1;
115
126
  }
@@ -122,4 +133,9 @@
122
133
  inset: auto;
123
134
  bottom: anchor(top);
124
135
  }
136
+ @position-try --menu-top-left {
137
+ inset: auto;
138
+ bottom: anchor(top);
139
+ left: anchor(left);
140
+ }
125
141
  </style>
@@ -5,6 +5,7 @@ export interface MenuProps extends HTMLAttributes<HTMLDivElement> {
5
5
  anchor?: HTMLElement | undefined;
6
6
  showPopover?: () => void;
7
7
  hidePopover?: () => void;
8
+ position?: 'bottom-left' | 'bottom';
8
9
  element?: HTMLDivElement;
9
10
  }
10
11
  interface ButtonProps extends HTMLButtonAttributes {
@@ -1,45 +1,774 @@
1
1
  <script lang="ts">
2
2
  import Menu from '../menu/Menu.svelte'
3
- import TextField from '../text-field/TextField.svelte'
3
+ import { isFirstInvalidControlInForm } from '../text-field/report-validity.js'
4
+ import { generateUUIDv4 } from '../utils.ts'
5
+ import type { SelectProps } from './types.ts'
6
+ import Item from '../list/Item.svelte'
4
7
 
5
- let { children, value = $bindable() } = $props()
8
+ let {
9
+ options = [],
10
+ value = $bindable(),
11
+ error = false,
12
+ errorText = '',
13
+ supportingText = '',
14
+ start,
15
+ label,
16
+ style,
17
+ noAsterisk = false,
18
+ variant = 'filled',
19
+ element = $bindable(),
20
+ ...attributes
21
+ }: SelectProps = $props()
22
+
23
+ let errorTextRaw: string = $state(errorText)
24
+ $effect(() => {
25
+ errorTextRaw = errorText
26
+ })
27
+ let selectElement: HTMLSelectElement | undefined = $state()
28
+ let menuElement: HTMLDivElement | undefined = $state()
29
+ let menuId = $state(`--select-${generateUUIDv4()}`)
30
+ let menuOpen = $state(false)
31
+ let selectedLabel = $derived<string>(
32
+ options.find((option) => option.value === value)?.label || '',
33
+ )
34
+ let clientWidth = $state(0)
35
+ $effect(() => {
36
+ if (value !== '') {
37
+ error = false
38
+ errorTextRaw = errorText
39
+ }
40
+ })
41
+ $effect(() => {
42
+ if (selectElement) {
43
+ selectElement.form?.addEventListener('reset', () => {
44
+ error = false
45
+ value = ''
46
+ })
47
+ selectElement.addEventListener('invalid', (event) => {
48
+ event.preventDefault()
49
+ const { currentTarget } = event as Event & {
50
+ currentTarget: HTMLInputElement | HTMLTextAreaElement
51
+ }
52
+ error = true
53
+ if (errorText === '') {
54
+ errorTextRaw = currentTarget.validationMessage
55
+ }
56
+ if (isFirstInvalidControlInForm(currentTarget.form, currentTarget)) {
57
+ currentTarget.focus()
58
+ }
59
+ })
60
+
61
+ selectElement.addEventListener('select', (event) => {
62
+ const { currentTarget } = event as Event & {
63
+ currentTarget: HTMLSelectElement
64
+ }
65
+ if (currentTarget.checkValidity()) {
66
+ error = false
67
+ errorTextRaw = errorText
68
+ }
69
+ })
70
+ }
71
+ })
6
72
  </script>
7
73
 
8
- <select class="custom-select">
9
- <option>Option 1</option>
10
- <option>Option 2</option>
11
- <option>Option 3</option>
12
- </select>
74
+ {#snippet arrows()}
75
+ <svg height="5" viewBox="7 10 10 5" focusable="false">
76
+ <polygon class="down" stroke="none" fill-rule="evenodd" points="7 10 12 15 17 10"></polygon>
77
+ <polygon class="up" stroke="none" fill-rule="evenodd" points="7 15 12 10 17 15"></polygon>
78
+ </svg>
79
+ {/snippet}
80
+
81
+ <label
82
+ style={(variant === 'outlined'
83
+ ? '--top-space:1rem;--bottom-space:1rem;--floating-label-top:-0.5rem;--floating-label-left:-2.25rem;--_focus-outline-width:3px;'
84
+ : !label?.length
85
+ ? '--top-space:1rem;--bottom-space:1rem;'
86
+ : '') + style}
87
+ class={['text-field', attributes.class]}
88
+ bind:this={element}
89
+ bind:clientWidth
90
+ >
91
+ <div
92
+ class="field"
93
+ class:error
94
+ class:no-label={!label?.length}
95
+ class:with-start={start}
96
+ class:menu-open={menuOpen}
97
+ class:with-end={true}
98
+ class:disabled={attributes.disabled}
99
+ class:outlined={variant === 'outlined'}
100
+ >
101
+ <div class="container-overflow">
102
+ {#if variant === 'filled'}
103
+ <div class="background"></div>
104
+ <div class="state-layer"></div>
105
+ <div class="active-indicator"></div>
106
+ {/if}
107
+ {#if variant === 'outlined'}
108
+ <div class="np-outline">
109
+ <div class="outline-start"></div>
110
+ {#if label?.length}
111
+ <div class="label-wrapper">
112
+ <span class="label">{label}{noAsterisk || !attributes.required ? '' : '*'} </span>
113
+ </div>
114
+ <div class="outline-notch">
115
+ <span class="notch np-hidden" aria-hidden="true"
116
+ >{label}{noAsterisk || !attributes.required ? '' : '*'}</span
117
+ >
118
+ </div>
119
+ {/if}
120
+ <div class="outline-end"></div>
121
+ </div>
122
+ {/if}
123
+ <div class="np-container" style="anchor-name:{menuId};">
124
+ {#if start}
125
+ <div class="start">
126
+ <span class="icon leading">{@render start()}</span>
127
+ </div>
128
+ {/if}
129
+ <div class="middle">
130
+ {#if variant === 'filled'}
131
+ <div class="label-wrapper">
132
+ {#if label?.length}
133
+ <span class="label">{label}{noAsterisk || !attributes.required ? '' : '*'} </span>
134
+ {/if}
135
+ </div>
136
+ {/if}
137
+ <div class="content">
138
+ <select
139
+ aria-label={label}
140
+ {...attributes}
141
+ onclick={(event) => {
142
+ menuElement?.togglePopover(true)
143
+ event.preventDefault()
144
+ }}
145
+ onkeydown={(event) => {
146
+ if (event.key === 'Tab') {
147
+ menuElement?.togglePopover(false)
148
+ } else {
149
+ event.preventDefault()
150
+ if (
151
+ event.key === 'ArrowDown' ||
152
+ event.key === 'ArrowUp' ||
153
+ event.key === 'Enter'
154
+ ) {
155
+ menuElement?.showPopover()
156
+ ;(menuElement?.firstElementChild as HTMLElement)?.focus()
157
+ }
158
+ }
159
+ }}
160
+ bind:value
161
+ bind:this={selectElement}
162
+ >
163
+ {#each options as option}
164
+ <option value={option.value} selected={option.value === value}
165
+ >{option.label}</option
166
+ >
167
+ {/each}
168
+ </select>
169
+ <div class="input">
170
+ {#if selectedLabel}
171
+ {selectedLabel}
172
+ {:else}
173
+ &nbsp;
174
+ {/if}
175
+ </div>
176
+ </div>
177
+ </div>
178
+ <div class="end">
179
+ <span class="icon trailing">
180
+ {@render arrows()}
181
+ </span>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ {#if supportingText || (errorTextRaw && error)}
186
+ <div class="supporting-text" role={error ? 'alert' : undefined}>
187
+ <span>
188
+ {error && errorTextRaw ? errorTextRaw : supportingText}
189
+ </span>
190
+ </div>
191
+ {/if}
192
+ </div>
193
+ </label>
194
+
195
+ <Menu
196
+ style="position-anchor:{menuId};min-width: {clientWidth}px;"
197
+ popover="manual"
198
+ position="bottom-left"
199
+ anchor={element}
200
+ ontoggle={({ newState }) => {
201
+ if (newState === 'open') {
202
+ menuOpen = true
203
+ } else {
204
+ menuOpen = false
205
+ }
206
+ }}
207
+ bind:element={menuElement}
208
+ >
209
+ {#each options as option}
210
+ <Item
211
+ onclick={(event) => {
212
+ value = option.value
213
+ menuElement?.hidePopover()
214
+ selectElement?.focus()
215
+ event.preventDefault()
216
+ }}
217
+ onkeydown={(event) => {
218
+ if (event.key === 'ArrowDown') {
219
+ ;(event.currentTarget?.nextElementSibling as HTMLElement)?.focus()
220
+ event.preventDefault()
221
+ }
222
+ if (event.key === 'ArrowUp') {
223
+ ;(event.currentTarget?.previousElementSibling as HTMLElement)?.focus()
224
+ event.preventDefault()
225
+ }
226
+ if (event.key === 'Enter') {
227
+ value = option.value
228
+ menuElement?.hidePopover()
229
+ event.preventDefault()
230
+ }
231
+ }}
232
+ variant="button"
233
+ selected={value === option.value}>{option.label}</Item
234
+ >
235
+ {/each}
236
+ </Menu>
13
237
 
14
238
  <style>
15
- .custom-select {
16
- appearance: none; /* Entfernt standardmäßigen Stil */
17
- -webkit-appearance: none; /* Für Safari */
18
- -moz-appearance: none; /* Für Firefox */
19
- background-color: none;
20
- border-width: 1px;
21
- border-style: solid;
22
- border-color: var(--np-color-outline);
23
- border-radius: var(--np-shape-corner-extra-small);
24
- padding: 8px 12px;
25
- font-size: 16px;
239
+ .active-indicator {
240
+ inset: auto 0 0 0;
241
+ pointer-events: none;
242
+ position: absolute;
243
+ width: 100%;
244
+ z-index: 1;
245
+ }
246
+ .field.menu-open .active-indicator::after,
247
+ .field:has(select:focus-visible) .active-indicator::after {
248
+ opacity: 1;
249
+ }
250
+ .active-indicator::after {
251
+ opacity: 0;
252
+ transition: opacity 150ms cubic-bezier(0.2, 0, 0, 1);
253
+ }
254
+ .active-indicator::before,
255
+ .active-indicator::after {
256
+ border-bottom: 1px solid var(--np-color-on-surface-variant);
257
+ inset: auto 0 0 0;
258
+ content: '';
259
+ position: absolute;
260
+ width: 100%;
261
+ }
262
+ .active-indicator::after {
263
+ border-bottom-color: var(--np-color-primary);
264
+ border-bottom-width: 3px;
265
+ }
266
+ .error .active-indicator::before {
267
+ border-bottom-color: var(--np-color-error);
268
+ }
269
+ .error .active-indicator::after {
270
+ border-bottom-color: var(--np-color-error);
271
+ }
272
+ .disabled .active-indicator::before {
273
+ border-bottom-color: var(--np-color-on-surface);
274
+ border-bottom-width: 1px;
275
+ opacity: 0.38;
276
+ }
277
+ .background {
278
+ background: var(
279
+ --np-text-field-filled-background-color,
280
+ var(--np-color-surface-container-highest)
281
+ );
282
+ }
283
+ .disabled .background {
284
+ background: var(--np-color-on-surface);
285
+ opacity: 0.04;
286
+ }
287
+ .background,
288
+ .state-layer {
289
+ border-radius: inherit;
290
+ inset: 0;
291
+ pointer-events: none;
292
+ position: absolute;
293
+ }
294
+ .np-container {
295
+ align-items: center;
296
+ border-radius: inherit;
297
+ display: flex;
298
+ flex: 1;
299
+ max-height: 100%;
300
+ min-height: 100%;
301
+ min-width: min-content;
302
+ position: relative;
303
+ }
304
+ .outlined .container-overflow {
305
+ border-start-start-radius: var(--np-shape-corner-extra-small);
306
+ border-start-end-radius: var(--np-shape-corner-extra-small);
307
+ border-end-end-radius: var(--np-shape-corner-extra-small);
308
+ border-end-start-radius: var(--np-shape-corner-extra-small);
309
+ }
310
+ .container-overflow {
311
+ border-start-start-radius: var(--np-shape-corner-extra-small);
312
+ border-start-end-radius: var(--np-shape-corner-extra-small);
313
+ border-end-end-radius: var(--np-shape-corner-none);
314
+ border-end-start-radius: var(--np-shape-corner-none);
315
+ display: flex;
316
+ height: 100%;
317
+ position: relative;
318
+ }
319
+ .text-field {
320
+ display: inline-flex;
321
+ resize: both;
322
+ text-align: start;
323
+ }
324
+
325
+ .field.disabled {
326
+ cursor: default;
327
+ }
328
+
329
+ .field {
330
+ display: flex;
331
+ flex: 1;
332
+ flex-direction: column;
333
+ writing-mode: horizontal-tb;
334
+ max-width: 100%;
335
+ min-width: 210px;
336
+ }
337
+
338
+ .supporting-text {
339
+ display: flex;
340
+ gap: 1rem;
341
+ font-size: 0.75rem;
342
+ line-height: 1rem;
343
+ color: var(--np-color-on-surface-variant);
344
+ justify-content: space-between;
345
+ padding: 0.25rem 1rem 0;
346
+ }
347
+ .error .supporting-text {
348
+ color: var(--np-color-error);
349
+ }
350
+ .disabled .supporting-text {
351
+ color: var(--np-color-on-surface);
352
+ opacity: 0.38;
353
+ }
354
+
355
+ .field:not(.disabled):hover .state-layer {
356
+ visibility: visible;
357
+ }
358
+
359
+ .disabled {
360
+ pointer-events: none;
361
+ }
362
+ .field:not(.disabled):hover .state-layer {
363
+ background: var(--np-color-on-surface);
364
+ opacity: 0.08;
365
+ }
366
+ .resizable .np-container > * {
367
+ top: var(--_focus-outline-width, 3px);
368
+ inset-inline-start: var(--_focus-outline-width, 0);
369
+ }
370
+ .content * {
371
+ all: unset;
372
+ color: currentColor;
373
+ font-size: 1rem;
374
+ line-height: 1.5rem;
375
+ overflow-wrap: revert;
376
+ white-space: revert;
377
+ }
378
+
379
+ .content select {
380
+ width: 0;
381
+ }
382
+
383
+ .middle {
384
+ align-items: stretch;
385
+ align-self: baseline;
386
+ flex: 1;
387
+ }
388
+
389
+ .input {
390
+ caret-color: var(--np-color-primary);
391
+ overflow-x: hidden;
392
+ text-align: inherit;
393
+ width: 100%;
394
+
395
+ &::placeholder {
396
+ color: currentColor;
397
+ opacity: 1;
398
+ }
399
+
400
+ &::-webkit-calendar-picker-indicator {
401
+ display: none;
402
+ }
403
+
404
+ &::-webkit-search-decoration,
405
+ &::-webkit-search-cancel-button {
406
+ display: none;
407
+ }
408
+
409
+ @media (forced-colors: active) {
410
+ background: none;
411
+ }
412
+ }
413
+
414
+ .no-label .content,
415
+ .field.menu-open .content,
416
+ .field:has(select:focus-visible) .content,
417
+ .field:has(select option:checked:not([value=''])) .content {
418
+ opacity: 1;
419
+ }
420
+
421
+ .field:not(.error).menu-open .down,
422
+ .field:not(.error):has(select:focus-visible) .down {
423
+ color: var(--np-color-primary);
424
+ }
425
+ .icon .down {
426
+ transition: color 75ms linear 75ms;
427
+ }
428
+ .icon .up {
429
+ opacity: 0;
430
+ transition: color 75ms linear 75ms;
431
+ }
432
+
433
+ .content {
434
+ color: var(--np-color-on-surface);
435
+ display: flex;
436
+ flex: 1 1 0%;
437
+ opacity: 1;
438
+ transition: opacity 83ms cubic-bezier(0.2, 0, 0, 1);
439
+ }
440
+ .disabled .content {
441
+ color: var(--np-color-on-surface);
442
+ }
443
+ .field:not(.with-end) .content .input-wrapper,
444
+ .field:not(.with-end) .content .input {
445
+ padding-inline-end: 16px;
446
+ }
447
+ .outline-start,
448
+ .field:not(.with-start) .content .input-wrapper,
449
+ .field:not(.with-start) .content .input {
450
+ padding-inline-start: 16px;
451
+ }
452
+
453
+ .content .input {
454
+ padding-top: var(--top-space, 1.5rem);
455
+ padding-bottom: var(--bottom-space, 0.5rem);
456
+ }
457
+
458
+ .input-wrapper {
459
+ display: flex;
460
+ }
461
+
462
+ .input-wrapper > * {
463
+ all: inherit;
464
+ padding: 0;
465
+ }
466
+
467
+ .content .input-wrapper {
468
+ padding-top: var(--top-space, 1.5rem);
469
+ padding-bottom: var(--bottom-space, 0.5rem);
470
+ }
471
+
472
+ .start {
473
+ color: var(--np-color-on-surface-variant);
474
+ margin-left: 0.75rem;
475
+ margin-right: 1rem;
476
+ }
477
+ .end {
478
+ color: var(--np-color-on-surface-variant);
479
+ margin-left: 1rem;
480
+ margin-right: 0.75rem;
481
+ }
482
+ .error .start,
483
+ .error .end {
484
+ color: var(--np-color-error);
485
+ }
486
+ .disabled .start,
487
+ .disabled .end {
26
488
  color: var(--np-color-on-surface);
27
- cursor: pointer;
28
- width: 200px;
489
+ opacity: 0.38;
490
+ }
491
+ .start,
492
+ .middle,
493
+ .end {
494
+ display: flex;
495
+ box-sizing: border-box;
496
+ height: 100%;
29
497
  position: relative;
30
498
  }
499
+ .start,
500
+ .end {
501
+ align-items: center;
502
+ justify-content: center;
503
+ }
504
+ .icon {
505
+ display: flex;
506
+ color: currentColor;
507
+ align-items: center;
508
+ justify-content: center;
509
+ fill: currentColor;
510
+ position: relative;
511
+ }
512
+ :global(.icon svg) {
513
+ fill: currentColor;
514
+ }
31
515
 
32
- .custom-select:focus {
33
- outline: none;
34
- border-color: var(--np-color-primary);
35
- border-width: 3px;
516
+ .label-wrapper {
517
+ user-select: none;
518
+ pointer-events: none;
519
+ inset: 0;
520
+ position: absolute;
521
+ text-align: initial;
522
+ }
523
+ .field:not(.with-end) .label-wrapper {
524
+ margin-inline-end: 1rem;
525
+ }
526
+ .field:not(.with-start) .label-wrapper {
527
+ margin-inline-start: 1rem;
528
+ }
529
+ .with-start .np-outline .label-wrapper {
530
+ left: 3.25rem;
531
+ }
532
+ .with-end .np-outline .label-wrapper {
533
+ margin-inline-end: 3.25rem;
534
+ }
535
+
536
+ .with-start.menu-open .label-wrapper,
537
+ .with-start:has(select:focus-visible option:checked:not([value=''])) .label-wrapper,
538
+ .with-start:has(select option:checked:not([value=''])) .label-wrapper,
539
+ .with-start:has(select:focus-visible) .label-wrapper {
540
+ right: -2.25rem;
36
541
  }
37
542
 
38
- /* Optional: Ein Pfeil-Icon simulieren */
39
- .custom-select::after {
40
- content: '';
543
+ .with-end.menu-open .label-wrapper,
544
+ .with-end:has(select:focus-visible option:checked:not([value=''])) .label-wrapper,
545
+ .with-end:has(select option:checked:not([value=''])) .label-wrapper,
546
+ .with-end:has(select:focus-visible) .label-wrapper {
547
+ margin-inline-end: 1rem;
548
+ }
549
+ .notch {
550
+ font-size: 0.75rem;
551
+ line-height: 1rem;
552
+ }
553
+ .notch.np-hidden {
554
+ opacity: 0;
555
+ }
556
+
557
+ .label.np-hidden {
558
+ opacity: 0;
559
+ }
560
+
561
+ .field:not(.menu-open):has(select:not(:focus-visible)) .label {
562
+ position: absolute;
563
+ top: 1rem;
564
+ left: 0rem;
565
+ }
566
+
567
+ .field.menu-open .label,
568
+ .field:has(select:focus-visible option:checked:not([value=''])) .label,
569
+ .field:has(select option:checked:not([value=''])) .label,
570
+ .field:has(select:focus-visible) .label {
571
+ font-size: 0.75rem;
572
+ line-height: 1rem;
573
+ transform-origin: top left;
574
+ position: absolute;
575
+ top: var(--floating-label-top, 0.5rem);
576
+ }
577
+
578
+ .with-start.menu-open .label,
579
+ .with-start:has(select:focus-visible option:checked:not([value=''])) .label,
580
+ .with-start:has(select option:checked:not([value=''])) .label,
581
+ .with-start:has(select:focus-visible) .label {
582
+ left: var(--floating-label-left, 0);
583
+ }
584
+ .label {
585
+ transition-property: all;
586
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
587
+ transition-duration: 150ms;
588
+ box-sizing: border-box;
589
+ color: var(--np-color-on-surface-variant);
590
+ overflow: hidden;
591
+ max-width: 100%;
592
+ text-overflow: ellipsis;
593
+ white-space: nowrap;
594
+ z-index: 1;
595
+ font-size: 1rem;
596
+ line-height: 1.5rem;
597
+ width: min-content;
598
+ }
599
+
600
+ .field.menu-open .label,
601
+ .field:has(select:focus-visible) .label {
602
+ color: var(--np-color-primary);
603
+ }
604
+ .error .label,
605
+ .error.menu-open .label,
606
+ .error:has(select:focus-visible) .label {
607
+ color: var(--np-color-error);
608
+ }
609
+ .disabled .label {
610
+ color: var(--np-color-on-surface);
611
+ }
612
+ .disabled .label:not(.np-hidden) {
613
+ opacity: 0.38;
614
+ }
615
+ .resizable:not(.disabled) .np-container {
616
+ resize: inherit;
617
+ overflow: hidden;
618
+ }
619
+ .disabled.no-label .content,
620
+ .disabled:has(select option:checked:not([value=''])) .content {
621
+ opacity: 0.38;
622
+ }
623
+ .field,
624
+ .container-overflow {
625
+ resize: inherit;
626
+ }
627
+ .resizable .np-container {
628
+ bottom: 3px;
629
+ inset-inline-end: var(--_focus-outline-width, 0);
630
+ clip-path: inset(3px 0 0 var(--_focus-outline-width));
631
+ }
632
+ .outline-start,
633
+ .outline-end {
634
+ border: inherit;
635
+ border-radius: inherit;
636
+ box-sizing: border-box;
637
+ position: relative;
638
+ }
639
+ .outline-start::before,
640
+ .outline-start::after,
641
+ .outline-end::before,
642
+ .outline-end::after {
643
+ border: inherit;
644
+ content: '';
645
+ inset: 0;
41
646
  position: absolute;
42
- right: 12px;
647
+ }
648
+ .outline-start::before,
649
+ .outline-start::after {
650
+ border-inline-start-style: solid;
651
+ border-inline-end-style: none;
652
+ border-start-start-radius: inherit;
653
+ border-start-end-radius: 0;
654
+ border-end-start-radius: inherit;
655
+ border-end-end-radius: 0;
656
+ margin-inline-end: 0.25rem;
657
+ }
658
+ .outline-end::before,
659
+ .outline-end::after {
660
+ border-inline-start-style: none;
661
+ border-inline-end-style: solid;
662
+ border-start-start-radius: 0;
663
+ border-start-end-radius: inherit;
664
+ border-end-start-radius: 0;
665
+ border-end-end-radius: inherit;
666
+ }
667
+ .outline-notch::before,
668
+ .outline-notch::after {
669
+ border: inherit;
670
+ content: '';
671
+ inset: 0;
672
+ position: absolute;
673
+ }
674
+ .outline-start::before,
675
+ .outline-end::before,
676
+ .outline-notch::before {
677
+ border-width: 1px;
678
+ }
679
+ .outline-start::before,
680
+ .outline-start::after,
681
+ .outline-end::before,
682
+ .outline-end::after {
683
+ border-bottom-style: solid;
684
+ border-top-style: solid;
685
+ }
686
+ .outline-notch::after {
687
+ border-bottom-style: solid;
688
+ border-top-style: none;
689
+ }
690
+ .outline-notch::before {
691
+ border-bottom-style: solid;
692
+ border-top-style: solid;
693
+ }
694
+
695
+ .field.menu-open .outline-notch::before,
696
+ .field:has(select:focus-visible) .outline-notch::before,
697
+ .field:has(select option:checked:not([value=''])) .outline-notch::before {
698
+ border-top-style: none;
699
+ }
700
+
701
+ .outline-notch::before,
702
+ .outline-notch::after {
703
+ border-inline-start-style: none;
704
+ border-inline-end-style: none;
705
+ border-start-start-radius: 0;
706
+ border-start-end-radius: 0;
707
+ border-end-start-radius: 0;
708
+ border-end-end-radius: 0;
709
+ }
710
+ .outline-notch {
711
+ align-items: flex-start;
712
+ border: inherit;
713
+ display: flex;
714
+ margin-inline-start: -0.25rem;
715
+ margin-inline-end: 0.25rem;
716
+ max-width: calc(100% - 2rem);
717
+ padding: 0 0.25rem;
718
+ position: relative;
719
+ }
720
+ .outline-end {
721
+ flex-grow: 1;
722
+ margin-inline-start: calc(-1 * 4px);
723
+ }
724
+ .outline-start::after,
725
+ .outline-end::after,
726
+ .outline-notch::after {
727
+ border-width: 3px;
728
+ }
729
+ .outline-start::after,
730
+ .outline-end::after,
731
+ .outline-notch::after {
732
+ opacity: 0;
733
+ transition: opacity 150ms cubic-bezier(0.2, 0, 0, 1);
734
+ }
735
+
736
+ .field.menu-open .outline-start::after,
737
+ .field.menu-open .outline-end::after,
738
+ .field.menu-open .outline-notch::after,
739
+ .field:has(select:focus-visible) .outline-start::after,
740
+ .field:has(select:focus-visible) .outline-end::after,
741
+ .field:has(select:focus-visible) .outline-notch::after {
742
+ opacity: 1;
743
+ }
744
+ .np-outline {
745
+ border-color: var(--np-color-outline);
746
+ border-radius: inherit;
747
+ display: flex;
43
748
  pointer-events: none;
749
+ height: 100%;
750
+ position: absolute;
751
+ width: 100%;
752
+ z-index: 1;
753
+ }
754
+
755
+ .field.menu-open .np-outline,
756
+ .field:has(select:focus-visible) .np-outline {
757
+ border-color: var(--np-color-primary);
758
+ color: var(--np-color-primary);
759
+ }
760
+ .error .np-outline,
761
+ .error.menu-open .np-outline,
762
+ .error:has(select:focus-visible) .np-outline {
763
+ border-color: var(--np-color-error);
764
+ }
765
+ .disabled .np-outline {
766
+ border-color: var(--np-color-on-surface);
767
+ color: var(--np-color-on-surface);
768
+ }
769
+ .disabled .outline-start,
770
+ .disabled .outline-end,
771
+ .disabled .outline-notch {
772
+ opacity: 0.12;
44
773
  }
45
774
  </style>
@@ -1,6 +1,4 @@
1
- declare const Select: import("svelte").Component<{
2
- children: any;
3
- value?: any;
4
- }, {}, "value">;
1
+ import type { SelectProps } from './types.ts';
2
+ declare const Select: import("svelte").Component<SelectProps, {}, "element" | "value">;
5
3
  type Select = ReturnType<typeof Select>;
6
4
  export default Select;
@@ -1,6 +1,17 @@
1
- import type { HTMLAttributes } from 'svelte/elements';
2
- export interface SelectProps extends HTMLAttributes<HTMLDivElement> {
3
- forceHover?: boolean;
4
- element?: HTMLDivElement;
5
- forElement?: HTMLElement;
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLSelectAttributes } from 'svelte/elements';
3
+ export interface SelectProps extends HTMLSelectAttributes {
4
+ label?: string;
5
+ supportingText?: string;
6
+ error?: boolean;
7
+ errorText?: string;
8
+ variant?: 'outlined' | 'filled';
9
+ start?: Snippet;
10
+ end?: Snippet;
11
+ noAsterisk?: boolean;
12
+ element?: HTMLSpanElement;
13
+ options: {
14
+ value: string | number;
15
+ label: string;
16
+ }[];
6
17
  }
@@ -4,11 +4,11 @@
4
4
 
5
5
  let {
6
6
  value = $bindable(),
7
- error,
8
- errorText,
9
- prefixText,
10
- suffixText,
11
- supportingText,
7
+ error = false,
8
+ errorText = '',
9
+ prefixText = '',
10
+ suffixText = '',
11
+ supportingText = '',
12
12
  start,
13
13
  end,
14
14
  label,
@@ -20,7 +20,7 @@
20
20
  ...attributes
21
21
  }: TextFieldProps = $props()
22
22
 
23
- let errorTextRaw: string | undefined = $state()
23
+ let errorTextRaw: string = $state(errorText)
24
24
  $effect(() => {
25
25
  errorTextRaw = errorText
26
26
  })
@@ -38,7 +38,7 @@
38
38
  currentTarget: HTMLInputElement | HTMLTextAreaElement
39
39
  }
40
40
  error = true
41
- if (errorText === undefined) {
41
+ if (errorText === '') {
42
42
  errorTextRaw = currentTarget.validationMessage
43
43
  }
44
44
  if (isFirstInvalidControlInForm(currentTarget.form, currentTarget)) {
@@ -88,6 +88,9 @@
88
88
  <div class="np-outline">
89
89
  <div class="outline-start"></div>
90
90
  {#if label?.length}
91
+ <div class="label-wrapper">
92
+ <span class="label">{label}{noAsterisk || !attributes.required ? '' : '*'} </span>
93
+ </div>
91
94
  <div class="outline-notch">
92
95
  <span class="notch np-hidden" aria-hidden="true"
93
96
  >{label}{noAsterisk || !attributes.required ? '' : '*'}</span
@@ -104,11 +107,13 @@
104
107
  </div>
105
108
  {/if}
106
109
  <div class="middle">
107
- <div class="label-wrapper">
108
- {#if label?.length}
109
- <span class="label">{label}{noAsterisk || !attributes.required ? '' : '*'} </span>
110
- {/if}
111
- </div>
110
+ {#if variant === 'filled'}
111
+ <div class="label-wrapper">
112
+ {#if label?.length}
113
+ <span class="label">{label}{noAsterisk || !attributes.required ? '' : '*'} </span>
114
+ {/if}
115
+ </div>
116
+ {/if}
112
117
  <div class="content">
113
118
  {#if attributes.type === 'textarea'}
114
119
  <textarea
@@ -458,6 +463,29 @@
458
463
  .field:not(.with-start) .label-wrapper {
459
464
  margin-inline-start: 1rem;
460
465
  }
466
+ .with-start .np-outline .label-wrapper {
467
+ left: 3.25rem;
468
+ }
469
+ .with-end .np-outline .label-wrapper {
470
+ margin-inline-end: 3.25rem;
471
+ }
472
+ .with-start:has(input:focus-visible:not(:placeholder-shown)) .label-wrapper,
473
+ .with-start:has(input:focus-visible) .label-wrapper,
474
+ .with-start:has(input:not(:placeholder-shown)) .label-wrapper,
475
+ .with-start:has(textarea:focus-visible:not(:placeholder-shown)) .label-wrapper,
476
+ .with-start:has(textarea:focus-visible) .label-wrapper,
477
+ .with-start:has(textarea:not(:placeholder-shown)) .label-wrapper {
478
+ right: -2.25rem;
479
+ }
480
+
481
+ .with-end:has(input:focus-visible:not(:placeholder-shown)) .label-wrapper,
482
+ .with-end:has(input:focus-visible) .label-wrapper,
483
+ .with-end:has(input:not(:placeholder-shown)) .label-wrapper,
484
+ .with-end:has(textarea:focus-visible:not(:placeholder-shown)) .label-wrapper,
485
+ .with-end:has(textarea:focus-visible) .label-wrapper,
486
+ .with-end:has(textarea:not(:placeholder-shown)) .label-wrapper {
487
+ margin-inline-end: 1rem;
488
+ }
461
489
  .notch {
462
490
  font-size: 0.75rem;
463
491
  line-height: 1rem;
@@ -608,7 +636,8 @@
608
636
  }
609
637
  .field:has(input:focus-visible) .outline-notch::before,
610
638
  .field:has(textarea:focus-visible) .outline-notch::before,
611
- .field:has(input:not(:placeholder-shown)) .outline-notch::before {
639
+ .field:has(input:not(:placeholder-shown)) .outline-notch::before,
640
+ .field:has(textarea:not(:placeholder-shown)) .outline-notch::before {
612
641
  border-top-style: none;
613
642
  }
614
643
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.7.8",
3
+ "version": "0.8.1",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {
@@ -55,7 +55,7 @@
55
55
  "@material/material-color-utilities": "^0.3.0",
56
56
  "@playwright/test": "^1.49.1",
57
57
  "@sveltejs/adapter-vercel": "^5.5.2",
58
- "@sveltejs/kit": "^2.15.0",
58
+ "@sveltejs/kit": "^2.15.1",
59
59
  "@sveltejs/package": "^2.3.7",
60
60
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
61
61
  "@types/eslint": "^9.6.1",
@@ -69,7 +69,7 @@
69
69
  "svelte": "^5.16.0",
70
70
  "svelte-check": "^4.1.1",
71
71
  "typescript": "^5.7.2",
72
- "typescript-eslint": "^8.18.2",
72
+ "typescript-eslint": "^8.19.0",
73
73
  "vite": "^6.0.6",
74
74
  "vitest": "^2.1.8"
75
75
  },