@granularjs/ui 0.1.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.
Files changed (220) hide show
  1. package/README.md +116 -0
  2. package/dist/fonts/Arimo-400.ttf +0 -0
  3. package/dist/fonts/Arimo-500.ttf +1449 -0
  4. package/dist/fonts/Arimo-600.ttf +1449 -0
  5. package/dist/fonts/Arimo-700.ttf +0 -0
  6. package/dist/fonts/Inter-400.woff2 +0 -0
  7. package/dist/fonts/Inter-500.woff2 +0 -0
  8. package/dist/fonts/Inter-600.woff2 +0 -0
  9. package/dist/fonts/Inter-700.woff2 +0 -0
  10. package/dist/fonts/Poppins-400.ttf +0 -0
  11. package/dist/fonts/Poppins-500.ttf +0 -0
  12. package/dist/fonts/Poppins-600.ttf +0 -0
  13. package/dist/fonts/Poppins-700.ttf +0 -0
  14. package/dist/granular-ui.min.js +3605 -0
  15. package/dist/granular-ui.min.js.map +7 -0
  16. package/package.json +55 -0
  17. package/src/components/Accordion.js +25 -0
  18. package/src/components/ActionIcon.js +20 -0
  19. package/src/components/Affix.js +11 -0
  20. package/src/components/Alert.js +33 -0
  21. package/src/components/Anchor.js +8 -0
  22. package/src/components/AppBar.js +14 -0
  23. package/src/components/Avatar.js +13 -0
  24. package/src/components/AvatarGroup.js +8 -0
  25. package/src/components/Badge.js +22 -0
  26. package/src/components/BadgeGroup.js +8 -0
  27. package/src/components/Blockquote.js +8 -0
  28. package/src/components/BottomBar.js +43 -0
  29. package/src/components/Breadcrumbs.js +19 -0
  30. package/src/components/Burger.js +13 -0
  31. package/src/components/Button.js +37 -0
  32. package/src/components/Calendar.js +109 -0
  33. package/src/components/Card.js +40 -0
  34. package/src/components/Center.js +8 -0
  35. package/src/components/Checkbox.js +46 -0
  36. package/src/components/CheckboxGroup.js +8 -0
  37. package/src/components/Chip.js +35 -0
  38. package/src/components/Code.js +8 -0
  39. package/src/components/Col.js +8 -0
  40. package/src/components/Collapse.js +8 -0
  41. package/src/components/Container.js +19 -0
  42. package/src/components/CopyButton.js +30 -0
  43. package/src/components/DateInput.js +123 -0
  44. package/src/components/DatePicker.js +7 -0
  45. package/src/components/Divider.js +22 -0
  46. package/src/components/Drawer.js +32 -0
  47. package/src/components/EventCalendar.js +972 -0
  48. package/src/components/Fieldset.js +12 -0
  49. package/src/components/Flex.js +25 -0
  50. package/src/components/Grid.js +8 -0
  51. package/src/components/GridTable.js +99 -0
  52. package/src/components/Group.js +29 -0
  53. package/src/components/HoverCard.js +24 -0
  54. package/src/components/Icon.js +19 -0
  55. package/src/components/Image.js +8 -0
  56. package/src/components/Indicator.js +21 -0
  57. package/src/components/Kbd.js +8 -0
  58. package/src/components/List.js +77 -0
  59. package/src/components/Loading.js +29 -0
  60. package/src/components/LoadingOverlay.js +9 -0
  61. package/src/components/Menu.js +129 -0
  62. package/src/components/Modal.js +61 -0
  63. package/src/components/MultiSelect.js +153 -0
  64. package/src/components/NavLink.js +72 -0
  65. package/src/components/Notification.js +42 -0
  66. package/src/components/Notifications.js +59 -0
  67. package/src/components/NumberField.js +389 -0
  68. package/src/components/NumberInput.js +5 -0
  69. package/src/components/Pagination.js +56 -0
  70. package/src/components/Paper.js +20 -0
  71. package/src/components/PasswordInput.js +29 -0
  72. package/src/components/PinInput.js +218 -0
  73. package/src/components/Popover.js +38 -0
  74. package/src/components/Popper.js +25 -0
  75. package/src/components/Progress.js +27 -0
  76. package/src/components/ProgressRing.js +11 -0
  77. package/src/components/Radio.js +22 -0
  78. package/src/components/RadioGroup.js +8 -0
  79. package/src/components/RangePicker.js +45 -0
  80. package/src/components/RangeSlider.js +143 -0
  81. package/src/components/Rating.js +42 -0
  82. package/src/components/ScrollArea.js +11 -0
  83. package/src/components/SearchInput.js +17 -0
  84. package/src/components/SegmentedControl.js +39 -0
  85. package/src/components/Select.js +71 -0
  86. package/src/components/SelectSearch.js +37 -0
  87. package/src/components/Sidebar.js +136 -0
  88. package/src/components/SimpleGrid.js +11 -0
  89. package/src/components/Skeleton.js +24 -0
  90. package/src/components/Slider.js +126 -0
  91. package/src/components/Space.js +8 -0
  92. package/src/components/Stack.js +27 -0
  93. package/src/components/Stepper.js +20 -0
  94. package/src/components/Switch.js +16 -0
  95. package/src/components/SwitchGroup.js +8 -0
  96. package/src/components/Table.js +42 -0
  97. package/src/components/Tabs.js +194 -0
  98. package/src/components/Tag.js +8 -0
  99. package/src/components/Text.js +42 -0
  100. package/src/components/TextInput.js +74 -0
  101. package/src/components/Textarea.js +15 -0
  102. package/src/components/Timeline.js +22 -0
  103. package/src/components/Title.js +18 -0
  104. package/src/components/Toast.js +16 -0
  105. package/src/components/ToastStack.js +21 -0
  106. package/src/components/Tooltip.js +12 -0
  107. package/src/hooks/useDisclosure.js +13 -0
  108. package/src/index.js +98 -0
  109. package/src/theme/fonts/Arimo-400.ttf +0 -0
  110. package/src/theme/fonts/Arimo-500.ttf +1449 -0
  111. package/src/theme/fonts/Arimo-600.ttf +1449 -0
  112. package/src/theme/fonts/Arimo-700.ttf +0 -0
  113. package/src/theme/fonts/Inter-400.woff2 +0 -0
  114. package/src/theme/fonts/Inter-500.woff2 +0 -0
  115. package/src/theme/fonts/Inter-600.woff2 +0 -0
  116. package/src/theme/fonts/Inter-700.woff2 +0 -0
  117. package/src/theme/fonts/Poppins-400.ttf +0 -0
  118. package/src/theme/fonts/Poppins-500.ttf +0 -0
  119. package/src/theme/fonts/Poppins-600.ttf +0 -0
  120. package/src/theme/fonts/Poppins-700.ttf +0 -0
  121. package/src/theme/icons.js +10 -0
  122. package/src/theme/styles.js +3630 -0
  123. package/src/theme/theme.js +71 -0
  124. package/src/utils.js +75 -0
  125. package/types/components/Accordion.d.ts +1 -0
  126. package/types/components/ActionIcon.d.ts +1 -0
  127. package/types/components/Affix.d.ts +1 -0
  128. package/types/components/Alert.d.ts +1 -0
  129. package/types/components/Anchor.d.ts +1 -0
  130. package/types/components/AppBar.d.ts +1 -0
  131. package/types/components/Avatar.d.ts +1 -0
  132. package/types/components/AvatarGroup.d.ts +1 -0
  133. package/types/components/Badge.d.ts +1 -0
  134. package/types/components/BadgeGroup.d.ts +1 -0
  135. package/types/components/Blockquote.d.ts +1 -0
  136. package/types/components/BottomBar.d.ts +4 -0
  137. package/types/components/Breadcrumbs.d.ts +1 -0
  138. package/types/components/Burger.d.ts +1 -0
  139. package/types/components/Button.d.ts +1 -0
  140. package/types/components/Calendar.d.ts +1 -0
  141. package/types/components/Card.d.ts +1 -0
  142. package/types/components/Center.d.ts +1 -0
  143. package/types/components/Checkbox.d.ts +1 -0
  144. package/types/components/CheckboxGroup.d.ts +1 -0
  145. package/types/components/Chip.d.ts +1 -0
  146. package/types/components/Code.d.ts +1 -0
  147. package/types/components/Col.d.ts +1 -0
  148. package/types/components/Collapse.d.ts +1 -0
  149. package/types/components/Container.d.ts +1 -0
  150. package/types/components/CopyButton.d.ts +1 -0
  151. package/types/components/DateInput.d.ts +1 -0
  152. package/types/components/DatePicker.d.ts +1 -0
  153. package/types/components/Divider.d.ts +1 -0
  154. package/types/components/Drawer.d.ts +1 -0
  155. package/types/components/EventCalendar.d.ts +1 -0
  156. package/types/components/Fieldset.d.ts +1 -0
  157. package/types/components/Flex.d.ts +1 -0
  158. package/types/components/Grid.d.ts +1 -0
  159. package/types/components/GridTable.d.ts +5 -0
  160. package/types/components/Group.d.ts +1 -0
  161. package/types/components/HoverCard.d.ts +1 -0
  162. package/types/components/Icon.d.ts +1 -0
  163. package/types/components/Image.d.ts +1 -0
  164. package/types/components/Indicator.d.ts +1 -0
  165. package/types/components/Kbd.d.ts +1 -0
  166. package/types/components/List.d.ts +5 -0
  167. package/types/components/Loading.d.ts +1 -0
  168. package/types/components/LoadingOverlay.d.ts +1 -0
  169. package/types/components/Menu.d.ts +2 -0
  170. package/types/components/Modal.d.ts +1 -0
  171. package/types/components/MultiSelect.d.ts +1 -0
  172. package/types/components/NavLink.d.ts +1 -0
  173. package/types/components/Notification.d.ts +1 -0
  174. package/types/components/Notifications.d.ts +1 -0
  175. package/types/components/NumberField.d.ts +1 -0
  176. package/types/components/NumberInput.d.ts +1 -0
  177. package/types/components/Pagination.d.ts +1 -0
  178. package/types/components/Paper.d.ts +1 -0
  179. package/types/components/PasswordInput.d.ts +1 -0
  180. package/types/components/PinInput.d.ts +1 -0
  181. package/types/components/Popover.d.ts +1 -0
  182. package/types/components/Popper.d.ts +1 -0
  183. package/types/components/Progress.d.ts +1 -0
  184. package/types/components/ProgressRing.d.ts +1 -0
  185. package/types/components/Radio.d.ts +1 -0
  186. package/types/components/RadioGroup.d.ts +1 -0
  187. package/types/components/RangePicker.d.ts +1 -0
  188. package/types/components/RangeSlider.d.ts +1 -0
  189. package/types/components/Rating.d.ts +1 -0
  190. package/types/components/ScrollArea.d.ts +1 -0
  191. package/types/components/SearchInput.d.ts +1 -0
  192. package/types/components/SegmentedControl.d.ts +1 -0
  193. package/types/components/Select.d.ts +1 -0
  194. package/types/components/SelectSearch.d.ts +1 -0
  195. package/types/components/Sidebar.d.ts +1 -0
  196. package/types/components/SimpleGrid.d.ts +1 -0
  197. package/types/components/Skeleton.d.ts +1 -0
  198. package/types/components/Slider.d.ts +5 -0
  199. package/types/components/Space.d.ts +1 -0
  200. package/types/components/Stack.d.ts +1 -0
  201. package/types/components/Stepper.d.ts +1 -0
  202. package/types/components/Switch.d.ts +1 -0
  203. package/types/components/SwitchGroup.d.ts +1 -0
  204. package/types/components/Table.d.ts +1 -0
  205. package/types/components/Tabs.d.ts +1 -0
  206. package/types/components/Tag.d.ts +1 -0
  207. package/types/components/Text.d.ts +1 -0
  208. package/types/components/TextInput.d.ts +1 -0
  209. package/types/components/Textarea.d.ts +1 -0
  210. package/types/components/Timeline.d.ts +1 -0
  211. package/types/components/Title.d.ts +1 -0
  212. package/types/components/Toast.d.ts +1 -0
  213. package/types/components/ToastStack.d.ts +1 -0
  214. package/types/components/Tooltip.d.ts +1 -0
  215. package/types/hooks/useDisclosure.d.ts +1 -0
  216. package/types/index.d.ts +93 -0
  217. package/types/theme/icons.d.ts +10 -0
  218. package/types/theme/styles.d.ts +1 -0
  219. package/types/theme/theme.d.ts +2 -0
  220. package/types/utils.d.ts +12 -0
