noph-ui 0.13.3 → 0.14.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.
@@ -0,0 +1,209 @@
1
+ <script lang="ts">
2
+ import IconButton from '../button/IconButton.svelte'
3
+ import CloseIcon from '../icons/CloseIcon.svelte'
4
+ import Ripple from '../ripple/Ripple.svelte'
5
+ import type { InputChipProps } from './types.ts'
6
+
7
+ let {
8
+ selected = $bindable(),
9
+ disabled = false,
10
+ label = '',
11
+ icon,
12
+ element = $bindable(),
13
+ ariaLabelRemove = 'Remove',
14
+ onremove,
15
+ name,
16
+ value,
17
+ group = $bindable(),
18
+ defaultSelected,
19
+ ...attributes
20
+ }: InputChipProps = $props()
21
+
22
+ let chipLabel: HTMLLabelElement | undefined = $state()
23
+
24
+ $effect(() => {
25
+ if (group && value) {
26
+ selected = group.includes(value)
27
+ }
28
+ })
29
+
30
+ $effect(() => {
31
+ if (value && group) {
32
+ const index = group.indexOf(value)
33
+ if (selected) {
34
+ if (index < 0) {
35
+ group?.push(value)
36
+ group = group
37
+ }
38
+ } else {
39
+ if (index >= 0) {
40
+ group.splice(index, 1)
41
+ group = group
42
+ }
43
+ }
44
+ }
45
+ })
46
+ </script>
47
+
48
+ <div
49
+ {...attributes}
50
+ bind:this={element}
51
+ class={[
52
+ 'np-filter-chip',
53
+ icon ? 'np-filter-chip-icon' : '',
54
+ disabled ? 'np-filter-chip-disabled' : '',
55
+ attributes.class,
56
+ ]}
57
+ >
58
+ <label bind:this={chipLabel} class="np-filter-chip-label">
59
+ {#if icon}
60
+ <div class="np-chip-icon">
61
+ {@render icon()}
62
+ </div>
63
+ {/if}
64
+ <div class="np-chip-label">{label}</div>
65
+ <input
66
+ type="checkbox"
67
+ bind:checked={selected}
68
+ onclick={(e) => e.preventDefault()}
69
+ {value}
70
+ {name}
71
+ {disabled}
72
+ defaultChecked={defaultSelected}
73
+ />
74
+ </label>
75
+ {#if !disabled}
76
+ <Ripple forElement={chipLabel} />
77
+ {/if}
78
+ <IconButton
79
+ {disabled}
80
+ type="button"
81
+ --np-icon-button-container-height="1.75rem"
82
+ --np-icon-button-container-width="1.75rem"
83
+ --np-icon-button-icon-size="1.125rem"
84
+ aria-label={ariaLabelRemove}
85
+ onclick={(
86
+ event: MouseEvent & {
87
+ currentTarget: EventTarget & HTMLButtonElement
88
+ },
89
+ ) => {
90
+ if (element === undefined) {
91
+ return
92
+ }
93
+ onremove?.(event)
94
+ }}
95
+ >
96
+ <CloseIcon />
97
+ </IconButton>
98
+ </div>
99
+
100
+ <style>
101
+ .np-filter-chip {
102
+ position: relative;
103
+ display: inline-flex;
104
+ align-items: center;
105
+ user-select: none;
106
+ border-radius: var(--np-filter-chip-container-shape, var(--np-shape-corner-small));
107
+ --np-icon-button-icon-color: var(--np-color-on-surface-variant);
108
+ --np-icon-size: 1.125rem;
109
+ padding-right: 2px;
110
+ }
111
+ .np-filter-chip-label input {
112
+ opacity: 0;
113
+ position: absolute;
114
+ pointer-events: none;
115
+ }
116
+ .np-filter-chip-label {
117
+ cursor: pointer;
118
+ display: inline-flex;
119
+ align-items: center;
120
+ height: 2rem;
121
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
122
+ color: var(--np-color-on-surface-variant);
123
+ fill: currentColor;
124
+ gap: 0.5rem;
125
+ z-index: 1;
126
+ padding-left: 1rem;
127
+ padding-right: 2px;
128
+ }
129
+ .np-chip-icon {
130
+ color: var(--np-color-primary);
131
+ display: flex;
132
+ }
133
+ .np-filter-chip-icon .np-filter-chip-label {
134
+ padding-left: 0.5rem;
135
+ }
136
+ .np-chip-label {
137
+ line-height: 1.25rem;
138
+ font-size: 0.875rem;
139
+ font-weight: 500;
140
+ padding-right: 2px;
141
+ }
142
+ .np-filter-chip::before {
143
+ content: '';
144
+ position: absolute;
145
+ inset: 0;
146
+ border-radius: inherit;
147
+ pointer-events: none;
148
+ background-color: var(--np-color-surface-container-low);
149
+ border-width: 1px;
150
+ border-style: solid;
151
+ border-color: var(--np-filter-chip-outline-color, var(--np-color-outline-variant));
152
+ }
153
+ .np-filter-chip:has(input:checked)::before {
154
+ border-width: 0;
155
+ background-color: var(--np-color-secondary-container);
156
+ }
157
+ .np-filter-chip:has(input:checked) {
158
+ --np-icon-button-icon-color: var(--np-color-on-secondary-container);
159
+ }
160
+ .np-filter-chip:has(input:checked) .np-filter-chip-label {
161
+ color: var(--np-color-on-secondary-container);
162
+ }
163
+ .np-filter-chip-label:focus-visible {
164
+ outline-width: 0;
165
+ }
166
+ .np-filter-chip:has(input:focus-visible) {
167
+ outline-style: solid;
168
+ outline-color: var(--np-color-primary);
169
+ outline-width: 3px;
170
+ outline-offset: 2px;
171
+ animation: focusAnimation 0.3s ease forwards;
172
+ }
173
+ @keyframes focusAnimation {
174
+ 0% {
175
+ outline-width: 3px;
176
+ }
177
+ 50% {
178
+ outline-width: 6px;
179
+ }
180
+ 100% {
181
+ outline-width: 3px;
182
+ }
183
+ }
184
+
185
+ .np-filter-chip-disabled .np-filter-chip-label {
186
+ cursor: default;
187
+ color: var(--np-color-on-surface);
188
+ opacity: 0.38;
189
+ }
190
+ .np-filter-chip-disabled:has(input:checked)::before {
191
+ opacity: 0.12;
192
+ }
193
+ .np-filter-chip-disabled:has(input:not(:checked)).np-filter-chip::after {
194
+ content: '';
195
+ position: absolute;
196
+ inset: 0;
197
+ border-radius: inherit;
198
+ pointer-events: none;
199
+ border-width: 1px;
200
+ border-style: solid;
201
+ border-color: var(--np-color-on-surface);
202
+ opacity: 0.12;
203
+ }
204
+ .np-filter-chip-disabled::before {
205
+ background-color: var(--np-color-on-surface);
206
+ opacity: 0.12;
207
+ border-width: 0;
208
+ }
209
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { InputChipProps } from './types.ts';
2
+ declare const InputChip: import("svelte").Component<InputChipProps, {}, "element" | "group" | "selected">;
3
+ type InputChip = ReturnType<typeof InputChip>;
4
+ export default InputChip;
@@ -1,2 +1,3 @@
1
1
  export { default as FilterChip } from './FilterChip.svelte';
2
+ export { default as InputChip } from './InputChip.svelte';
2
3
  export { default as ChipSet } from './ChipSet.svelte';
@@ -1,2 +1,3 @@
1
1
  export { default as FilterChip } from './FilterChip.svelte';
2
+ export { default as InputChip } from './InputChip.svelte';
2
3
  export { default as ChipSet } from './ChipSet.svelte';
@@ -17,3 +17,18 @@ export interface FilterChipProps extends HTMLAttributes<HTMLDivElement> {
17
17
  currentTarget: EventTarget & HTMLButtonElement;
18
18
  }) => void;
19
19
  }
