@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.
- package/README.md +116 -0
- package/dist/fonts/Arimo-400.ttf +0 -0
- package/dist/fonts/Arimo-500.ttf +1449 -0
- package/dist/fonts/Arimo-600.ttf +1449 -0
- package/dist/fonts/Arimo-700.ttf +0 -0
- package/dist/fonts/Inter-400.woff2 +0 -0
- package/dist/fonts/Inter-500.woff2 +0 -0
- package/dist/fonts/Inter-600.woff2 +0 -0
- package/dist/fonts/Inter-700.woff2 +0 -0
- package/dist/fonts/Poppins-400.ttf +0 -0
- package/dist/fonts/Poppins-500.ttf +0 -0
- package/dist/fonts/Poppins-600.ttf +0 -0
- package/dist/fonts/Poppins-700.ttf +0 -0
- package/dist/granular-ui.min.js +3605 -0
- package/dist/granular-ui.min.js.map +7 -0
- package/package.json +55 -0
- package/src/components/Accordion.js +25 -0
- package/src/components/ActionIcon.js +20 -0
- package/src/components/Affix.js +11 -0
- package/src/components/Alert.js +33 -0
- package/src/components/Anchor.js +8 -0
- package/src/components/AppBar.js +14 -0
- package/src/components/Avatar.js +13 -0
- package/src/components/AvatarGroup.js +8 -0
- package/src/components/Badge.js +22 -0
- package/src/components/BadgeGroup.js +8 -0
- package/src/components/Blockquote.js +8 -0
- package/src/components/BottomBar.js +43 -0
- package/src/components/Breadcrumbs.js +19 -0
- package/src/components/Burger.js +13 -0
- package/src/components/Button.js +37 -0
- package/src/components/Calendar.js +109 -0
- package/src/components/Card.js +40 -0
- package/src/components/Center.js +8 -0
- package/src/components/Checkbox.js +46 -0
- package/src/components/CheckboxGroup.js +8 -0
- package/src/components/Chip.js +35 -0
- package/src/components/Code.js +8 -0
- package/src/components/Col.js +8 -0
- package/src/components/Collapse.js +8 -0
- package/src/components/Container.js +19 -0
- package/src/components/CopyButton.js +30 -0
- package/src/components/DateInput.js +123 -0
- package/src/components/DatePicker.js +7 -0
- package/src/components/Divider.js +22 -0
- package/src/components/Drawer.js +32 -0
- package/src/components/EventCalendar.js +972 -0
- package/src/components/Fieldset.js +12 -0
- package/src/components/Flex.js +25 -0
- package/src/components/Grid.js +8 -0
- package/src/components/GridTable.js +99 -0
- package/src/components/Group.js +29 -0
- package/src/components/HoverCard.js +24 -0
- package/src/components/Icon.js +19 -0
- package/src/components/Image.js +8 -0
- package/src/components/Indicator.js +21 -0
- package/src/components/Kbd.js +8 -0
- package/src/components/List.js +77 -0
- package/src/components/Loading.js +29 -0
- package/src/components/LoadingOverlay.js +9 -0
- package/src/components/Menu.js +129 -0
- package/src/components/Modal.js +61 -0
- package/src/components/MultiSelect.js +153 -0
- package/src/components/NavLink.js +72 -0
- package/src/components/Notification.js +42 -0
- package/src/components/Notifications.js +59 -0
- package/src/components/NumberField.js +389 -0
- package/src/components/NumberInput.js +5 -0
- package/src/components/Pagination.js +56 -0
- package/src/components/Paper.js +20 -0
- package/src/components/PasswordInput.js +29 -0
- package/src/components/PinInput.js +218 -0
- package/src/components/Popover.js +38 -0
- package/src/components/Popper.js +25 -0
- package/src/components/Progress.js +27 -0
- package/src/components/ProgressRing.js +11 -0
- package/src/components/Radio.js +22 -0
- package/src/components/RadioGroup.js +8 -0
- package/src/components/RangePicker.js +45 -0
- package/src/components/RangeSlider.js +143 -0
- package/src/components/Rating.js +42 -0
- package/src/components/ScrollArea.js +11 -0
- package/src/components/SearchInput.js +17 -0
- package/src/components/SegmentedControl.js +39 -0
- package/src/components/Select.js +71 -0
- package/src/components/SelectSearch.js +37 -0
- package/src/components/Sidebar.js +136 -0
- package/src/components/SimpleGrid.js +11 -0
- package/src/components/Skeleton.js +24 -0
- package/src/components/Slider.js +126 -0
- package/src/components/Space.js +8 -0
- package/src/components/Stack.js +27 -0
- package/src/components/Stepper.js +20 -0
- package/src/components/Switch.js +16 -0
- package/src/components/SwitchGroup.js +8 -0
- package/src/components/Table.js +42 -0
- package/src/components/Tabs.js +194 -0
- package/src/components/Tag.js +8 -0
- package/src/components/Text.js +42 -0
- package/src/components/TextInput.js +74 -0
- package/src/components/Textarea.js +15 -0
- package/src/components/Timeline.js +22 -0
- package/src/components/Title.js +18 -0
- package/src/components/Toast.js +16 -0
- package/src/components/ToastStack.js +21 -0
- package/src/components/Tooltip.js +12 -0
- package/src/hooks/useDisclosure.js +13 -0
- package/src/index.js +98 -0
- package/src/theme/fonts/Arimo-400.ttf +0 -0
- package/src/theme/fonts/Arimo-500.ttf +1449 -0
- package/src/theme/fonts/Arimo-600.ttf +1449 -0
- package/src/theme/fonts/Arimo-700.ttf +0 -0
- package/src/theme/fonts/Inter-400.woff2 +0 -0
- package/src/theme/fonts/Inter-500.woff2 +0 -0
- package/src/theme/fonts/Inter-600.woff2 +0 -0
- package/src/theme/fonts/Inter-700.woff2 +0 -0
- package/src/theme/fonts/Poppins-400.ttf +0 -0
- package/src/theme/fonts/Poppins-500.ttf +0 -0
- package/src/theme/fonts/Poppins-600.ttf +0 -0
- package/src/theme/fonts/Poppins-700.ttf +0 -0
- package/src/theme/icons.js +10 -0
- package/src/theme/styles.js +3630 -0
- package/src/theme/theme.js +71 -0
- package/src/utils.js +75 -0
- package/types/components/Accordion.d.ts +1 -0
- package/types/components/ActionIcon.d.ts +1 -0
- package/types/components/Affix.d.ts +1 -0
- package/types/components/Alert.d.ts +1 -0
- package/types/components/Anchor.d.ts +1 -0
- package/types/components/AppBar.d.ts +1 -0
- package/types/components/Avatar.d.ts +1 -0
- package/types/components/AvatarGroup.d.ts +1 -0
- package/types/components/Badge.d.ts +1 -0
- package/types/components/BadgeGroup.d.ts +1 -0
- package/types/components/Blockquote.d.ts +1 -0
- package/types/components/BottomBar.d.ts +4 -0
- package/types/components/Breadcrumbs.d.ts +1 -0
- package/types/components/Burger.d.ts +1 -0
- package/types/components/Button.d.ts +1 -0
- package/types/components/Calendar.d.ts +1 -0
- package/types/components/Card.d.ts +1 -0
- package/types/components/Center.d.ts +1 -0
- package/types/components/Checkbox.d.ts +1 -0
- package/types/components/CheckboxGroup.d.ts +1 -0
- package/types/components/Chip.d.ts +1 -0
- package/types/components/Code.d.ts +1 -0
- package/types/components/Col.d.ts +1 -0
- package/types/components/Collapse.d.ts +1 -0
- package/types/components/Container.d.ts +1 -0
- package/types/components/CopyButton.d.ts +1 -0
- package/types/components/DateInput.d.ts +1 -0
- package/types/components/DatePicker.d.ts +1 -0
- package/types/components/Divider.d.ts +1 -0
- package/types/components/Drawer.d.ts +1 -0
- package/types/components/EventCalendar.d.ts +1 -0
- package/types/components/Fieldset.d.ts +1 -0
- package/types/components/Flex.d.ts +1 -0
- package/types/components/Grid.d.ts +1 -0
- package/types/components/GridTable.d.ts +5 -0
- package/types/components/Group.d.ts +1 -0
- package/types/components/HoverCard.d.ts +1 -0
- package/types/components/Icon.d.ts +1 -0
- package/types/components/Image.d.ts +1 -0
- package/types/components/Indicator.d.ts +1 -0
- package/types/components/Kbd.d.ts +1 -0
- package/types/components/List.d.ts +5 -0
- package/types/components/Loading.d.ts +1 -0
- package/types/components/LoadingOverlay.d.ts +1 -0
- package/types/components/Menu.d.ts +2 -0
- package/types/components/Modal.d.ts +1 -0
- package/types/components/MultiSelect.d.ts +1 -0
- package/types/components/NavLink.d.ts +1 -0
- package/types/components/Notification.d.ts +1 -0
- package/types/components/Notifications.d.ts +1 -0
- package/types/components/NumberField.d.ts +1 -0
- package/types/components/NumberInput.d.ts +1 -0
- package/types/components/Pagination.d.ts +1 -0
- package/types/components/Paper.d.ts +1 -0
- package/types/components/PasswordInput.d.ts +1 -0
- package/types/components/PinInput.d.ts +1 -0
- package/types/components/Popover.d.ts +1 -0
- package/types/components/Popper.d.ts +1 -0
- package/types/components/Progress.d.ts +1 -0
- package/types/components/ProgressRing.d.ts +1 -0
- package/types/components/Radio.d.ts +1 -0
- package/types/components/RadioGroup.d.ts +1 -0
- package/types/components/RangePicker.d.ts +1 -0
- package/types/components/RangeSlider.d.ts +1 -0
- package/types/components/Rating.d.ts +1 -0
- package/types/components/ScrollArea.d.ts +1 -0
- package/types/components/SearchInput.d.ts +1 -0
- package/types/components/SegmentedControl.d.ts +1 -0
- package/types/components/Select.d.ts +1 -0
- package/types/components/SelectSearch.d.ts +1 -0
- package/types/components/Sidebar.d.ts +1 -0
- package/types/components/SimpleGrid.d.ts +1 -0
- package/types/components/Skeleton.d.ts +1 -0
- package/types/components/Slider.d.ts +5 -0
- package/types/components/Space.d.ts +1 -0
- package/types/components/Stack.d.ts +1 -0
- package/types/components/Stepper.d.ts +1 -0
- package/types/components/Switch.d.ts +1 -0
- package/types/components/SwitchGroup.d.ts +1 -0
- package/types/components/Table.d.ts +1 -0
- package/types/components/Tabs.d.ts +1 -0
- package/types/components/Tag.d.ts +1 -0
- package/types/components/Text.d.ts +1 -0
- package/types/components/TextInput.d.ts +1 -0
- package/types/components/Textarea.d.ts +1 -0
- package/types/components/Timeline.d.ts +1 -0
- package/types/components/Title.d.ts +1 -0
- package/types/components/Toast.d.ts +1 -0
- package/types/components/ToastStack.d.ts +1 -0
- package/types/components/Tooltip.d.ts +1 -0
- package/types/hooks/useDisclosure.d.ts +1 -0
- package/types/index.d.ts +93 -0
- package/types/theme/icons.d.ts +10 -0
- package/types/theme/styles.d.ts +1 -0
- package/types/theme/theme.d.ts +2 -0
- 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,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
|
+
}
|