From bf2ef338e914e5a52b4c79609adefab5ca1146cb Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Sun, 13 Apr 2025 07:56:23 -0700 Subject: [PATCH] partially refactor components to ui package --- components/Button.svelte | 150 ++++++++++ components/CenterBox.svelte | 11 + components/Combobox.svelte | 467 ++++++++++++++++++++++++++++++ components/FramelessButton.svelte | 40 +++ components/Label.svelte | 23 ++ components/Link.svelte | 57 ++++ components/PhoneInput.svelte | 174 +++++++++++ components/PinInput.svelte | 188 ++++++++++++ components/RadioGroup.svelte | 52 ++++ components/Select.svelte | 66 +++++ components/Spinner.svelte | 53 ++++ components/StateMachine.svelte | 454 +++++++++++++++++++++++++++++ components/StyledRawInput.svelte | 54 ++++ components/TextInput.svelte | 48 +++ components/TimezoneInput.svelte | 104 +++++++ components/ToggleGroup.svelte | 71 +++++ components/ToggleSelect.svelte | 37 +++ eslint.config.js | 3 + index.ts | 17 ++ package.json | 32 ++ tsconfig.json | 3 + 21 files changed, 2104 insertions(+) create mode 100644 components/Button.svelte create mode 100644 components/CenterBox.svelte create mode 100644 components/Combobox.svelte create mode 100644 components/FramelessButton.svelte create mode 100644 components/Label.svelte create mode 100644 components/Link.svelte create mode 100644 components/PhoneInput.svelte create mode 100644 components/PinInput.svelte create mode 100644 components/RadioGroup.svelte create mode 100644 components/Select.svelte create mode 100644 components/Spinner.svelte create mode 100644 components/StateMachine.svelte create mode 100644 components/StyledRawInput.svelte create mode 100644 components/TextInput.svelte create mode 100644 components/TimezoneInput.svelte create mode 100644 components/ToggleGroup.svelte create mode 100644 components/ToggleSelect.svelte create mode 100644 eslint.config.js create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/components/Button.svelte b/components/Button.svelte new file mode 100644 index 0000000..c967b72 --- /dev/null +++ b/components/Button.svelte @@ -0,0 +1,150 @@ + + + + + diff --git a/components/CenterBox.svelte b/components/CenterBox.svelte new file mode 100644 index 0000000..a55975f --- /dev/null +++ b/components/CenterBox.svelte @@ -0,0 +1,11 @@ + + +
+
+ {@render children()} +
+
diff --git a/components/Combobox.svelte b/components/Combobox.svelte new file mode 100644 index 0000000..9876cdc --- /dev/null +++ b/components/Combobox.svelte @@ -0,0 +1,467 @@ + + + + + + {#if open} +
{ + if (e.key === 'Escape') { + open = false; + searchInput?.focus(); + } + }} + tabindex="0" + > + {#each filteredItems as item, i (i + item.value)} +
{ + value = item; + open = false; + searchInput?.focus(); + onchange?.({ value: item }); + }} + onkeydown={() => {}} + tabindex="-1" + > + {#if item.icon} + {@render item.icon(item)} + {/if} + +
+ {#if item.render} + {@render item.render(item)} + {:else} + {getLabel(item)} + {/if} +
+ + {#if item?.infotext} +
+ {item.infotext} +
+ {/if} + + {#if value?.value === item.value} +
+ +
+ {/if} +
+ {:else} + {notFoundMessage} + {/each} +
+ {/if} +
+ +
+ + {#if label} + + {/if} + + + { + valid = e.detail.valid; + onvalidate?.(e); + }} + /> + + +
+ {@render searchInputBox()} +
+ + + {#if invalidMessage} +
+ +
+ {/if} +
+ +{#snippet searchInputBox(caret: boolean = true)} +
+ {#if iconVisible} +
+ {#if useHighlighted && highlighted?.icon} + {@render highlighted.icon(highlighted)} + {:else if value?.icon} + {@render value.icon(value)} + {:else} + ❌ + {/if} +
+ {/if} + + { + if (!open) { + setTimeout(() => { + searchInput?.select(); + }, 100); + } + + open = true; + }} + onkeydown={(e) => { + if (!searchInput) return; + + if (e.key === 'Tab' || e.key === 'Enter') { + if (open && highlighted && highlighted.value !== value?.value) { + value = highlighted; + onchange?.({ value: highlighted }); + } + if (e.key === 'Enter') { + e.preventDefault(); + } + + open = false; + return; + } else if (e.key === 'Escape') { + open = false; + return; + } + + // open the picker + open = true; + + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + searching = false; + console.log('arrowNavOnly = true'); + e.preventDefault(); + } + + if (e.key === 'ArrowDown') { + const nextIndex = getHightlightedID() + 1; + if (nextIndex < filteredItems.length) { + highlighted = filteredItems[nextIndex]; + } + return; + } else if (e.key === 'ArrowUp') { + const prevIndex = getHightlightedID() - 1; + if (prevIndex >= 0) { + highlighted = filteredItems[prevIndex]; + } + return; + } + }} + oninput={() => { + if (!searchInput) return; + searchValue = searchInput.value; + searching = true; + }} + /> + + {#if (value && value.infotext) || (highlighted && useHighlighted && highlighted.infotext)} +
+ {useHighlighted && highlighted?.infotext ? highlighted.infotext : value?.infotext} +
+ {/if} + + {#if caret} + { + open = !open; + if (open) searchInput?.focus(); + }} + /> + {/if} +
+{/snippet} + + diff --git a/components/FramelessButton.svelte b/components/FramelessButton.svelte new file mode 100644 index 0000000..2a42c1f --- /dev/null +++ b/components/FramelessButton.svelte @@ -0,0 +1,40 @@ + + +{#snippet iconSnippet()} + {icon} +{/snippet} + + diff --git a/components/Label.svelte b/components/Label.svelte new file mode 100644 index 0000000..228eea3 --- /dev/null +++ b/components/Label.svelte @@ -0,0 +1,23 @@ + + + diff --git a/components/Link.svelte b/components/Link.svelte new file mode 100644 index 0000000..ce9c385 --- /dev/null +++ b/components/Link.svelte @@ -0,0 +1,57 @@ + + + + + + {@render children()} + diff --git a/components/PhoneInput.svelte b/components/PhoneInput.svelte new file mode 100644 index 0000000..f760394 --- /dev/null +++ b/components/PhoneInput.svelte @@ -0,0 +1,174 @@ + + +{#snippet renderIcon(item: ComboboxItem)} + {#if countrycodeMap[item.value]?.flag} + {countrycodeMap[item.value].flag} + {/if} +{/snippet} + +
+ {#if label} + + {/if} + + + + +
+
+ { + country = countrycodeMap[e.value.value]; + }} + onvalidate={(e) => { + countriesValid = e.detail.valid; + }} + /> +
+ +
+ !required || (value !== undefined && value.isValid()) + }} + onvalidate={(e) => { + numberValid = e.detail.valid; + }} + onkeydown={(e) => { + if (e.ctrlKey || e.key.length > 1) { + return; + } + + if (/[0-9-()+]/.test(e.key)) { + return; + } + + e.preventDefault(); + }} + oninput={(event: Event) => { + const e = event as InputEvent; + if (!e.target) return; + const input = e.target as HTMLInputElement; + + const formatter = new AsYouType(formatterCountryCode); + const formatted = formatter.input(input.value); + + if (formatted.length >= input.value.length) input.value = formatted; + value = formatter.getNumber(); + console.log('updated value', value, value?.isValid()); + + if (formatter.isValid() && formatter.isInternational() && value) { + const country = formatter.getCountry(); + if (country) { + setCountryByISO(country); + input.value = value.formatNational(); + } + } + }} + /> +
+
+ +
+ +
+
diff --git a/components/PinInput.svelte b/components/PinInput.svelte new file mode 100644 index 0000000..d3d3d00 --- /dev/null +++ b/components/PinInput.svelte @@ -0,0 +1,188 @@ + + + { + valid = e.detail.valid; + }} + bind:this={hiddenInput} +/> + +{#if label} + +{/if} + +
+
+ {#each { length: length } as _, i} + = value.length && 'border-red-500!' + ]} + value={value[i] || ''} + {required} + maxlength="1" + onfocus={onfocusinput(i)} + onmousedown={onmousedown(i)} + onkeydown={onkeydowninput(i)} + bind:this={inputs[i]} + placeholder="0" + /> + {/each} +
+
diff --git a/components/RadioGroup.svelte b/components/RadioGroup.svelte new file mode 100644 index 0000000..3235ffd --- /dev/null +++ b/components/RadioGroup.svelte @@ -0,0 +1,52 @@ + + +
+ +
+ {#each items as i} + {@const item = group.getItem(i)} +
+
+ {#if item.checked} + + {/if} +
+ + + {i} + +
+ {/each} +
+ +
diff --git a/components/Select.svelte b/components/Select.svelte new file mode 100644 index 0000000..024b1d8 --- /dev/null +++ b/components/Select.svelte @@ -0,0 +1,66 @@ + + +
+ + + +
+ +
+
diff --git a/components/Spinner.svelte b/components/Spinner.svelte new file mode 100644 index 0000000..ed94245 --- /dev/null +++ b/components/Spinner.svelte @@ -0,0 +1,53 @@ + + +
+
+
+
+
+
+ + diff --git a/components/StateMachine.svelte b/components/StateMachine.svelte new file mode 100644 index 0000000..a83044a --- /dev/null +++ b/components/StateMachine.svelte @@ -0,0 +1,454 @@ + + + + + +{#snippet progress()} + {#each { length: pages.length - 2 } as _, i} +
+ {#if i >= index} + {i + 1} + {:else} + check_small + {/if} +
+ {/each} +{/snippet} + +
+ + {#if backButtonVisible} + + {/if} + + +
+ {@render progress()} +
+
+ + +
+ +
+ {@render progress()} +
+ + +
+ +
+
+ + + + {getText(hero.text)} + +
+ +
e.preventDefault()} + bind:this={formElement} + > +
+ + {#key index} +
+ {@render page.snippet()} +
+ {/key} +
+ + + {#if index < pages.length - 1 && page.button && $height > 0} +
+ +
+ {/if} +
+
+ + diff --git a/components/StyledRawInput.svelte b/components/StyledRawInput.svelte new file mode 100644 index 0000000..51d8551 --- /dev/null +++ b/components/StyledRawInput.svelte @@ -0,0 +1,54 @@ + + + { + valid = e.detail.valid; + if (onvalidate) onvalidate(e); + }} + bind:this={ref} +/> diff --git a/components/TextInput.svelte b/components/TextInput.svelte new file mode 100644 index 0000000..617d5cf --- /dev/null +++ b/components/TextInput.svelte @@ -0,0 +1,48 @@ + + +
+ {#if label} + + {/if} + + { + valid = e.detail.valid; + }} + /> + +
+ +
+
diff --git a/components/TimezoneInput.svelte b/components/TimezoneInput.svelte new file mode 100644 index 0000000..c4cc208 --- /dev/null +++ b/components/TimezoneInput.svelte @@ -0,0 +1,104 @@ + + + + + + +{#snippet timezoneLabel(item: ComboboxItem)} + {@html wbr(item.label ?? 'Missing label')} +{/snippet} diff --git a/components/ToggleGroup.svelte b/components/ToggleGroup.svelte new file mode 100644 index 0000000..cc167a6 --- /dev/null +++ b/components/ToggleGroup.svelte @@ -0,0 +1,71 @@ + + +
+ {#if label && name} + + {/if} + +
+ {#if name} + { + valid = e.detail.valid; + }} + /> + {/if} + + {#each items as item} + + {item} + + {/each} +
+ + {#if name} +
+ +
+ {/if} +
diff --git a/components/ToggleSelect.svelte b/components/ToggleSelect.svelte new file mode 100644 index 0000000..b74df25 --- /dev/null +++ b/components/ToggleSelect.svelte @@ -0,0 +1,37 @@ + + +{#if name} + +{/if} + + diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9cadde2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,3 @@ +import { config } from '@repo/eslint-config/index.js'; + +export default config; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..ad9aef4 --- /dev/null +++ b/index.ts @@ -0,0 +1,17 @@ +export { default as Button } from './components/Button.svelte'; +export { default as CenterBox } from './components/CenterBox.svelte'; +export { default as Combobox } from './components/Combobox.svelte'; +export { default as FramelessButton } from './components/FramelessButton.svelte'; +export { default as Label } from './components/Label.svelte'; +export { default as Link } from './components/Link.svelte'; +export { default as PhoneInput } from './components/PhoneInput.svelte'; +export { default as PinInput } from './components/PinInput.svelte'; +export { default as RadioGroup } from './components/RadioGroup.svelte'; +export { default as Select } from './components/Select.svelte'; +export { default as Spinner } from './components/Spinner.svelte'; +export { default as StateMachine, type StateMachinePage } from './components/StateMachine.svelte'; +export { default as StyledRawInput } from './components/StyledRawInput.svelte'; +export { default as TextInput } from './components/TextInput.svelte'; +export { default as TimezoneInput } from './components/TimezoneInput.svelte'; +export { default as ToggleGroup } from './components/ToggleGroup.svelte'; +export { default as ToggleSelect } from './components/ToggleSelect.svelte'; diff --git a/package.json b/package.json new file mode 100644 index 0000000..35a828a --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "@repo/ui", + "version": "0.0.0", + "type": "module", + "module": "index.ts", + "main": "index.ts", + "exports": { + ".": { + "types": "./index.ts", + "svelte": "./index.ts" + } + }, + "scripts": { + "lint": "eslint ." + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/tailwindcss-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "eslint": "^9.24.0", + "svelte": "^5.25.3", + "@sveltejs/kit": "^2.20.2" + }, + "dependencies": { + "@repo/validate": "workspace:*", + "@jsrob/svelte-portal": "^0.2.1", + "country-state-city": "^3.2.1", + "libphonenumber-js": "^1.12.6", + "melt": "^0.12.0", + "phosphor-svelte": "^3.0.1" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1a392e8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": ["@repo/typescript-config/svelte.json"] +}