20
+ export interface InputChipProps extends HTMLAttributes<HTMLDivElement> {
21
+ selected?: boolean;
22
+ disabled?: boolean;
23
+ label?: string;
24
+ icon?: Snippet;
25
+ ariaLabelRemove?: string;
26
+ element?: HTMLDivElement;
27
+ name?: string;
28
+ value?: string;
29
+ group?: (string | number)[] | null;
30
+ defaultSelected?: boolean | null;
31
+ onremove?: (event: MouseEvent & {
32
+ currentTarget: EventTarget & HTMLButtonElement;
33
+ }) => void;
34
+ }
@@ -18,13 +18,35 @@
18
18
  placeholder = ' ',
19
19
  element = $bindable(),
20
20
  inputElement = $bindable(),
21
+ reportValidity = $bindable(),
22
+ checkValidity = $bindable(),
21
23
  ...attributes
22
24
  }: TextFieldProps = $props()
23
25
 
24
26
  let errorRaw: boolean = $state(error)
25
27
  let errorTextRaw: string = $state(errorText)
26
28
  let focusOnInvalid = $state(true)
27
- let checkValidity = $state(false)
29
+ let doValidity = $state(false)
30
+
31
+ reportValidity = () => {
32
+ if (inputElement) {
33
+ const valid = inputElement.reportValidity()
34
+ if (valid) {
35
+ errorRaw = error
36
+ errorTextRaw = errorText
37
+ }
38
+ return valid
39
+ }
40
+ return false
41
+ }
42
+
43
+ checkValidity = () => {
44
+ if (inputElement) {
45
+ return inputElement.checkValidity()
46
+ }
47
+ return false
48
+ }
49
+
28
50
  $effect(() => {
29
51
  errorRaw = error
30
52
  errorTextRaw = errorText
@@ -37,7 +59,7 @@
37
59
  value = ''
38
60
  })
