@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,389 @@
1
+ import { cx, splitPropsChildren, resolveValue } from '../utils.js';
2
+ import { Div, Span, when, after, state } from '@granularjs/core';
3
+ import { TextInput } from './TextInput.js';
4
+
5
+ export function NumberField(...args) {
6
+ const { props, rawProps } = splitPropsChildren(args, {
7
+ size: 'md',
8
+ step: 1,
9
+ allowDecimal: true,
10
+ allowNegative: true,
11
+ clampBehavior: 'blur',
12
+ hideControls: false,
13
+ decimalSeparator: '.',
14
+ thousandSeparator: '',
15
+ format: null,
16
+ prefix: '',
17
+ suffix: '',
18
+ });
19
+ const {
20
+ value,
21
+ min,
22
+ max,
23
+ step,
24
+ size,
25
+ allowDecimal,
26
+ allowNegative,
27
+ decimalSeparator,
28
+ thousandSeparator,
29
+ decimalScale,
30
+ clampBehavior,
31
+ hideControls,
32
+ format,
33
+ locale,
34
+ currency,
35
+ formatOptions,
36
+ prefix,
37
+ suffix,
38
+ leftSection,
39
+ rightSection,
40
+ className,
41
+ onChange: computed_onChange,
42
+ onInput: computed_onInput,
43
+ ...rest
44
+ } = props;
45
+ const { onChange: _onChange, onInput: _onInput, onBlur, onFocus, onKeyDown } = rawProps;
46
+
47
+ const onChange = (e) => {
48
+ _onChange?.(e.target?.value ?? '');
49
+ _onInput?.(e.target?.value ?? '');
50
+ }
51
+ const onInput = onChange;
52
+
53
+ const currentState = state('');
54
+
55
+ const escapeRegExp = (value) => String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
56
+
57
+ const getScale = () => {
58
+ const allowDec = !!resolveValue(allowDecimal);
59
+ if (!allowDec) return 0;
60
+ const resolved = resolveValue(decimalScale);
61
+ if (resolved != null && resolved !== '' && Number.isFinite(Number(resolved))) return Math.max(0, Number(resolved));
62
+ const fmt = resolveValue(format);
63
+ if (fmt === 'currency' || fmt === 'percent') return 2;
64
+ return 0;
65
+ };
66
+
67
+ const parseNumber = (raw) => {
68
+ const decSep = resolveValue(decimalSeparator) ?? '.';
69
+ const thousSep = resolveValue(thousandSeparator) ?? '';
70
+ let rawValue = String(raw ?? '');
71
+ if (thousSep) {
72
+ rawValue = rawValue.split(thousSep).join('');
73
+ }
74
+ if (!rawValue || rawValue === '-' || rawValue === decSep || rawValue.endsWith(decSep)) return null;
75
+ const normalized = rawValue.replace(decSep, '.');
76
+ const num = Number(normalized);
77
+ if (!Number.isFinite(num)) return null;
78
+ return num;
79
+ };
80
+
81
+ const addThousandSeparators = (intPart) => {
82
+ const thousSep = resolveValue(thousandSeparator) ?? '';
83
+ if (!thousSep) return intPart;
84
+ return intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousSep);
85
+ };
86
+
87
+ const formatNumber = (num) => {
88
+ const decSep = resolveValue(decimalSeparator) ?? '.';
89
+ const scale = getScale();
90
+ const numeric = Number(num);
91
+ if (!Number.isFinite(numeric)) return '';
92
+ const sign = numeric < 0 ? '-' : '';
93
+ const abs = Math.abs(numeric);
94
+ let [int, dec = ''] = String(abs).split('.');
95
+ const formattedInt = addThousandSeparators(int);
96
+ if (scale > 0) {
97
+ const trimmed = dec.slice(0, scale).padEnd(scale, '0');
98
+ return `${sign}${formattedInt}${decSep}${trimmed}`;
99
+ }
100
+ return `${sign}${formattedInt}`;
101
+ };
102
+
103
+ const formatWithIntl = (num, kind) => {
104
+ try {
105
+ const resolvedLocale = resolveValue(locale);
106
+ const resolvedCurrency = resolveValue(currency) ?? 'USD';
107
+ const options = resolveValue(formatOptions) ?? {};
108
+ const scale = getScale();
109
+ const style = kind === 'currency' ? 'currency' : 'decimal';
110
+ const formatter = new Intl.NumberFormat(resolvedLocale, {
111
+ style,
112
+ currency: resolvedCurrency,
113
+ ...(scale > 0 ? { minimumFractionDigits: scale, maximumFractionDigits: scale } : { maximumFractionDigits: 0 }),
114
+ ...options,
115
+ });
116
+ return formatter.format(num);
117
+ } catch {
118
+ return formatNumber(num);
119
+ }
120
+ };
121
+
122
+ const clampValue = (num) => {
123
+ let next = num;
124
+ const minValue = resolveValue(min);
125
+ const maxValue = resolveValue(max);
126
+ if (minValue != null && Number.isFinite(Number(minValue))) next = Math.max(next, Number(minValue));
127
+ if (maxValue != null && Number.isFinite(Number(maxValue))) next = Math.min(next, Number(maxValue));
128
+ if (!resolveValue(allowNegative) && next < 0) next = 0;
129
+ return next;
130
+ };
131
+
132
+ const resolveSuffix = () => {
133
+ const suffixValue = resolveValue(suffix) ?? '';
134
+ const fmt = resolveValue(format);
135
+ if (!suffixValue && fmt === 'percent') return '%';
136
+ return suffixValue;
137
+ };
138
+
139
+ const buildVisual = (raw, sign, fmt) => {
140
+ const prefixValue = resolveValue(prefix) ?? '';
141
+ const suffixValue = resolveSuffix();
142
+ const normalizedRaw = String(raw ?? '').replace(/\u00A0/g, ' ');
143
+ const normalizedPrefix = String(prefixValue ?? '').replace(/\u00A0/g, ' ');
144
+ const normalizedSuffix = String(suffixValue ?? '').replace(/\u00A0/g, ' ');
145
+ const hasPrefix = normalizedPrefix && normalizedRaw.startsWith(normalizedPrefix);
146
+ const hasSuffix = normalizedSuffix && normalizedRaw.endsWith(normalizedSuffix);
147
+ const finalPrefix = hasPrefix ? '' : prefixValue;
148
+ const finalSuffix = hasSuffix ? '' : suffixValue;
149
+ return `${sign ?? ''}${finalPrefix}${raw}${finalSuffix}`;
150
+ };
151
+
152
+ const makeSanitizedFromDigits = (digitsValue, sign) => {
153
+ const scale = getScale();
154
+ const sep = resolveValue(decimalSeparator) ?? '.';
155
+ const digits = String(digitsValue ?? '').replace(/\D/g, '');
156
+ const baseDigits = digits || '0';
157
+ const padded = scale > 0 ? baseDigits.padStart(scale + 1, '0') : baseDigits;
158
+ const rawInt = scale > 0 ? padded.slice(0, -scale) : padded;
159
+ const intPart = rawInt.replace(/^0+(?=\d)/, '') || '0';
160
+ const decPart = scale > 0 ? padded.slice(-scale) : '';
161
+ const sanitized = scale > 0 ? `${intPart}${sep}${decPart}` : intPart;
162
+ return `${sign ?? ''}${sanitized}`;
163
+ };
164
+
165
+ const stripAffixes = (raw) => {
166
+ const prefixValue = resolveValue(prefix) ?? '';
167
+ const suffixValue = resolveSuffix();
168
+ let out = String(raw ?? '');
169
+ const normalizedPrefix = String(prefixValue ?? '').replace(/\u00A0/g, ' ');
170
+ const normalizedSuffix = String(suffixValue ?? '').replace(/\u00A0/g, ' ');
171
+ if (normalizedPrefix) {
172
+ const normalizedOut = out.replace(/\u00A0/g, ' ');
173
+ if (normalizedOut.startsWith(normalizedPrefix)) out = out.slice(prefixValue.length);
174
+ }
175
+ if (normalizedSuffix) {
176
+ const normalizedOut = out.replace(/\u00A0/g, ' ');
177
+ if (normalizedOut.endsWith(normalizedSuffix)) out = out.slice(0, -suffixValue.length);
178
+ }
179
+ return out;
180
+ };
181
+
182
+ const sanitizeFromInput = (rawInput) => {
183
+ const allowNeg = !!resolveValue(allowNegative);
184
+ const body = stripAffixes(rawInput);
185
+ const sign = allowNeg && body.includes('-') ? '-' : '';
186
+ const digits = body.replace(/\D/g, '');
187
+ let sanitized = makeSanitizedFromDigits(digits, sign);
188
+ if (resolveValue(clampBehavior) === 'strict') {
189
+ const parsed = parseNumber(sanitized);
190
+ if (parsed != null) {
191
+ const clamped = clampValue(parsed);
192
+ const clampedSign = clamped < 0 ? '-' : '';
193
+ sanitized = `${clampedSign}${formatNumber(Math.abs(clamped))}`;
194
+ }
195
+ }
196
+ return sanitized;
197
+ };
198
+
199
+ const normalizeIncoming = (next) => {
200
+ if (next == null || next === '') return makeSanitizedFromDigits('', '');
201
+ if (typeof next === 'number' && Number.isFinite(next)) {
202
+ const clamped = resolveValue(clampBehavior) === 'strict' ? clampValue(next) : next;
203
+ const sign = clamped < 0 ? '-' : '';
204
+ return `${sign}${formatNumber(Math.abs(clamped))}`;
205
+ }
206
+ const inputText = stripAffixes(next);
207
+ const sep = resolveValue(decimalSeparator) ?? '.';
208
+ const allowNeg = !!resolveValue(allowNegative);
209
+ const sign = allowNeg && String(inputText).includes('-') ? '-' : '';
210
+ const filtered = String(inputText ?? '').replace(new RegExp(`[^0-9${escapeRegExp(sep)}]`, 'g'), '');
211
+ const parsed = parseNumber(`${sign}${filtered}`);
212
+ if (parsed != null) {
213
+ const clamped = resolveValue(clampBehavior) === 'strict' ? clampValue(parsed) : parsed;
214
+ const clampedSign = clamped < 0 ? '-' : '';
215
+ return `${clampedSign}${formatNumber(Math.abs(clamped))}`;
216
+ }
217
+ return sanitizeFromInput(next);
218
+ };
219
+
220
+ let lastExternalValue = undefined;
221
+ const updateFromExternal = (next) => {
222
+ const resolved = resolveValue(next);
223
+ if (resolved === undefined) return;
224
+
225
+ const normalized = normalizeIncoming(resolved);
226
+ if (normalized === currentState.get()) return;
227
+ lastExternalValue = normalized;
228
+ currentState.set(normalized);
229
+ };
230
+
231
+ updateFromExternal(value);
232
+ after(value).change((next) => {
233
+ updateFromExternal(next)
234
+ });
235
+
236
+ after(currentState).change((next) => {
237
+ if (next === lastExternalValue) {
238
+ lastExternalValue = undefined;
239
+ return;
240
+ }
241
+ lastExternalValue = undefined;
242
+ const parsed = parseNumber(next);
243
+ if (parsed == null) {
244
+ onChange?.(next ?? '');
245
+ return;
246
+ }
247
+ onChange?.(parsed);
248
+ });
249
+
250
+ const hasRightSection = after(rightSection).compute((next) => next != null && next !== false);
251
+ const showControls = after(hideControls, hasRightSection).compute(([nextHidden, nextRight]) =>
252
+ !resolveValue(nextHidden) && !nextRight
253
+ );
254
+ const inputMode = after(allowDecimal).compute((next) => (resolveValue(next) ? 'decimal' : 'numeric'));
255
+
256
+ const inputFormat = after(
257
+ format,
258
+ prefix,
259
+ suffix,
260
+ allowDecimal,
261
+ allowNegative,
262
+ decimalSeparator,
263
+ thousandSeparator,
264
+ decimalScale,
265
+ clampBehavior,
266
+ min,
267
+ max,
268
+ locale,
269
+ currency,
270
+ formatOptions
271
+ ).compute(() => ({
272
+ mode: 'both',
273
+ format: (raw) => {
274
+ const sanitized = sanitizeFromInput(raw);
275
+ const fmt = resolveValue(format);
276
+ const allowNeg = !!resolveValue(allowNegative);
277
+ const sign = allowNeg && sanitized.startsWith('-') ? '-' : '';
278
+ const parsed = parseNumber(sanitized);
279
+ let visual = sanitized.replace(sign, '');
280
+ if (parsed != null) {
281
+ const abs = Math.abs(parsed);
282
+ if (typeof fmt === 'function') {
283
+ try {
284
+ visual = fmt(abs);
285
+ } catch {
286
+ visual = formatNumber(abs);
287
+ }
288
+ } else if (fmt === 'currency') {
289
+ visual = formatWithIntl(abs, 'currency');
290
+ } else if (fmt === 'decimal') {
291
+ const resolvedLocale = resolveValue(locale);
292
+ const resolvedOptions = resolveValue(formatOptions);
293
+ visual = resolvedLocale || resolvedOptions ? formatWithIntl(abs, 'decimal') : formatNumber(abs);
294
+ } else if (fmt === 'percent') {
295
+ visual = formatNumber(abs);
296
+ } else {
297
+ visual = formatNumber(abs);
298
+ }
299
+ }
300
+ return { value: sanitized, raw: sanitized, visual: buildVisual(visual, sign, fmt) };
301
+ },
302
+ }));
303
+
304
+ const stepBy = (direction) => {
305
+ const current = parseNumber(currentState.get());
306
+ const delta = Number(resolveValue(step) ?? 1);
307
+ const base = current == null ? 0 : current;
308
+ const next = clampValue(base + delta * direction);
309
+ currentState.set(formatNumber(next));
310
+ };
311
+
312
+ const setCaretToEnd = (target) => {
313
+ if (!target || typeof target.setSelectionRange !== 'function') return;
314
+ const setToEnd = () => {
315
+ try {
316
+ const end = String(target.value ?? '').length;
317
+ target.setSelectionRange(end, end);
318
+ } catch { }
319
+ };
320
+ if (typeof requestAnimationFrame === 'function') {
321
+ requestAnimationFrame(setToEnd);
322
+ } else {
323
+ setToEnd();
324
+ }
325
+ };
326
+
327
+ const handleInput = (ev) => {
328
+ onInput?.(ev);
329
+ const target = ev?.target;
330
+ if (!target) return;
331
+ setCaretToEnd(target);
332
+ };
333
+
334
+ const handleBlur = (ev) => {
335
+ onBlur?.(ev);
336
+ if (resolveValue(clampBehavior) !== 'blur') return;
337
+ const parsed = parseNumber(currentState.get());
338
+ if (parsed == null) return;
339
+ const clamped = clampValue(parsed);
340
+ currentState.set(formatNumber(clamped));
341
+ };
342
+
343
+ const handleFocus = (ev) => {
344
+ onFocus?.(ev);
345
+ setCaretToEnd(ev?.target);
346
+ };
347
+
348
+ const handleKeyDown = (ev) => {
349
+ onKeyDown?.(ev);
350
+ const target = ev?.target;
351
+ if (!target) return;
352
+ const prefixValue = resolveValue(prefix) ?? '';
353
+ const suffixValue = resolveSuffix();
354
+ if (suffixValue && typeof target.selectionEnd === 'number') {
355
+ target.selectionEnd = Math.min(target.selectionEnd, String(target.value ?? '').length - suffixValue.length);
356
+ }
357
+ if (prefixValue && typeof target.selectionStart === 'number') {
358
+ target.selectionStart = Math.max(target.selectionStart, prefixValue.length);
359
+ }
360
+ };
361
+
362
+ const controls = Div(
363
+ { className: 'g-ui-number-field-controls' },
364
+ Span({ className: 'g-ui-number-field-control', onClick: () => stepBy(1) }, '+'),
365
+ Span({ className: 'g-ui-number-field-control', onClick: () => stepBy(-1) }, '−')
366
+ );
367
+ const controlsWrapper = Div({ className: 'g-ui-number-field-controls-wrapper' }, controls);
368
+ const finalRightSection = after(showControls, rightSection).compute(([nextControls, nextRight]) => {
369
+ if (nextControls) return controlsWrapper;
370
+ return nextRight;
371
+ });
372
+
373
+ return TextInput({
374
+ ...rest,
375
+ size,
376
+ className: cx('g-ui-number-field', className),
377
+ leftSection,
378
+ rightSection: finalRightSection,
379
+ type: 'text',
380
+ inputMode,
381
+ inputClassName: cx('g-ui-input-number'),
382
+ value: currentState,
383
+ format: inputFormat,
384
+ onInput: handleInput,
385
+ onBlur: handleBlur,
386
+ onFocus: handleFocus,
387
+ onKeyDown: handleKeyDown,
388
+ });
389
+ }
@@ -0,0 +1,5 @@
1
+ import { NumberField } from './NumberField.js';
2
+
3
+ export function NumberInput(...args) {
4
+ return NumberField(...args);
5
+ }
@@ -0,0 +1,56 @@
1
+ import { Button, Div, state, after } from '@granularjs/core';
2
+ import { cx, splitPropsChildren, classVar, resolveValue } from '../utils.js';
3
+
4
+ export function Pagination(...args) {
5
+ const { props, rawProps } = splitPropsChildren(args, { total: 1, size: 'md' });
6
+ const { page, total, size, className, ...rest } = props;
7
+ const { onChange } = rawProps;
8
+ const currentState = state(resolveValue(page ?? 1));
9
+ after(page).change((next) => {
10
+ const resolved = resolveValue(next);
11
+ if (resolved == null) return;
12
+ currentState.set(resolved);
13
+ });
14
+ const setPage = (next) => {
15
+ const totalValue = Number(resolveValue(total)) || 1;
16
+ const clamped = Math.max(1, Math.min(totalValue, next));
17
+ currentState.set(clamped);
18
+ onChange?.(clamped);
19
+ };
20
+ const items = [];
21
+ const totalValue = Number(resolveValue(total)) || 1;
22
+ for (let i = 1; i <= totalValue; i += 1) items.push(i);
23
+ return Div(
24
+ {
25
+ ...rest,
26
+ className: cx('g-ui-pagination', classVar('g-ui-pagination-size-', size, 'md'), props.className ?? className),
27
+ },
28
+ Button(
29
+ {
30
+ className: 'g-ui-pagination-item',
31
+ onClick: () => setPage((currentState.get?.() ?? currentState) - 1),
32
+ disabled: after(currentState).compute((v) => v <= 1),
33
+ },
34
+ '<'
35
+ ),
36
+ items.map((i) =>
37
+ Button(
38
+ {
39
+ className: after(currentState).compute((v) =>
40
+ cx('g-ui-pagination-item', i === v && 'g-ui-pagination-item-active')
41
+ ),
42
+ onClick: () => setPage(i),
43
+ },
44
+ String(i)
45
+ )
46
+ ),
47
+ Button(
48
+ {
49
+ className: 'g-ui-pagination-item',
50
+ onClick: () => setPage((currentState.get?.() ?? currentState) + 1),
51
+ disabled: after(currentState).compute((v) => v >= total),
52
+ },
53
+ '>'
54
+ )
55
+ );
56
+ }
@@ -0,0 +1,20 @@
1
+ import { Div } from '@granularjs/core';
2
+ import { cx, splitPropsChildren } from '../utils.js';
3
+
4
+ export function Paper(...args) {
5
+ const { props, children } = splitPropsChildren(args, { padding: 'md', radius: 'md', shadow: 'none' });
6
+ const { padding, radius, shadow, className, ...rest } = props;
7
+ return Div(
8
+ {
9
+ ...rest,
10
+ className: cx(
11
+ 'g-ui-paper',
12
+ [padding, (value) => `g-ui-card-padding-${value}`],
13
+ [radius, (value) => `g-ui-card-radius-${value}`],
14
+ [shadow, (value) => `g-ui-card-shadow-${value}`],
15
+ className
16
+ ),
17
+ },
18
+ children
19
+ );
20
+ }
@@ -0,0 +1,29 @@
1
+ import { Button, after, when, state } from '@granularjs/core';
2
+ import { splitPropsChildren } from '../utils.js';
3
+ import { TextInput } from './TextInput.js';
4
+
5
+ export function PasswordInput(...args) {
6
+ const { props, rawProps } = splitPropsChildren(args, { size: 'md' });
7
+ const { size, className, rightSection, ...rest } = props;
8
+ const { onChange } = rawProps;
9
+ const visible = state(false);
10
+ const inputType = after(visible).compute((next) => {
11
+ if (next) return 'text';
12
+ return 'password';
13
+ });
14
+ const computedRightSection = after(rightSection).compute((next) =>
15
+ next ?? Button(
16
+ { className: 'g-ui-password-toggle', onClick: () => visible.set(!visible.get()) },
17
+ when(visible, () => 'Hide', () => 'Show')
18
+ )
19
+ );
20
+
21
+ return TextInput({
22
+ ...rest,
23
+ size,
24
+ className,
25
+ onChange,
26
+ rightSection: computedRightSection,
27
+ type: inputType,
28
+ });
29
+ }