@joyautomation/salt 0.1.3 → 0.1.5

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.
@@ -3,39 +3,94 @@
3
3
  import Input from './Input.svelte'
4
4
  import Select from './Select.svelte'
5
5
  import Switch from './Switch.svelte'
6
- import type { FormInputs } from './types.js'
6
+ import type { FormGroup, FormInputs } from './types.js'
7
7
  import { slide } from 'svelte/transition'
8
8
 
9
9
  const {
10
10
  inputs: propInputs,
11
+ groups: propGroups,
11
12
  action,
12
13
  buttonText = 'Submit',
14
+ resetButtonText = 'Reset',
15
+ showReset = true,
13
16
  onsubmitstart,
14
17
  onsubmitend
15
18
  }: {
16
- inputs: FormInputs
19
+ inputs?: FormInputs
20
+ groups?: FormGroup[]
17
21
  action: string
18
22
  buttonText?: string
23
+ resetButtonText?: string
24
+ showReset?: boolean
19
25
  onsubmitstart?: () => void
20
26
  onsubmitend?: () => void
21
27
  } = $props()
22
28
 
23
- let inputs = $state(propInputs)
29
+ let groups = $state(propGroups ?? [{ rows: propInputs ?? [] }])
24
30
  let submitting = $state(false)
31
+ let defaultValues: Map<string, string> = $state(new Map())
25
32
 
26
33
  $effect(() => {
27
- inputs = propInputs
34
+ groups = propGroups ?? [{ rows: propInputs ?? [] }]
35
+ })
36
+
37
+ const allInputs = $derived(groups.flatMap((g) => g.rows))
38
+
39
+ // Capture defaults on first render
40
+ $effect(() => {
41
+ if (defaultValues.size === 0) {
42
+ const defaults = new Map<string, string>()
43
+ for (const row of allInputs) {
44
+ for (const input of row) {
45
+ defaults.set(input.id, input.value)
46
+ }
47
+ }
48
+ defaultValues = defaults
49
+ }
28
50
  })
29
51
 