39
61
  inputElement.addEventListener('input', () => {
40
- checkValidity = true
62
+ doValidity = true
41
63
  })
42
64
  inputElement.addEventListener('invalid', (event) => {
43
65
  event.preventDefault()
@@ -53,13 +75,10 @@
53
75
  }
54
76
  })
55
77
 
56
- inputElement.addEventListener('blur', (event) => {
57
- const { currentTarget } = event as Event & {
58
- currentTarget: HTMLInputElement | HTMLTextAreaElement
59
- }
60
- if (checkValidity) {
78
+ inputElement.addEventListener('blur', () => {
79
+ if (doValidity) {
61
80
  focusOnInvalid = false
62
- if (currentTarget.checkValidity()) {
81
+ if (checkValidity()) {
63
82
  errorRaw = error
64
83
  errorTextRaw = errorText
65
84
  }
@@ -1,4 +1,4 @@
1
1
  import type { TextFieldProps } from './types.ts';
2
- declare const TextField: import("svelte").Component<TextFieldProps, {}, "element" | "value" | "inputElement">;
2
+ declare const TextField: import("svelte").Component<TextFieldProps, {}, "element" | "value" | "inputElement" | "reportValidity" | "checkValidity">;
3
3
  type TextField = ReturnType<typeof TextField>;
4
4
  export default TextField;
@@ -13,6 +13,8 @@ interface FieldProps {
13
13
  noAsterisk?: boolean;
14
14
  element?: HTMLSpanElement;
15
15
  inputElement?: HTMLInputElement | HTMLTextAreaElement;
16
+ reportValidity?: () => boolean;
17
+ checkValidity?: () => boolean;
16
18
  }
17
19
  export interface InputFieldProps extends HTMLInputAttributes, FieldProps {
18
20
  type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url' | 'datetime-local';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {