@aspect-ops/exon-ui 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -46,22 +46,24 @@ npm install @aspect-ops/exon-ui
46
46
 
47
47
  ### Form Components
48
48
 
49
- | Component | Description |
50
- | --------------- | ---------------------------------------- |
51
- | `TextInput` | Text input with validation states |
52
- | `Textarea` | Multi-line input with auto-resize |
53
- | `Select` | Dropdown select with keyboard navigation |
54
- | `Checkbox` | Single checkbox with indeterminate state |
55
- | `CheckboxGroup` | Group of checkboxes with shared state |
56
- | `Radio` | Single radio button |
57
- | `RadioGroup` | Radio button group with orientation |
58
- | `Switch` | Toggle switch component |
59
- | `FormField` | Label wrapper with helper/error text |
60
- | `SearchInput` | Search with autocomplete suggestions |
61
- | `DatePicker` | Date selection with calendar popup |
62
- | `TimePicker` | Time selection with hour/minute picker |
63
- | `FileUpload` | Drag-drop file upload with previews |
64
- | `OTPInput` | One-time password input |
49
+ | Component | Description |
50
+ | --------------- | ------------------------------------------ |
51
+ | `TextInput` | Text input with validation states |
52
+ | `Textarea` | Multi-line input with auto-resize |
53
+ | `Select` | Dropdown select with keyboard navigation |
54
+ | `Checkbox` | Single checkbox with indeterminate state |
55
+ | `CheckboxGroup` | Group of checkboxes with shared state |
56
+ | `Radio` | Single radio button |
57
+ | `RadioGroup` | Radio button group with orientation |
58
+ | `Switch` | Toggle switch component |
59
+ | `FormField` | Label wrapper with helper/error text |
60
+ | `SearchInput` | Search with autocomplete suggestions |
61
+ | `DatePicker` | Date selection with calendar popup |
62
+ | `TimePicker` | Time selection with hour/minute picker |
63
+ | `FileUpload` | Drag-drop file upload with previews |
64
+ | `OTPInput` | One-time password input |
65
+ | `NumberInput` | Number input with +/- buttons and keyboard |
66
+ | `ToggleGroup` | Single/multi select button group |
65
67
 
66
68
  ### Navigation Components
67
69
 
@@ -74,6 +76,7 @@ npm install @aspect-ops/exon-ui
74
76
  | `Navbar`, `NavItem` | Responsive header with mobile menu |
75
77
  | `Sidebar`, `SidebarItem`, `SidebarGroup` | Collapsible sidebar navigation |
76
78
  | `Stepper`, `StepperStep` | Multi-step progress indicator |
79
+ | `Pagination` | Page navigation with ellipsis |
77
80
 
78
81
  ### Data Display Components
79
82
 
@@ -492,6 +495,128 @@ One-time password input with auto-focus and paste support.
492
495
  <OTPInput bind:value={otp} length={6} oncomplete={(code) => console.log('OTP:', code)} />
