@kenlo/sds-design-system 0.9.9
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 +129 -0
- package/components/Accordion/Accordion.css +186 -0
- package/components/Accordion/accordion-variants.js +17 -0
- package/components/Accordion/accordion.marko +264 -0
- package/components/AccordionGroup/AccordionGroup.css +35 -0
- package/components/AccordionGroup/accordion-group.marko +17 -0
- package/components/Alert/Alert.css +104 -0
- package/components/Alert/alert-variants.js +35 -0
- package/components/Alert/alert.marko +94 -0
- package/components/AttachmentList/AttachmentList.css +92 -0
- package/components/AttachmentList/attachment-list.marko +51 -0
- package/components/AttachmentList/attachment-row.marko +167 -0
- package/components/Avatar/Avatar.css +110 -0
- package/components/Avatar/avatar-variants.js +12 -0
- package/components/Avatar/avatar.marko +71 -0
- package/components/Badge/Badge.css +381 -0
- package/components/Badge/badge-dismissible.marko +143 -0
- package/components/Badge/badge-variants.js +35 -0
- package/components/Badge/badge.marko +134 -0
- package/components/BottomSheet/BottomSheet.css +300 -0
- package/components/BottomSheet/bottom-sheet.marko +340 -0
- package/components/BulkActionBar/BulkActionBar.css +88 -0
- package/components/BulkActionBar/bulk-action-bar.marko +186 -0
- package/components/Button/Button.css +340 -0
- package/components/Button/button-variants.js +25 -0
- package/components/Button/button.marko +139 -0
- package/components/Card/Card.css +391 -0
- package/components/Card/card-body.marko +5 -0
- package/components/Card/card-collapsible.marko +94 -0
- package/components/Card/card-footer.marko +12 -0
- package/components/Card/card-header.marko +30 -0
- package/components/Card/card-info-pair.marko +17 -0
- package/components/Card/card-info-row.marko +12 -0
- package/components/Card/card-section.marko +14 -0
- package/components/Card/card-variants.js +24 -0
- package/components/Card/card.marko +124 -0
- package/components/Checkbox/Checkbox.css +249 -0
- package/components/Checkbox/checkbox-variants.js +21 -0
- package/components/Checkbox/checkbox.marko +128 -0
- package/components/ColorPicker/ColorPicker.css +253 -0
- package/components/ColorPicker/color-maths.js +104 -0
- package/components/ColorPicker/color-picker.marko +474 -0
- package/components/DatePicker/DatePicker.css +429 -0
- package/components/DatePicker/date-logic.js +225 -0
- package/components/DatePicker/date-picker.marko +492 -0
- package/components/Drawer/Drawer.css +174 -0
- package/components/Drawer/drawer.marko +262 -0
- package/components/Floating/Floating.css +28 -0
- package/components/Floating/floating-position.js +251 -0
- package/components/Floating/floating.marko +291 -0
- package/components/Form/Form.css +15 -0
- package/components/Form/form-context.js +69 -0
- package/components/Form/form.marko +73 -0
- package/components/GradientPicker/GradientPicker.css +179 -0
- package/components/GradientPicker/gradient-picker.marko +398 -0
- package/components/Grid/Grid.css +161 -0
- package/components/Grid/col.marko +36 -0
- package/components/Grid/grid-variants.js +43 -0
- package/components/Grid/grid.marko +25 -0
- package/components/Header/Header.css +457 -0
- package/components/Header/header.marko +523 -0
- package/components/Icon/Icon.css +44 -0
- package/components/Icon/icon-colors.js +31 -0
- package/components/Icon/icon.marko +56 -0
- package/components/IconButton/IconButton.css +85 -0
- package/components/IconButton/icon-button-variants.js +18 -0
- package/components/IconButton/icon-button.marko +103 -0
- package/components/Incrementer/Incrementer.css +94 -0
- package/components/Incrementer/incrementer.marko +70 -0
- package/components/Input/Input.css +547 -0
- package/components/Input/input-countries.js +109 -0
- package/components/Input/input-masks.js +218 -0
- package/components/Input/input-types.js +194 -0
- package/components/Input/input.marko +530 -0
- package/components/Menu/Menu.css +651 -0
- package/components/Menu/menu-variants.js +89 -0
- package/components/Menu/menu.marko +560 -0
- package/components/Modal/Modal.css +160 -0
- package/components/Modal/modal.marko +296 -0
- package/components/Nav/Nav.css +298 -0
- package/components/Nav/nav-variants.js +23 -0
- package/components/Nav/nav.marko +164 -0
- package/components/Notification/Notification.css +78 -0
- package/components/Notification/notification-variants.js +30 -0
- package/components/Notification/notification.marko +55 -0
- package/components/Pagination/Pagination.css +78 -0
- package/components/Pagination/pagination-pages.js +66 -0
- package/components/Pagination/pagination.marko +141 -0
- package/components/Pill/Pill.css +193 -0
- package/components/Pill/pill-variants.js +16 -0
- package/components/Pill/pill.marko +165 -0
- package/components/Popover/Popover.css +121 -0
- package/components/Popover/popover.marko +96 -0
- package/components/Radio/Radio.css +230 -0
- package/components/Radio/radio-variants.js +16 -0
- package/components/Radio/radio.marko +93 -0
- package/components/RadioGroup/RadioGroup.css +32 -0
- package/components/RadioGroup/radio-group.marko +93 -0
- package/components/SearchBar/SearchBar.css +181 -0
- package/components/SearchBar/search-bar-variants.js +24 -0
- package/components/SearchBar/search-bar.marko +157 -0
- package/components/Select/Select.css +427 -0
- package/components/Select/select-options.js +163 -0
- package/components/Select/select.marko +449 -0
- package/components/Separator/Separator.css +22 -0
- package/components/Separator/separator.marko +14 -0
- package/components/Skeleton/Skeleton.css +100 -0
- package/components/Skeleton/skeleton-avatar.marko +28 -0
- package/components/Skeleton/skeleton-button.marko +22 -0
- package/components/Skeleton/skeleton-image.marko +20 -0
- package/components/Skeleton/skeleton-input.marko +22 -0
- package/components/Skeleton/skeleton-variants.js +16 -0
- package/components/Skeleton/skeleton.marko +53 -0
- package/components/Spinner/Spinner.css +173 -0
- package/components/Spinner/spinner-geometry.js +39 -0
- package/components/Spinner/spinner-overlay.marko +97 -0
- package/components/Spinner/spinner.marko +88 -0
- package/components/StatCard/StatCard.css +90 -0
- package/components/StatCard/stat-card-group.marko +5 -0
- package/components/StatCard/stat-card.marko +57 -0
- package/components/Switch/Switch.css +209 -0
- package/components/Switch/switch.marko +99 -0
- package/components/Table/Table.css +270 -0
- package/components/Table/table-cell.marko +28 -0
- package/components/Table/table-header.marko +27 -0
- package/components/Table/table-logic.js +129 -0
- package/components/Table/table.marko +372 -0
- package/components/Text/Text.css +79 -0
- package/components/Text/text-variants.js +25 -0
- package/components/Text/text.marko +48 -0
- package/components/TextEditor/TextEditor.css +78 -0
- package/components/TextEditor/safe-url.js +50 -0
- package/components/TextEditor/text-editor-commands.js +22 -0
- package/components/TextEditor/text-editor.marko +253 -0
- package/components/Toast/Toast.css +88 -0
- package/components/Toast/toast-api.js +19 -0
- package/components/Toast/toast-store.js +118 -0
- package/components/Toast/toast-variants.js +16 -0
- package/components/Toast/toast.marko +93 -0
- package/components/Toast/toaster.marko +70 -0
- package/components/Toggle/Toggle.css +169 -0
- package/components/Toggle/toggle.marko +93 -0
- package/components/Tooltip/Tooltip.css +115 -0
- package/components/Tooltip/tooltip.marko +77 -0
- package/components/Uploader/Uploader.css +180 -0
- package/components/Uploader/crop-geometry.js +161 -0
- package/components/Uploader/uploader.marko +269 -0
- package/foundations/Scrollbar/Scrollbar.css +26 -0
- package/foundations/attrs.js +27 -0
- package/foundations/breakpoints.js +27 -0
- package/foundations/contrast.js +74 -0
- package/foundations/custom-scheme.js +222 -0
- package/foundations/overlay.js +108 -0
- package/foundations/styles.css +284 -0
- package/foundations/text.js +22 -0
- package/foundations/theme-server.js +81 -0
- package/foundations/theme.js +42 -0
- package/foundations/tokens.css +1656 -0
- package/icons/custom/custom-video-horizontal-filled.marko +6 -0
- package/icons/custom/custom-video-horizontal-regular.marko +6 -0
- package/icons/custom/custom-video-tour-3d-filled.marko +7 -0
- package/icons/custom/custom-video-tour-3d-regular.marko +9 -0
- package/icons/custom/custom-video-vertical-filled.marko +6 -0
- package/icons/custom/custom-video-vertical-regular.marko +6 -0
- package/icons/generated/fluent-add-regular.marko +3 -0
- package/icons/generated/fluent-alert-regular.marko +3 -0
- package/icons/generated/fluent-apps-regular.marko +3 -0
- package/icons/generated/fluent-arrow-download-regular.marko +3 -0
- package/icons/generated/fluent-arrow-left-regular.marko +3 -0
- package/icons/generated/fluent-arrow-right-regular.marko +3 -0
- package/icons/generated/fluent-arrow-rotate-clockwise-regular.marko +3 -0
- package/icons/generated/fluent-arrow-rotate-counterclockwise-regular.marko +3 -0
- package/icons/generated/fluent-border-all-regular.marko +3 -0
- package/icons/generated/fluent-calendar-regular.marko +3 -0
- package/icons/generated/fluent-checkmark-circle-filled.marko +3 -0
- package/icons/generated/fluent-checkmark-regular.marko +3 -0
- package/icons/generated/fluent-chevron-down-regular.marko +3 -0
- package/icons/generated/fluent-chevron-left-regular.marko +3 -0
- package/icons/generated/fluent-chevron-right-regular.marko +3 -0
- package/icons/generated/fluent-chevron-up-regular.marko +3 -0
- package/icons/generated/fluent-circle-half-fill-regular.marko +3 -0
- package/icons/generated/fluent-clock-regular.marko +3 -0
- package/icons/generated/fluent-color-regular.marko +3 -0
- package/icons/generated/fluent-crop-regular.marko +3 -0
- package/icons/generated/fluent-dark-theme-regular.marko +3 -0
- package/icons/generated/fluent-delete-regular.marko +3 -0
- package/icons/generated/fluent-desktop-regular.marko +3 -0
- package/icons/generated/fluent-dismiss-circle-filled.marko +3 -0
- package/icons/generated/fluent-dismiss-regular.marko +3 -0
- package/icons/generated/fluent-document-pdf-regular.marko +3 -0
- package/icons/generated/fluent-document-regular.marko +3 -0
- package/icons/generated/fluent-eye-off-regular.marko +3 -0
- package/icons/generated/fluent-eye-regular.marko +3 -0
- package/icons/generated/fluent-eyedropper-regular.marko +3 -0
- package/icons/generated/fluent-grid-regular.marko +3 -0
- package/icons/generated/fluent-history-regular.marko +3 -0
- package/icons/generated/fluent-home-regular.marko +3 -0
- package/icons/generated/fluent-image-regular.marko +3 -0
- package/icons/generated/fluent-info-regular.marko +3 -0
- package/icons/generated/fluent-link-regular.marko +3 -0
- package/icons/generated/fluent-more-horizontal-regular.marko +3 -0
- package/icons/generated/fluent-navigation-regular.marko +3 -0
- package/icons/generated/fluent-paint-brush-regular.marko +3 -0
- package/icons/generated/fluent-question-circle-regular.marko +3 -0
- package/icons/generated/fluent-search-regular.marko +3 -0
- package/icons/generated/fluent-settings-regular.marko +3 -0
- package/icons/generated/fluent-square-hint-regular.marko +3 -0
- package/icons/generated/fluent-subtract-regular.marko +3 -0
- package/icons/generated/fluent-text-align-center-regular.marko +3 -0
- package/icons/generated/fluent-text-align-left-regular.marko +3 -0
- package/icons/generated/fluent-text-align-right-regular.marko +3 -0
- package/icons/generated/fluent-text-bold-regular.marko +3 -0
- package/icons/generated/fluent-text-bullet-list-regular.marko +3 -0
- package/icons/generated/fluent-text-font-regular.marko +3 -0
- package/icons/generated/fluent-text-font-size-regular.marko +3 -0
- package/icons/generated/fluent-text-italic-regular.marko +3 -0
- package/icons/generated/fluent-text-number-list-ltr-regular.marko +3 -0
- package/icons/generated/fluent-video-regular.marko +3 -0
- package/icons/generated/fluent-warning-filled.marko +3 -0
- package/icons/generated/fluent-weather-moon-regular.marko +3 -0
- package/icons/generated/fluent-weather-sunny-regular.marko +3 -0
- package/icons/generated/fluent-zoom-in-regular.marko +3 -0
- package/icons/generated/fluent-zoom-out-regular.marko +3 -0
- package/icons/manifest.json +61 -0
- package/icons/social/social-facebook-color.marko +7 -0
- package/icons/social/social-facebook.marko +6 -0
- package/icons/social/social-google-color.marko +20 -0
- package/icons/social/social-google.marko +6 -0
- package/icons/social/social-instagram-color.marko +16 -0
- package/icons/social/social-instagram.marko +6 -0
- package/icons/social/social-linkedin-color.marko +8 -0
- package/icons/social/social-linkedin.marko +8 -0
- package/icons/social/social-messenger-color.marko +15 -0
- package/icons/social/social-messenger.marko +6 -0
- package/icons/social/social-pinterest-color.marko +6 -0
- package/icons/social/social-pinterest.marko +6 -0
- package/icons/social/social-telegram-color.marko +7 -0
- package/icons/social/social-telegram.marko +6 -0
- package/icons/social/social-whatsapp-color.marko +8 -0
- package/icons/social/social-whatsapp.marko +7 -0
- package/icons/social/social-x-color.marko +7 -0
- package/icons/social/social-x.marko +6 -0
- package/icons/social/social-youtube-color.marko +7 -0
- package/icons/social/social-youtube.marko +6 -0
- package/marko.json +203 -0
- package/package.json +55 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Masks and validators for the Input.
|
|
3
|
+
*
|
|
4
|
+
* Ported from kdls-design-system v2.5.1 (0f3edf8) components/Input/Input.tsx, where all of
|
|
5
|
+
* this lived inside the component file. It is separated here because it is pure, because
|
|
6
|
+
* it is the part where a mistake is invisible — a CPF check digit computed wrongly does
|
|
7
|
+
* not look wrong, it just quietly refuses a real customer — and because it can then be
|
|
8
|
+
* tested on its own, exhaustively.
|
|
9
|
+
*
|
|
10
|
+
* The check-digit algorithms for CPF and CNPJ are carried verbatim. The phone masks are
|
|
11
|
+
* not: KDLS wrote the same nine-branch pattern out nine times, and they are expressed as
|
|
12
|
+
* a table here. The test asserts the output for every digit length of every country, so
|
|
13
|
+
* the restructuring is proven rather than assumed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const digitsOf = (value) => String(value ?? '').replace(/\D/g, '');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* local@domain.ext — deliberately permissive; the server is what actually validates.
|
|
20
|
+
*
|
|
21
|
+
* Divergence from KDLS, which ends `\.[^\s@]+$` and so accepts `a@b.c`. There is no
|
|
22
|
+
* one-letter top-level domain, and the commonest way to produce one is a typo in the last
|
|
23
|
+
* character of a real address — exactly the mistake a field like this exists to catch.
|
|
24
|
+
* Two is the floor, and everything else stays as permissive as KDLS.
|
|
25
|
+
*/
|
|
26
|
+
export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
27
|
+
|
|
28
|
+
export const isValidEmail = (value) => EMAIL_RE.test(String(value ?? '').trim());
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Applies a pattern to a run of digits. `#` takes the next digit; anything else is a
|
|
32
|
+
* literal, emitted only while digits remain, so a half-typed number never ends in a
|
|
33
|
+
* dangling bracket or dash.
|
|
34
|
+
*
|
|
35
|
+
* One rule beyond that, and it is the one that makes this match KDLS exactly: while
|
|
36
|
+
* everything typed still fits inside the FIRST group, the digits come back bare. Typing
|
|
37
|
+
* a Brazilian area code reads "1" then "11", not "(1" then "(11" — the brackets appear
|
|
38
|
+
* with the number they wrap. KDLS expressed this as a length branch per pattern; it is
|
|
39
|
+
* the same behaviour, stated once.
|
|
40
|
+
*/
|
|
41
|
+
export function applyPattern(raw, pattern) {
|
|
42
|
+
const digits = digitsOf(raw);
|
|
43
|
+
if (!digits) return '';
|
|
44
|
+
|
|
45
|
+
const firstGroup = /^#+/.exec(pattern.replace(/^[^#]*/, ''));
|
|
46
|
+
const firstGroupLength = firstGroup ? firstGroup[0].length : 0;
|
|
47
|
+
if (digits.length <= firstGroupLength) return digits;
|
|
48
|
+
|
|
49
|
+
let out = '';
|
|
50
|
+
let index = 0;
|
|
51
|
+
for (const char of pattern) {
|
|
52
|
+
if (index >= digits.length) break;
|
|
53
|
+
if (char === '#') {
|
|
54
|
+
out += digits[index];
|
|
55
|
+
index += 1;
|
|
56
|
+
} else {
|
|
57
|
+
out += char;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Phone patterns per country code. A country whose entry is a function picks its pattern
|
|
65
|
+
* by how many digits were typed — Brazil is the only one, where a landline is ten digits
|
|
66
|
+
* and a mobile eleven.
|
|
67
|
+
*/
|
|
68
|
+
export const PHONE_PATTERNS = {
|
|
69
|
+
'+55': (length) => (length > 10 ? '(##) #####-####' : '(##) ####-####'),
|
|
70
|
+
'+1': '(###) ###-####',
|
|
71
|
+
'+54': '(##) ####-####',
|
|
72
|
+
'+52': '(###) ###-####',
|
|
73
|
+
'+56': '# ####-####',
|
|
74
|
+
'+57': '(###) ###-####',
|
|
75
|
+
'+351': '### ### ###',
|
|
76
|
+
'+33': '# ## ## ## ##',
|
|
77
|
+
'+34': '### ### ###',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** How many digits a complete number has, per country. */
|
|
81
|
+
export const PHONE_DIGIT_COUNTS = {
|
|
82
|
+
'+55': [10, 11],
|
|
83
|
+
'+1': [10],
|
|
84
|
+
'+54': [10],
|
|
85
|
+
'+52': [10],
|
|
86
|
+
'+56': [9],
|
|
87
|
+
'+57': [10],
|
|
88
|
+
'+351': [9],
|
|
89
|
+
'+33': [9],
|
|
90
|
+
'+34': [9],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Mobile only. Brazil is the one country where this differs from the landline count. */
|
|
94
|
+
export const CEL_DIGIT_COUNTS = { ...PHONE_DIGIT_COUNTS, '+55': [11] };
|
|
95
|
+
|
|
96
|
+
export function applyPhoneMask(raw, ddi) {
|
|
97
|
+
const entry = PHONE_PATTERNS[ddi];
|
|
98
|
+
if (!entry) return digitsOf(raw);
|
|
99
|
+
const digits = digitsOf(raw);
|
|
100
|
+
const pattern = typeof entry === 'function' ? entry(digits.length) : entry;
|
|
101
|
+
const capacity = (pattern.match(/#/g) || []).length;
|
|
102
|
+
return applyPattern(digits.slice(0, capacity), pattern);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isValidNumberFor(value, ddi, counts) {
|
|
106
|
+
const length = digitsOf(value).length;
|
|
107
|
+
if (ddi && counts[ddi]) return counts[ddi].includes(length);
|
|
108
|
+
// A country with no declared mask still has to look like a phone number.
|
|
109
|
+
return length >= 7;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Divergence from KDLS, in the line above: KDLS checks an exact digit count for +55 and
|
|
113
|
+
accepts "at least seven digits" for every other country. So a seven-digit number passed
|
|
114
|
+
as American, and the field that exists to catch a number typed short did not.
|
|
115
|
+
|
|
116
|
+
Here the exact count is used wherever the country declares one, which is nine of the
|
|
117
|
+
fifteen offered — the ones whose format the port could state with confidence. The other
|
|
118
|
+
six, and any DDI this table has never heard of, keep the loose rule. That is deliberate:
|
|
119
|
+
a wrong count refuses a number the reader is holding in their hand, which is worse than
|
|
120
|
+
a loose one letting a short number through to the server. */
|
|
121
|
+
|
|
122
|
+
export const isValidPhone = (value, ddi) => isValidNumberFor(value, ddi, PHONE_DIGIT_COUNTS);
|
|
123
|
+
export const isValidCelPhone = (value, ddi) => isValidNumberFor(value, ddi, CEL_DIGIT_COUNTS);
|
|
124
|
+
|
|
125
|
+
export const DOCUMENT_TYPES = ['cep', 'cpf', 'cnpj', 'cpf-cnpj'];
|
|
126
|
+
|
|
127
|
+
const DOCUMENT_PATTERNS = {
|
|
128
|
+
cep: '#####-###',
|
|
129
|
+
cpf: '###.###.###-##',
|
|
130
|
+
cnpj: '##.###.###/####-##',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function applyDocumentMask(raw, type) {
|
|
134
|
+
// cpf-cnpj switches shape at the twelfth digit, which is where a CPF can no longer fit.
|
|
135
|
+
if (type === 'cpf-cnpj') {
|
|
136
|
+
return applyDocumentMask(raw, digitsOf(raw).length <= 11 ? 'cpf' : 'cnpj');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const pattern = DOCUMENT_PATTERNS[type];
|
|
140
|
+
if (!pattern) return digitsOf(raw);
|
|
141
|
+
|
|
142
|
+
const capacity = (pattern.match(/#/g) || []).length;
|
|
143
|
+
return applyPattern(digitsOf(raw).slice(0, capacity), pattern);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Is the mask full — never mind whether the number is real.
|
|
148
|
+
*
|
|
149
|
+
* The weaker of the two checks a document can get. A caller that turns validateDocument off
|
|
150
|
+
* keeps this one: the field still refuses half a CPF, it just stops arguing about whether
|
|
151
|
+
* the digits add up.
|
|
152
|
+
*/
|
|
153
|
+
export function isMaskComplete(value, type) {
|
|
154
|
+
if (type === 'cpf-cnpj') {
|
|
155
|
+
const count = digitsOf(value).length;
|
|
156
|
+
return count === 11 || count === 14;
|
|
157
|
+
}
|
|
158
|
+
const pattern = DOCUMENT_PATTERNS[type];
|
|
159
|
+
if (!pattern) return Boolean(digitsOf(value).length);
|
|
160
|
+
return digitsOf(value).length === (pattern.match(/#/g) || []).length;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isValidDate(value) {
|
|
164
|
+
const d = digitsOf(value);
|
|
165
|
+
if (d.length !== 8) return false;
|
|
166
|
+
|
|
167
|
+
const day = parseInt(d.slice(0, 2), 10);
|
|
168
|
+
const month = parseInt(d.slice(2, 4), 10);
|
|
169
|
+
const year = parseInt(d.slice(4, 8), 10);
|
|
170
|
+
|
|
171
|
+
if (month < 1 || month > 12) return false;
|
|
172
|
+
if (day < 1) return false;
|
|
173
|
+
if (year < 1000 || year > 9999) return false;
|
|
174
|
+
|
|
175
|
+
const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
176
|
+
const daysInMonth = [0, 31, isLeap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
177
|
+
return day <= daysInMonth[month];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* CPF check digits. Carried verbatim from KDLS.
|
|
182
|
+
* Eleven repeated digits pass the arithmetic and are rejected explicitly — 111.111.111-11
|
|
183
|
+
* is a valid-looking number that no person has.
|
|
184
|
+
*/
|
|
185
|
+
export function isValidCPF(value) {
|
|
186
|
+
const d = digitsOf(value);
|
|
187
|
+
if (d.length !== 11 || /^(\d)\1{10}$/.test(d)) return false;
|
|
188
|
+
|
|
189
|
+
const checkDigit = (length) => {
|
|
190
|
+
const sum = Array.from({ length }, (_, i) => Number(d[i]) * (length + 1 - i))
|
|
191
|
+
.reduce((a, b) => a + b, 0);
|
|
192
|
+
const remainder = 11 - (sum % 11);
|
|
193
|
+
return remainder >= 10 ? 0 : remainder;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return checkDigit(9) === Number(d[9]) && checkDigit(10) === Number(d[10]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** CNPJ check digits. Carried verbatim from KDLS, same repeated-digit rejection. */
|
|
200
|
+
export function isValidCNPJ(value) {
|
|
201
|
+
const d = digitsOf(value);
|
|
202
|
+
if (d.length !== 14 || /^(\d)\1{13}$/.test(d)) return false;
|
|
203
|
+
|
|
204
|
+
const checkDigit = (weights) => {
|
|
205
|
+
const sum = weights.reduce((acc, weight, i) => acc + Number(d[i]) * weight, 0);
|
|
206
|
+
const remainder = sum % 11;
|
|
207
|
+
return remainder < 2 ? 0 : 11 - remainder;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return checkDigit([5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[12])
|
|
211
|
+
&& checkDigit([6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[13]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const isValidCpfOrCnpj = (value) => (
|
|
215
|
+
digitsOf(value).length <= 11 ? isValidCPF(value) : isValidCNPJ(value)
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
export { digitsOf };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Input vocabulary, and the validation each type brings with it.
|
|
3
|
+
*
|
|
4
|
+
* Ported from kdls-design-system v2.5.1 (0f3edf8) components/Input/Input.tsx.
|
|
5
|
+
* The masks and check digits live in ./input-masks.js.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
applyDocumentMask, applyPhoneMask, isValidCPF, isValidCNPJ, isValidCpfOrCnpj, isValidDate, isValidPhone, isValidCelPhone, isValidEmail, isMaskComplete,
|
|
9
|
+
} from './input-masks.js';
|
|
10
|
+
|
|
11
|
+
export const INPUT_TYPES = [
|
|
12
|
+
'text', 'email', 'password', 'number', 'url', 'textbox',
|
|
13
|
+
'tel', 'tel-inter', 'cel', 'cep', 'cpf', 'cnpj', 'cpf-cnpj',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The phone with a country selector, as a TYPE rather than a flag on another type.
|
|
18
|
+
*
|
|
19
|
+
* Decided by Rafael. KDLS spells it `phoneInternational={true}` alongside `type="tel"`,
|
|
20
|
+
* which is a boolean that means nothing on eleven of the twelve types and has to be
|
|
21
|
+
* guarded against the other one. What it actually describes is a kind of field, and the
|
|
22
|
+
* prop that says which kind of field this is, is `type`.
|
|
23
|
+
*
|
|
24
|
+
* Its label stays "Telefone": the country selector is how the number is entered, not a
|
|
25
|
+
* different thing being asked for.
|
|
26
|
+
*/
|
|
27
|
+
export const PHONE_TYPES_INTERNATIONAL = ['tel-inter'];
|
|
28
|
+
|
|
29
|
+
/* Divergence: no `date`, no `birthdate`, no `search`.
|
|
30
|
+
|
|
31
|
+
`search` goes for the same reason the dates do: sds-search-bar is the search field, it
|
|
32
|
+
looks the same on purpose, and it is where the search behaviour will live when it
|
|
33
|
+
arrives. A `type="search"` here would be a second answer to the same question, and the
|
|
34
|
+
one that stops being maintained.
|
|
35
|
+
|
|
36
|
+
KDLS gives the Input a dd/mm/yyyy mask and calls it a date field. SDS has a DatePicker,
|
|
37
|
+
which does the same job and more — it validates the calendar rather than the digits, it
|
|
38
|
+
knows about ranges and times, and it cannot be handed 31/02. Two ways to ask for a date
|
|
39
|
+
in one system means the answer depends on which one the writer happened to find.
|
|
40
|
+
Removed rather than deprecated: a mask that looks like a date field and is not one is
|
|
41
|
+
worse than no field at all. */
|
|
42
|
+
|
|
43
|
+
export const INPUT_FEEDBACKS = ['none', 'success', 'alert', 'error'];
|
|
44
|
+
export const INPUT_WIDTHS = ['hug', 'fill'];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A type whose default label is untouched validates itself. KDLS tied this to the label
|
|
48
|
+
* on purpose: a field relabelled "Telefone do síndico" is still a phone, but one
|
|
49
|
+
* relabelled "Código interno" is not, and the component cannot tell which — so it only
|
|
50
|
+
* claims to know when the label is the one it shipped with.
|
|
51
|
+
*
|
|
52
|
+
* Every type is listed, including the four that validate nothing. Rafael, 2026-09-12:
|
|
53
|
+
* choosing a type should always propose the label that goes with it, and `number`, `url`
|
|
54
|
+
* and `textbox` were leaving whatever the last type had put there — pick CPF, then
|
|
55
|
+
* Número, and the field still said "CPF". Those four have no entry in VALIDATORS, so
|
|
56
|
+
* being here changes nothing about what is checked.
|
|
57
|
+
*/
|
|
58
|
+
export const TYPE_DEFAULT_LABELS = {
|
|
59
|
+
text: 'Nome',
|
|
60
|
+
email: 'E-mail',
|
|
61
|
+
number: 'Número',
|
|
62
|
+
url: 'Link',
|
|
63
|
+
textbox: 'Mensagem',
|
|
64
|
+
password: 'Senha',
|
|
65
|
+
tel: 'Telefone',
|
|
66
|
+
'tel-inter': 'Telefone',
|
|
67
|
+
cel: 'Celular',
|
|
68
|
+
cep: 'CEP',
|
|
69
|
+
cpf: 'CPF',
|
|
70
|
+
cnpj: 'CNPJ',
|
|
71
|
+
'cpf-cnpj': 'CPF ou CNPJ',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** What a failing built-in validation says. */
|
|
75
|
+
export const TYPE_MESSAGES = {
|
|
76
|
+
email: 'E-mail inválido',
|
|
77
|
+
tel: 'Telefone inválido',
|
|
78
|
+
'tel-inter': 'Telefone inválido',
|
|
79
|
+
cel: 'Celular inválido',
|
|
80
|
+
cep: 'CEP inválido',
|
|
81
|
+
cpf: 'CPF inválido',
|
|
82
|
+
cnpj: 'CNPJ inválido',
|
|
83
|
+
'cpf-cnpj': 'CPF ou CNPJ inválido',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const VALIDATORS = {
|
|
87
|
+
email: isValidEmail,
|
|
88
|
+
tel: (value, ddi) => isValidPhone(value, ddi),
|
|
89
|
+
'tel-inter': (value, ddi) => isValidPhone(value, ddi),
|
|
90
|
+
cel: (value, ddi) => isValidCelPhone(value, ddi),
|
|
91
|
+
cep: (value) => value.replace(/\D/g, '').length === 8,
|
|
92
|
+
cpf: isValidCPF,
|
|
93
|
+
cnpj: isValidCNPJ,
|
|
94
|
+
'cpf-cnpj': isValidCpfOrCnpj,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const DOCUMENT_TYPES = ['cep', 'cpf', 'cnpj', 'cpf-cnpj'];
|
|
98
|
+
export const PHONE_TYPES = ['tel', 'tel-inter', 'cel'];
|
|
99
|
+
|
|
100
|
+
/** Does this type carry a country selector? Only the one that says so in its name. */
|
|
101
|
+
export const isInternationalPhone = (type) => PHONE_TYPES_INTERNATIONAL.includes(type);
|
|
102
|
+
|
|
103
|
+
export function assertInputType(type) {
|
|
104
|
+
if (!INPUT_TYPES.includes(type)) {
|
|
105
|
+
throw new Error(`Unknown input type: "${type}". Allowed: ${INPUT_TYPES.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
return type;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function assertFeedback(feedback) {
|
|
111
|
+
if (!INPUT_FEEDBACKS.includes(feedback)) {
|
|
112
|
+
throw new Error(`Unknown input feedback: "${feedback}". Allowed: ${INPUT_FEEDBACKS.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
return feedback;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function assertWidth(width) {
|
|
118
|
+
if (!INPUT_WIDTHS.includes(width)) {
|
|
119
|
+
throw new Error(`Unknown input width: "${width}". Allowed: ${INPUT_WIDTHS.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
return width;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The value as the field shows it: masked for documents and phones, raw otherwise. */
|
|
125
|
+
export function formatValue(value, type, ddi = '+55') {
|
|
126
|
+
if (DOCUMENT_TYPES.includes(type)) return applyDocumentMask(value, type);
|
|
127
|
+
if (PHONE_TYPES.includes(type)) return applyPhoneMask(value, ddi);
|
|
128
|
+
return value ?? '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The on-screen keyboard a phone should offer.
|
|
133
|
+
*
|
|
134
|
+
* Neither system had this, and the gap only shows on a phone: a document renders as
|
|
135
|
+
* `type="text"` — it has to, the mask is ours — and a text field opens the letter
|
|
136
|
+
* keyboard. So a customer typing a CPF got QWERTY and had to find the number key, for a
|
|
137
|
+
* field that accepts nothing but digits.
|
|
138
|
+
*
|
|
139
|
+
* `numeric` rather than `decimal`: no separator is ever typed into these, the mask writes
|
|
140
|
+
* the dots itself. Everything else is left to the browser, which already picks the right
|
|
141
|
+
* keyboard from `email`, `tel`, `url` and `number`.
|
|
142
|
+
*/
|
|
143
|
+
export function inputModeFor(type) {
|
|
144
|
+
return DOCUMENT_TYPES.includes(type) ? 'numeric' : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The native input type to render. Documents are text — their mask is ours, not the browser's. */
|
|
148
|
+
export function nativeTypeFor(type, showPassword = false) {
|
|
149
|
+
if (type === 'password') return showPassword ? 'text' : 'password';
|
|
150
|
+
if (DOCUMENT_TYPES.includes(type)) return 'text';
|
|
151
|
+
if (PHONE_TYPES.includes(type)) return 'tel';
|
|
152
|
+
if (type === 'textbox') return 'text';
|
|
153
|
+
return type;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validates a value. Returns an error message, or null.
|
|
158
|
+
*
|
|
159
|
+
* Empty is never an error here — that is what `required` is for, and the browser already
|
|
160
|
+
* enforces it. Rules run before the built-in check, so a caller's own message wins.
|
|
161
|
+
*/
|
|
162
|
+
export const REQUIRED_MESSAGE = 'Campo obrigatório';
|
|
163
|
+
|
|
164
|
+
export function validate(
|
|
165
|
+
value,
|
|
166
|
+
{ type, label, rules = [], ddi, required = false, validateDocument = true } = {},
|
|
167
|
+
) {
|
|
168
|
+
const text = String(value ?? '');
|
|
169
|
+
|
|
170
|
+
// An empty field is only a problem when something was required of it. KDLS leaves this to
|
|
171
|
+
// the browser, which sds-form turns off — its bubbles cannot be styled or themed and
|
|
172
|
+
// speak the browser's language rather than the product's. So the message is ours.
|
|
173
|
+
if (!text) return required ? REQUIRED_MESSAGE : null;
|
|
174
|
+
|
|
175
|
+
for (const rule of rules) {
|
|
176
|
+
if (!rule.valid(text)) return rule.message;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const defaultLabel = TYPE_DEFAULT_LABELS[type];
|
|
180
|
+
if (defaultLabel !== undefined && label !== defaultLabel) return null;
|
|
181
|
+
|
|
182
|
+
const validator = VALIDATORS[type];
|
|
183
|
+
if (!validator) return null;
|
|
184
|
+
|
|
185
|
+
// A document type can be checked two ways: that the mask is full, or that the number
|
|
186
|
+
// itself is arithmetically possible. KDLS lets a caller keep the first and drop the
|
|
187
|
+
// second — for a form that accepts a test CPF, or one whose numbers are validated
|
|
188
|
+
// somewhere else and should not be refused twice.
|
|
189
|
+
if (!validateDocument && DOCUMENT_TYPES.includes(type)) {
|
|
190
|
+
return isMaskComplete(value, type) ? null : (TYPE_MESSAGES[type] ?? null);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return validator(text, ddi) ? null : (TYPE_MESSAGES[type] ?? null);
|
|
194
|
+
}
|