@@ -0,0 +1,218 @@
1
+ import { Div, Input, after, state, when, list } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, classVar, resolveValue } from '../utils.js';
3
+
4
+ export function PinInput(...args) {
5
+ const { props, rawProps } = splitPropsChildren(args, {
6
+ length: 4,
7
+ size: 'md',
8
+ type: 'alphanumeric',
9
+ mask: false,
10
+ placeholder: '○',
11
+ disabled: false,
12
+ error: false,
13
+ oneTimeCode: false,
14
+ });
15
+
16
+ const {
17
+ length,
18
+ value,
19
+ size,
20
+ type,
21
+ mask,
22
+ placeholder,
23
+ disabled,
24
+ error,
25
+ oneTimeCode,
26
+ className,
27
+ } = props;
28
+
29
+ const { onChange, onComplete } = rawProps;
30
+
31
+ const currentLength = state(resolveValue(length) ?? 4);
32
+ const getLength = () => currentLength.get();
33
+
34
+ after(length).change((next) => {
35
+ const newLen = resolveValue(next) ?? 4;
36
+ const oldLen = currentLength.get();
37
+ if (newLen === oldLen) return;
38
+ currentLength.set(newLen);
39
+ const currentValues = currentState.get();
40
+ const currentNodes = inputNodes.get();
41
+ currentState.set(Array.from({ length: newLen }, (_, i) => currentValues[i] ?? ''));
42
+ inputNodes.set(Array.from({ length: newLen }, (_, i) => currentNodes[i] ?? null));
43
+ });
44
+
45
+ const inputNodes = state(Array.from({ length: getLength() }, () => null));
46
+
47
+ const currentState = state(
48
+ Array.from({ length: getLength() }, (_, i) => {
49
+ const initial = resolveValue(value);
50
+ if (typeof initial === 'string') return initial[i] ?? '';
51
+ if (Array.isArray(initial)) return initial[i] ?? '';
52
+ return '';
53
+ })
54
+ );
55
+
56
+ after(value).change((next) => {
57
+ if (next == null) return;
58
+ const len = getLength();
59
+ if (typeof next === 'string') {
60
+ currentState.set(Array.from({ length: len }, (_, i) => next[i] ?? ''));
61
+ } else if (Array.isArray(next)) {
62
+ currentState.set(Array.from({ length: len }, (_, i) => next[i] ?? ''));
63
+ }
64
+ });
65
+
66
+ const getValueString = () => currentState.get().join('');
67
+
68
+ const setValue = (next) => {
69
+ currentState.set(next);
70
+ const str = next.join('');
71
+ onChange?.(str);
72
+ if (next.every((v) => v !== '') && next.length === getLength()) {
73
+ onComplete?.(str);
74
+ }
75
+ };
76
+
77
+ const focusInput = (index) => {
78
+ const len = getLength();
79
+ const nodes = inputNodes.get();
80
+ if (index >= 0 && index < len && nodes[index]) {
81
+ nodes[index].focus();
82
+ nodes[index].select();
83
+ }
84
+ };
85
+
86
+ const getTypeRegex = () => {
87
+ const t = resolveValue(type);
88
+ if (t === 'number') return /^[0-9]$/;
89
+ if (t instanceof RegExp) return t;
90
+ return /^[a-zA-Z0-9]$/;
91
+ };
92
+
93
+ const handleInput = (idx, ev) => {
94
+ const inputValue = ev.target.value;
95
+ const regex = getTypeRegex();
96
+
97
+ if (inputValue.length > 1) {
98
+ handlePaste(idx, inputValue);
99
+ return;
100
+ }
101
+
102
+ if (inputValue && !regex.test(inputValue)) {
103
+ ev.target.value = currentState.get()[idx] ?? '';
104
+ return;
105
+ }
106
+
107
+ const current = currentState.get().slice();
108
+ current[idx] = inputValue;
109
+ setValue(current);
110
+
111
+ if (inputValue && idx < getLength() - 1) {
112
+ focusInput(idx + 1);
113
+ }
114
+ };
115
+
116
+ const handlePaste = (startIdx, pastedValue) => {
117
+ const regex = getTypeRegex();
118
+ const chars = pastedValue.split('').filter((c) => regex.test(c));
119
+ const current = currentState.get().slice();
120
+ const len = getLength();
121
+
122
+ chars.forEach((char, i) => {
123
+ const targetIdx = startIdx + i;
124
+ if (targetIdx < len) {
125
+ current[targetIdx] = char;
126
+ }
127
+ });
128
+
129
+ setValue(current);
130
+
131
+ const nextEmpty = current.findIndex((v, i) => i >= startIdx && v === '');
132
+ if (nextEmpty >= 0) {
133
+ focusInput(nextEmpty);
134
+ } else {
135
+ focusInput(Math.min(startIdx + chars.length, len - 1));
136
+ }
137
+ };
138
+
139
+ const handleKeyDown = (idx, ev) => {
140
+ if (ev.key === 'Backspace') {
141
+ const current = currentState.get().slice();
142
+ if (current[idx] === '' && idx > 0) {
143
+ ev.preventDefault();
144
+ current[idx - 1] = '';
145
+ setValue(current);
146
+ focusInput(idx - 1);
147
+ } else if (current[idx] !== '') {
148
+ current[idx] = '';
149
+ setValue(current);
150
+ }
151
+ } else if (ev.key === 'ArrowLeft' && idx > 0) {
152
+ ev.preventDefault();
153
+ focusInput(idx - 1);
154
+ } else if (ev.key === 'ArrowRight' && idx < getLength() - 1) {
155
+ ev.preventDefault();
156
+ focusInput(idx + 1);
157
+ }
158
+ };
159
+
160
+ const handleFocus = (idx, ev) => {
161
+ ev.target.select();
162
+ };
163
+
164
+ const handleContainerClick = () => {
165
+ const current = currentState.get();
166
+ const firstEmpty = current.findIndex((v) => v === '');
167
+ if (firstEmpty >= 0) {
168
+ focusInput(firstEmpty);
169
+ } else {
170
+ focusInput(current.length - 1);
171
+ }
172
+ };
173
+
174
+ const inputType = after(mask).compute((m) => (resolveValue(m) ? 'password' : 'text'));
175
+ const inputMode = after(type).compute((t) => (resolveValue(t) === 'number' ? 'numeric' : 'text'));
176
+ const autoComplete = after(oneTimeCode).compute((o) => (resolveValue(o) ? 'one-time-code' : 'off'));
177
+ const isDisabled = after(disabled).compute((d) => !!resolveValue(d));
178
+ const hasError = after(error).compute((e) => !!resolveValue(e));
179
+
180
+ const indices = after(currentLength).compute((len) =>
181
+ Array.from({ length: len }, (_, i) => i)
182
+ );
183
+
184
+ const renderInput = (idx) =>
185
+ Input({
186
+ className: cx(
187
+ 'g-ui-pin-input-field',
188
+ when(hasError, () => 'g-ui-pin-input-error')
189
+ ),
190
+ type: inputType,
191
+ inputMode,
192
+ autocomplete: autoComplete,
193
+ maxLength: 2,
194
+ placeholder: when(isDisabled, () => '', () => placeholder),
195
+ disabled: isDisabled,
196
+ value: after(currentState, idx).compute(([values, i]) => values[i] ?? ''),
197
+ node: inputNodes[idx.get()],
198
+ onInput: (ev) => handleInput(idx.get(), ev),
199
+ onChange: (ev) => handleInput(idx.get(), ev),
200
+ onKeyDown: (ev) => handleKeyDown(idx.get(), ev),
201
+ onFocus: (ev) => handleFocus(idx.get(), ev),
202
+ });
203
+
204
+ return Div(
205
+ {
206
+ className: cx(
207
+ 'g-ui-pin-input',
208
+ classVar('g-ui-input-size-', size, 'md'),
209
+ className
210
+ ),
211
+ onClick: handleContainerClick,
212
+ },
213
+ Div(
214
+ { className: 'g-ui-pin-input-wrapper' },
215
+ list(indices, renderInput)
216
+ )
217
+ );
218
+ }
@@ -0,0 +1,38 @@
1
+ import { Div, when, after, state } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, resolveValue } from '../utils.js';
3
+
4
+ export function Popover(...args) {
5
+ const { props, rawProps, children } = splitPropsChildren(args, { position: 'left' });
6
+ const { opened, position, content, className, ...rest } = props;
7
+ const { onChange } = rawProps;
8
+ const currentState = state(resolveValue(opened) ?? false);
9
+ after(opened).change((next) => {
10
+ const resolved = resolveValue(next);
11
+ if (resolved == null) return;
12
+ currentState.set(!!resolved);
13
+ });
14
+
15
+ const setOpen = (next) => {
16
+ currentState.set(next);
17
+ onChange?.(next);
18
+ };
19
+
20
+ return Div(
21
+ { ...rest, className: cx('g-ui-popover', props.className ?? className) },
22
+ Div({ onClick: () => setOpen(!currentState.get()) }, children),
23
+ when(
24
+ currentState,
25
+ () =>
26
+ Div(
27
+ {
28
+ className: cx(
29
+ 'g-ui-popover-dropdown',
30
+ position === 'right' && 'g-ui-popover-right',
31
+ position === 'center' && 'g-ui-popover-center'
32
+ ),
33
+ },
34
+ content
35
+ )
36
+ )
37
+ );
38
+ }
@@ -0,0 +1,25 @@
1
+ import { Div, when, after, state } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, resolveValue } from '../utils.js';
3
+
4
+ export function Popper(...args) {
5
+ const { props, rawProps, children } = splitPropsChildren(args);
6
+ const { opened, content, className, ...rest } = props;
7
+ const { onChange } = rawProps;
8
+ const currentState = state(resolveValue(opened) ?? false);
9
+ after(opened).change((next) => {
10
+ const resolved = resolveValue(next);
11
+ if (resolved == null) return;
12
+ currentState.set(!!resolved);
13
+ });
14
+
15
+ const setOpen = (next) => {
16
+ currentState.set(next);
17
+ onChange?.(next);
18
+ };
19
+
20
+ return Div(
21
+ { ...rest, className: cx('g-ui-popper', className) },
22
+ Div({ onClick: () => setOpen(!currentState.get()) }, children),
23
+ when(currentState, () => Div({ className: 'g-ui-popper-dropdown' }, content))
24
+ );
25
+ }
@@ -0,0 +1,27 @@
1
+ import { Div } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, classVar } from '../utils.js';
3
+
4
+ export function Progress(...args) {
5
+ const { props } = splitPropsChildren(args, { value: 0, color: 'primary', size: 'md' });
6
+ const { value, color, size, className, ...rest } = props;
7
+ return Div(
8
+ {
9
+ ...rest,
10
+ className: cx(
11
+ 'g-ui-progress',
12
+ classVar('g-ui-progress-size-', size, 'md'),
13
+ [value, (next) => {
14
+ const pct = Math.max(0, Math.min(100, Number(next) || 0));
15
+ const bucket = Math.round(pct / 5) * 5;
16
+ return `g-ui-progress-${bucket}`;
17
+ }],
18
+ [color, (next) => {
19
+ if (next) return `g-ui-progress-${next}`;
20
+ return '';
21
+ }],
22
+ className
23
+ ),
24
+ },
25
+ Div({ className: 'g-ui-progress-bar' })
26
+ );
27
+ }
@@ -0,0 +1,11 @@
1
+ import { Div } from '@granularjs/core';
2
+ import { cx, splitPropsChildren } from '../utils.js';
3
+
4
+ export function ProgressRing(...args) {
5
+ const { props } = splitPropsChildren(args, { size: 'md' });
6
+ const { size, className, ...rest } = props;
7
+ return Div({
8
+ ...rest,
9
+ className: cx('g-ui-progress-ring', [size, (value) => `g-ui-progress-ring-size-${value}`], className),
10
+ });
11
+ }
@@ -0,0 +1,22 @@
1
+ import { Div, Input, Label, Span, when } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, classVar } from '../utils.js';
3
+
4
+ export function Radio(...args) {
5
+ const { props } = splitPropsChildren(args, { size: 'md' });
6
+ const { label, description, size, className, inputProps, ...rest } = props;
7
+ const control = Label(
8
+ { className: 'g-ui-radio-control' },
9
+ Input({
10
+ type: 'radio',
11
+ className: cx('g-ui-radio-input', classVar('g-ui-radio-size-', size, 'md'), inputProps?.className),
12
+ ...rest,
13
+ }),
14
+ when(label, () => Span({ className: 'g-ui-radio-label' }, label))
15
+ );
16
+
17
+ return Div(
18
+ { className: cx('g-ui-radio', classVar('g-ui-radio-size-', size, 'md'), className) },
19
+ control,
20
+ when(description, () => Span({ className: 'g-ui-radio-description' }, description))
21
+ );
22
+ }
@@ -0,0 +1,8 @@
1
+ import { Div } from '@granularjs/core';
2
+ import { cx, splitPropsChildren } from '../utils.js';
3
+
4
+ export function RadioGroup(...args) {
5
+ const { props, children } = splitPropsChildren(args);
6
+ const { className, ...rest } = props;
7
+ return Div({ ...rest, className: cx('g-ui-stack g-ui-gap-sm', className) }, children);
8
+ }
@@ -0,0 +1,45 @@
1
+ import { Div, state, after } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, resolveValue } from '../utils.js';
3
+ import { TextInput } from './TextInput.js';
4
+
5
+ export function RangePicker(...args) {
6
+ const { props, rawProps } = splitPropsChildren(args, { size: 'md' });
7
+ const { value, size, className, ...rest } = props;
8
+ const { onChange } = rawProps;
9
+ const currentState = state(resolveValue(value) ?? ['', '']);
10
+
11
+ after(value).change((next) => {
12
+ const resolved = resolveValue(next);
13
+ if (resolved == null) return;
14
+ currentState.set(resolved);
15
+ });
16
+
17
+ const setValue = (next) => {
18
+ currentState.set(next);
19
+ onChange?.(next);
20
+ };
21
+
22
+ return Div(
23
+ { ...rest, className: cx('g-ui-range-picker', props.className ?? className) },
24
+ TextInput({
25
+ size,
26
+ type: 'text',
27
+ inputMode: 'numeric',
28
+ value: after(currentState).compute((current) => current?.[0] ?? ''),
29
+ onInput: (ev) => {
30
+ const current = currentState.get() ?? ['', ''];
31
+ setValue([ev.target.value, current[1]]);
32
+ },
33
+ }),
34
+ TextInput({
35
+ size,
36
+ type: 'text',
37
+ inputMode: 'numeric',
38
+ value: after(currentState).compute((current) => current?.[1] ?? ''),
39
+ onInput: (ev) => {
40
+ const current = currentState.get() ?? ['', ''];
41
+ setValue([current[0], ev.target.value]);
42
+ },
43
+ })
44
+ );
45
+ }
@@ -0,0 +1,143 @@
1
+ import { Div, after, state, when } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, classVar, classFlag, resolveBool, resolveValue } from '../utils.js';
3
+ import { SliderMark } from './Slider.js';
4
+
5
+ export function RangeSlider(...args) {
6
+ const { props, rawProps } = splitPropsChildren(args, { min: 0, max: 100, step: 1, size: 'md' });
7
+ const { value, marks, min, max, step, size, disabled, className, ...rest } = props;
8
+ const { onChange } = rawProps;
9
+ const currentState = state(resolveValue(value ?? [min, max]));
10
+ const getBounds = () => {
11
+ const minValue = Number(resolveValue(min));
12
+ const maxValue = Number(resolveValue(max));
13
+ if (!Number.isFinite(minValue) || !Number.isFinite(maxValue)) {
14
+ return { minValue: 0, maxValue: 100 };
15
+ }
16
+ return { minValue: Math.min(minValue, maxValue), maxValue: Math.max(minValue, maxValue) };
17
+ };
18
+ const getStep = () => {
19
+ const stepValue = Number(resolveValue(step));
20
+ if (Number.isFinite(stepValue) && stepValue > 0) return stepValue;
21
+ return 1;
22
+ };
23
+ const normalize = (vals) => {
24
+ const { minValue, maxValue } = getBounds();
25
+ const stepValue = getStep();
26
+ let list = [minValue, maxValue];
27
+ if (Array.isArray(vals)) list = vals;
28
+ const first = Math.max(minValue, Math.min(maxValue, Number(list[0])));
29
+ const second = Math.max(minValue, Math.min(maxValue, Number(list[1])));
30
+ const low = Math.round(Math.min(first, second) / stepValue) * stepValue;
31
+ const high = Math.round(Math.max(first, second) / stepValue) * stepValue;
32
+ return [low, high];
33
+ };
34
+ after(value).change((next) => {
35
+ if (next == null) return;
36
+ currentState.set(normalize(next));
37
+ });
38
+ const setValue = (next) => {
39
+ const normalized = normalize(next);
40
+ if (
41
+ normalized?.[0] === currentState.get()?.[0] &&
42
+ normalized?.[1] === currentState.get()?.[1]
43
+ ) {
44
+ return;
45
+ }
46
+ currentState.set(normalized);
47
+ onChange?.(normalized);
48
+ };
49
+ const percent = after(currentState).compute((vals) => {
50
+ const { minValue, maxValue } = getBounds();
51
+ const range = maxValue - minValue;
52
+ const [low, high] = normalize(vals);
53
+ if (range <= 0) return { lowPct: 0, highPct: 0 };
54
+ const lowPct = ((low - minValue) / range) * 100;
55
+ const highPct = ((high - minValue) / range) * 100;
56
+ return {
57
+ lowPct: Math.max(0, Math.min(100, lowPct)),
58
+ highPct: Math.max(0, Math.min(100, highPct)),
59
+ };
60
+ });
61
+ const updateFromEvent = (ev, getRect, thumb) => {
62
+ const rect = getRect?.();
63
+ if (!rect || rect.width === 0) return;
64
+ const x = Math.min(Math.max(ev.clientX - rect.left, 0), rect.width);
65
+ const ratio = x / rect.width;
66
+ const { minValue, maxValue } = getBounds();
67
+ const nextValue = minValue + ratio * (maxValue - minValue);
68
+ let current = currentState;
69
+ if (typeof currentState.get === 'function') current = currentState.get();
70
+ const [low, high] = normalize(current);
71
+ if (thumb === 'low') setValue([nextValue, high]);
72
+ else setValue([low, nextValue]);
73
+ };
74
+ const startDrag = (ev, forcedThumb, trackEl) => {
75
+ if (resolveBool(disabled)) return;
76
+ ev.preventDefault?.();
77
+ const track = trackEl || ev.currentTarget;
78
+ const getRect = () => track.getBoundingClientRect();
79
+ const rect = getRect();
80
+ let percentValue = percent;
81
+ if (typeof percent.get === 'function') percentValue = percent.get();
82
+ const { lowPct, highPct } = percentValue;
83
+ const clickPct = ((ev.clientX - rect.left) / rect.width) * 100;
84
+ let thumb = forcedThumb;
85
+ if (!thumb) {
86
+ const isLow = Math.abs(clickPct - lowPct) <= Math.abs(clickPct - highPct);
87
+ thumb = 'high';
88
+ if (isLow) thumb = 'low';
89
+ }
90
+ track.setPointerCapture?.(ev.pointerId);
91
+ updateFromEvent(ev, getRect, thumb);
92
+ const handleMove = (moveEv) => updateFromEvent(moveEv, getRect, thumb);
93
+ const handleUp = () => {
94
+ track.releasePointerCapture?.(ev.pointerId);
95
+ window.removeEventListener('pointermove', handleMove);
96
+ window.removeEventListener('pointerup', handleUp);
97
+ };
98
+ window.addEventListener('pointermove', handleMove);
99
+ window.addEventListener('pointerup', handleUp);
100
+ };
101
+
102
+ return Div(
103
+ {
104
+ ...rest,
105
+ className: cx(
106
+ 'g-ui-range-slider',
107
+ classVar('g-ui-slider-size-', size, 'md'),
108
+ classFlag('g-ui-slider-disabled', disabled),
109
+ props.className ?? className
110
+ ),
111
+ },
112
+ Div(
113
+ { className: 'g-ui-slider-track', onPointerDown: (ev) => startDrag(ev) },
114
+ Div({
115
+ className: 'g-ui-slider-bar',
116
+ style: after(percent).compute(({ lowPct, highPct }) => ({
117
+ left: `${lowPct}%`,
118
+ width: `${Math.max(0, highPct - lowPct)}%`,
119
+ })),
120
+ }),
121
+ Div({
122
+ className: 'g-ui-slider-thumb',
123
+ style: after(percent).compute(({ lowPct }) => ({ left: `${lowPct}%` })),
124
+ onPointerDown: (ev) => {
125
+ ev.stopPropagation?.();
126
+ startDrag(ev, 'low', ev.currentTarget.parentElement);
127
+ },
128
+ }),
129
+ Div({
130
+ className: 'g-ui-slider-thumb',
131
+ style: after(percent).compute(({ highPct }) => ({ left: `${highPct}%` })),
132
+ onPointerDown: (ev) => {
133
+ ev.stopPropagation?.();
134
+ startDrag(ev, 'high', ev.currentTarget.parentElement);
135
+ },
136
+ })
137
+ ),
138
+ when(marks, () => Div(
139
+ { className: 'g-ui-slider-marks' },
140
+ marks.map((mark) => SliderMark({ mark, getBounds }))
141
+ ))
142
+ );
143
+ }
@@ -0,0 +1,42 @@
1
+ import { Span, after, state } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, classVar, resolveValue } from '../utils.js';
3
+
4
+ export function Rating(...args) {
5
+ const { props, rawProps } = splitPropsChildren(args, { value: 0, max: 5, size: 'md' });
6
+ const { value, max, size, className, ...rest } = props;
7
+ const { onChange } = rawProps;
8
+ const currentState = state(resolveValue(value));
9
+ after(value).change((next) => {
10
+ const resolved = resolveValue(next);
11
+ if (resolved == null) return;
12
+ currentState.set(resolved);
13
+ });
14
+ const setValue = (next) => {
15
+ currentState.set(next);
16
+ onChange?.(next);
17
+ };
18
+ const items = [];
19
+ const maxValue = Number(resolveValue(max)) || 0;
20
+ for (let i = 1; i <= maxValue; i += 1) items.push(i);
21
+ return Span(
22
+ {
23
+ ...rest,
24
+ className: cx('g-ui-rating', classVar('g-ui-rating-size-', size, 'md'), props.className ?? className),
25
+ },
26
+ items.map((i) =>
27
+ Span(
28
+ {
29
+ className: cx(
30
+ 'g-ui-rating-item',
31
+ after(currentState).compute((current) => {
32
+ if (i <= current) return 'g-ui-rating-item-active';
33
+ return '';
34
+ })
35
+ ),
36
+ onClick: () => setValue(i),
37
+ },
38
+ '★'
39
+ )
40
+ )
41
+ );
42
+ }
@@ -0,0 +1,11 @@
1
+ import { Div } from '@granularjs/core';
2
+ import { cx, splitPropsChildren } from '../utils.js';
3
+
4
+ export function ScrollArea(...args) {
5
+ const { props, children } = splitPropsChildren(args, { size: 'md' });
6
+ const { size, className, ...rest } = props;
7
+ return Div(
8
+ { ...rest, className: cx('g-ui-scroll-area', [size, (value) => `g-ui-scroll-area-${value}`], className) },
9
+ children
10
+ );
11
+ }
@@ -0,0 +1,17 @@
1
+ import { Span, Div } from '@granularjs/core';
2
+ import { splitPropsChildren } from '../utils.js';
3
+ import { searchSvg } from '../theme/icons.js';
4
+ import { TextInput } from './TextInput.js';
5
+
6
+ export function SearchInput(...args) {
7
+ const { props } = splitPropsChildren(args, { size: 'md' });
8
+ const { size, className, ...rest } = props;
9
+ return TextInput({
10
+ ...rest,
11
+ size,
12
+ className,
13
+ leftSection: Div({ className: 'g-ui-search-input-left-section' , innerHTML: searchSvg }),
14
+ type: 'text',
15
+ inputMode: 'search',
16
+ });
17
+ }
@@ -0,0 +1,39 @@
1
+ import { Div, after, state } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, classVar, resolveValue } from '../utils.js';
3
+
4
+ export function SegmentedControl(...args) {
5
+ const { props, rawProps } = splitPropsChildren(args, { data: [], size: 'sm' });
6
+ const { value, data, size, scroll, className, ...rest } = props;
7
+ const { onChange } = rawProps;
8
+ const currentState = state(resolveValue(value));
9
+ after(value).change((next) => {
10
+ const resolved = resolveValue(next);
11
+ if (resolved == null) return;
12
+ currentState.set(resolved);
13
+ });
14
+ const setValue = (next) => {
15
+ currentState.set(next);
16
+ onChange?.(next);
17
+ };
18
+ return Div(
19
+ { ...rest, className: cx(scroll && 'g-ui-segmented-scroll') },
20
+ Div(
21
+ { className: cx('g-ui-segmented', classVar('g-ui-segmented-size-', size, 'sm'), props.className ?? className) },
22
+ data.map((item) =>
23
+ Div(
24
+ {
25
+ className: cx(
26
+ 'g-ui-segmented-item',
27
+ after(currentState).compute((current) => {
28
+ if (item.value === current) return 'g-ui-segmented-active';
29
+ return '';
30
+ })
31
+ ),
32
+ onClick: () => setValue(item.value),
33
+ },
34
+ item.label
35
+ )
36
+ )
37
+ )
38
+ );
39
+ }