493
496
  ```
494
497
 
498
+ ### NumberInput
499
+
500
+ Number input with increment/decrement buttons and keyboard support.
501
+
502
+ **Props:**
503
+
504
+ | Prop | Type | Default | Description |
505
+ | --------------- | ------------------------- | ------- | --------------------------- |
506
+ | `value` | `number \| null` | `null` | Bindable number value |
507
+ | `min` | `number` | - | Minimum allowed value |
508
+ | `max` | `number` | - | Maximum allowed value |
509
+ | `step` | `number` | `1` | Increment/decrement step |
510
+ | `disabled` | `boolean` | `false` | Disabled state |
511
+ | `placeholder` | `string` | - | Input placeholder |
512
+ | `error` | `boolean` | `false` | Error state styling |
513
+ | `onValueChange` | `(value: number) => void` | - | Callback when value changes |
514
+
515
+ **Usage:**
516
+
517
+ ```svelte
518
+ <script>
519
+ import { NumberInput } from '@aspect-ops/exon-ui';
520
+
521
+ let quantity = $state(1);
522
+ </script>
523
+
524
+ <NumberInput bind:value={quantity} min={1} max={99} step={1} />
525
+
526
+ <!-- With error state -->
527
+ <NumberInput bind:value={quantity} min={0} max={10} error={quantity > 10} />
528
+ ```
529
+
530
+ ### ToggleGroup
531
+
532
+ Button group for single or multiple selection.
533
+
534
+ **Props (ToggleGroup):**
535
+
536
+ | Prop | Type | Default | Description |
537
+ | --------------- | ---------------------------- | -------------- | --------------------------- |
538
+ | `type` | `'single' \| 'multiple'` | `'single'` | Selection mode |
539
+ | `value` | `string \| string[]` | `'' \| []` | Bindable selected value(s) |
540
+ | `onValueChange` | `(value) => void` | - | Callback when value changes |
541
+ | `disabled` | `boolean` | `false` | Disabled state |
542
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Layout direction |
543
+
544
+ **Props (ToggleGroupItem):**
545
+
546
+ | Prop | Type | Default | Description |
547
+ | ---------- | --------- | ------- | --------------------- |
548
+ | `value` | `string` | - | Item value (required) |
549
+ | `disabled` | `boolean` | `false` | Disabled state |
550
+
551
+ **Usage:**
552
+
553
+ ```svelte
554
+ <script>
555
+ import { ToggleGroup, ToggleGroupItem } from '@aspect-ops/exon-ui';
556
+
557
+ let alignment = $state('left');
558
+ let formats = $state([]);
559
+ </script>
560
+
561
+ <!-- Single selection (radio-like) -->
562
+ <ToggleGroup bind:value={alignment} type="single">
563
+ {#snippet children()}
564
+ <ToggleGroupItem value="left">Left</ToggleGroupItem>
565
+ <ToggleGroupItem value="center">Center</ToggleGroupItem>
566
+ <ToggleGroupItem value="right">Right</ToggleGroupItem>
567
+ {/snippet}
568
+ </ToggleGroup>
569
+
570
+ <!-- Multiple selection (checkbox-like) -->
571
+ <ToggleGroup bind:value={formats} type="multiple">
572
+ {#snippet children()}
573
+ <ToggleGroupItem value="bold">B</ToggleGroupItem>
574
+ <ToggleGroupItem value="italic">I</ToggleGroupItem>
575
+ <ToggleGroupItem value="underline">U</ToggleGroupItem>
576
+ {/snippet}
577
+ </ToggleGroup>
578
+ ```
579
+
580
+ ### Pagination
581
+
582
+ Page navigation with ellipsis for large page ranges.
583
+
584
+ **Props:**
585
+
586
+ | Prop | Type | Default | Description |
587
+ | -------------- | ------------------------ | ------- | --------------------------------- |
588
+ | `currentPage` | `number` | - | Current page number (required) |
589
+ | `totalPages` | `number` | - | Total number of pages (required) |
590
+ | `siblingCount` | `number` | `1` | Pages to show around current page |
591
+ | `onPageChange` | `(page: number) => void` | - | Callback when page changes |
592
+
593
+ **Usage:**
594
+
595
+ ```svelte
596
+ <script>
597
+ import { Pagination } from '@aspect-ops/exon-ui';
598
+
599
+ let currentPage = $state(1);
600
+ const totalPages = 20;
601
+ </script>
602
+
603
+ <Pagination {currentPage} {totalPages} onPageChange={(page) => (currentPage = page)} />
604
+
605
+ <!-- With more visible siblings -->
606
+ <Pagination
607
+ {currentPage}
608
+ {totalPages}
609
+ siblingCount={2}
610
+ onPageChange={(page) => (currentPage = page)}
611
+ />
612
+ ```
613
+
614
+ **Keyboard Navigation:**
615
+
616
+ - `←` / `→`: Previous / Next page
617
+ - `Home`: First page
618
+ - `End`: Last page
619
+
495
620
  ## Data Display Components
496
621
 
497
622
  ### Accordion
@@ -0,0 +1,293 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ value?: number | null;
4
+ min?: number;
5
+ max?: number;
6
+ step?: number;
7
+ disabled?: boolean;
8
+ placeholder?: string;
9
+ name?: string;
10
+ id?: string;
11
+ error?: boolean;
12
+ class?: string;
13
+ onValueChange?: (value: number | null) => void;
14
+ }
15
+
16
+ let {
17
+ value = $bindable(null),
18
+ min,
19
+ max,
20
+ step = 1,
21
+ disabled = false,
22
+ placeholder,
23
+ name,
24
+ id,
25
+ error = false,
26
+ class: className = '',
27
+ onValueChange
28
+ }: Props = $props();
29
+
30
+ let inputValue = $state(value?.toString() ?? '');
31
+
32
+ // Sync internal state with external value prop
33
+ $effect(() => {
34
+ inputValue = value?.toString() ?? '';
35
+ });
36
+
37
+ const isDecrementDisabled = $derived(
38
+ disabled || (min !== undefined && value !== null && value <= min)
39
+ );
40
+ const isIncrementDisabled = $derived(
41
+ disabled || (max !== undefined && value !== null && value >= max)
42
+ );
43
+
44
+ function handleIncrement() {
45
+ if (isIncrementDisabled) return;
46
+
47
+ const currentVal = value ?? 0;
48
+ const newValue = currentVal + step;
49
+ const clampedValue = max !== undefined ? Math.min(newValue, max) : newValue;
50
+
51
+ value = clampedValue;
52
+ onValueChange?.(clampedValue);
53
+ }
54
+
55
+ function handleDecrement() {
56
+ if (isDecrementDisabled) return;
57
+
58
+ const currentVal = value ?? 0;
59
+ const newValue = currentVal - step;
60
+ const clampedValue = min !== undefined ? Math.max(newValue, min) : newValue;
61
+
62
+ value = clampedValue;
63
+ onValueChange?.(clampedValue);
64
+ }
65
+
66
+ function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
67
+ inputValue = e.currentTarget.value;
68
+
69
+ if (inputValue === '') {
70
+ value = null;
71
+ onValueChange?.(null);
72
+ return;
73
+ }
74
+
75
+ const parsed = parseFloat(inputValue);
76
+ if (!isNaN(parsed)) {
77
+ value = parsed;
78
+ onValueChange?.(parsed);
79
+ }
80
+ }
81
+
82
+ function handleBlur() {
83
+ if (value === null) {
84
+ inputValue = '';
85
+ return;
86
+ }
87
+
88
+ // Clamp value to min/max on blur
89
+ let clampedValue = value;
90
+ if (min !== undefined && clampedValue < min) {
91
+ clampedValue = min;
92
+ }
93
+ if (max !== undefined && clampedValue > max) {
94
+ clampedValue = max;
95
+ }
96
+
97
+ if (clampedValue !== value) {
98
+ value = clampedValue;
99
+ onValueChange?.(clampedValue);
100
+ }
101
+
102
+ inputValue = clampedValue.toString();
103
+ }
104
+
105
+ function handleKeyDown(e: KeyboardEvent) {
106
+ if (e.key === 'ArrowUp') {
107
+ e.preventDefault();
108
+ handleIncrement();
109
+ } else if (e.key === 'ArrowDown') {
110
+ e.preventDefault();
111
+ handleDecrement();
112
+ }
113
+ }
114
+ </script>
115
+
116
+ <div
117
+ class="number-input {error ? 'number-input--error' : ''} {disabled
118
+ ? 'number-input--disabled'
119
+ : ''} {className}"
120
+ >
121
+ <button
122
+ type="button"
123
+ class="number-input__button number-input__button--decrement"
124
+ disabled={isDecrementDisabled}
125
+ onclick={handleDecrement}
126
+ aria-label="Decrement value"
127
+ >
128
+ <svg
129
+ xmlns="http://www.w3.org/2000/svg"
130
+ width="16"
131
+ height="16"
132
+ viewBox="0 0 24 24"
133
+ fill="none"
134
+ stroke="currentColor"
135
+ stroke-width="2"
136
+ stroke-linecap="round"
137
+ stroke-linejoin="round"
138
+ >
139
+ <line x1="5" y1="12" x2="19" y2="12"></line>
140
+ </svg>
141
+ </button>
142
+
143
+ <input
144
+ type="text"
145
+ inputmode="decimal"
146
+ class="number-input__field"
147
+ role="spinbutton"
148
+ aria-valuemin={min}
149
+ aria-valuemax={max}
150
+ aria-valuenow={value ?? undefined}
151
+ {name}
152
+ {id}
153
+ {placeholder}
154
+ {disabled}
155
+ aria-invalid={error}
156
+ value={inputValue}
157
+ oninput={handleInput}
158
+ onblur={handleBlur}
159
+ onkeydown={handleKeyDown}
160
+ />
161
+
162
+ <button
163
+ type="button"
164
+ class="number-input__button number-input__button--increment"
165
+ disabled={isIncrementDisabled}
166
+ onclick={handleIncrement}
167
+ aria-label="Increment value"
168
+ >
169
+ <svg
170
+ xmlns="http://www.w3.org/2000/svg"
171
+ width="16"
172
+ height="16"
173
+ viewBox="0 0 24 24"
174
+ fill="none"
175
+ stroke="currentColor"
176
+ stroke-width="2"
177
+ stroke-linecap="round"
178
+ stroke-linejoin="round"
179
+ >
180
+ <line x1="12" y1="5" x2="12" y2="19"></line>
181
+ <line x1="5" y1="12" x2="19" y2="12"></line>
182
+ </svg>
183
+ </button>
184
+ </div>
185
+
186
+ <style>
187
+ .number-input {
188
+ display: flex;
189
+ align-items: stretch;
190
+ width: 100%;
191
+ gap: 0;
192
+ }
193
+
194
+ .number-input--disabled {
195
+ opacity: 0.5;
196
+ }
197
+
198
+ .number-input__button {
199
+ display: flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ min-width: var(--touch-target-min, 44px);
203
+ min-height: var(--touch-target-min, 44px);
204
+ padding: 0;
205
+ border: 1px solid var(--color-border, #e5e7eb);
206
+ background: var(--color-bg, #ffffff);
207
+ color: var(--color-text, #1f2937);
208
+ font-family: inherit;
209
+ cursor: pointer;
210
+ transition: all var(--transition-fast, 150ms ease);
211
+ -webkit-tap-highlight-color: transparent;
212
+ }
213
+
214
+ .number-input__button:hover:not(:disabled) {
215
+ background: var(--color-bg-muted, #f3f4f6);
216
+ border-color: var(--color-border-hover, #d1d5db);
217
+ }
218
+
219
+ .number-input__button:active:not(:disabled) {
220
+ background: var(--color-border, #e5e7eb);
221
+ }
222
+
223
+ .number-input__button:focus-visible {
224
+ outline: 2px solid var(--color-primary, #3b82f6);
225
+ outline-offset: 2px;
226
+ z-index: 1;
227
+ }
228
+
229
+ .number-input__button:disabled {
230
+ opacity: 0.3;
231
+ cursor: not-allowed;
232
+ pointer-events: none;
233
+ }
234
+
235
+ .number-input__button--decrement {
236
+ border-top-left-radius: var(--radius-md, 0.375rem);
237
+ border-bottom-left-radius: var(--radius-md, 0.375rem);
238
+ border-right: none;
239
+ }
240
+
241
+ .number-input__button--increment {
242
+ border-top-right-radius: var(--radius-md, 0.375rem);
243
+ border-bottom-right-radius: var(--radius-md, 0.375rem);
244
+ border-left: none;
245
+ }
246
+
247
+ .number-input__field {
248
+ flex: 1;
249
+ min-width: 0;
250
+ min-height: var(--touch-target-min, 44px);
251
+ padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
252
+ border: 1px solid var(--color-border, #e5e7eb);
253
+ border-left: none;
254
+ border-right: none;
255
+ background: var(--color-bg, #ffffff);
256
+ color: var(--color-text, #1f2937);
257
+ font-family: inherit;
258
+ font-size: var(--text-sm, 0.875rem);
259
+ line-height: 1.5;
260
+ text-align: center;
261
+ transition:
262
+ border-color var(--transition-fast, 150ms ease),
263
+ box-shadow var(--transition-fast, 150ms ease);
264
+ -webkit-tap-highlight-color: transparent;
265
+ }
266
+
267
+ .number-input__field::placeholder {
268
+ color: var(--color-text-muted, #9ca3af);
269
+ }
270
+
271
+ .number-input__field:focus {
272
+ outline: none;
273
+ border-color: var(--color-primary, #3b82f6);
274
+ box-shadow: 0 0 0 3px var(--color-primary-alpha, rgba(59, 130, 246, 0.1));
275
+ z-index: 1;
276
+ }
277
+
278
+ .number-input__field:disabled {
279
+ background: var(--color-bg-muted, #f3f4f6);
280
+ cursor: not-allowed;
281
+ }
282
+
283
+ /* Error state */
284
+ .number-input--error .number-input__field,
285
+ .number-input--error .number-input__button {
286
+ border-color: var(--color-error, #ef4444);
287
+ }
288
+
289
+ .number-input--error .number-input__field:focus {
290
+ border-color: var(--color-error, #ef4444);
291
+ box-shadow: 0 0 0 3px var(--color-error-alpha, rgba(239, 68, 68, 0.1));
292
+ }
293
+ </style>
@@ -0,0 +1,16 @@
1
+ interface Props {
2
+ value?: number | null;
3
+ min?: number;
4
+ max?: number;
5
+ step?: number;
6
+ disabled?: boolean;
7
+ placeholder?: string;
8
+ name?: string;
9
+ id?: string;
10
+ error?: boolean;
11
+ class?: string;
12
+ onValueChange?: (value: number | null) => void;
13
+ }
14
+ declare const NumberInput: import("svelte").Component<Props, {}, "value">;
15
+ type NumberInput = ReturnType<typeof NumberInput>;
16
+ export default NumberInput;
@@ -0,0 +1 @@
1
+ export { default as NumberInput } from './NumberInput.svelte';
@@ -0,0 +1 @@
1
+ export { default as NumberInput } from './NumberInput.svelte';
@@ -0,0 +1,243 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ currentPage: number;
4
+ totalPages: number;
5
+ siblingCount?: number;
6
+ onPageChange?: (page: number) => void;
7
+ class?: string;
8
+ }
9
+
10
+ let {
11
+ currentPage,
12
+ totalPages,
13
+ siblingCount = 1,
14
+ onPageChange,
15
+ class: className = ''
16
+ }: Props = $props();
17
+
18
+ const canGoPrevious = $derived(currentPage > 1);
19
+ const canGoNext = $derived(currentPage < totalPages);
20
+
21
+ // Generate page numbers with ellipsis logic
22
+ const pageNumbers = $derived(() => {
23
+ const pages: (number | 'ellipsis-start' | 'ellipsis-end')[] = [];
24
+
25
+ // Always show first page
26
+ pages.push(1);
27
+
28
+ // Calculate the range around current page
29
+ const leftSibling = Math.max(currentPage - siblingCount, 2);
30
+ const rightSibling = Math.min(currentPage + siblingCount, totalPages - 1);
31
+
32
+ // Add ellipsis after first page if needed
33
+ if (leftSibling > 2) {
34
+ pages.push('ellipsis-start');
35
+ }
36
+
37
+ // Add sibling pages
38
+ for (let i = leftSibling; i <= rightSibling; i++) {
39
+ pages.push(i);
40
+ }
41
+
42
+ // Add ellipsis before last page if needed
43
+ if (rightSibling < totalPages - 1) {
44
+ pages.push('ellipsis-end');
45
+ }
46
+
47
+ // Always show last page (if more than 1 page)
48
+ if (totalPages > 1) {
49
+ pages.push(totalPages);
50
+ }
51
+
52
+ return pages;
53
+ });
54
+
55
+ function handlePageClick(page: number) {
56
+ if (page !== currentPage && page >= 1 && page <= totalPages) {
57
+ onPageChange?.(page);
58
+ }
59
+ }
60
+
61
+ function handlePrevious() {
62
+ if (canGoPrevious) {
63
+ onPageChange?.(currentPage - 1);
64
+ }
65
+ }
66
+
67
+ function handleNext() {
68
+ if (canGoNext) {
69
+ onPageChange?.(currentPage + 1);
70
+ }
71
+ }
72
+
73
+ function handleKeyDown(event: KeyboardEvent) {
74
+ switch (event.key) {
75
+ case 'ArrowLeft':
76
+ event.preventDefault();
77
+ handlePrevious();
78
+ break;
79
+ case 'ArrowRight':
80
+ event.preventDefault();
81
+ handleNext();
82
+ break;
83
+ case 'Home':
84
+ event.preventDefault();
85
+ handlePageClick(1);
86
+ break;
87
+ case 'End':
88
+ event.preventDefault();
89
+ handlePageClick(totalPages);
90
+ break;
91
+ }
92
+ }
93
+ </script>
94
+
95
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
96
+ <nav class="pagination {className}" aria-label="Pagination">
97
+ <div class="pagination-wrapper" role="group" onkeydown={handleKeyDown}>
98
+ <button
99
+ class="pagination-button pagination-button--previous"
100
+ disabled={!canGoPrevious}
101
+ onclick={handlePrevious}
102
+ aria-label="Previous page"
103
+ >
104
+ Previous
105
+ </button>
106
+
107
+ <div class="pagination-pages">
108
+ {#each pageNumbers() as item}
109
+ {#if typeof item === 'number'}
110
+ <button
111
+ class="pagination-button pagination-button--page"
112
+ class:pagination-button--current={item === currentPage}
113
+ onclick={() => handlePageClick(item)}
114
+ aria-label={item === currentPage ? `Current page, page ${item}` : `Go to page ${item}`}
115
+ aria-current={item === currentPage ? 'page' : undefined}
116
+ >
117
+ {item}
118
+ </button>
119
+ {:else}
120
+ <span class="pagination-ellipsis" aria-hidden="true">...</span>
121
+ {/if}
122
+ {/each}
123
+ </div>
124
+
125
+ <button
126
+ class="pagination-button pagination-button--next"
127
+ disabled={!canGoNext}
128
+ onclick={handleNext}
129
+ aria-label="Next page"
130
+ >
131
+ Next
132
+ </button>
133
+ </div>
134
+ </nav>
135
+
136
+ <style>
137
+ .pagination {
138
+ font-family: inherit;
139
+ }
140
+
141
+ .pagination-wrapper {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 0.5rem;
145
+ }
146
+
147
+ .pagination-pages {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 0.25rem;
151
+ }
152
+
153
+ .pagination-button {
154
+ min-width: 2.75rem;
155
+ min-height: 2.75rem; /* 44px minimum touch target */
156
+ padding: 0.5rem 0.75rem;
157
+ border: 1px solid var(--pagination-border-color, #d1d5db);
158
+ border-radius: var(--pagination-border-radius, 0.375rem);
159
+ background-color: var(--pagination-bg-color, #ffffff);
160
+ color: var(--pagination-text-color, #374151);
161
+ font-family: inherit;
162
+ font-size: 0.875rem;
163
+ font-weight: 500;
164
+ cursor: pointer;
165
+ transition: all 0.2s ease;
166
+ }
167
+
168
+ .pagination-button:hover:not(:disabled) {
169
+ background-color: var(--pagination-hover-bg-color, #f3f4f6);
170
+ border-color: var(--pagination-hover-border-color, #9ca3af);
171
+ }
172
+
173
+ .pagination-button:focus-visible {
174
+ outline: 2px solid var(--pagination-focus-color, #3b82f6);
175
+ outline-offset: 2px;
176
+ }
177
+
178
+ .pagination-button:disabled {
179
+ opacity: 0.5;
180
+ cursor: not-allowed;
181
+ }
182
+
183
+ .pagination-button--page {
184
+ min-width: 2.75rem;
185
+ }
186
+
187
+ .pagination-button--current {
188
+ background-color: var(--pagination-current-bg-color, #3b82f6);
189
+ color: var(--pagination-current-text-color, #ffffff);
190
+ border-color: var(--pagination-current-border-color, #3b82f6);
191
+ }
192
+
193
+ .pagination-button--current:hover {
194
+ background-color: var(--pagination-current-hover-bg-color, #2563eb);
195
+ border-color: var(--pagination-current-hover-border-color, #2563eb);
196
+ }
197
+
198
+ .pagination-button--previous,
199
+ .pagination-button--next {
200
+ padding-left: 1rem;
201
+ padding-right: 1rem;
202
+ }
203
+
204
+ .pagination-ellipsis {
205
+ display: inline-flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ min-width: 2.75rem;
209
+ min-height: 2.75rem;
210
+ padding: 0.5rem;
211
+ color: var(--pagination-ellipsis-color, #6b7280);
212
+ font-size: 0.875rem;
213
+ }
214
+
215
+ /* Mobile responsive: hide some siblings on small screens */
216
+ @media (max-width: 640px) {
217
+ .pagination-wrapper {
218
+ gap: 0.25rem;
219
+ }
220
+
221
+ .pagination-pages {
222
+ gap: 0.125rem;
223
+ }
224
+
225
+ .pagination-button--previous,
226
+ .pagination-button--next {
227
+ padding-left: 0.75rem;
228
+ padding-right: 0.75rem;
229
+ font-size: 0.75rem;
230
+ }
231
+
232
+ .pagination-button--page {
233
+ min-width: 2.75rem;
234
+ padding: 0.5rem;
235
+ font-size: 0.75rem;
236
+ }
237
+
238
+ .pagination-ellipsis {
239
+ min-width: 2rem;
240
+ padding: 0.25rem;
241
+ }
242
+ }
243
+ </style>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ currentPage: number;
3
+ totalPages: number;
4
+ siblingCount?: number;
5
+ onPageChange?: (page: number) => void;
6
+ class?: string;
7
+ }
8
+ declare const Pagination: import("svelte").Component<Props, {}, "">;
9
+ type Pagination = ReturnType<typeof Pagination>;
10
+ export default Pagination;
@@ -0,0 +1 @@
1
+ export { default as Pagination } from './Pagination.svelte';
@@ -0,0 +1 @@
1
+ export { default as Pagination } from './Pagination.svelte';
@@ -0,0 +1,91 @@
1
+ <script lang="ts">
2
+ import { setContext } from 'svelte';
3
+ import type { ToggleGroupType, ToggleGroupOrientation } from '../../types/index.js';
4
+
5
+ interface Props {
6
+ type?: ToggleGroupType;
7
+ value?: string | string[];
8
+ onValueChange?: (value: string | string[]) => void;
9
+ disabled?: boolean;
10
+ orientation?: ToggleGroupOrientation;
11
+ class?: string;
12
+ children?: import('svelte').Snippet;
13
+ }
14
+
15
+ let {
16
+ type = 'single',
17
+ value = $bindable(type === 'single' ? '' : []),
18
+ onValueChange,
19
+ disabled = false,
20
+ orientation = 'horizontal',
21
+ class: className = '',
22
+ children
23
+ }: Props = $props();
24
+
25
+ const handleToggle = (itemValue: string) => {
26
+ if (disabled) return;
27
+
28
+ let newValue: string | string[];
29
+
30
+ if (type === 'single') {
31
+ // Single mode: toggle or select new value
32
+ newValue = (value as string) === itemValue ? '' : itemValue;
33
+ } else {
34
+ // Multiple mode: toggle item in array
35
+ const currentArray = (value as string[]) || [];
36
+ if (currentArray.includes(itemValue)) {
37
+ newValue = currentArray.filter((v) => v !== itemValue);
38
+ } else {
39
+ newValue = [...currentArray, itemValue];
40
+ }
41
+ }
42
+
43
+ value = newValue;
44
+ onValueChange?.(newValue);
45
+ };
46
+
47
+ const isSelected = (itemValue: string): boolean => {
48
+ if (type === 'single') {
49
+ return (value as string) === itemValue;
50
+ } else {
51
+ return ((value as string[]) || []).includes(itemValue);
52
+ }
53
+ };
54
+
55
+ // Set context for child items
56
+ setContext('toggleGroup', {
57
+ get type() {
58
+ return type;
59
+ },
60
+ get disabled() {
61
+ return disabled;
62
+ },
63
+ onToggle: handleToggle,
64
+ isSelected
65
+ });
66
+ </script>
67
+
68
+ <div
69
+ class="toggle-group toggle-group--{orientation} {className}"
70
+ role={type === 'single' ? 'radiogroup' : 'group'}
71
+ aria-disabled={disabled}
72
+ >
73
+ {#if children}
74
+ {@render children()}
75
+ {/if}
76
+ </div>
77
+
78
+ <style>
79
+ .toggle-group {
80
+ display: inline-flex;
81
+ font-family: inherit;
82
+ }
83
+
84
+ .toggle-group--horizontal {
85
+ flex-direction: row;
86
+ }
87
+
88
+ .toggle-group--vertical {
89
+ flex-direction: column;
90
+ }
91
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { ToggleGroupType, ToggleGroupOrientation } from '../../types/index.js';
2
+ interface Props {
3
+ type?: ToggleGroupType;
4
+ value?: string | string[];
5
+ onValueChange?: (value: string | string[]) => void;
6
+ disabled?: boolean;
7
+ orientation?: ToggleGroupOrientation;
8
+ class?: string;
9
+ children?: import('svelte').Snippet;
10
+ }
11
+ declare const ToggleGroup: import("svelte").Component<Props, {}, "value">;
12
+ type ToggleGroup = ReturnType<typeof ToggleGroup>;
13
+ export default ToggleGroup;
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte';
3
+
4
+ interface Props {
5
+ value: string;
6
+ disabled?: boolean;
7
+ class?: string;
8
+ children?: import('svelte').Snippet;
9
+ }
10
+
11
+ let { value, disabled = false, class: className = '', children }: Props = $props();
12
+
13
+ const context = getContext<{
14
+ type: 'single' | 'multiple';
15
+ disabled: boolean;
16
+ onToggle: (value: string) => void;
17
+ isSelected: (value: string) => boolean;
18
+ }>('toggleGroup');
19
+
20
+ const isGroupDisabled = context?.disabled ?? false;
21
+ const isDisabled = $derived(disabled || isGroupDisabled);
22
+ const isPressed = $derived(context?.isSelected(value) ?? false);
23
+
24
+ const handleClick = () => {
25
+ if (isDisabled) return;
26
+ context?.onToggle(value);
27
+ };
28
+
29
+ const handleKeydown = (e: KeyboardEvent) => {
30
+ const target = e.currentTarget as HTMLElement;
31
+ const group = target.parentElement;
32
+ if (!group) return;
33
+
34
+ const items = Array.from(group.querySelectorAll('[role="radio"], [role="button"]'));
35
+ const currentIndex = items.indexOf(target);
36
+
37
+ let nextIndex = currentIndex;
38
+
39
+ switch (e.key) {
40
+ case 'ArrowRight':
41
+ case 'ArrowDown':
42
+ e.preventDefault();
43
+ nextIndex = (currentIndex + 1) % items.length;
44
+ break;
45
+ case 'ArrowLeft':
46
+ case 'ArrowUp':
47
+ e.preventDefault();
48
+ nextIndex = currentIndex - 1;
49
+ if (nextIndex < 0) nextIndex = items.length - 1;
50
+ break;
51
+ case ' ':
52
+ case 'Enter':
53
+ e.preventDefault();
54
+ handleClick();
55
+ return;
56
+ default:
57
+ return;
58
+ }
59
+
60
+ (items[nextIndex] as HTMLElement)?.focus();
61
+ };
62
+ </script>
63
+
64
+ <button
65
+ class="toggle-group-item {isPressed ? 'toggle-group-item--pressed' : ''} {className}"
66
+ role={context?.type === 'single' ? 'radio' : 'button'}
67
+ aria-pressed={isPressed}
68
+ aria-checked={context?.type === 'single' ? isPressed : undefined}
69
+ aria-disabled={isDisabled}
70
+ disabled={isDisabled}
71
+ tabindex={isDisabled ? -1 : 0}
72
+ onclick={handleClick}
73
+ onkeydown={handleKeydown}
74
+ >
75
+ {#if children}
76
+ {@render children()}
77
+ {/if}
78
+ </button>
79
+
80
+ <style>
81
+ .toggle-group-item {
82
+ position: relative;
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ min-height: var(--touch-target-min, 44px);
87
+ min-width: var(--touch-target-min, 44px);
88
+ padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
89
+ background: var(--color-bg, #ffffff);
90
+ color: var(--color-text, #1f2937);
91
+ border: 1px solid var(--color-border, #e5e7eb);
92
+ font-family: inherit;
93
+ font-size: var(--text-sm, 0.875rem);
94
+ font-weight: 500;
95
+ cursor: pointer;
96
+ transition: all var(--transition-fast, 150ms ease);
97
+ -webkit-tap-highlight-color: transparent;
98
+ margin-left: -1px;
99
+ }
100
+
101
+ .toggle-group-item:first-child {
102
+ margin-left: 0;
103
+ border-top-left-radius: var(--radius-md, 0.375rem);
104
+ border-bottom-left-radius: var(--radius-md, 0.375rem);
105
+ }
106
+
107
+ .toggle-group-item:last-child {
108
+ border-top-right-radius: var(--radius-md, 0.375rem);
109
+ border-bottom-right-radius: var(--radius-md, 0.375rem);
110
+ }
111
+
112
+ :global(.toggle-group--vertical) .toggle-group-item {
113
+ margin-left: 0;
114
+ margin-top: -1px;
115
+ }
116
+
117
+ :global(.toggle-group--vertical) .toggle-group-item:first-child {
118
+ margin-top: 0;
119
+ border-top-left-radius: var(--radius-md, 0.375rem);
120
+ border-top-right-radius: var(--radius-md, 0.375rem);
121
+ border-bottom-left-radius: 0;
122
+ }
123
+
124
+ :global(.toggle-group--vertical) .toggle-group-item:last-child {
125
+ border-top-right-radius: 0;
126
+ border-bottom-left-radius: var(--radius-md, 0.375rem);
127
+ border-bottom-right-radius: var(--radius-md, 0.375rem);
128
+ }
129
+
130
+ .toggle-group-item:hover:not(:disabled) {
131
+ background: var(--color-bg-muted, #f3f4f6);
132
+ z-index: 1;
133
+ }
134
+
135
+ .toggle-group-item:focus-visible {
136
+ outline: 2px solid var(--color-primary, #3b82f6);
137
+ outline-offset: 2px;
138
+ z-index: 2;
139
+ }
140
+
141
+ .toggle-group-item--pressed {
142
+ background: var(--color-primary, #3b82f6);
143
+ color: var(--color-text-inverse, #ffffff);
144
+ border-color: var(--color-primary, #3b82f6);
145
+ z-index: 1;
146
+ }
147
+
148
+ .toggle-group-item--pressed:hover:not(:disabled) {
149
+ background: var(--color-primary-hover, #2563eb);
150
+ border-color: var(--color-primary-hover, #2563eb);
151
+ }
152
+
153
+ .toggle-group-item:disabled {
154
+ opacity: 0.5;
155
+ cursor: not-allowed;
156
+ pointer-events: none;
157
+ }
158
+ </style>
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ value: string;
3
+ disabled?: boolean;
4
+ class?: string;
5
+ children?: import('svelte').Snippet;
6
+ }
7
+ declare const ToggleGroupItem: import("svelte").Component<Props, {}, "">;
8
+ type ToggleGroupItem = ReturnType<typeof ToggleGroupItem>;
9
+ export default ToggleGroupItem;
@@ -0,0 +1,3 @@
1
+ export { default as ToggleGroup } from './ToggleGroup.svelte';
2
+ export { default as ToggleGroupItem } from './ToggleGroupItem.svelte';
3
+ export type { ToggleGroupType, ToggleGroupOrientation, ToggleGroupProps, ToggleGroupItemProps } from '../../types/index.js';
@@ -0,0 +1,2 @@
1
+ export { default as ToggleGroup } from './ToggleGroup.svelte';
2
+ export { default as ToggleGroupItem } from './ToggleGroupItem.svelte';
package/dist/index.d.ts CHANGED
@@ -16,6 +16,9 @@ export { SearchInput } from './components/SearchInput/index.js';
16
16
  export { DatePicker } from './components/DatePicker/index.js';
17
17
  export { FileUpload } from './components/FileUpload/index.js';
18
18
  export { OTPInput } from './components/OTPInput/index.js';
19
+ export { TimePicker } from './components/TimePicker/index.js';
20
+ export { Rating } from './components/Rating/index.js';
21
+ export { ToggleGroup, ToggleGroupItem } from './components/ToggleGroup/index.js';
19
22
  export { default as Tabs } from './components/Tabs/Tabs.svelte';
20
23
  export { default as TabList } from './components/Tabs/TabList.svelte';
21
24
  export { default as TabTrigger } from './components/Tabs/TabTrigger.svelte';
@@ -71,4 +74,4 @@ export { ActionSheet, ActionSheetItem } from './components/ActionSheet/index.js'
71
74
  export { PullToRefresh } from './components/PullToRefresh/index.js';
72
75
  export { SwipeActions, SwipeAction } from './components/SwipeActions/index.js';
73
76
  export { FAB, FABGroup } from './components/FAB/index.js';
74
- export type { ButtonProps, ButtonVariant, ButtonSize, TypographyProps, TypographyVariant, IconProps, IconSize, BadgeProps, BadgeVariant, LinkProps, InputSize, TextInputType, FormFieldProps, TextInputProps, TextareaProps, SelectOption, SelectProps, CheckboxProps, RadioProps, RadioGroupProps, SwitchProps, SearchInputProps, DatePickerProps, FileUploadProps, OTPInputProps, TimeFormat, TimePickerProps, RatingProps, TabsOrientation, TabsProps, TabListProps, TabTriggerProps, TabContentProps, MenuProps, MenuTriggerProps, MenuContentProps, MenuItemProps, MenuSeparatorProps, MenuSubProps, MenuSubTriggerProps, MenuSubContentProps, BreadcrumbItemData, BreadcrumbsProps, BreadcrumbItemProps, BottomNavItemData, BottomNavProps, BottomNavItemProps, NavItemProps, NavbarProps, SidebarItemData, SidebarProps, SidebarItemProps, SidebarGroupProps, StepperOrientation, StepperProps, StepperStepProps, AlertVariant, AlertProps, ToastVariant, ToastPosition, ToastData, ToastProps, ToastContainerProps, ModalSize, ModalProps, ModalHeaderProps, ModalBodyProps, ModalFooterProps, ProgressSize, ProgressBarProps, ProgressCircleProps, SpinnerProps, TooltipSide, TooltipProps, PopoverSide, PopoverProps, PopoverTriggerProps, PopoverContentProps, SkeletonVariant, SkeletonProps, CardVariant, CardProps, CardHeaderProps, CardBodyProps, CardFooterProps, AvatarSize, AvatarShape, AvatarProps, AvatarGroupProps, TagVariant, TagSize, TagProps, ChipVariant, ChipColor, ChipSize, ChipProps, ChipGroupProps, EmptyStateSize, EmptyStateProps, ListProps, ListItemProps, TableProps, TableHeadProps, TableHeaderAlign, TableHeaderSortDirection, TableHeaderProps, TableBodyProps, TableRowProps, TableCellAlign, TableCellProps, AccordionProps, AccordionItemProps, SliderProps, CarouselProps, CarouselSlideProps, ImageObjectFit, ImageRounded, ImageProps, ContainerSize, ContainerProps, GridAlign, GridJustify, SpacingToken, GridProps, GridItemProps, StackDirection, StackProps, DividerOrientation, DividerProps, SpacerSize, SpacerProps, AspectRatioPreset, AspectRatioProps, CenterProps, BoxProps, SafeAreaEdge, SafeAreaProps, BottomSheetSnapPoint, BottomSheetProps, BottomSheetHeaderProps, BottomSheetBodyProps, ActionSheetAction, ActionSheetProps, ActionSheetItemProps, PullToRefreshProps, SwipeActionSide, SwipeActionData, SwipeActionsProps, SwipeActionProps, FABSize, FABPosition, FABProps, FABAction, FABGroupProps } from './types/index.js';
77
+ export type { ButtonProps, ButtonVariant, ButtonSize, TypographyProps, TypographyVariant, IconProps, IconSize, BadgeProps, BadgeVariant, LinkProps, InputSize, TextInputType, FormFieldProps, TextInputProps, TextareaProps, SelectOption, SelectProps, CheckboxProps, RadioProps, RadioGroupProps, SwitchProps, SearchInputProps, DatePickerProps, FileUploadProps, OTPInputProps, TimeFormat, TimePickerProps, RatingProps, ToggleGroupType, ToggleGroupOrientation, ToggleGroupProps, ToggleGroupItemProps, TabsOrientation, TabsProps, TabListProps, TabTriggerProps, TabContentProps, MenuProps, MenuTriggerProps, MenuContentProps, MenuItemProps, MenuSeparatorProps, MenuSubProps, MenuSubTriggerProps, MenuSubContentProps, BreadcrumbItemData, BreadcrumbsProps, BreadcrumbItemProps, BottomNavItemData, BottomNavProps, BottomNavItemProps, NavItemProps, NavbarProps, SidebarItemData, SidebarProps, SidebarItemProps, SidebarGroupProps, StepperOrientation, StepperProps, StepperStepProps, AlertVariant, AlertProps, ToastVariant, ToastPosition, ToastData, ToastProps, ToastContainerProps, ModalSize, ModalProps, ModalHeaderProps, ModalBodyProps, ModalFooterProps, ProgressSize, ProgressBarProps, ProgressCircleProps, SpinnerProps, TooltipSide, TooltipProps, PopoverSide, PopoverProps, PopoverTriggerProps, PopoverContentProps, SkeletonVariant, SkeletonProps, CardVariant, CardProps, CardHeaderProps, CardBodyProps, CardFooterProps, AvatarSize, AvatarShape, AvatarProps, AvatarGroupProps, TagVariant, TagSize, TagProps, ChipVariant, ChipColor, ChipSize, ChipProps, ChipGroupProps, EmptyStateSize, EmptyStateProps, ListProps, ListItemProps, TableProps, TableHeadProps, TableHeaderAlign, TableHeaderSortDirection, TableHeaderProps, TableBodyProps, TableRowProps, TableCellAlign, TableCellProps, AccordionProps, AccordionItemProps, SliderProps, CarouselProps, CarouselSlideProps, ImageObjectFit, ImageRounded, ImageProps, ContainerSize, ContainerProps, GridAlign, GridJustify, SpacingToken, GridProps, GridItemProps, StackDirection, StackProps, DividerOrientation, DividerProps, SpacerSize, SpacerProps, AspectRatioPreset, AspectRatioProps, CenterProps, BoxProps, SafeAreaEdge, SafeAreaProps, BottomSheetSnapPoint, BottomSheetProps, BottomSheetHeaderProps, BottomSheetBodyProps, ActionSheetAction, ActionSheetProps, ActionSheetItemProps, PullToRefreshProps, SwipeActionSide, SwipeActionData, SwipeActionsProps, SwipeActionProps, FABSize, FABPosition, FABProps, FABAction, FABGroupProps } from './types/index.js';
package/dist/index.js CHANGED
@@ -20,6 +20,9 @@ export { SearchInput } from './components/SearchInput/index.js';
20
20
  export { DatePicker } from './components/DatePicker/index.js';
21
21
  export { FileUpload } from './components/FileUpload/index.js';
22
22
  export { OTPInput } from './components/OTPInput/index.js';
23
+ export { TimePicker } from './components/TimePicker/index.js';
24
+ export { Rating } from './components/Rating/index.js';
25
+ export { ToggleGroup, ToggleGroupItem } from './components/ToggleGroup/index.js';
23
26
  // Navigation Components
24
27
  export { default as Tabs } from './components/Tabs/Tabs.svelte';
25
28
  export { default as TabList } from './components/Tabs/TabList.svelte';
@@ -131,4 +131,4 @@ export type { AlertVariant, AlertProps, ToastVariant, ToastPosition, ToastData,
131
131
  export type { CardVariant, CardProps, CardHeaderProps, CardBodyProps, CardFooterProps, AvatarSize, AvatarShape, AvatarProps, AvatarGroupProps, TagVariant, TagSize, TagProps, ChipVariant, ChipColor, ChipSize, ChipProps, ChipGroupProps, EmptyStateSize, EmptyStateProps, ListProps, ListItemProps, TableProps, TableHeadProps, TableHeaderAlign, TableHeaderSortDirection, TableHeaderProps, TableBodyProps, TableRowProps, TableCellAlign, TableCellProps, AccordionProps, AccordionItemProps, SliderProps, CarouselProps, CarouselSlideProps, ImageObjectFit, ImageRounded, ImageProps } from './data-display.js';
132
132
  export type { ContainerSize, ContainerProps, GridAlign, GridJustify, SpacingToken, GridProps, GridItemProps, StackDirection, StackProps, DividerOrientation, DividerProps, SpacerSize, SpacerProps, AspectRatioPreset, AspectRatioProps, CenterProps, BoxProps } from './layout.js';
133
133
  export type { SafeAreaEdge, SafeAreaProps, BottomSheetSnapPoint, BottomSheetProps, BottomSheetHeaderProps, BottomSheetBodyProps, ActionSheetAction, ActionSheetProps, ActionSheetItemProps, PullToRefreshProps, SwipeActionSide, SwipeActionData, SwipeActionsProps, SwipeActionProps, FABSize, FABPosition, FABProps, FABAction, FABGroupProps } from './mobile.js';
134
- export type { SearchInputProps, DatePickerProps, FileUploadProps, OTPInputProps, TimeFormat, TimePickerProps, RatingProps } from './input.js';
134
+ export type { SearchInputProps, DatePickerProps, FileUploadProps, OTPInputProps, TimeFormat, TimePickerProps, RatingProps, ToggleGroupType, ToggleGroupOrientation, ToggleGroupProps, ToggleGroupItemProps } from './input.js';
@@ -65,3 +65,18 @@ export interface RatingProps {
65
65
  showValue?: boolean;
66
66
  class?: string;
67
67
  }
68
+ export type ToggleGroupType = 'single' | 'multiple';
69
+ export type ToggleGroupOrientation = 'horizontal' | 'vertical';
70
+ export interface ToggleGroupProps {
71
+ type?: ToggleGroupType;
72
+ value?: string | string[];
73
+ onValueChange?: (value: string | string[]) => void;
74
+ disabled?: boolean;
75
+ orientation?: ToggleGroupOrientation;
76
+ class?: string;
77
+ }
78
+ export interface ToggleGroupItemProps {
79
+ value: string;
80
+ disabled?: boolean;
81
+ class?: string;
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspect-ops/exon-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Reusable Svelte UI components for web and Capacitor mobile apps",
5
5
  "author": "Exon Team",
6
6
  "license": "MIT",