noph-ui 0.25.5 → 0.26.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.
@@ -8,7 +8,7 @@
8
8
  let {
9
9
  options = [],
10
10
  value = $bindable(),
11
- variant = 'filled',
11
+ variant = 'outlined',
12
12
  element = $bindable(),
13
13
  populated,
14
14
  reportValidity = $bindable(),
@@ -0,0 +1,402 @@
1
+ <script lang="ts">
2
+ import type { HTMLSelectAttributes } from 'svelte/elements'
3
+ interface SelectProps extends HTMLSelectAttributes {
4
+ label?: string
5
+ noAsterisk?: boolean
6
+ supportingText?: string
7
+ error?: boolean
8
+ errorText?: string
9
+ variant?: 'outlined' | 'filled'
10
+ element?: HTMLSpanElement
11
+ }
12
+ let {
13
+ id,
14
+ supportingText,
15
+ error,
16
+ errorText,
17
+ variant = 'outlined',
18
+ value = $bindable(),
19
+ label,
20
+ disabled,
21
+ children,
22
+ onchange,
23
+ oninvalid,
24
+ ...attributes
25
+ }: SelectProps = $props()
26
+ const uid = $props.id()
27
+ const selectId = id ?? `select-${uid}`
28
+
29
+ let animateLabel = $state(false)
30
+ let errorTextRaw = $state(errorText)
31
+ let errorRaw = $state(error)
32
+ </script>
33
+
34
+ <div
35
+ class={[
36
+ 'np-select-container',
37
+ variant,
38
+ errorRaw && 'error',
39
+ disabled && 'disabled',
40
+ (value === undefined || value === '' || value === null) && 'is-empty',
41
+ animateLabel && 'label-animate',
42
+ ]}
43
+ aria-disabled={disabled}
44
+ >
45
+ {#if variant === 'outlined'}
46
+ <div class="np-select-outline">
47
+ <div class="np-select-outline-start"></div>
48
+ <div class="np-select-outline-notch">
49
+ {#if label}
50
+ <label for={selectId}>{label}</label>
51
+ {/if}
52
+ </div>
53
+ <div class="np-select-outline-end"></div>
54
+ </div>
55
+ {:else if variant === 'filled'}
56
+ <div class="np-select-filled">
57
+ {#if label}
58
+ <label for={selectId}>{label}</label>
59
+ {/if}
60
+ </div>
61
+ <div class="np-select-state-layer"></div>
62
+ {/if}
63
+ <svg class="arrow" height="5" viewBox="7 10 10 5" focusable="false">
64
+ <polygon stroke="none" fill-rule="evenodd" points="7 10 12 15 17 10"></polygon>
65
+ </svg>
66
+ <select
67
+ onchange={(event) => {
68
+ animateLabel = true
69
+ onchange?.(event)
70
+ }}
71
+ oninvalid={(event) => {
72
+ event.preventDefault()
73
+ const { currentTarget } = event
74
+ errorRaw = true
75
+ if (errorText === undefined) {
76
+ errorTextRaw = currentTarget.validationMessage
77
+ }
78
+ oninvalid?.(event)
79
+ }}
80
+ {disabled}
81
+ id={selectId}
82
+ aria-invalid={error}
83
+ bind:value
84
+ {...attributes}
85
+ >
86
+ {@render children?.()}
87
+ </select>
88
+ {#if supportingText || (errorTextRaw && errorRaw)}
89
+ <div class="supporting-text" role={errorRaw ? 'alert' : undefined}>
90
+ {errorRaw && errorTextRaw ? errorTextRaw : supportingText}
91
+ </div>
92
+ {/if}
93
+ </div>
94
+
95
+ <style>
96
+ .np-select-container {
97
+ --easing-fast: ease-out 150ms;
98
+ all: unset;
99
+ position: relative;
100
+ font-size: 1rem;
101
+ display: inline-block;
102
+ min-width: 200px;
103
+ color: var(--np-color-on-surface-variant);
104
+ }
105
+
106
+ select {
107
+ all: unset;
108
+ &,
109
+ &::picker(select) {
110
+ appearance: base-select;
111
+ scrollbar-width: thin;
112
+ }
113
+ font-size: inherit;
114
+ width: 100%;
115
+ box-sizing: border-box;
116
+ line-height: 1.5rem;
117
+ height: 3.5rem;
118
+ color: var(--np-color-on-surface);
119
+ &::picker-icon {
120
+ display: none;
121
+ }
122
+ }
123
+
124
+ ::picker(select) {
125
+ background-color: var(--np-color-surface-container);
126
+ border-radius: var(--np-shape-corner-extra-small);
127
+ box-shadow: var(--np-elevation-2);
128
+ border: none;
129
+ opacity: 0;
130
+ transition:
131
+ opacity var(--easing-fast),
132
+ display 150ms allow-discrete,
133
+ overlay 150ms allow-discrete;
134
+ }
135
+
136
+ select:open::picker(select) {
137
+ opacity: 1;
138
+ @starting-style {
139
+ opacity: 0;
140
+ }
141
+ }
142
+
143
+ .outlined select {
144
+ padding: 1rem;
145
+ }
146
+
147
+ .filled select {
148
+ padding-inline: 1rem;
149
+ padding-block-start: 1.5rem;
150
+ padding-block-end: 0.5rem;
151
+ }
152
+
153
+ .np-select-outline {
154
+ pointer-events: none;
155
+ width: 100%;
156
+ height: 3.5rem;
157
+ position: absolute;
158
+ display: flex;
159
+ box-sizing: border-box;
160
+ border-color: var(--np-color-outline);
161
+ }
162
+ .np-select-container:hover .np-select-outline {
163
+ border-color: var(--np-color-on-surface);
164
+ color: var(--np-color-on-surface);
165
+ }
166
+
167
+ .np-select-container:focus-within .np-select-outline {
168
+ border-color: var(--np-color-primary);
169
+ }
170
+
171
+ .np-select-outline-start,
172
+ .np-select-outline-notch,
173
+ .np-select-outline-end {
174
+ transition:
175
+ border-color var(--easing-fast),
176
+ border-width var(--easing-fast);
177
+ }
178
+ .np-select-outline-start {
179
+ width: 0.75rem;
180
+ box-sizing: inherit;
181
+ border-style: solid;
182
+ border-color: inherit;
183
+ border-block-width: 1px;
184
+ border-inline-start-width: 1px;
185
+ border-inline-end-width: 0;
186
+ border-start-start-radius: var(--np-shape-corner-extra-small);
187
+ border-end-start-radius: var(--np-shape-corner-extra-small);
188
+ }
189
+
190
+ .np-select-container:focus-within .np-select-outline-start {
191
+ border-block-width: 3px;
192
+ border-inline-start-width: 3px;
193
+ }
194
+
195
+ .arrow {
196
+ fill: currentColor;
197
+ position: absolute;
198
+ pointer-events: none;
199
+ inset-inline-end: 0.75rem;
200
+ inset-block-start: calc(1.75rem - 3px);
201
+ transition: rotate var(--easing-fast);
202
+ }
203
+
204
+ .np-select-container:has(select:open) .arrow {
205
+ rotate: 180deg;
206
+ }
207
+
208
+ .np-select-outline-notch {
209
+ box-sizing: inherit;
210
+ border-style: solid;
211
+ border-color: inherit;
212
+ border-block-start-width: 0;
213
+ border-block-end-width: 1px;
214
+ border-inline-width: 0;
215
+ }
216
+
217
+ .np-select-container:focus-within .np-select-outline-notch {
218
+ border-block-end-width: 3px;
219
+ }
220
+
221
+ .np-select-outline-end {
222
+ flex: 1;
223
+ box-sizing: inherit;
224
+ border-style: solid;
225
+ border-color: inherit;
226
+ border-block-width: 1px;
227
+ border-inline-start-width: 0;
228
+ border-inline-end-width: 1px;
229
+ border-start-end-radius: var(--np-shape-corner-extra-small);
230
+ border-end-end-radius: var(--np-shape-corner-extra-small);
231
+ }
232
+
233
+ .np-select-container:focus-within .np-select-outline-end {
234
+ border-block-width: 3px;
235
+ border-inline-end-width: 3px;
236
+ }
237
+
238
+ .np-select-filled {
239
+ position: absolute;
240
+ pointer-events: none;
241
+ box-sizing: border-box;
242
+ width: 100%;
243
+ height: 3.5rem;
244
+ z-index: -1;
245
+ background: var(--np-color-surface-container-highest);
246
+ border-start-start-radius: var(--np-shape-corner-extra-small);
247
+ border-start-end-radius: var(--np-shape-corner-extra-small);
248
+ border-block-end: 1px solid var(--np-color-on-surface-variant);
249
+ transition:
250
+ border-color var(--easing-fast),
251
+ border-width var(--easing-fast);
252
+ }
253
+
254
+ .np-select-container:focus-within .np-select-filled {
255
+ border-block-end: 3px solid var(--np-color-primary);
256
+ }
257
+
258
+ .np-select-state-layer {
259
+ position: absolute;
260
+ pointer-events: none;
261
+ width: 100%;
262
+ height: 3.5rem;
263
+ background: var(--np-color-on-surface);
264
+ opacity: 0;
265
+ }
266
+
267
+ .np-select-container:not(.disabled):hover .np-select-state-layer {
268
+ opacity: 0.08;
269
+ }
270
+
271
+ label {
272
+ display: inline-block;
273
+ pointer-events: none;
274
+ line-height: 1rem;
275
+ padding-inline: 0.25rem;
276
+ overflow: hidden;
277
+ font-size: 0.75rem;
278
+ }
279
+
280
+ .np-select-outline label {
281
+ transform: translateY(-0.5rem);
282
+ }
283
+
284
+ .np-select-filled label {
285
+ padding-inline-start: 1rem;
286
+ transform: translateY(0.5rem);
287
+ }
288
+
289
+ .label-animate label {
290
+ transition-property: font-size;
291
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
292
+ transition-duration: 150ms;
293
+ }
294
+
295
+ .np-select-container:focus-within label {
296
+ color: var(--np-color-primary);
297
+ }
298
+
299
+ .label-animate:not(.is-empty) .np-select-outline label {
300
+ animation: slideUpOutline 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
301
+ }
302
+
303
+ @keyframes slideUpOutline {
304
+ from {
305
+ transform: translateY(1rem);
306
+ }
307
+ to {
308
+ transform: translateY(-0.5rem);
309
+ }
310
+ }
311
+
312
+ .label-animate:not(.is-empty) .np-select-filled label {
313
+ animation: slideUpFilled 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
314
+ }
315
+
316
+ @keyframes slideUpFilled {
317
+ from {
318
+ transform: translateY(1.25rem);
319
+ }
320
+ to {
321
+ transform: translateY(0.5rem);
322
+ }
323
+ }
324
+
325
+ .is-empty .np-select-outline label {
326
+ transform: translateY(0);
327
+ font-size: 1rem;
328
+ inset-block-start: 1.25rem;
329
+ position: absolute;
330
+ }
331
+
332
+ .is-empty.label-animate .np-select-outline label {
333
+ animation: slideDownOutline 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
334
+ }
335
+
336
+ @keyframes slideDownOutline {
337
+ from {
338
+ transform: translateY(-1.5rem);
339
+ }
340
+ to {
341
+ transform: translateY(0);
342
+ }
343
+ }
344
+
345
+ .is-empty .np-select-filled label {
346
+ font-size: 1rem;
347
+ transform: translateY(1.25rem);
348
+ }
349
+
350
+ .is-empty.label-animate .np-select-filled label {
351
+ animation: slideDownFilled 150ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
352
+ }
353
+
354
+ @keyframes slideDownFilled {
355
+ from {
356
+ transform: translateY(0.5rem);
357
+ }
358
+ to {
359
+ transform: translateY(1.25rem);
360
+ }
361
+ }
362
+
363
+ .supporting-text {
364
+ font-size: 0.75rem;
365
+ line-height: 1rem;
366
+ padding: 0.25rem 1rem 0;
367
+ }
368
+
369
+ .error .supporting-text,
370
+ .error label,
371
+ .error .arrow,
372
+ .error:focus-within label {
373
+ color: var(--np-color-error);
374
+ }
375
+ .error .np-select-outline,
376
+ .error:hover .np-select-outline,
377
+ .error:focus-within .np-select-outline,
378
+ .error .np-select-filled,
379
+ .error:focus-within .np-select-filled {
380
+ border-color: var(--np-color-error);
381
+ }
382
+
383
+ .disabled .np-select-filled {
384
+ background: color-mix(in srgb, var(--np-color-on-surface) 4%, transparent);
385
+ border-block-end-color: color-mix(in srgb, var(--np-color-on-surface) 38%, transparent);
386
+ }
387
+
388
+ .disabled,
389
+ .disabled label,
390
+ .disabled .supporting-text,
391
+ .disabled .arrow,
392
+ .disabled:hover .np-select-outline,
393
+ .disabled select,
394
+ .disabled:hover select {
395
+ color: color-mix(in srgb, var(--np-color-on-surface) 38%, transparent);
396
+ }
397
+
398
+ .disabled:hover .np-select-outline,
399
+ .disabled .np-select-outline {
400
+ border-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
401
+ }
402
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { HTMLSelectAttributes } from 'svelte/elements';
2
+ interface SelectProps extends HTMLSelectAttributes {
3
+ label?: string;
4
+ noAsterisk?: boolean;
5
+ supportingText?: string;
6
+ error?: boolean;
7
+ errorText?: string;
8
+ variant?: 'outlined' | 'filled';
9
+ element?: HTMLSpanElement;
10
+ }
11
+ declare const NativeSelect: import("svelte").Component<SelectProps, {}, "value">;
12
+ type NativeSelect = ReturnType<typeof NativeSelect>;
13
+ export default NativeSelect;
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ import Ripple from '../ripple/Ripple.svelte'
3
+ import type { Snippet } from 'svelte'
4
+ import type { HTMLOptionAttributes } from 'svelte/elements'
5
+
6
+ interface OptionProps extends HTMLOptionAttributes {
7
+ start?: Snippet
8
+ end?: Snippet
9
+ }
10
+
11
+ let { start, end, children, ...attributes }: OptionProps = $props()
12
+ </script>
13
+
14
+ <option {...attributes}>
15
+ <Ripple />
16
+ {#if start}
17
+ {@render start()}
18
+ {/if}
19
+ {@render children?.()}
20
+ {#if end}
21
+ {@render end()}
22
+ {/if}
23
+ </option>
24
+
25
+ <style>
26
+ option {
27
+ cursor: pointer;
28
+ position: relative;
29
+ display: flex;
30
+ padding: 0.5rem 1rem;
31
+ gap: 0.75rem;
32
+ height: 2rem;
33
+ background: var(--np-surface);
34
+
35
+ &:checked {
36
+ background-color: var(--np-color-secondary-container);
37
+ color: var(--np-color-on-secondary-container);
38
+ }
39
+
40
+ &::checkmark {
41
+ display: none;
42
+ }
43
+
44
+ &:first-child {
45
+ margin-top: 0.5rem;
46
+ }
47
+ &:last-child {
48
+ margin-bottom: 0.5rem;
49
+ }
50
+
51
+ &:disabled {
52
+ color: color-mix(in srgb, var(--np-color-on-surface) 38%, transparent);
53
+ background-color: color-mix(in srgb, var(--np-color-on-surface) 10%, transparent);
54
+ }
55
+ &:focus-visible {
56
+ outline-style: solid;
57
+ outline-color: var(--np-color-secondary);
58
+ outline-width: 3px;
59
+ outline-offset: -3px;
60
+ border-radius: var(--np-shape-corner-extra-small);
61
+ animation: focusAnimation 0.3s ease forwards;
62
+ }
63
+ }
64
+
65
+ @keyframes focusAnimation {
66
+ 0% {
67
+ outline-offset: -3px;
68
+ outline-width: 3px;
69
+ }
70
+ 50% {
71
+ outline-offset: -6px;
72
+ outline-width: 6px;
73
+ }
74
+ 100% {
75
+ outline-offset: -3px;
76
+ outline-width: 3px;
77
+ }
78
+ }
79
+ </style>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLOptionAttributes } from 'svelte/elements';
3
+ interface OptionProps extends HTMLOptionAttributes {
4
+ start?: Snippet;
5
+ end?: Snippet;
6
+ }
7
+ declare const Option: import("svelte").Component<OptionProps, {}, "">;
8
+ type Option = ReturnType<typeof Option>;
9
+ export default Option;
@@ -19,7 +19,7 @@
19
19
  label,
20
20
  style,
21
21
  noAsterisk = false,
22
- variant = 'filled',
22
+ variant = 'outlined',
23
23
  element = $bindable(),
24
24
  required,
25
25
  disabled,
@@ -1 +1,3 @@
1
1
  export { default as Select } from './Select.svelte';
2
+ export { default as NativeSelect } from './NativeSelect.svelte';
3
+ export { default as Option } from './Option.svelte';
@@ -1 +1,3 @@
1
1
  export { default as Select } from './Select.svelte';
2
+ export { default as NativeSelect } from './NativeSelect.svelte';
3
+ export { default as Option } from './Option.svelte';
@@ -15,7 +15,7 @@
15
15
  label,
16
16
  style,
17
17
  noAsterisk = false,
18
- variant = 'filled',
18
+ variant = 'outlined',
19
19
  element = $bindable(),
20
20
  populated = false,
21
21
  inputElement = $bindable(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.25.5",
3
+ "version": "0.26.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {
@@ -58,7 +58,7 @@
58
58
  "@material/material-color-utilities": "^0.3.0",
59
59
  "@playwright/test": "^1.55.0",
60
60
  "@sveltejs/adapter-vercel": "^5.10.2",
61
- "@sveltejs/kit": "^2.41.0",
61
+ "@sveltejs/kit": "^2.42.2",
62
62
  "@sveltejs/package": "^2.5.2",
63
63
  "@sveltejs/vite-plugin-svelte": "^6.2.0",
64
64
  "@types/eslint": "^9.6.1",
@@ -68,12 +68,12 @@
68
68
  "globals": "^16.4.0",
69
69
  "prettier": "^3.6.2",
70
70
  "prettier-plugin-svelte": "^3.4.0",
71
- "publint": "^0.3.12",
72
- "svelte": "^5.38.10",
71
+ "publint": "^0.3.13",
72
+ "svelte": "^5.39.2",
73
73
  "svelte-check": "^4.3.1",
74
74
  "typescript": "^5.9.2",
75
75
  "typescript-eslint": "^8.44.0",
76
- "vite": "^7.1.5",
76
+ "vite": "^7.1.6",
77
77
  "vitest": "^3.2.4"
78
78
  },
79
79
  "svelte": "./dist/index.js",