30
52
  const valid = $derived(
31
- inputs.every((row) =>
53
+ allInputs.every((row) =>
32
54
  row.every((input) => {
33
55
  return input.validations.every(([validation]) => {
34
- return !validation(input.value, inputs)
56
+ return !validation(input.value, allInputs)
35
57
  })
36
58
  })
37
59
  )
38
60
  )
61
+
62
+ const hasChanges = $derived(
63
+ allInputs.some((row) =>
64
+ row.some((input) => {
65
+ return defaultValues.get(input.id) !== input.value
66
+ })
67
+ )
68
+ )
69
+
70
+ function isChanged(inputId: string, value: string): boolean {
71
+ return defaultValues.has(inputId) && defaultValues.get(inputId) !== value
72
+ }
73
+
74
+ function reset() {
75
+ for (const row of allInputs) {
76
+ for (const input of row) {
77
+ const def = defaultValues.get(input.id)
78
+ if (def !== undefined) {
79
+ input.value = def
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ function updateDefaults() {
86
+ const defaults = new Map<string, string>()
87
+ for (const row of allInputs) {
88
+ for (const input of row) {
89
+ defaults.set(input.id, input.value)
90
+ }
91
+ }
92
+ defaultValues = defaults
93
+ }
39
94
  </script>
40
95
 
41
96
  <form
@@ -47,50 +102,118 @@
47
102
  onsubmitstart?.()
48
103
  return async ({ update }) => {
49
104
  submitting = false
105
+ updateDefaults()
50
106
  onsubmitend?.()
51
107
  update({ reset: false })
52
108
  }
53
109
  }}
54
110
  >
55
- {#each inputs as row}
56
- <div class="form__row">
57
- {#each row as input}
58
- <div>
59
- {#if input.type === 'select'}
60
- <Select {...input} bind:value={input.value} {inputs} options={input.options || []} />
61
- {:else if input.type === 'checkbox'}
62
- <Switch {...input} bind:value={input.value} {inputs} />
63
- {:else}
64
- <Input {...input} bind:value={input.value} {inputs} />
65
- {/if}
111
+ {#each groups as group}
112
+ <fieldset class="form__group">
113
+ {#if group.heading}
114
+ <legend class="form__group-heading">{group.heading}</legend>
115
+ {/if}
116
+ {#each group.rows as row}
117
+ <div class="form__row">
118
+ {#each row as input}
119
+ <div>
120
+ {#if input.type === 'select'}
121
+ <Select
122
+ {...input}
123
+ bind:value={input.value}
124
+ inputs={allInputs}
125
+ options={input.options || []}
126
+ changed={isChanged(input.id, input.value)}
127
+ />
128
+ {:else if input.type === 'checkbox'}
129
+ <Switch
130
+ {...input}
131
+ bind:value={input.value}
132
+ inputs={allInputs}
133
+ changed={isChanged(input.id, input.value)}
134
+ />
135
+ {:else}
136
+ <Input
137
+ {...input}
138
+ bind:value={input.value}
139
+ inputs={allInputs}
140
+ changed={isChanged(input.id, input.value)}
141
+ />
142
+ {/if}
143
+ </div>
144
+ {/each}
66
145
  </div>
67
146
  {/each}
68
- </div>
147
+ </fieldset>
69
148
  {/each}
70
- <button class="button--primary" disabled={!valid || submitting}>
71
- {#if submitting}
72
- <div class="form__spinner-container" transition:slide>
73
- <span class="form__spinner"></span>
74
- </div>
75
- {:else}
76
- <div transition:slide>
77
- {buttonText}
78
- </div>
149
+ <div class="form__actions">
150
+ <button class="button--primary" disabled={!valid || submitting}>
151
+ {#if submitting}
152
+ <div class="form__spinner-container" transition:slide>
153
+ <span class="form__spinner"></span>
154
+ </div>
155
+ {:else}
156
+ <div transition:slide>
157
+ {buttonText}
158
+ </div>
159
+ {/if}
160
+ </button>
161
+ {#if showReset && hasChanges}
162
+ <button type="button" class="button--secondary form__reset" onclick={reset} transition:slide>
163
+ {resetButtonText}
164
+ </button>
79
165
  {/if}
80
- </button>
166
+ </div>
81
167
  </form>
82
168
 
83
169
  <style>.form {
84
170
  display: flex;
85
171
  flex-direction: column;
172
+ gap: calc(var(--spacing-unit) * 4);
173
+ }
174
+
175
+ .form__group {
176
+ border: none;
177
+ padding: 0;
178
+ margin: 0;
179
+ display: flex;
180
+ flex-direction: column;
181
+ gap: calc(var(--spacing-unit) * 1);
182
+ }
183
+
184
+ .form__group-heading {
185
+ font-size: var(--text-base);
186
+ font-weight: 600;
187
+ color: var(--theme-text);
188
+ padding: 0;
86
189
  }
87
- .form > .form__row {
190
+
191
+ .form__row {
88
192
  display: flex;
89
193
  }
90
- .form > .form__row > * {
194
+ .form__row > * {
91
195
  flex-grow: 1;
92
196
  }
93
197
 
198
+ .form__actions {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: calc(var(--spacing-unit) * 2);
202
+ }
203
+
204
+ .form__reset {
205
+ background: transparent;
206
+ border: 1px solid var(--theme-neutral-400);
207
+ color: var(--theme-text);
208
+ padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3);
209
+ border-radius: var(--rounded-sm);
210
+ cursor: pointer;
211
+ font-size: var(--text-sm);
212
+ }
213
+ .form__reset:hover {
214
+ background: var(--theme-neutral-200);
215
+ }
216
+
94
217
  .form__spinner-container {
95
218
  display: flex;
96
219
  justify-content: center;
@@ -1,8 +1,11 @@
1
- import type { FormInputs } from './types.js';
1
+ import type { FormGroup, FormInputs } from './types.js';
2
2
  type $$ComponentProps = {
3
- inputs: FormInputs;
3
+ inputs?: FormInputs;
4
+ groups?: FormGroup[];
4
5
  action: string;
5
6
  buttonText?: string;
7
+ resetButtonText?: string;
8
+ showReset?: boolean;
6
9
  onsubmitstart?: () => void;
7
10
  onsubmitend?: () => void;
8
11
  };
@@ -12,8 +12,14 @@
12
12
  validations,
13
13
  inputs,
14
14
  touched = false,
15
+ changed = false,
15
16
  onblur = () => {}
16
- }: InputProps & { inputs?: FormInputs; touched?: boolean; onblur?: () => void } = $props()
17
+ }: InputProps & {
18
+ inputs?: FormInputs
19
+ touched?: boolean
20
+ changed?: boolean
21
+ onblur?: () => void
22
+ } = $props()
17
23
  const validationResult = $derived(
18
24
  validations.find(([validation]) => {
19
25
  return validation(value, inputs ?? [])
@@ -21,7 +27,11 @@
21
27
  )
22
28
  </script>
23
29
 
24
- <div class="input" class:input--invalid={touched && validationResult != null}>
30
+ <div
31
+ class="input"
32
+ class:input--invalid={touched && validationResult != null}
33
+ class:input--changed={changed}
34
+ >
25
35
  {#if label != null}
26
36
  <label for={id}>{label}</label>
27
37
  {/if}
@@ -72,6 +82,10 @@
72
82
  font-family: inherit;
73
83
  }
74
84
 
85
+ .input--changed > input, .input--changed > textarea {
86
+ border-left: solid 3px var(--theme-primary);
87
+ }
88
+
75
89
  .input--invalid > input, .input--invalid > textarea {
76
90
  border: solid 1px var(--theme-error-400, var(--red-400));
77
91
  }</style>
@@ -2,6 +2,7 @@ import type { FormInputs, InputProps } from './types.js';
2
2
  type $$ComponentProps = InputProps & {
3
3
  inputs?: FormInputs;
4
4
  touched?: boolean;
5
+ changed?: boolean;
5
6
  onblur?: () => void;
6
7
  };
7
8
  declare const Input: import("svelte").Component<$$ComponentProps, {}, "value">;
@@ -9,8 +9,13 @@
9
9
  value = $bindable(''),
10
10
  validations,
11
11
  inputs,
12
- options
13
- }: InputProps & { inputs?: FormInputs; options: { value: string; label: string }[] } = $props()
12
+ options,
13
+ changed = false
14
+ }: InputProps & {
15
+ inputs?: FormInputs
16
+ options: { value: string; label: string }[]
17
+ changed?: boolean
18
+ } = $props()
14
19
  const validationResult = $derived(
15
20
  validations.find(([validation]) => {
16
21
  return validation(value, inputs ?? [])
@@ -18,7 +23,11 @@
18
23
  )
19
24
  </script>
20
25
 
21
- <div class="select-field" class:select-field--invalid={validationResult != null}>
26
+ <div
27
+ class="select-field"
28
+ class:select-field--invalid={validationResult != null}
29
+ class:select-field--changed={changed}
30
+ >
22
31
  {#if label != null}
23
32
  <label for={id}>{label}</label>
24
33
  {/if}
@@ -64,6 +73,10 @@
64
73
  border-color: var(--theme-primary);
65
74
  }
66
75
 
76
+ .select-field--changed > select {
77
+ border-left: solid 3px var(--theme-primary);
78
+ }
79
+
67
80
  .select-field--invalid > select {
68
81
  border: solid 1px var(--theme-error-400, var(--red-400));
69
82
  }</style>
@@ -5,6 +5,7 @@ type $$ComponentProps = InputProps & {
5
5
  value: string;
6
6
  label: string;
7
7
  }[];
8
+ changed?: boolean;
8
9
  };
9
10
  declare const Select: import("svelte").Component<$$ComponentProps, {}, "value">;
10
11
  type Select = ReturnType<typeof Select>;
@@ -6,9 +6,8 @@
6
6
  name,
7
7
  label,
8
8
  value = $bindable('false'),
9
- validations,
10
- inputs
11
- }: InputProps & { inputs?: FormInputs } = $props()
9
+ changed = false
10
+ }: InputProps & { inputs?: FormInputs; changed?: boolean } = $props()
12
11
 
13
12
  const checked = $derived(value === 'true')
14
13
 
@@ -24,7 +23,7 @@
24
23
  }
25
24
  </script>
26
25
 
27
- <div class="switch-field">
26
+ <div class="switch-field" class:switch-field--changed={changed}>
28
27
  {#if label != null}
29
28
  <label for={id}>{label}</label>
30
29
  {/if}
@@ -57,6 +56,11 @@
57
56
  text-transform: capitalize;
58
57
  }
59
58
 
59
+ .switch-field--changed {
60
+ padding-left: calc(var(--spacing-unit) * 1.5);
61
+ border-left: solid 3px var(--theme-primary);
62
+ }
63
+
60
64
  .switch {
61
65
  position: relative;
62
66
  display: inline-block;
@@ -1,6 +1,7 @@
1
1
  import type { FormInputs, InputProps } from './types.js';
2
2
  type $$ComponentProps = InputProps & {
3
3
  inputs?: FormInputs;
4
+ changed?: boolean;
4
5
  };
5
6
  declare const Switch: import("svelte").Component<$$ComponentProps, {}, "value">;
6
7
  type Switch = ReturnType<typeof Switch>;
@@ -3,4 +3,4 @@ export { default as Input } from './Input.svelte';
3
3
  export { default as Select } from './Select.svelte';
4
4
  export { default as Switch } from './Switch.svelte';
5
5
  export { default as SearchableSelect } from './SearchableSelect.svelte';
6
- export type { InputProps, FormInputs, FormInputsPartial } from './types.js';
6
+ export type { InputProps, FormInputs, FormInputsPartial, FormGroup } from './types.js';
@@ -13,3 +13,7 @@ export type InputProps = {
13
13
  };
14
14
  export type FormInputs = InputProps[][];
15
15
  export type FormInputsPartial = (Pick<InputProps, 'name'> & Partial<InputProps>)[][];
16
+ export type FormGroup = {
17
+ heading?: string;
18
+ rows: InputProps[][];
19
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joyautomation/salt",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "scripts": {
5
5
  "generate:icons": "node scripts/generate-icons.js",
6
6
  "dev": "vite dev",