├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .git-blame-ignore-revs ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql-analysis.yml │ ├── main.yml │ ├── pull-request.yml │ └── size-limit.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── pre-commit └── pre-push ├── .lintstagedrc ├── .nvmrc ├── .prettierrc.cjs ├── .size-limit.cjs ├── .storybook ├── main.js ├── manager-head.html ├── preview-head.html └── preview.jsx ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.cjs ├── doctor-storybook.log ├── eslint.config.js ├── jest.config.cjs ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon-docs-development.svg ├── favicon-docs-production.svg ├── favicon-stories-development.svg ├── favicon-stories-production.svg ├── favicon.ico └── robots.txt ├── scripts ├── add-banner.cjs ├── ci │ ├── measure-size.js │ └── set-message.cjs ├── copy-files.cjs └── replace-version.cjs ├── size-limit-report └── .gitignore ├── src ├── _internal │ ├── hooks │ │ ├── __mocks__ │ │ │ └── use-warn.ts │ │ ├── index.ts │ │ ├── use-chained-callback.ts │ │ ├── use-debounced-value.ts │ │ ├── use-deprecation-warning.ts │ │ ├── use-effect-once.ts │ │ ├── use-event.ts │ │ ├── use-is-first-render.ts │ │ ├── use-sync-ref.ts │ │ ├── use-timer │ │ │ ├── index.ts │ │ │ ├── timer.ts │ │ │ ├── use-timer.test.ts │ │ │ └── use-timer.ts │ │ ├── use-update-effect.ts │ │ ├── use-warn.test.tsx │ │ └── use-warn.ts │ └── index.ts ├── components │ ├── Block.tsx │ ├── GlobalStyles.tsx │ ├── GridProvider.tsx │ ├── HiddenInput.tsx │ ├── OpenTrasition.tsx │ ├── Root.tsx │ ├── actions │ │ ├── Action │ │ │ ├── Action.stories.tsx │ │ │ └── Action.tsx │ │ ├── Button │ │ │ ├── Button.stories.tsx │ │ │ ├── Button.tsx │ │ │ ├── button.test.tsx │ │ │ └── index.ts │ │ ├── ButtonGroup │ │ │ └── ButtonGroup.tsx │ │ ├── index.ts │ │ └── use-action.ts │ ├── content │ │ ├── ActiveZone │ │ │ ├── ActiveZone.stories.tsx │ │ │ └── ActiveZone.tsx │ │ ├── Alert │ │ │ ├── Alert.stories.tsx │ │ │ ├── Alert.tsx │ │ │ ├── alert.test.tsx │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── use-alert.ts │ │ ├── Avatar │ │ │ ├── Avatar.stories.tsx │ │ │ └── Avatar.tsx │ │ ├── Badge │ │ │ ├── Badge.stories.tsx │ │ │ └── Badge.tsx │ │ ├── Card │ │ │ ├── Card.stories.tsx │ │ │ └── Card.tsx │ │ ├── Content.tsx │ │ ├── CopyPasteBlock │ │ │ ├── CopyPasteBlock.stories.tsx │ │ │ ├── CopyPasteBlock.tsx │ │ │ └── index.ts │ │ ├── CopySnippet │ │ │ ├── CopySnippet.stories.tsx │ │ │ ├── CopySnippet.tsx │ │ │ └── index.ts │ │ ├── Divider.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Paragraph.tsx │ │ ├── Placeholder │ │ │ ├── Placeholder.stories.tsx │ │ │ └── Placeholder.tsx │ │ ├── PrismCode │ │ │ ├── PrismCode.stories.tsx │ │ │ ├── PrismCode.tsx │ │ │ ├── __tests__ │ │ │ │ ├── PrismCode.test.tsx │ │ │ │ └── diffHighlight.test.ts │ │ │ └── prismSetup.tsx │ │ ├── PrismDiffCode │ │ │ ├── PrismDiffCode.stories.tsx │ │ │ └── PrismDiffCode.tsx │ │ ├── Result │ │ │ └── Result.tsx │ │ ├── Skeleton │ │ │ ├── Skeleton.stories.tsx │ │ │ └── Skeleton.tsx │ │ ├── Tag │ │ │ ├── Tag.stories.tsx │ │ │ └── Tag.tsx │ │ ├── Text.tsx │ │ └── Title.tsx │ ├── fields │ │ ├── Checkbox │ │ │ ├── Checkbox.stories.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── CheckboxGroup.stories.tsx │ │ │ ├── CheckboxGroup.tsx │ │ │ ├── checkbox-group.test.tsx │ │ │ ├── checkbox.test.tsx │ │ │ ├── context.ts │ │ │ └── index.ts │ │ ├── ComboBox │ │ │ ├── ComboBox.stories.tsx │ │ │ ├── ComboBox.tsx │ │ │ ├── combobox.test.tsx │ │ │ └── index.ts │ │ ├── DatePicker │ │ │ ├── DateInput.stories.tsx │ │ │ ├── DateInput.tsx │ │ │ ├── DateInputBase.tsx │ │ │ ├── DatePicker.stories.tsx │ │ │ ├── DatePicker.tsx │ │ │ ├── DatePickerButton.tsx │ │ │ ├── DatePickerElement.tsx │ │ │ ├── DatePickerInput.tsx │ │ │ ├── DatePickerSegment.tsx │ │ │ ├── DateRangePicker.stories.tsx │ │ │ ├── DateRangePicker.tsx │ │ │ ├── DateRangeSeparatedPicker.stories.tsx │ │ │ ├── DateRangeSeparatedPicker.tsx │ │ │ ├── TimeInput.stories.tsx │ │ │ ├── TimeInput.tsx │ │ │ ├── index.ts │ │ │ ├── intl.ts │ │ │ ├── parseDate.ts │ │ │ ├── props.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── FileInput │ │ │ ├── FileInput.stories.tsx │ │ │ └── FileInput.tsx │ │ ├── Input │ │ │ ├── Input.tsx │ │ │ └── index.ts │ │ ├── NumberInput │ │ │ ├── NumberInput.stories.tsx │ │ │ ├── NumberInput.tsx │ │ │ └── StepButton.tsx │ │ ├── PasswordInput │ │ │ ├── PasswordInput.stories.tsx │ │ │ ├── PasswordInput.tsx │ │ │ └── password-input.test.tsx │ │ ├── RadioGroup │ │ │ ├── Radio.tsx │ │ │ ├── RadioGroup.stories.tsx │ │ │ ├── RadioGroup.tsx │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ └── radio.test.tsx │ │ ├── SearchInput │ │ │ ├── SearchInput.stories.tsx │ │ │ ├── SearchInput.tsx │ │ │ └── index.ts │ │ ├── Select │ │ │ ├── Select.stories.tsx │ │ │ ├── Select.tsx │ │ │ ├── index.ts │ │ │ └── select.test.tsx │ │ ├── Slider │ │ │ ├── Gradation.tsx │ │ │ ├── Header.tsx │ │ │ ├── RangeSlider.stories.tsx │ │ │ ├── RangeSlider.tsx │ │ │ ├── Slider.stories.tsx │ │ │ ├── Slider.tsx │ │ │ ├── SliderBase.tsx │ │ │ ├── SliderInput.tsx │ │ │ ├── SliderThumb.tsx │ │ │ ├── SliderTrack.tsx │ │ │ ├── elements.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── Switch │ │ │ ├── Switch.stories.tsx │ │ │ ├── Switch.tsx │ │ │ ├── index.ts │ │ │ └── switch.test.tsx │ │ ├── TextArea │ │ │ ├── TextArea.stories.tsx │ │ │ ├── TextArea.tsx │ │ │ ├── index.ts │ │ │ └── textarea.test.tsx │ │ ├── TextInput │ │ │ ├── TextInput.stories.tsx │ │ │ ├── TextInput.tsx │ │ │ ├── TextInputBase.tsx │ │ │ ├── index.ts │ │ │ └── text-input.test.tsx │ │ ├── TextInputMapper │ │ │ ├── TextInputMapper.stories.tsx │ │ │ ├── TextInputMapper.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── form │ │ ├── FieldWrapper │ │ │ ├── FieldWrapper.stories.tsx │ │ │ ├── FieldWrapper.tsx │ │ │ ├── extract-field-wrapper-props.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── Form │ │ │ ├── ComplexForm.stories.tsx │ │ │ ├── Field.tsx │ │ │ ├── Form.tsx │ │ │ ├── ResetButton │ │ │ │ ├── ResetButton.tsx │ │ │ │ └── index.ts │ │ │ ├── SubmitButton │ │ │ │ ├── SubmitButton.tsx │ │ │ │ └── index.ts │ │ │ ├── SubmitError.tsx │ │ │ ├── field.test.tsx │ │ │ ├── index.ts │ │ │ ├── submit-error.test.tsx │ │ │ ├── submit.test.tsx │ │ │ ├── types.ts │ │ │ ├── use-field │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── use-field-props.tsx │ │ │ │ └── use-field.ts │ │ │ ├── use-form.tsx │ │ │ ├── validation.test.ts │ │ │ └── validation.ts │ │ ├── Label.tsx │ │ ├── index.ts │ │ └── wrapper.tsx │ ├── layout │ │ ├── Flex.tsx │ │ ├── Flow.tsx │ │ ├── Grid.tsx │ │ ├── Panel.tsx │ │ ├── Prefix.tsx │ │ ├── ResizablePanel.stories.tsx │ │ ├── ResizablePanel.tsx │ │ ├── Space.tsx │ │ └── Suffix.tsx │ ├── navigation │ │ ├── LegacyTabs │ │ │ ├── LegacyTabs.stories.tsx │ │ │ └── LegacyTabs.tsx │ │ └── Link │ │ │ ├── Link.stories.tsx │ │ │ └── Link.tsx │ ├── organisms │ │ ├── FileTabs │ │ │ └── FileTabs.tsx │ │ ├── Modal │ │ │ └── Modal.tsx │ │ └── StatsCard │ │ │ └── StatsCard.tsx │ ├── other │ │ ├── Base64Upload │ │ │ ├── Base64Upload.stories.tsx │ │ │ └── Base64Upload.tsx │ │ ├── Calendar │ │ │ ├── Calendar.tsx │ │ │ ├── CalendarCell.tsx │ │ │ ├── CalendarGrid.tsx │ │ │ └── RangeCalendar.tsx │ │ └── CloudLogo │ │ │ ├── CloudLogo.stories.tsx │ │ │ └── CloudLogo.tsx │ ├── overlays │ │ ├── AlertDialog │ │ │ ├── AlertDialog.stories.tsx │ │ │ ├── AlertDialog.tsx │ │ │ ├── AlertDialogApiProvider.tsx │ │ │ ├── AlertDialogZone.tsx │ │ │ ├── index.ts │ │ │ ├── tests │ │ │ │ └── use-alert-dialog-api.test.tsx │ │ │ └── types.ts │ │ ├── Dialog │ │ │ ├── Dialog.tsx │ │ │ ├── DialogContainer.tsx │ │ │ ├── DialogForm.tsx │ │ │ ├── DialogTrigger.tsx │ │ │ ├── context.tsx │ │ │ ├── dialog-container.tsx │ │ │ ├── index.ts │ │ │ └── stories │ │ │ │ ├── Dialog.stories.tsx │ │ │ │ └── DialogForm.stories.tsx │ │ ├── Modal │ │ │ ├── Modal.tsx │ │ │ ├── OpenTransition.tsx │ │ │ ├── Overlay.tsx │ │ │ ├── Popover.tsx │ │ │ ├── Tray.tsx │ │ │ ├── Underlay.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── NewNotifications │ │ │ ├── Bar │ │ │ │ ├── FloatingNotification.tsx │ │ │ │ ├── NotificationsBar.tsx │ │ │ │ ├── TransitionComponent.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ └── notifications-bar.test.tsx │ │ │ │ └── index.ts │ │ │ ├── Dialog │ │ │ │ ├── NotificationsDialogTrigger.tsx │ │ │ │ └── index.ts │ │ │ ├── Notification.tsx │ │ │ ├── NotificationView │ │ │ │ ├── NotificationAction.tsx │ │ │ │ ├── NotificationCloseButton.tsx │ │ │ │ ├── NotificationDescription.tsx │ │ │ │ ├── NotificationFooter.tsx │ │ │ │ ├── NotificationHeader.tsx │ │ │ │ ├── NotificationIcon.tsx │ │ │ │ ├── NotificationProvider.tsx │ │ │ │ ├── NotificationView.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── notification.test.tsx │ │ │ │ └── types.ts │ │ │ ├── Notifications.stories.tsx │ │ │ ├── NotificationsContext │ │ │ │ ├── NotificationsProvider.tsx │ │ │ │ ├── index.ts │ │ │ │ └── use-notifications.ts │ │ │ ├── NotificationsList │ │ │ │ ├── NotificationsList.tsx │ │ │ │ ├── NotificationsListItem.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── notification-list.test.tsx │ │ │ │ └── types.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── use-notification-list-item.ts │ │ │ │ ├── use-notifications-api.ts │ │ │ │ ├── use-notifications-list.ts │ │ │ │ └── use-notifications-observer.ts │ │ │ ├── index.ts │ │ │ ├── notification.test.tsx │ │ │ └── types.ts │ │ ├── Notification │ │ │ ├── Notification.stories.tsx │ │ │ └── Notification.tsx │ │ ├── OverlayWrapper.tsx │ │ ├── Toasts │ │ │ ├── Toast.tsx │ │ │ ├── Toasts.stories.tsx │ │ │ ├── index.ts │ │ │ ├── toast.test.tsx │ │ │ ├── types.ts │ │ │ └── use-toasts-api.ts │ │ └── Tooltip │ │ │ ├── Tooltip.stories.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── TooltipProvider.tsx │ │ │ ├── TooltipTrigger.tsx │ │ │ ├── context.ts │ │ │ └── index.ts │ ├── pickers │ │ └── Menu │ │ │ ├── Menu.stories.tsx │ │ │ ├── Menu.tsx │ │ │ ├── MenuButton.tsx │ │ │ ├── MenuItem.tsx │ │ │ ├── MenuSection.tsx │ │ │ ├── MenuTrigger.tsx │ │ │ ├── context.ts │ │ │ └── styled.tsx │ ├── portal │ │ ├── Portal.tsx │ │ ├── PortalProvider.ts │ │ ├── index.ts │ │ ├── storybook │ │ │ ├── portal.stories.tsx │ │ │ └── templates │ │ │ │ ├── CustomRoot.tsx │ │ │ │ ├── PortalOrder.tsx │ │ │ │ ├── basic.tsx │ │ │ │ └── index.ts │ │ ├── types.ts │ │ └── usePortal.ts │ ├── shared │ │ ├── InvalidIcon.tsx │ │ └── ValidIcon.tsx │ └── status │ │ ├── LoadingAnimation │ │ ├── LoadingAnimation.stories.tsx │ │ ├── LoadingAnimation.tsx │ │ └── index.ts │ │ ├── Spin │ │ ├── Cube.tsx │ │ ├── InternalSpinner.tsx │ │ ├── Spin.stories.tsx │ │ ├── Spin.tsx │ │ ├── SpinsContainer.tsx │ │ ├── index.ts │ │ └── types.ts │ │ └── index.ts ├── data │ └── themes.ts ├── icons │ ├── AiIcon.tsx │ ├── AreaChartIcon.tsx │ ├── BackwardIcon.tsx │ ├── BarChartIcon.tsx │ ├── BellFilledIcon.tsx │ ├── BellIcon.tsx │ ├── BooleanIcon.tsx │ ├── CalendarEditIcon.tsx │ ├── CalendarIcon.tsx │ ├── CaretDownIcon.tsx │ ├── CaretUpIcon.tsx │ ├── CheckCircleFilledIcon.tsx │ ├── CheckCircleIcon.tsx │ ├── CheckIcon.tsx │ ├── CircleFilledIcon.tsx │ ├── ClearIcon.tsx │ ├── CloseCircleFilledIcon.tsx │ ├── CloseCircleIcon.tsx │ ├── CloseIcon.tsx │ ├── CodeIcon.tsx │ ├── CopyIcon.tsx │ ├── CountIcon.tsx │ ├── CubeIcon.tsx │ ├── DangerIcon.tsx │ ├── DashboardIcon.tsx │ ├── DatabaseIcon.tsx │ ├── DirectionIcon.tsx │ ├── DonutIcon.tsx │ ├── DownIcon.tsx │ ├── EditIcon.tsx │ ├── ExclamationCircleFilledIcon.tsx │ ├── ExclamationCircleIcon.tsx │ ├── ExclamationIcon.tsx │ ├── EyeIcon.tsx │ ├── EyeInvisibleIcon.tsx │ ├── FilterIcon.tsx │ ├── FolderFilledIcon.tsx │ ├── FolderIcon.tsx │ ├── FolderOpenFilledIcon.tsx │ ├── FolderOpenIcon.tsx │ ├── ForwardIcon.tsx │ ├── HierarchyIcon.tsx │ ├── Icon.tsx │ ├── Icons.stories.tsx │ ├── InfoCircleIcon.tsx │ ├── InfoIcon.tsx │ ├── KeyIcon.tsx │ ├── LeftIcon.tsx │ ├── LineChartIcon.tsx │ ├── LoadingIcon.tsx │ ├── LockFilledIcon.tsx │ ├── LockIcon.tsx │ ├── MoreIcon.tsx │ ├── NotAllowedIcon.tsx │ ├── NumberIcon.tsx │ ├── PauseCircleFilledIcon.tsx │ ├── PauseCircleIcon.tsx │ ├── PauseIcon.tsx │ ├── PieChartIcon.tsx │ ├── PlayCircleIcon.tsx │ ├── PlayIcon.tsx │ ├── PlusIcon.tsx │ ├── ReloadIcon.tsx │ ├── ReportIcon.tsx │ ├── ReturnIcon.tsx │ ├── RightIcon.tsx │ ├── SchemeIcon.tsx │ ├── SearchIcon.tsx │ ├── SettingsIcon.tsx │ ├── ShieldFilledIcon.tsx │ ├── ShieldIcon.tsx │ ├── SlashIcon.tsx │ ├── SparklesIcon.tsx │ ├── SqlIcon.tsx │ ├── StatsIcon.tsx │ ├── StopIcon.tsx │ ├── StringIcon.tsx │ ├── SwitchIcon.tsx │ ├── TableIcon.tsx │ ├── ThumbsDownIcon.tsx │ ├── ThumbsUpIcon.tsx │ ├── ThunderboltCrossedIcon.tsx │ ├── ThunderboltFilledIcon.tsx │ ├── ThunderboltIcon.tsx │ ├── TimeIcon.tsx │ ├── UnlockIcon.tsx │ ├── UpIcon.tsx │ ├── UserGroupIcon.tsx │ ├── UserIcon.tsx │ ├── UserLockIcon.tsx │ ├── ViewIcon.tsx │ ├── WarningFilledIcon.tsx │ ├── WarningIcon.tsx │ ├── add-new-icon.js │ ├── index.ts │ └── wrap-icon.tsx ├── images │ ├── cube-cloud-logo.svg │ └── cube.svg ├── index.ts ├── provider.tsx ├── providers │ └── TrackingProvider.tsx ├── services │ └── notification.tsx ├── shared │ ├── form.ts │ └── index.ts ├── stories │ ├── Block.stories.tsx │ ├── Form.docs.mdx │ ├── Form.stories.jsx │ ├── FormFieldArgs.ts │ ├── Introduction.docs.mdx │ ├── Layout.docs.mdx │ ├── Layout.stories.jsx │ ├── Paragraph.stories.tsx │ ├── Result.docs.mdx │ ├── Result.stories.tsx │ ├── Styles.docs.mdx │ ├── Styles.stories.tsx │ ├── Tasty.docs.mdx │ ├── Tasty.stories.jsx │ ├── Text.stories.tsx │ ├── Typography.docs.mdx │ ├── Typography.stories.tsx │ ├── assets │ │ ├── code-brackets.svg │ │ ├── colors.svg │ │ ├── comments.svg │ │ ├── direction.svg │ │ ├── flow.svg │ │ ├── plugin.svg │ │ ├── repo.svg │ │ └── stackalt.svg │ ├── components │ │ ├── ConfirmDeletionDialogForm.tsx │ │ ├── DialogFormApp.tsx │ │ └── StyledButton.tsx │ └── lists │ │ └── baseProps.ts ├── tasty │ ├── __snapshots__ │ │ └── tasty.test.tsx.snap │ ├── index.ts │ ├── providers │ │ └── BreakpointsProvider.tsx │ ├── styles.test.ts │ ├── styles │ │ ├── align.ts │ │ ├── border.ts │ │ ├── boxShadow.combinator.ts │ │ ├── color.ts │ │ ├── createStyle.ts │ │ ├── dimension.ts │ │ ├── display.ts │ │ ├── fade.ts │ │ ├── fill.ts │ │ ├── flow.ts │ │ ├── font.ts │ │ ├── fontStyle.ts │ │ ├── gap.ts │ │ ├── groupRadius.ts │ │ ├── height.ts │ │ ├── index.ts │ │ ├── inset.ts │ │ ├── justify.ts │ │ ├── list.ts │ │ ├── margin.ts │ │ ├── marginBlock.ts │ │ ├── marginInline.ts │ │ ├── outline.ts │ │ ├── padding.ts │ │ ├── paddingBlock.ts │ │ ├── paddingInline.ts │ │ ├── place.ts │ │ ├── predefined.ts │ │ ├── preset.ts │ │ ├── radius.ts │ │ ├── reset.ts │ │ ├── scrollbar.test.ts │ │ ├── scrollbar.ts │ │ ├── shadow.ts │ │ ├── styledScrollbar.ts │ │ ├── transition.ts │ │ ├── types.ts │ │ └── width.ts │ ├── tasty.test.tsx │ ├── tasty.tsx │ ├── types.ts │ └── utils │ │ ├── cache-wrapper.ts │ │ ├── case-converter.ts │ │ ├── colors.ts │ │ ├── dotize.ts │ │ ├── filterBaseProps.ts │ │ ├── getDisplayName.ts │ │ ├── getModCombinations.ts │ │ ├── mergeStyles.ts │ │ ├── modAttrs.ts │ │ ├── renderStyles.ts │ │ ├── responsive.ts │ │ ├── string.ts │ │ ├── styles.test.ts │ │ ├── styles.ts │ │ └── warnings.ts ├── test │ ├── index.ts │ ├── render.tsx │ ├── setup.ts │ └── utils │ │ ├── index.ts │ │ └── wait.ts ├── tokens.ts ├── type-checks.tsx ├── utils │ ├── ResizeSensor.ts │ ├── modules.ts │ ├── promise.ts │ ├── random.ts │ ├── range.ts │ ├── react │ │ ├── Slots.tsx │ │ ├── chain.ts │ │ ├── index.ts │ │ ├── interactions.ts │ │ ├── isTextOnly.ts │ │ ├── mapProps.ts │ │ ├── mergeProps.tsx │ │ ├── nullableValue.ts │ │ ├── useCombinedRefs.ts │ │ ├── useId.tsx │ │ ├── useLayoutEffect.tsx │ │ ├── useQaProps.ts │ │ ├── useViewportSize.ts │ │ └── wrapNodeIfPlain.ts │ ├── transitions.ts │ ├── tree.ts │ └── warnings.ts └── version.ts ├── tasty.md ├── tsconfig.es.json ├── tsconfig.json └── vite.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "cube-js/cube-ui-kit" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | [*.{js,jsx,ts,tsx}] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | ij_typescript_spaces_within_imports = true 13 | ij_javascript_spaces_within_object_type_braces = true 14 | ij_javascript_spaces_within_object_literal_braces = true 15 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # eslint: props and import order fix 2 | 1f6220eeb7fc9c28f83f02eb113e92b8542fec89 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Describe changes 2 | 3 | 8 | 9 | ##### Checklist 10 | 11 | Before taking this PR from the draft, please, make sure you have done the following: 12 | 13 | 14 | 15 | - [ ] Pipeline is passed 16 | - [ ] Tests are added (including unit tests and stories in the storybook) 17 | - [ ] Tests are passed successfully 18 | - [ ] If you're adding a new component/new props, add stories that describe how this component/prop works 19 | - [ ] Changeset(s) is(are) added 20 | - [ ] You have passed the threshold of the library size 21 | - [ ] Commit message follows [commit guidelines](https://github.com/cube-js/cube-ui-kit/blob/main/CONTRIBUTING.md) 22 | 23 | Closes: N/A 24 | 25 | # 26 | 27 | ### Other information 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | .vscode 5 | .cache 6 | dist 7 | storybook-static 8 | storybook-docs 9 | build-storybook.log 10 | .vercel 11 | graph.png 12 | stats.html 13 | .rollup.cache 14 | .eslintcache 15 | yarn-error.log 16 | coverage 17 | size-limit-report/stats.json 18 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm test 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.(js|ts|tsx|jsx)": ["prettier --write", "eslint --cache --fix"] 3 | } 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.19.1 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | importOrder: [ 5 | '^:node', 6 | '', 7 | '', 8 | '', 9 | '', 10 | '^[.]{2,}', 11 | '', 12 | '^[.]/(?!index)', 13 | '', 14 | '^[./]', 15 | '', 16 | '', 17 | '^[.]{2,}', 18 | '^[./]', 19 | ], 20 | importOrderTypeScriptVersion: '5.0.0', 21 | plugins: ['@ianvs/prettier-plugin-sort-imports'], 22 | }; 23 | -------------------------------------------------------------------------------- /.size-limit.cjs: -------------------------------------------------------------------------------- 1 | const StatoscopeWebpackPlugin = require('@statoscope/webpack-plugin').default; 2 | 3 | const { join } = require('path'); 4 | 5 | const reportFolder = process.env.REPORT_FOLDER ?? './size-limit-report'; 6 | 7 | module.exports = [ 8 | { 9 | name: 'All', 10 | path: './dist/es/index.js', 11 | webpack: true, 12 | import: '*', 13 | modifyWebpackConfig: (webpackConfig) => { 14 | webpackConfig.plugins.push( 15 | new StatoscopeWebpackPlugin({ 16 | name: 'all', 17 | normalizeStats: true, 18 | saveOnlyStats: true, 19 | saveStatsTo: join(reportFolder, 'stats.json'), 20 | }), 21 | ); 22 | }, 23 | limit: '260kB', 24 | }, 25 | { 26 | name: 'Tree shaking (just a Button)', 27 | path: './dist/es/index.js', 28 | webpack: true, 29 | import: '{ Button }', 30 | limit: '23 kB', 31 | }, 32 | { 33 | name: 'Tree shaking (just an Icon)', 34 | path: './dist/es/index.js', 35 | webpack: true, 36 | import: '{ AiIcon }', 37 | limit: '12 kB', 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import remarkGfm from 'remark-gfm'; 3 | 4 | /** @type {import('@storybook/core-common').StorybookConfig} */ 5 | const config = { 6 | staticDirs: ['../public'], 7 | 8 | framework: { 9 | name: '@storybook/react-vite', 10 | options: {}, 11 | }, 12 | 13 | features: { 14 | postcss: false, 15 | emotionAlias: false, 16 | buildStoriesJson: true, 17 | interactionsDebugger: true, 18 | argTypeTargetsV7: false, 19 | modernInlineRender: true, 20 | }, 21 | 22 | stories: ['../src/**/*.docs.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 23 | 24 | addons: [ 25 | '@storybook/addon-links', 26 | '@storybook/addon-essentials', 27 | '@storybook/addon-interactions', 28 | { 29 | name: 'storybook-addon-turbo-build', 30 | options: { 31 | esbuildMinifyOptions: { 32 | target: 'es2021', 33 | }, 34 | }, 35 | }, 36 | { 37 | name: '@storybook/addon-docs', 38 | options: { 39 | mdxPluginOptions: { 40 | mdxCompileOptions: { 41 | remarkPlugins: [remarkGfm], 42 | }, 43 | }, 44 | }, 45 | }, 46 | ], 47 | 48 | docs: { 49 | autodocs: true, 50 | }, 51 | }; 52 | export default config; 53 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /.storybook/preview.jsx: -------------------------------------------------------------------------------- 1 | import { DocsContainer } from '@storybook/addon-docs'; 2 | import { configure } from '@storybook/test'; 3 | import isChromatic from 'chromatic/isChromatic'; 4 | import { config } from 'react-transition-group'; 5 | import { Root } from '../src'; 6 | 7 | configure({ testIdAttribute: 'data-qa', asyncUtilTimeout: 10000 }); 8 | 9 | if (isChromatic()) { 10 | // disabling transitions 11 | config.disabled = true; 12 | } 13 | 14 | export const parameters = { 15 | docs: { 16 | container: ({ children, context }) => ( 17 | 18 | {children} 19 | 20 | ), 21 | }, 22 | backgrounds: { 23 | default: 'transparent', 24 | values: [ 25 | { name: 'transparent', value: 'transparent' }, 26 | { name: 'gray', value: 'rgba(243,243,250, 1)' }, 27 | ], 28 | }, 29 | }; 30 | 31 | export const decorators = [ 32 | (Story) => ( 33 | 34 | 35 | 36 | ), 37 | ]; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cube Dev, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI Kit for Cube Dev Projects 2 | 3 | Based on React Aria and `tasty` styling library. 4 | 5 | ## Available Scripts 6 | 7 | ### pnpm start 8 | 9 | Runs the test page in the development mode. 10 | Open http://localhost:8080 to view it in the browser. 11 | 12 | The page will reload if you make edits. 13 | You will also see any lint errors in the console. 14 | 15 | ### pnpm storybook 16 | 17 | Run storybook with all the components of UI Kit. 18 | 19 | Deployed version of the Storybook from the `main` branch is here: https://cube-uikit-storybook.netlify.app/ 20 | 21 | ### pnpm build 22 | 23 | Builds a static copy of UIKit to the `dist/` folder. 24 | Your app is ready to be deployed! 25 | 26 | ### pnpm test 27 | 28 | Not yet implemented 29 | 30 | ## License 31 | 32 | This project is licensed under the MIT License - see the LICENSE file for details. 33 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | process.env.SC_DISABLE_SPEEDY = 'false'; 2 | 3 | /** @type {import('@jest/types').Config.InitialOptions} */ 4 | const config = { 5 | coverageDirectory: './coverage/', 6 | testEnvironment: 'jsdom', 7 | transform: { 8 | '^.+\\.(t|j)sx?$': [ 9 | '@swc/jest', 10 | { 11 | jsc: { 12 | parser: { syntax: 'typescript', tsx: true }, 13 | target: 'es2021', 14 | transform: { react: { runtime: 'automatic' } }, 15 | }, 16 | }, 17 | ], 18 | }, 19 | setupFilesAfterEnv: ['./src/test/setup.ts'], 20 | }; 21 | 22 | module.exports = config; 23 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "storybook-docs/" 3 | [[headers]] 4 | for = "*.json" 5 | [headers.values] 6 | Access-Control-Allow-Origin = "*" -------------------------------------------------------------------------------- /public/favicon-docs-development.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 🐞 12 | -------------------------------------------------------------------------------- /public/favicon-docs-production.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-js/cube-ui-kit/4f9656676e683f05fa700d8393ed4085784ff0b9/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scripts/ci/set-message.cjs: -------------------------------------------------------------------------------- 1 | const dedent = require('dedent'); 2 | 3 | module.exports = async function setMessage({ 4 | header, 5 | body, 6 | prNumber, 7 | repo, 8 | github, 9 | }) { 10 | const commentList = await github.paginate( 11 | 'GET /repos/:owner/:repo/issues/:issue_number/comments', 12 | { 13 | ...repo, 14 | issue_number: prNumber, 15 | }, 16 | ); 17 | 18 | const commentBody = dedent` 19 | ${header} 20 | 21 | ${body} 22 | `; 23 | 24 | const comment = commentList.find((comment) => 25 | comment.body.startsWith(header), 26 | ); 27 | 28 | if (!comment) { 29 | await github.rest.issues.createComment({ 30 | ...repo, 31 | issue_number: prNumber, 32 | body: commentBody, 33 | }); 34 | } else { 35 | await github.rest.issues.updateComment({ 36 | ...repo, 37 | comment_id: comment.id, 38 | body: commentBody, 39 | }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /scripts/replace-version.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | console.log('Replacing version in compiled files...'); 5 | 6 | // Read package.json and extract the version 7 | const packageJsonPath = path.resolve(__dirname, '../package.json'); 8 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 9 | const version = packageJson.version; 10 | 11 | // Define the directory where compiled files are located 12 | const distDir = path.resolve(__dirname, '../dist'); 13 | 14 | // Function to replace version in all .js files in the dist directory 15 | function replaceVersionInFiles(dir) { 16 | const files = fs.readdirSync(dir); 17 | 18 | files.forEach((file) => { 19 | const filePath = path.join(dir, file); 20 | const stat = fs.statSync(filePath); 21 | 22 | if (stat.isDirectory()) { 23 | replaceVersionInFiles(filePath); // Recurse into subdirectories 24 | } else if (file.endsWith('.js')) { 25 | let content = fs.readFileSync(filePath, 'utf8'); 26 | // Replace placeholder with version, wrapped in quotes 27 | content = content.replace(/__UIKIT_VERSION__/g, `${version}`); 28 | fs.writeFileSync(filePath, content); 29 | } 30 | }); 31 | } 32 | 33 | // Execute the replacement 34 | replaceVersionInFiles(distDir); 35 | -------------------------------------------------------------------------------- /size-limit-report/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-js/cube-ui-kit/4f9656676e683f05fa700d8393ed4085784ff0b9/size-limit-report/.gitignore -------------------------------------------------------------------------------- /src/_internal/hooks/__mocks__/use-warn.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | export const useWarn = jest.fn(); 3 | -------------------------------------------------------------------------------- /src/_internal/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-deprecation-warning'; 2 | export * from './use-event'; 3 | export * from './use-sync-ref'; 4 | export * from './use-chained-callback'; 5 | export * from './use-timer'; 6 | export * from './use-effect-once'; 7 | export * from './use-warn'; 8 | export * from './use-is-first-render'; 9 | export * from './use-debounced-value'; 10 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-chained-callback.ts: -------------------------------------------------------------------------------- 1 | import { useEvent } from './use-event'; 2 | 3 | export function useChainedCallback( 4 | ...callbacks: (((...args: any) => any) | null | undefined | boolean)[] 5 | ) { 6 | return useEvent((...args: any[]) => { 7 | callbacks.forEach((callback) => { 8 | if (typeof callback === 'function') { 9 | callback(...args); 10 | } 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-deprecation-warning.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { deprecationWarning } from '../../utils/warnings'; 4 | 5 | export function useDeprecationWarning( 6 | ...args: Parameters 7 | ) { 8 | const didWarn = React.useRef(false); 9 | 10 | useEffect(() => { 11 | if (didWarn.current) return; 12 | 13 | didWarn.current = true; 14 | deprecationWarning(...args); 15 | }, [args[0]]); 16 | } 17 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-effect-once.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback, useEffect } from 'react'; 2 | 3 | export function useEffectOnce(effect: EffectCallback) { 4 | useEffect(effect, []); 5 | } 6 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-event.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { useSyncRef } from './use-sync-ref'; 4 | 5 | /** 6 | * useEvent shim from the latest React RFC. 7 | * 8 | * @see https://github.com/reactjs/rfcs/pull/220 9 | * @see https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md#internal-implementation 10 | */ 11 | export function useEvent< 12 | Func extends (...args: Args) => Result, 13 | Args extends Parameters = Parameters, 14 | Result extends ReturnType = ReturnType, 15 | >(callback: Func): (...args: Args) => Result { 16 | const callbackRef = useSyncRef(callback); 17 | 18 | return useCallback((...args) => callbackRef.current(...args), []); 19 | } 20 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-is-first-render.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export function useIsFirstRender(): boolean { 4 | const isFirst = useRef(true); 5 | 6 | if (isFirst.current) { 7 | isFirst.current = false; 8 | 9 | return true; 10 | } 11 | 12 | return isFirst.current; 13 | } 14 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-sync-ref.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useRef } from 'react'; 2 | 3 | import { useLayoutEffect } from '../../utils/react'; 4 | 5 | export function useSyncRef(value: T): MutableRefObject { 6 | const ref = useRef(value); 7 | 8 | useLayoutEffect(() => { 9 | ref.current = value; 10 | }); 11 | 12 | return ref; 13 | } 14 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-timer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './timer'; 2 | export * from './use-timer'; 3 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-timer/use-timer.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import { useEvent } from '../use-event'; 4 | 5 | import { Timer } from './timer'; 6 | 7 | export type UseTimerProps = { 8 | callback?: () => void; 9 | delay?: number | null; 10 | isDisabled?: boolean; 11 | timer?: Timer | null; 12 | }; 13 | 14 | export function useTimer(props: UseTimerProps = {}) { 15 | const { callback, delay, isDisabled, timer: propsTimer = null } = props; 16 | 17 | const callbackEvent = useEvent(() => callback?.()); 18 | const [timer, setTimer] = useState(() => { 19 | if (propsTimer) { 20 | return propsTimer; 21 | } 22 | 23 | if (typeof delay === 'number') { 24 | return new Timer(callbackEvent, delay); 25 | } 26 | 27 | return null; 28 | }); 29 | 30 | if (isDisabled) { 31 | timer?.reset(); 32 | } else { 33 | timer?.resume(); 34 | } 35 | 36 | useEffect(() => { 37 | setTimer(propsTimer); 38 | return () => timer?.reset(); 39 | }, [propsTimer]); 40 | 41 | useEffect(() => { 42 | if (propsTimer) return; 43 | 44 | if (typeof delay === 'number') { 45 | setTimer(new Timer(callbackEvent, delay)); 46 | } 47 | 48 | return () => timer?.reset(); 49 | }, [delay, propsTimer]); 50 | 51 | useEffect(() => () => timer?.reset(), [timer]); 52 | 53 | return { timer } as const; 54 | } 55 | -------------------------------------------------------------------------------- /src/_internal/hooks/use-update-effect.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, EffectCallback, useEffect } from 'react'; 2 | 3 | import { useIsFirstRender } from './use-is-first-render'; 4 | 5 | export function useUpdateEffect(effect: EffectCallback, deps?: DependencyList) { 6 | const isFirst = useIsFirstRender(); 7 | 8 | useEffect(() => { 9 | if (!isFirst) { 10 | return effect(); 11 | } 12 | }, deps); 13 | } 14 | -------------------------------------------------------------------------------- /src/_internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | -------------------------------------------------------------------------------- /src/components/Block.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | import { 4 | AllBaseProps, 5 | CONTAINER_STYLES, 6 | ContainerStyleProps, 7 | extractStyles, 8 | filterBaseProps, 9 | tasty, 10 | } from '../tasty'; 11 | 12 | const BlockElement = tasty({ 13 | styles: { 14 | display: 'block', 15 | }, 16 | }); 17 | 18 | export interface CubeBlockProps 19 | extends Omit, 20 | ContainerStyleProps {} 21 | 22 | export const Block = forwardRef(function Block(props: CubeBlockProps, ref) { 23 | const styles = extractStyles(props, CONTAINER_STYLES); 24 | 25 | return ( 26 | 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/HiddenInput.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HiddenInput = styled.input<{ $isButton: boolean }>` 4 | &&&&& { 5 | display: block; 6 | font-family: inherit; 7 | font-size: 100%; 8 | line-height: 1.15; 9 | margin: 0; 10 | overflow: visible; 11 | box-sizing: border-box; 12 | padding: 0; 13 | position: absolute; 14 | top: 0; 15 | right: 0; 16 | left: 0; 17 | bottom: 0; 18 | opacity: 0.0001; 19 | z-index: 1; 20 | width: 100%; 21 | height: 100%; 22 | cursor: ${({ $isButton }) => ($isButton ? 'pointer' : 'default')}; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/components/OpenTrasition.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement } from 'react'; 2 | import { Transition } from 'react-transition-group'; 3 | 4 | const OPEN_STATES = { 5 | entering: false, 6 | entered: true, 7 | }; 8 | 9 | /** 10 | * Timeout issues adding css animations to enter may be related to 11 | * https://github.com/reactjs/react-transition-group/issues/189 or 12 | * https://github.com/reactjs/react-transition-group/issues/22 13 | * my VM isn't good enough to debug accurately and get a better answer. 14 | * 15 | * As a result, use enter 0 so that is-open is applied once entered 16 | * it doesn't matter if we know when the css-animation is done on entering 17 | * for exiting though, give time for the css-animation to play 18 | * before removing from the DOM 19 | * **note** hitting esc bypasses exit animation for anyone testing. 20 | */ 21 | 22 | export function OpenTransition(props) { 23 | return ( 24 | 25 | {(state) => 26 | Children.map( 27 | props.children, 28 | (child) => 29 | child && cloneElement(child, { isOpen: !!OPEN_STATES[state] }), 30 | ) 31 | } 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/actions/Action/Action.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryFn } from '@storybook/react'; 2 | 3 | import { baseProps } from '../../../stories/lists/baseProps'; 4 | 5 | import { Action, CubeActionProps } from './Action'; 6 | 7 | export default { 8 | title: 'Actions/Action', 9 | component: Action, 10 | parameters: { controls: { exclude: baseProps } }, 11 | argTypes: {}, 12 | }; 13 | 14 | const Template: StoryFn = (props) => ; 15 | 16 | export const Default = Template.bind({}); 17 | Default.args = { 18 | children: 'Action', 19 | }; 20 | 21 | export const Disabled = Template.bind({}); 22 | Disabled.args = { 23 | children: 'Action', 24 | isDisabled: true, 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/actions/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | -------------------------------------------------------------------------------- /src/components/actions/ButtonGroup/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | import { tasty } from '../../../tasty'; 4 | import { useSlotProps } from '../../../utils/react'; 5 | import { CubeSpaceProps, Space } from '../../layout/Space'; 6 | 7 | const ButtonGroupElement = tasty(Space, { 8 | qa: 'ButtonGroup', 9 | styles: { 10 | gridArea: 'buttonGroup', 11 | }, 12 | }); 13 | 14 | export const ButtonGroup = forwardRef(function ButtonGroup( 15 | props: CubeSpaceProps, 16 | ref, 17 | ) { 18 | return ( 19 | 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { Button as __Button } from './Button'; 2 | import { ButtonGroup } from './ButtonGroup/ButtonGroup'; 3 | 4 | const Button = Object.assign( 5 | __Button as typeof __Button & { 6 | Group: typeof ButtonGroup; 7 | }, 8 | { Group: ButtonGroup }, 9 | ); 10 | 11 | export * from './Button'; 12 | export * from './Action/Action'; 13 | export * from './use-action'; 14 | export { Button, ButtonGroup }; 15 | -------------------------------------------------------------------------------- /src/components/content/ActiveZone/ActiveZone.stories.tsx: -------------------------------------------------------------------------------- 1 | import { baseProps } from '../../../stories/lists/baseProps'; 2 | import { Tooltip } from '../../overlays/Tooltip/Tooltip'; 3 | import { TooltipTrigger } from '../../overlays/Tooltip/TooltipTrigger'; 4 | 5 | import { ActiveZone } from './ActiveZone'; 6 | 7 | export default { 8 | title: 'Content/ActiveZone', 9 | component: ActiveZone, 10 | parameters: { 11 | controls: { 12 | exclude: baseProps, 13 | }, 14 | }, 15 | }; 16 | 17 | const Template = ({ isDisabled, label }) => ( 18 | {label} 19 | ); 20 | 21 | const TooltipTemplate = ({ isDisabled, label }) => ( 22 | 23 | {label} 24 | Tooltip 25 | 26 | ); 27 | 28 | export const Default = Template.bind({}); 29 | Default.args = { 30 | label: 'ActiveZone', 31 | }; 32 | 33 | export const WithTooltip = TooltipTemplate.bind({}); 34 | WithTooltip.args = { 35 | label: 'ActiveZone', 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/content/Alert/Alert.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryFn } from '@storybook/react'; 2 | 3 | import { baseProps } from '../../../stories/lists/baseProps'; 4 | 5 | import { Alert } from './Alert'; 6 | import { CubeAlertProps } from './types'; 7 | 8 | export default { 9 | title: 'Content/Alert', 10 | component: Alert, 11 | parameters: { controls: { exclude: baseProps } }, 12 | args: { children: 'Card content' }, 13 | } as Meta; 14 | 15 | const Template: StoryFn = (args) => ; 16 | 17 | export const Default = Template.bind({}); 18 | 19 | export const Success = Template.bind({}); 20 | Success.args = { 21 | theme: 'success', 22 | }; 23 | 24 | export const Danger = Template.bind({}); 25 | Danger.args = { 26 | theme: 'danger', 27 | }; 28 | 29 | export const Note = Template.bind({}); 30 | Note.args = { 31 | theme: 'note', 32 | }; 33 | 34 | export const Disabled = Template.bind({}); 35 | Disabled.args = { 36 | isDisabled: true, 37 | }; 38 | 39 | export const DisabledWithType = Template.bind({}); 40 | DisabledWithType.args = { 41 | theme: 'danger', 42 | isDisabled: true, 43 | }; 44 | 45 | export const CustomStyling = Template.bind({}); 46 | CustomStyling.args = { 47 | padding: '4x', 48 | textAlign: 'center', 49 | fill: '#danger-bg', 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/content/Alert/Alert.tsx: -------------------------------------------------------------------------------- 1 | import { ForwardedRef, forwardRef } from 'react'; 2 | 3 | import THEMES from '../../../data/themes'; 4 | import { tasty } from '../../../tasty'; 5 | 6 | import { CubeAlertProps } from './types'; 7 | import { useAlert } from './use-alert'; 8 | 9 | const AlertElement = tasty({ 10 | name: 'Alert', 11 | role: 'alert', 12 | qa: 'Alert', 13 | styles: { 14 | display: 'block', 15 | flow: 'column', 16 | radius: '1cr', 17 | padding: '1.5x', 18 | preset: 't3', 19 | color: { 20 | '': '#dark-02', 21 | '[data-type="disabled"]': THEMES.disabled.color, 22 | }, 23 | fill: { 24 | '': '#clear', 25 | ...Object.keys(THEMES).reduce((map, type) => { 26 | map[`[data-type="${type}"]`] = THEMES[type].fill; 27 | 28 | return map; 29 | }, {}), 30 | }, 31 | border: { 32 | '': '#clear', 33 | ...Object.keys(THEMES).reduce((map, type) => { 34 | map[`[data-type="${type}"]`] = THEMES[type].border; 35 | 36 | return map; 37 | }, {}), 38 | }, 39 | }, 40 | }); 41 | 42 | export const Alert = forwardRef(function Alert( 43 | props: CubeAlertProps, 44 | ref: ForwardedRef, 45 | ) { 46 | const { styles, theme, filteredProps } = useAlert(props); 47 | 48 | return ( 49 | 55 | ); 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/content/Alert/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Alert'; 2 | export type { CubeAlertProps } from './types'; 3 | -------------------------------------------------------------------------------- /src/components/content/Alert/types.ts: -------------------------------------------------------------------------------- 1 | import THEMES from '../../../data/themes'; 2 | import { BaseProps, ContainerStyleProps, TextStyleProps } from '../../../tasty'; 3 | 4 | export interface CubeAlertProps 5 | extends Omit, 6 | ContainerStyleProps, 7 | TextStyleProps { 8 | /** 9 | * ***Deprecated*** 10 | * 11 | * @deprecated use `theme` prop instead 12 | * @default note 13 | */ 14 | type?: keyof typeof THEMES; 15 | /** 16 | * Changes the appearance of the Alert component 17 | * 18 | * @default note 19 | */ 20 | theme?: keyof typeof THEMES; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/content/Alert/use-alert.ts: -------------------------------------------------------------------------------- 1 | import { useDeprecationWarning } from '../../../_internal'; 2 | import { 3 | CONTAINER_STYLES, 4 | extractStyles, 5 | filterBaseProps, 6 | TEXT_STYLES, 7 | } from '../../../tasty'; 8 | 9 | import { CubeAlertProps } from './types'; 10 | 11 | const STYLE_LIST = [...CONTAINER_STYLES, ...TEXT_STYLES] as const; 12 | 13 | export function useAlert(props: CubeAlertProps) { 14 | const { type, isDisabled = false, theme } = props; 15 | 16 | const styles = extractStyles(props, STYLE_LIST); 17 | 18 | useDeprecationWarning(typeof type === 'undefined', { 19 | property: 'type', 20 | name: 'Alert', 21 | betterAlternative: 'theme', 22 | }); 23 | 24 | const _theme = isDisabled ? 'disabled' : theme ?? type ?? 'note'; 25 | 26 | return { 27 | styles, 28 | theme: _theme, 29 | filteredProps: filterBaseProps(props, { eventProps: true }), 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/content/Avatar/Avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { StoryFn } from '@storybook/react'; 2 | import { IconCoin } from '@tabler/icons-react'; 3 | 4 | import { baseProps } from '../../../stories/lists/baseProps'; 5 | 6 | import { Avatar, CubeAvatarProps } from './Avatar'; 7 | 8 | export default { 9 | title: 'Content/Avatar', 10 | component: Avatar, 11 | parameters: { 12 | controls: { 13 | exclude: baseProps, 14 | }, 15 | }, 16 | }; 17 | 18 | const Template: StoryFn = ({ label, icon, ...args }) => ( 19 | : null}> 20 | {label} 21 | 22 | ); 23 | 24 | export const Default = Template.bind({}); 25 | Default.args = { 26 | label: 'CU', 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/content/Avatar/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, ReactNode } from 'react'; 2 | 3 | import { 4 | BaseProps, 5 | CONTAINER_STYLES, 6 | ContainerStyleProps, 7 | Element, 8 | extractStyles, 9 | filterBaseProps, 10 | Styles, 11 | } from '../../../tasty'; 12 | 13 | const DEFAULT_STYLES = { 14 | display: 'grid', 15 | gap: '1x', 16 | flow: 'row', 17 | fill: '#purple', 18 | color: '#white', 19 | radius: 'round', 20 | placeContent: 'center', 21 | width: '@avatar-size @avatar-size @avatar-size', 22 | height: '@avatar-size @avatar-size @avatar-size', 23 | fontSize: 'calc(@avatar-size / 2)', 24 | lineHeight: 'calc(@avatar-size / 2)', 25 | fontWeight: 500, 26 | }; 27 | 28 | export interface CubeAvatarProps extends BaseProps, ContainerStyleProps { 29 | icon?: ReactNode; 30 | size?: Styles['size']; 31 | } 32 | 33 | export const Avatar = forwardRef(function Avatar( 34 | { size = '4x', icon, children, ...props }: CubeAvatarProps, 35 | ref, 36 | ) { 37 | const styles = extractStyles(props, CONTAINER_STYLES, { 38 | ...DEFAULT_STYLES, 39 | '--avatar-size': size, 40 | }); 41 | 42 | return ( 43 | 49 | {icon} 50 | {children} 51 | 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /src/components/content/Badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import { baseProps } from '../../../stories/lists/baseProps'; 2 | 3 | import { Badge } from './Badge'; 4 | 5 | export default { 6 | title: 'Content/Badge', 7 | component: Badge, 8 | parameters: { 9 | controls: { 10 | exclude: baseProps, 11 | }, 12 | }, 13 | }; 14 | 15 | const Template = ({ label, ...props }) => {label}; 16 | 17 | export const Default = Template.bind({}); 18 | Default.args = { 19 | label: '8', 20 | }; 21 | 22 | export const TwoDigit = Template.bind({}); 23 | TwoDigit.args = { 24 | label: '88', 25 | }; 26 | 27 | export const Long = Template.bind({}); 28 | Long.args = { 29 | label: 'label', 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/content/Card/Card.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from './Card'; 2 | 3 | export default { 4 | title: 'Content/Card', 5 | component: Card, 6 | argTypes: { 7 | shadow: { 8 | defaultValue: undefined, 9 | control: { 10 | type: 'radio', 11 | options: [ 12 | undefined, 13 | true, 14 | '0 1x 3x #shadow', 15 | '0 1x 3x #purple', 16 | '0 1x 3x #purple.50', 17 | ], 18 | }, 19 | }, 20 | radius: { 21 | defaultValue: undefined, 22 | control: { 23 | type: 'radio', 24 | options: [undefined, '1r', 'round'], 25 | }, 26 | }, 27 | border: { 28 | defaultValue: undefined, 29 | control: { 30 | type: 'radio', 31 | options: [undefined, true, '#purple'], 32 | }, 33 | }, 34 | padding: { 35 | defaultValue: undefined, 36 | control: { 37 | type: 'radio', 38 | options: [undefined, '1x', '2x', '1x top'], 39 | }, 40 | }, 41 | }, 42 | }; 43 | 44 | const Template = (args) => Card content; 45 | 46 | export const Default = Template.bind({}); 47 | -------------------------------------------------------------------------------- /src/components/content/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | import { 4 | BaseProps, 5 | CONTAINER_STYLES, 6 | ContainerStyleProps, 7 | extractStyles, 8 | filterBaseProps, 9 | tasty, 10 | } from '../../../tasty'; 11 | 12 | const CardElement = tasty({ 13 | role: 'region', 14 | styles: { 15 | display: 'block', 16 | flow: 'column', 17 | radius: '(1cr + 1bw)', 18 | fill: '#white', 19 | border: '#light-border', 20 | padding: '1.5x', 21 | preset: 't3', 22 | }, 23 | styleProps: CONTAINER_STYLES, 24 | }); 25 | 26 | export interface CubeCardProps extends BaseProps, ContainerStyleProps {} 27 | 28 | export const Card = forwardRef(function Card(props: CubeCardProps, ref) { 29 | const styles = extractStyles(props, CONTAINER_STYLES); 30 | 31 | return ( 32 | 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/content/Content.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | import { 4 | BaseProps, 5 | CONTAINER_STYLES, 6 | ContainerStyleProps, 7 | extractStyles, 8 | filterBaseProps, 9 | tasty, 10 | TEXT_STYLES, 11 | TextStyleProps, 12 | } from '../../tasty'; 13 | import { useSlotProps } from '../../utils/react'; 14 | 15 | const STYLE_LIST = [...CONTAINER_STYLES, ...TEXT_STYLES]; 16 | 17 | const ContentElement = tasty({ 18 | qa: 'Content', 19 | as: 'section', 20 | styles: { 21 | gridArea: 'content', 22 | preset: 'p3', 23 | color: '#dark-02', 24 | display: 'block', 25 | flow: 'column', 26 | gap: '2x', 27 | overflow: 'auto', 28 | scrollbar: 'styled', 29 | }, 30 | }); 31 | 32 | export interface CubeContentProps 33 | extends BaseProps, 34 | ContainerStyleProps, 35 | TextStyleProps {} 36 | 37 | export const Content = forwardRef(function Content( 38 | props: CubeContentProps, 39 | ref, 40 | ) { 41 | props = useSlotProps(props, 'content'); 42 | 43 | const styles = extractStyles(props, STYLE_LIST); 44 | 45 | return ( 46 | 51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/content/CopyPasteBlock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CopyPasteBlock'; 2 | -------------------------------------------------------------------------------- /src/components/content/CopySnippet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CopySnippet'; 2 | -------------------------------------------------------------------------------- /src/components/content/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | import { 4 | BaseProps, 5 | CONTAINER_STYLES, 6 | ContainerStyleProps, 7 | extractStyles, 8 | filterBaseProps, 9 | tasty, 10 | TEXT_STYLES, 11 | TextStyleProps, 12 | } from '../../tasty'; 13 | import { useSlotProps } from '../../utils/react'; 14 | 15 | const STYLE_LIST = [...CONTAINER_STYLES, ...TEXT_STYLES]; 16 | 17 | const FooterElement = tasty({ 18 | qa: 'Footer', 19 | 'data-id': 'Footer', 20 | styles: { 21 | gridArea: 'footer', 22 | display: 'block', 23 | flow: 'column', 24 | }, 25 | }); 26 | 27 | export interface CubeFooterProps 28 | extends BaseProps, 29 | ContainerStyleProps, 30 | TextStyleProps {} 31 | 32 | export const Footer = forwardRef(function Footer(props: CubeFooterProps, ref) { 33 | props = useSlotProps(props, 'footer'); 34 | 35 | const styles = extractStyles(props, STYLE_LIST); 36 | 37 | return ( 38 | 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/content/Header.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | import { 4 | BaseProps, 5 | CONTAINER_STYLES, 6 | ContainerStyleProps, 7 | extractStyles, 8 | filterBaseProps, 9 | tasty, 10 | TEXT_STYLES, 11 | TextStyleProps, 12 | } from '../../tasty'; 13 | import { useSlotProps } from '../../utils/react'; 14 | 15 | const STYLE_LIST = [...CONTAINER_STYLES, ...TEXT_STYLES]; 16 | 17 | const HeaderElement = tasty({ 18 | qa: 'Header', 19 | as: 'header', 20 | styles: { 21 | display: 'block', 22 | gridArea: 'header', 23 | flow: 'column', 24 | boxSizing: 'border-box', 25 | }, 26 | }); 27 | 28 | export interface CubeHeaderProps 29 | extends BaseProps, 30 | ContainerStyleProps, 31 | TextStyleProps {} 32 | 33 | export const Header = forwardRef(function Header(props: CubeHeaderProps, ref) { 34 | props = useSlotProps(props, 'header'); 35 | 36 | const styles = extractStyles(props, STYLE_LIST); 37 | 38 | return ( 39 | 44 | ); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/content/Paragraph.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | import { 4 | CONTAINER_STYLES, 5 | ContainerStyleProps, 6 | extractStyles, 7 | Styles, 8 | TEXT_STYLES, 9 | } from '../../tasty'; 10 | 11 | import { CubeTextProps, Text } from './Text'; 12 | 13 | const DEFAULT_STYLES: Styles = { 14 | preset: 'p3', 15 | color: '#dark-02', 16 | display: 'block', 17 | }; 18 | 19 | const STYLE_PROPS = [...CONTAINER_STYLES, ...TEXT_STYLES]; 20 | 21 | export interface CubeParagraphProps 22 | extends CubeTextProps, 23 | ContainerStyleProps {} 24 | 25 | export const Paragraph = forwardRef(function Paragraph( 26 | props: CubeParagraphProps, 27 | ref, 28 | ) { 29 | const styles = extractStyles(props, STYLE_PROPS, DEFAULT_STYLES); 30 | 31 | return ; 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/content/Placeholder/Placeholder.stories.tsx: -------------------------------------------------------------------------------- 1 | import { baseProps } from '../../../stories/lists/baseProps'; 2 | 3 | import { Placeholder } from './Placeholder'; 4 | 5 | export default { 6 | title: 'Content/Placeholder', 7 | component: Placeholder, 8 | parameters: { 9 | controls: { 10 | exclude: baseProps, 11 | }, 12 | }, 13 | }; 14 | 15 | const Template = (args) => ; 16 | 17 | export const Box = Template.bind({}); 18 | Box.args = {}; 19 | 20 | export const BigBox = Template.bind({}); 21 | BigBox.args = { 22 | height: '6x', 23 | }; 24 | 25 | export const Circle = Template.bind({}); 26 | Circle.args = { 27 | circle: true, 28 | size: '6x', 29 | }; 30 | 31 | export const Static = Template.bind({}); 32 | Static.args = { isStatic: true }; 33 | -------------------------------------------------------------------------------- /src/components/content/Skeleton/Skeleton.stories.tsx: -------------------------------------------------------------------------------- 1 | import { baseProps } from '../../../stories/lists/baseProps'; 2 | 3 | import { Skeleton } from './Skeleton'; 4 | 5 | export default { 6 | title: 'Content/Skeleton', 7 | component: Skeleton, 8 | parameters: { 9 | controls: { 10 | exclude: baseProps, 11 | }, 12 | }, 13 | }; 14 | 15 | const Template = (args) => { 16 | return ; 17 | }; 18 | 19 | export const Page = Template.bind({}); 20 | Page.args = { 21 | layout: 'page', 22 | qa: 'CustomSkeleton', 23 | }; 24 | 25 | export const Topbar = Template.bind({}); 26 | Topbar.args = { 27 | layout: 'topbar', 28 | }; 29 | 30 | export const Menu = Template.bind({}); 31 | Menu.args = { 32 | layout: 'menu', 33 | }; 34 | 35 | export const Tabs = Template.bind({}); 36 | Tabs.args = { 37 | layout: 'tabs', 38 | }; 39 | 40 | export const Stats = Template.bind({}); 41 | Stats.args = { 42 | layout: 'stats', 43 | }; 44 | 45 | export const Table = Template.bind({}); 46 | Table.args = { 47 | layout: 'table', 48 | }; 49 | 50 | export const Chart = Template.bind({}); 51 | Chart.args = { 52 | layout: 'chart', 53 | columns: 16, 54 | }; 55 | 56 | export const StaticPage = Template.bind({}); 57 | StaticPage.args = { 58 | layout: 'page', 59 | isStatic: true, 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/fields/Checkbox/CheckboxGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import { MULTIPLE_VALUE_ARG } from '../../../stories/FormFieldArgs'; 2 | import { baseProps } from '../../../stories/lists/baseProps'; 3 | 4 | import { Checkbox } from './Checkbox'; 5 | 6 | export default { 7 | title: 'Forms/CheckboxGroup', 8 | component: Checkbox.Group, 9 | parameters: { 10 | controls: { 11 | exclude: baseProps, 12 | }, 13 | }, 14 | argTypes: { 15 | ...MULTIPLE_VALUE_ARG, 16 | }, 17 | }; 18 | 19 | const Template = (props) => ( 20 | console.log('onChange event', query)} 24 | > 25 | One 26 | Two 27 | Three 28 | 29 | ); 30 | 31 | export const Default = Template.bind({}); 32 | Default.args = {}; 33 | 34 | export const Invalid = Template.bind({}); 35 | Invalid.args = { validationState: 'invalid' }; 36 | 37 | export const WithLabel = Template.bind({}); 38 | WithLabel.args = { label: 'Checkbox Group', 'aria-label': undefined }; 39 | -------------------------------------------------------------------------------- /src/components/fields/Checkbox/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { CheckboxGroupState } from 'react-stately'; 3 | 4 | export const CheckboxGroupContext = createContext( 5 | null, 6 | ); 7 | -------------------------------------------------------------------------------- /src/components/fields/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | export * from './CheckboxGroup'; 3 | -------------------------------------------------------------------------------- /src/components/fields/ComboBox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ComboBox'; 2 | -------------------------------------------------------------------------------- /src/components/fields/DatePicker/DatePickerButton.tsx: -------------------------------------------------------------------------------- 1 | import { CalendarIcon } from '../../../icons'; 2 | import { tasty } from '../../../tasty'; 3 | import { Button } from '../../actions'; 4 | 5 | export const DatePickerButton = tasty(Button, { 6 | icon: , 7 | styles: { 8 | radius: '1r right', 9 | border: 'top right bottom', 10 | backgroundClip: 'content-box', 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/fields/DatePicker/DatePickerElement.tsx: -------------------------------------------------------------------------------- 1 | import { tasty } from '../../../tasty'; 2 | import { Space } from '../../layout/Space'; 3 | 4 | export const DatePickerElement = tasty(Space, { 5 | styles: { 6 | gap: 0, 7 | outline: { 8 | '': '#purple-03.0', 9 | focused: '#purple-03', 10 | }, 11 | radius: true, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/fields/DatePicker/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TimeInput'; 2 | export * from './DateInput'; 3 | export * from './DatePicker'; 4 | export * from './DateRangePicker'; 5 | export * from './parseDate'; 6 | export * from './DateRangeSeparatedPicker'; 7 | export * from './types'; 8 | -------------------------------------------------------------------------------- /src/components/fields/DatePicker/intl.ts: -------------------------------------------------------------------------------- 1 | export const dateMessages = { 2 | time: 'Time', 3 | startTime: 'Start time', 4 | endTime: 'End time', 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/fields/DatePicker/parseDate.ts: -------------------------------------------------------------------------------- 1 | import { parseAbsolute } from '@internationalized/date'; 2 | 3 | export function parseAbsoluteDate(val: string | Date | undefined | null) { 4 | if (!val || (typeof val === 'string' && val.trim() === '')) { 5 | return undefined; 6 | } 7 | 8 | if (val instanceof Date) { 9 | return parseAbsolute(val.toISOString(), 'UTC'); 10 | } 11 | 12 | try { 13 | parseAbsolute(val, 'UTC'); 14 | } catch (e) { 15 | if (!Number.isNaN(Date.parse(val))) { 16 | return parseAbsolute(new Date(val).toISOString(), 'UTC'); 17 | } else { 18 | console.error('Invalid date format', val); 19 | return undefined; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/fields/DatePicker/props.ts: -------------------------------------------------------------------------------- 1 | import { AriaDatePickerProps, DateValue } from 'react-aria'; 2 | 3 | export const DEFAULT_DATE_PROPS: Partial> = { 4 | granularity: 'day', 5 | hideTimeZone: true, 6 | hourCycle: 24, 7 | shouldForceLeadingZeros: true, 8 | }; 9 | 10 | export const DEFAULT_TIME_PROPS: Partial> = { 11 | hideTimeZone: true, 12 | hourCycle: 24, 13 | shouldForceLeadingZeros: true, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/fields/DatePicker/types.ts: -------------------------------------------------------------------------------- 1 | import { DateValue } from 'react-aria'; 2 | 3 | export type Granularity = 'day' | 'hour' | 'minute' | 'second'; 4 | 5 | export interface DateFieldBase { 6 | /** The minimum allowed date that a user may select. */ 7 | minValue?: DateValue; 8 | /** The maximum allowed date that a user may select. */ 9 | maxValue?: DateValue; 10 | /** Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. */ 11 | isDateUnavailable?: (date: DateValue) => boolean; 12 | /** A placeholder date that influences the format of the placeholder shown when no value is selected. Defaults to today's date at midnight. */ 13 | placeholderValue?: T; 14 | /** Whether to display the time in 12 or 24 hour format. By default, this is determined by the user's locale. */ 15 | hourCycle?: 12 | 24; 16 | /** Determines the smallest unit that is displayed in the date picker. By default, this is `"day"` for dates, and `"minute"` for times. */ 17 | granularity?: Granularity; 18 | /** 19 | * Whether to hide the time zone abbreviation. 20 | * @default false 21 | */ 22 | hideTimeZone?: boolean; 23 | /** 24 | * Whether to always show leading zeros in the month, day, and hour fields. 25 | * By default, this is determined by the user's locale. 26 | */ 27 | shouldForceLeadingZeros?: boolean; 28 | isDisabled?: boolean; 29 | isReadOnly?: boolean; 30 | isRequired?: boolean; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/fields/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import { ForwardedRef, forwardRef } from 'react'; 2 | 3 | import { FileInput } from '../FileInput/FileInput'; 4 | import { NumberInput } from '../NumberInput/NumberInput'; 5 | import { PasswordInput } from '../PasswordInput/PasswordInput'; 6 | import { TextArea } from '../TextArea/TextArea'; 7 | import { TextInput } from '../TextInput'; 8 | 9 | type CubeInput = typeof TextInput & { 10 | Text: typeof TextInput; 11 | Password: typeof PasswordInput; 12 | Number: typeof NumberInput; 13 | TextArea: typeof TextArea; 14 | File: typeof FileInput; 15 | }; 16 | 17 | export const Input: CubeInput = Object.assign( 18 | forwardRef(function Input(props, ref: ForwardedRef) { 19 | return ; 20 | }), 21 | { 22 | Text: TextInput, 23 | Password: PasswordInput, 24 | Number: NumberInput, 25 | TextArea: TextArea, 26 | File: FileInput, 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /src/components/fields/Input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Input'; 2 | -------------------------------------------------------------------------------- /src/components/fields/NumberInput/NumberInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import { IconCoin } from '@tabler/icons-react'; 2 | 3 | import { NUMBER_VALUE_ARG } from '../../../stories/FormFieldArgs'; 4 | import { baseProps } from '../../../stories/lists/baseProps'; 5 | 6 | import { NumberInput } from './NumberInput'; 7 | 8 | export default { 9 | title: 'Forms/NumberInput', 10 | component: NumberInput, 11 | parameters: { 12 | controls: { 13 | exclude: baseProps, 14 | }, 15 | }, 16 | argTypes: { 17 | ...NUMBER_VALUE_ARG, 18 | }, 19 | }; 20 | 21 | const Template = ({ icon, ...props }) => ( 22 | : null} 24 | {...props} 25 | onChange={(query) => console.log('change', query)} 26 | /> 27 | ); 28 | 29 | export const Default = Template.bind({}); 30 | Default.args = {}; 31 | 32 | export const WithDefaultValue = Template.bind({}); 33 | WithDefaultValue.args = { defaultValue: 5 }; 34 | 35 | export const Small = Template.bind({}); 36 | Small.args = { 37 | size: 'small', 38 | }; 39 | 40 | export const Disabled = Template.bind({}); 41 | Disabled.args = { 42 | isDisabled: true, 43 | }; 44 | 45 | export const WithSuffixBefore = Template.bind({}); 46 | WithSuffixBefore.args = { 47 | suffix:
suffix
, 48 | suffixPosition: 'before', 49 | }; 50 | 51 | export const WithSuffixAfter = Template.bind({}); 52 | WithSuffixAfter.args = { 53 | suffix:
suffix
, 54 | suffixPosition: 'after', 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/fields/NumberInput/StepButton.tsx: -------------------------------------------------------------------------------- 1 | import { CaretDownIcon, CaretUpIcon } from '../../../icons'; 2 | import { Styles } from '../../../tasty'; 3 | import { Button } from '../../actions'; 4 | 5 | const STEP_BUTTON_STYLES: Styles = { 6 | width: '4x', 7 | radius: { 8 | '': '0 (1r - 1bw) 0 0', 9 | down: '0 0 (1r - 1bw) 0', 10 | }, 11 | fontSize: '12px', 12 | lineHeight: '12px', 13 | height: 'auto', 14 | fill: { 15 | '': '#dark.0', 16 | hovered: '#dark.04', 17 | pressed: '#purple.10', 18 | '[disabled]': '#dark.0', 19 | }, 20 | 21 | '@icon-size': { 22 | '': '14px', 23 | '[data-size="small"]': '13px', 24 | }, 25 | }; 26 | 27 | /** 28 | * Buttons for NumberField. 29 | */ 30 | export function StepButton(props) { 31 | return ( 32 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/stories/components/StyledButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '../../components/actions'; 2 | import { tasty } from '../../tasty'; 3 | 4 | export const StyledButton = tasty(Button, { 5 | styles: { 6 | padding: '3x 6x', 7 | preset: 't1', 8 | }, 9 | }); 10 | 11 | export const GlobalStyledHeading = tasty('.myButton', { 12 | display: 'inline-block', 13 | padding: '1x 2x', 14 | preset: 't2', 15 | border: true, 16 | radius: true, 17 | fill: { 18 | '': '#white', 19 | ':hover': '#purple.20', 20 | }, 21 | color: '#dark', 22 | }); 23 | 24 | export function Block() { 25 | return ( 26 | <> 27 | 123 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/stories/lists/baseProps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BASE_STYLES, 3 | BLOCK_STYLES, 4 | COLOR_STYLES, 5 | CONTAINER_STYLES, 6 | DIMENSION_STYLES, 7 | FLOW_STYLES, 8 | OUTER_STYLES, 9 | POSITION_STYLES, 10 | TEXT_STYLES, 11 | } from '../../tasty'; 12 | 13 | const allStyles: string[] = Array.from( 14 | new Set([ 15 | ...BASE_STYLES, 16 | ...POSITION_STYLES, 17 | ...CONTAINER_STYLES, 18 | ...BLOCK_STYLES, 19 | ...COLOR_STYLES, 20 | ...FLOW_STYLES, 21 | ...OUTER_STYLES, 22 | ...DIMENSION_STYLES, 23 | ...TEXT_STYLES, 24 | ...POSITION_STYLES, 25 | ]), 26 | ); 27 | 28 | export const baseProps = [ 29 | 'qa', 30 | 'qaVal', 31 | 'block', 32 | 'inline', 33 | 'style', 34 | 'styles', 35 | 'css', 36 | 'styleName', 37 | 'hidden', 38 | 'disabled', 39 | 'mods', 40 | 'breakpoints', 41 | 'isHidden', 42 | 'element', 43 | ...allStyles, 44 | ]; 45 | -------------------------------------------------------------------------------- /src/tasty/index.ts: -------------------------------------------------------------------------------- 1 | export { tasty, Element } from './tasty'; 2 | export * from './utils/filterBaseProps'; 3 | export * from './utils/colors'; 4 | export * from './utils/styles'; 5 | export * from './utils/modAttrs'; 6 | export * from './utils/responsive'; 7 | export * from './utils/renderStyles'; 8 | export * from './utils/dotize'; 9 | export * from './styles/list'; 10 | export * from './providers/BreakpointsProvider'; 11 | export * from './utils/mergeStyles'; 12 | export * from './utils/warnings'; 13 | export * from './utils/getDisplayName'; 14 | export type { 15 | TastyProps, 16 | GlobalTastyProps, 17 | AllBasePropsWithMods, 18 | } from './tasty'; 19 | export type { 20 | AllBaseProps, 21 | BaseProps, 22 | BaseStyleProps, 23 | DimensionStyleProps, 24 | ColorStyleProps, 25 | OuterStyleProps, 26 | PositionStyleProps, 27 | TextStyleProps, 28 | BlockStyleProps, 29 | ContainerStyleProps, 30 | BasePropsWithoutChildren, 31 | Props, 32 | FlowStyleProps, 33 | ShortGridStyles, 34 | GlobalStyledProps, 35 | TagName, 36 | } from './types'; 37 | export type { 38 | StylesInterface, 39 | Styles, 40 | StylesWithoutSelectors, 41 | NoType, 42 | Selector, 43 | SuffixForSelector, 44 | NotSelector, 45 | } from './styles/types'; 46 | -------------------------------------------------------------------------------- /src/tasty/providers/BreakpointsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode } from 'react'; 2 | 3 | // Default breakpoints mirror Bootstrap defaults without md and xxl breakpoints. 4 | export const BreakpointsContext = createContext([980]); 5 | 6 | interface BreakpointsProviderProps { 7 | value: number[]; 8 | children: ReactNode; 9 | } 10 | 11 | export function BreakpointsProvider({ 12 | value, 13 | children, 14 | }: BreakpointsProviderProps) { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/tasty/styles/align.ts: -------------------------------------------------------------------------------- 1 | export function alignStyle({ align }) { 2 | if (typeof align !== 'string') return; 3 | 4 | if (!align) return; 5 | 6 | return { 7 | 'align-items': align, 8 | 'align-content': align, 9 | }; 10 | } 11 | 12 | alignStyle.__lookupStyles = ['align']; 13 | -------------------------------------------------------------------------------- /src/tasty/styles/border.ts: -------------------------------------------------------------------------------- 1 | import { DIRECTIONS, filterMods, parseStyle } from '../utils/styles'; 2 | 3 | const BORDER_STYLES = [ 4 | 'none', 5 | 'hidden', 6 | 'dotted', 7 | 'dashed', 8 | 'solid', 9 | 'double', 10 | 'groove', 11 | 'ridge', 12 | 'inset', 13 | 'outset', 14 | ]; 15 | 16 | /** 17 | * 18 | * @param border 19 | * @return {{border: string}|*} 20 | */ 21 | export function borderStyle({ border }) { 22 | if (!border && border !== 0) return; 23 | 24 | if (border === true) border = '1bw'; 25 | 26 | const { values, mods, colors } = parseStyle(String(border)); 27 | 28 | const directions = filterMods(mods, DIRECTIONS); 29 | const typeMods = filterMods(mods, BORDER_STYLES); 30 | 31 | const value = values[0] || 'var(--border-width)'; 32 | const type = typeMods[0] || 'solid'; 33 | const borderColor = (colors && colors[0]) || 'var(--border-color)'; 34 | 35 | const styleValue = [value, type, borderColor].join(' '); 36 | 37 | if (!directions.length) { 38 | return { border: styleValue }; 39 | } 40 | 41 | const zeroValue = [0, type, borderColor].join(' '); 42 | 43 | return DIRECTIONS.reduce((styles, dir) => { 44 | if (mods.includes(dir)) { 45 | styles[`border-${dir}`] = styleValue; 46 | } else { 47 | styles[`border-${dir}`] = zeroValue; 48 | } 49 | 50 | return styles; 51 | }, {}); 52 | } 53 | 54 | borderStyle.__lookupStyles = ['border']; 55 | -------------------------------------------------------------------------------- /src/tasty/styles/boxShadow.combinator.ts: -------------------------------------------------------------------------------- 1 | // Deprecated: left as an example 2 | export function boxShadowCombinator(styles) { 3 | const values = boxShadowCombinator.__lookupStyles.reduce( 4 | (list: string[], style) => { 5 | const value = styles[style]; 6 | 7 | if (value) { 8 | list.push(`var(--local-${style}-box-shadow)`); 9 | } 10 | 11 | return list; 12 | }, 13 | [], 14 | ); 15 | 16 | if (!values.length) return ''; 17 | 18 | return { 'box-shadow': values.join(', ') }; 19 | } 20 | 21 | boxShadowCombinator.__lookupStyles = ['outline', 'shadow']; 22 | -------------------------------------------------------------------------------- /src/tasty/styles/color.ts: -------------------------------------------------------------------------------- 1 | import { parseColor } from '../utils/styles'; 2 | 3 | export function colorStyle({ color }) { 4 | if (!color) return ''; 5 | 6 | if (color === true) color = 'currentColor'; 7 | 8 | if (typeof color === 'string' && color.startsWith('#')) { 9 | color = parseColor(color).color || color; 10 | } 11 | 12 | const match = color.match(/var\(--(.+?)-color/); 13 | let name = ''; 14 | 15 | if (match) { 16 | name = match[1]; 17 | } 18 | 19 | const styles: any[] = [ 20 | { 21 | color: color, 22 | }, 23 | ]; 24 | 25 | if (name) { 26 | styles.push([ 27 | { 28 | '--current-color': `var(--${name}-color, ${name})`, 29 | '--current-color-rgb': `var(--${name}-color-rgb)`, 30 | }, 31 | ]); 32 | } 33 | 34 | return styles; 35 | } 36 | 37 | colorStyle.__lookupStyles = ['color']; 38 | -------------------------------------------------------------------------------- /src/tasty/styles/display.ts: -------------------------------------------------------------------------------- 1 | export function displayStyle({ display, hide }) { 2 | return { display: !hide ? display : 'none' }; 3 | } 4 | 5 | displayStyle.__lookupStyles = ['display', 'hide']; 6 | -------------------------------------------------------------------------------- /src/tasty/styles/fade.ts: -------------------------------------------------------------------------------- 1 | import { DIRECTIONS, filterMods, parseStyle } from '../utils/styles'; 2 | 3 | const DIRECTION_MAP = { 4 | right: 'to left', 5 | left: 'to right', 6 | top: 'to bottom', 7 | bottom: 'to top', 8 | }; 9 | 10 | export function fadeStyle({ fade }) { 11 | if (!fade) return ''; 12 | 13 | let { values, mods } = parseStyle(fade); 14 | 15 | let directions = filterMods(mods, DIRECTIONS); 16 | 17 | if (!values.length) { 18 | values = ['var(--fade-width)']; 19 | } 20 | 21 | if (!directions.length) { 22 | directions = ['top', 'right', 'bottom', 'left']; 23 | } 24 | 25 | return { 26 | mask: directions 27 | .map((direction: (typeof DIRECTIONS)[number], index: number) => { 28 | const size = values[index] || values[index % 2] || values[0]; 29 | 30 | return `linear-gradient(${DIRECTION_MAP[direction]}, rgb(0 0 0 / 0) 0%, rgb(0 0 0 / 1) ${size})`; 31 | }) 32 | .join(', '), 33 | 'mask-composite': 'intersect', 34 | }; 35 | } 36 | 37 | fadeStyle.__lookupStyles = ['fade']; 38 | -------------------------------------------------------------------------------- /src/tasty/styles/fill.ts: -------------------------------------------------------------------------------- 1 | import { parseStyle } from '../utils/styles'; 2 | 3 | export function fillStyle({ fill }) { 4 | if (!fill) return ''; 5 | 6 | if (fill.startsWith('#')) { 7 | fill = parseStyle(fill).colors[0] || fill; 8 | } 9 | 10 | const match = fill.match(/var\(--(.+?)-color/); 11 | let name = ''; 12 | 13 | if (match) { 14 | name = match[1]; 15 | } 16 | 17 | const styles: any[] = [ 18 | { 19 | 'background-color': fill, 20 | }, 21 | ]; 22 | 23 | if (name) { 24 | styles.push([ 25 | { 26 | $: '>*', 27 | '--context-fill-color': fill, 28 | '--context-fill-color-rgb': `var(--${name}-color-rgb)`, 29 | }, 30 | ]); 31 | } 32 | 33 | return styles; 34 | } 35 | 36 | fillStyle.__lookupStyles = ['fill']; 37 | -------------------------------------------------------------------------------- /src/tasty/styles/flow.ts: -------------------------------------------------------------------------------- 1 | export function flowStyle({ display = 'block', flow }) { 2 | let style; 3 | 4 | if (display.includes('grid')) { 5 | style = 'grid-auto-flow'; 6 | } else if (display.includes('flex')) { 7 | style = 'flex-flow'; 8 | } 9 | 10 | return style ? { [style]: flow } : null; 11 | } 12 | 13 | flowStyle.__lookupStyles = ['display', 'flow']; 14 | -------------------------------------------------------------------------------- /src/tasty/styles/font.ts: -------------------------------------------------------------------------------- 1 | export function fontStyle({ font }) { 2 | if (font == null || font === false) return null; 3 | 4 | const fontFamily = 5 | font === 'monospace' 6 | ? 'var(--monospace-font)' 7 | : font === true 8 | ? 'var(--font)' 9 | : `${font}, var(--font)`; 10 | 11 | return { 12 | 'font-family': fontFamily, 13 | '--font-family': fontFamily, 14 | }; 15 | } 16 | 17 | fontStyle.__lookupStyles = ['font']; 18 | -------------------------------------------------------------------------------- /src/tasty/styles/fontStyle.ts: -------------------------------------------------------------------------------- 1 | export function fontStyleStyle({ fontStyle }) { 2 | if (fontStyle === true) { 3 | fontStyle = 'italic'; 4 | } 5 | 6 | if (fontStyle !== 'inherit') { 7 | fontStyle = fontStyle ? 'italic' : 'normal'; 8 | } 9 | 10 | return { 'font-style': fontStyle }; 11 | } 12 | 13 | fontStyleStyle.__lookupStyles = ['fontStyle']; 14 | -------------------------------------------------------------------------------- /src/tasty/styles/height.ts: -------------------------------------------------------------------------------- 1 | import { dimensionStyle } from './dimension'; 2 | 3 | const dimension = dimensionStyle('height'); 4 | 5 | export function heightStyle({ height }) { 6 | return dimension(height); 7 | } 8 | 9 | heightStyle.__lookupStyles = ['height']; 10 | -------------------------------------------------------------------------------- /src/tasty/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { predefine } from './predefined'; 2 | 3 | const { STYLE_HANDLER_MAP, defineCustomStyle, defineStyleAlias } = predefine(); 4 | 5 | export { STYLE_HANDLER_MAP, defineCustomStyle, defineStyleAlias }; 6 | export * from './createStyle'; 7 | -------------------------------------------------------------------------------- /src/tasty/styles/inset.ts: -------------------------------------------------------------------------------- 1 | import { DIRECTIONS, filterMods, parseStyle } from '../utils/styles'; 2 | 3 | export function insetStyle({ inset }) { 4 | if (typeof inset === 'number') { 5 | inset = `${inset}px`; 6 | } 7 | 8 | if (!inset) return ''; 9 | 10 | if (inset === true) inset = '0 0 0 0'; 11 | 12 | let { values, mods } = parseStyle(inset); 13 | 14 | let directions = filterMods(mods, DIRECTIONS); 15 | 16 | if (!values.length) { 17 | values = ['0']; 18 | } 19 | 20 | if (!directions.length) { 21 | directions = DIRECTIONS; 22 | } 23 | 24 | return directions.reduce((styles, dir, index) => { 25 | styles[dir] = values[index] || values[index % 2] || values[0]; 26 | return styles; 27 | }, {}); 28 | } 29 | 30 | insetStyle.__lookupStyles = ['inset']; 31 | -------------------------------------------------------------------------------- /src/tasty/styles/justify.ts: -------------------------------------------------------------------------------- 1 | export function justifyStyle({ justify }) { 2 | if (typeof justify !== 'string') return; 3 | 4 | if (!justify) return; 5 | 6 | return { 7 | 'justify-items': justify, 8 | 'justify-content': justify, 9 | }; 10 | } 11 | 12 | justifyStyle.__lookupStyles = ['justify']; 13 | -------------------------------------------------------------------------------- /src/tasty/styles/marginBlock.ts: -------------------------------------------------------------------------------- 1 | import { parseStyle } from '../utils/styles'; 2 | 3 | export function marginBlockStyle({ 4 | marginBlock: margin, 5 | marginTop, 6 | marginBottom, 7 | }) { 8 | if (typeof margin === 'number') { 9 | margin = `${margin}px`; 10 | } 11 | 12 | if (!margin) return ''; 13 | 14 | if (margin === true) margin = '1x'; 15 | 16 | let { values } = parseStyle(margin); 17 | 18 | if (!values.length) { 19 | values = ['var(--gap)']; 20 | } 21 | 22 | const styles = {}; 23 | 24 | if (marginTop == null) { 25 | styles['margin-top'] = values[0]; 26 | } 27 | 28 | if (marginBottom == null) { 29 | styles['margin-bottom'] = values[1] || values[0]; 30 | } 31 | 32 | return styles; 33 | } 34 | 35 | marginBlockStyle.__lookupStyles = ['marginBlock', 'marginTop', 'marginBottom']; 36 | -------------------------------------------------------------------------------- /src/tasty/styles/marginInline.ts: -------------------------------------------------------------------------------- 1 | import { parseStyle } from '../utils/styles'; 2 | 3 | export function marginInlineStyle({ 4 | marginInline: margin, 5 | marginLeft, 6 | marginRight, 7 | }) { 8 | if (typeof margin === 'number') { 9 | margin = `${margin}px`; 10 | } 11 | 12 | if (!margin) return ''; 13 | 14 | if (margin === true) margin = '1x'; 15 | 16 | let { values } = parseStyle(margin); 17 | 18 | if (!values.length) { 19 | values = ['var(--gap)']; 20 | } 21 | 22 | const styles = {}; 23 | 24 | if (marginLeft == null) { 25 | styles['margin-left'] = values[0]; 26 | } 27 | 28 | if (marginRight == null) { 29 | styles['margin-right'] = values[1] || values[0]; 30 | } 31 | 32 | return styles; 33 | } 34 | 35 | marginInlineStyle.__lookupStyles = [ 36 | 'marginInline', 37 | 'marginLeft', 38 | 'marginRight', 39 | ]; 40 | -------------------------------------------------------------------------------- /src/tasty/styles/outline.ts: -------------------------------------------------------------------------------- 1 | import { filterMods, parseStyle } from '../utils/styles'; 2 | 3 | const BORDER_STYLES = [ 4 | 'none', 5 | 'hidden', 6 | 'dotted', 7 | 'dashed', 8 | 'solid', 9 | 'double', 10 | 'groove', 11 | 'ridge', 12 | 'inset', 13 | 'outset', 14 | ]; 15 | 16 | /** 17 | * 18 | * @param outline 19 | * @return {{outline: string}|*} 20 | */ 21 | export function outlineStyle({ outline }) { 22 | if (!outline && outline !== 0) return; 23 | 24 | if (outline === true) outline = '1ow'; 25 | 26 | const { values, mods, colors } = parseStyle(String(outline)); 27 | 28 | const typeMods = filterMods(mods, BORDER_STYLES); 29 | 30 | const value = values[0] || 'var(--outline-width)'; 31 | const type = typeMods[0] || 'solid'; 32 | const outlineColor = colors?.[0] || 'var(--outline-color)'; 33 | 34 | const styleValue = [value, type, outlineColor].join(' '); 35 | 36 | if (values.length > 1) { 37 | return { 38 | outline: styleValue, 39 | 'outline-offset': values[1], 40 | }; 41 | } 42 | 43 | return { outline: styleValue }; 44 | } 45 | 46 | outlineStyle.__lookupStyles = ['outline']; 47 | -------------------------------------------------------------------------------- /src/tasty/styles/paddingBlock.ts: -------------------------------------------------------------------------------- 1 | import { parseStyle } from '../utils/styles'; 2 | 3 | export function paddingBlockStyle({ 4 | paddingBlock: padding, 5 | paddingTop, 6 | paddingBottom, 7 | }) { 8 | if (typeof padding === 'number') { 9 | padding = `${padding}px`; 10 | } 11 | 12 | if (!padding) return ''; 13 | 14 | if (padding === true) padding = '1x'; 15 | 16 | let { values } = parseStyle(padding); 17 | 18 | if (!values.length) { 19 | values = ['var(--gap)']; 20 | } 21 | 22 | const styles = {}; 23 | 24 | if (paddingTop == null) { 25 | styles['padding-top'] = values[0]; 26 | } 27 | 28 | if (paddingBottom == null) { 29 | styles['padding-bottom'] = values[1] || values[0]; 30 | } 31 | 32 | return styles; 33 | } 34 | 35 | paddingBlockStyle.__lookupStyles = [ 36 | 'paddingBlock', 37 | 'paddingTop', 38 | 'paddingBottom', 39 | ]; 40 | -------------------------------------------------------------------------------- /src/tasty/styles/paddingInline.ts: -------------------------------------------------------------------------------- 1 | import { parseStyle } from '../utils/styles'; 2 | 3 | export function paddingInlineStyle({ 4 | paddingInline: padding, 5 | paddingLeft, 6 | paddingRight, 7 | }) { 8 | if (typeof padding === 'number') { 9 | padding = `${padding}px`; 10 | } 11 | 12 | if (!padding) return ''; 13 | 14 | if (padding === true) padding = '1x'; 15 | 16 | let { values } = parseStyle(padding); 17 | 18 | if (!values.length) { 19 | values = ['var(--gap)']; 20 | } 21 | 22 | const styles = {}; 23 | 24 | if (paddingLeft == null) { 25 | styles['padding-left'] = values[0]; 26 | } 27 | 28 | if (paddingRight == null) { 29 | styles['padding-right'] = values[1] || values[0]; 30 | } 31 | 32 | return styles; 33 | } 34 | 35 | paddingInlineStyle.__lookupStyles = [ 36 | 'paddingInline', 37 | 'paddingLeft', 38 | 'paddingRight', 39 | ]; 40 | -------------------------------------------------------------------------------- /src/tasty/styles/place.ts: -------------------------------------------------------------------------------- 1 | export function placeStyle({ place }) { 2 | if (typeof place !== 'string') return; 3 | 4 | if (!place) return; 5 | 6 | return { 7 | 'place-items': place, 8 | 'place-content': place, 9 | }; 10 | } 11 | 12 | placeStyle.__lookupStyles = ['place']; 13 | -------------------------------------------------------------------------------- /src/tasty/styles/shadow.ts: -------------------------------------------------------------------------------- 1 | import { parseStyle } from '../utils/styles'; 2 | 3 | function toBoxShadow(shadow) { 4 | const { values, mods, colors } = parseStyle(shadow); 5 | const mod = mods[0] || ''; 6 | const shadowColor = (colors && colors[0]) || 'var(--shadow-color)'; 7 | 8 | return [mod, ...values, shadowColor].join(' '); 9 | } 10 | 11 | export function shadowStyle({ shadow }) { 12 | if (!shadow) return ''; 13 | 14 | if (shadow === true) shadow = '0 5px 15px #shadow'; 15 | 16 | return { 17 | 'box-shadow': shadow.split(',').map(toBoxShadow).join(','), 18 | }; 19 | } 20 | 21 | shadowStyle.__lookupStyles = ['shadow']; 22 | -------------------------------------------------------------------------------- /src/tasty/styles/width.ts: -------------------------------------------------------------------------------- 1 | import { dimensionStyle } from './dimension'; 2 | 3 | const dimension = dimensionStyle('width'); 4 | 5 | export function widthStyle({ width }) { 6 | return dimension(width); 7 | } 8 | 9 | widthStyle.__lookupStyles = ['width']; 10 | -------------------------------------------------------------------------------- /src/tasty/utils/cache-wrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a function that caches the result up to the limit. 3 | */ 4 | export function cacheWrapper< 5 | T extends (firstArg: any, secondArg?: string) => any, 6 | >(handler: T, limit = 1000): T { 7 | let cache: { string?: ReturnType } = {}; 8 | let count = 0; 9 | 10 | return ((firstArg: any, secondArg?: string) => { 11 | const key = 12 | typeof firstArg === 'string' && secondArg == null 13 | ? firstArg 14 | : JSON.stringify([firstArg, secondArg]); 15 | 16 | if (!cache[key]) { 17 | if (count > limit) { 18 | cache = {}; 19 | count = 0; 20 | } 21 | 22 | count++; 23 | 24 | cache[key] = 25 | secondArg == null ? handler(firstArg) : handler(firstArg, secondArg); 26 | } 27 | 28 | return cache[key]; 29 | }) as T; 30 | } 31 | -------------------------------------------------------------------------------- /src/tasty/utils/case-converter.ts: -------------------------------------------------------------------------------- 1 | export function camelToKebab(str: string): string { 2 | return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); 3 | } 4 | -------------------------------------------------------------------------------- /src/tasty/utils/colors.ts: -------------------------------------------------------------------------------- 1 | export function color(name, opacity = 1) { 2 | if (opacity !== 1) { 3 | return `rgb(var(--${name}-color-rgb) / ${opacity})`; 4 | } 5 | 6 | return `var(--${name}-color)`; 7 | } 8 | -------------------------------------------------------------------------------- /src/tasty/utils/getDisplayName.ts: -------------------------------------------------------------------------------- 1 | import { ElementType } from 'react'; 2 | 3 | const DEFAULT_NAME = 'Anonymous'; 4 | 5 | export function getDisplayName( 6 | Component: ElementType, 7 | fallbackName = DEFAULT_NAME, 8 | ): string { 9 | if (typeof Component === 'function') { 10 | return Component.displayName ?? Component.name ?? fallbackName; 11 | } 12 | 13 | return fallbackName; 14 | } 15 | -------------------------------------------------------------------------------- /src/tasty/utils/getModCombinations.ts: -------------------------------------------------------------------------------- 1 | function filterCombinations(combinations) { 2 | return combinations.filter((combination) => { 3 | const list: string[] = []; 4 | 5 | return !combination.find((mod) => { 6 | const match = mod.match(/\[(.+?)[=\]]/); 7 | 8 | if (match) { 9 | if (list.includes(match[1])) { 10 | return true; 11 | } 12 | 13 | list.push(match[1]); 14 | 15 | return false; 16 | } 17 | 18 | return false; 19 | }); 20 | }); 21 | } 22 | 23 | export function getModCombinations(array: string[], allowEmpty?: boolean) { 24 | const result: string[][] = allowEmpty ? [[]] : []; 25 | 26 | if (array.length === 0) { 27 | return [array]; 28 | } 29 | 30 | if (array.length < 2) { 31 | return result.concat([array]); 32 | } 33 | 34 | const f = function (prefix: string[] = [], array: string[]) { 35 | for (let i = 0; i < array.length; i++) { 36 | result.push([...prefix, array[i]]); 37 | f([...prefix, array[i]], array.slice(i + 1)); 38 | } 39 | }; 40 | 41 | f([], array); 42 | 43 | return filterCombinations(result); 44 | } 45 | -------------------------------------------------------------------------------- /src/tasty/utils/mergeStyles.ts: -------------------------------------------------------------------------------- 1 | import { Styles, StylesWithoutSelectors } from '../styles/types'; 2 | 3 | import { isSelector } from './renderStyles'; 4 | 5 | export function mergeStyles(...objects: (Styles | undefined | null)[]): Styles { 6 | let styles: Styles = objects[0] ? { ...objects[0] } : {}; 7 | let pos = 1; 8 | 9 | while (pos in objects) { 10 | const selectorKeys = Object.keys(styles).filter( 11 | (key) => isSelector(key) && styles[key], 12 | ); 13 | const newStyles = objects[pos]; 14 | 15 | if (newStyles) { 16 | const resultStyles = { ...styles, ...newStyles }; 17 | 18 | for (let key of selectorKeys) { 19 | if (newStyles?.[key]) { 20 | resultStyles[key] = { 21 | ...(styles[key] as StylesWithoutSelectors), 22 | ...(newStyles[key] as StylesWithoutSelectors), 23 | }; 24 | } 25 | } 26 | 27 | styles = resultStyles; 28 | } 29 | 30 | pos++; 31 | } 32 | 33 | return styles; 34 | } 35 | -------------------------------------------------------------------------------- /src/tasty/utils/modAttrs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate data DOM attributes from modifier map. 3 | */ 4 | import { AllBaseProps } from '../types'; 5 | 6 | import { cacheWrapper } from './cache-wrapper'; 7 | import { camelToKebab } from './case-converter'; 8 | 9 | function modAttrs(map: AllBaseProps['mods']): Record | null { 10 | return map 11 | ? Object.keys(map).reduce((attrs, key) => { 12 | if (map[key]) { 13 | attrs[`data-is-${camelToKebab(key)}`] = ''; 14 | } 15 | 16 | return attrs; 17 | }, {}) 18 | : null; 19 | } 20 | 21 | const _modAttrs = cacheWrapper(modAttrs); 22 | 23 | export { _modAttrs as modAttrs }; 24 | -------------------------------------------------------------------------------- /src/tasty/utils/string.ts: -------------------------------------------------------------------------------- 1 | export function toSnakeCase(str) { 2 | return str.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`); 3 | } 4 | -------------------------------------------------------------------------------- /src/tasty/utils/warnings.ts: -------------------------------------------------------------------------------- 1 | const PREFIX = 'Tasty'; 2 | 3 | export function warn(...args) { 4 | console.warn(`${PREFIX}:`, ...args); 5 | } 6 | 7 | export function deprecationWarning( 8 | condition: any, 9 | { 10 | property, 11 | name, 12 | betterAlternative, 13 | reason, 14 | }: { 15 | property: string; 16 | name: string; 17 | betterAlternative: (() => string) | string; 18 | reason?: (() => string) | string; 19 | }, 20 | ) { 21 | if (condition) return; 22 | 23 | if (process.env.NODE_ENV === 'production') { 24 | return warn( 25 | `DEPRECATION ${name} "${property}" -> ${ 26 | typeof betterAlternative === 'function' 27 | ? betterAlternative() 28 | : betterAlternative 29 | }`, 30 | ); 31 | } 32 | 33 | // we can make deprecations even better if we add the md syntax in the console. 34 | // anyway, everything down below will be stripped in the production build 35 | console.group(`⚠️ ${PREFIX}: Deprecation in ${name}`); 36 | warn( 37 | `"${property}" is deprecated, consider better alternative: ${ 38 | typeof betterAlternative === 'function' 39 | ? betterAlternative() 40 | : betterAlternative 41 | }`, 42 | ); 43 | reason && warn(`Reason: ${typeof reason === 'function' ? reason() : reason}`); 44 | console.groupEnd(); 45 | } 46 | -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './render'; 3 | 4 | export { renderHook } from '@testing-library/react-hooks'; 5 | export type { RenderHookOptions } from '@testing-library/react-hooks'; 6 | -------------------------------------------------------------------------------- /src/test/render.tsx: -------------------------------------------------------------------------------- 1 | import { render, RenderOptions } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import { CubeFormInstance, CubeFormProps, Form } from '../components/form'; 5 | import { Root } from '../components/Root'; 6 | 7 | export function renderWithRoot( 8 | ui: React.ReactElement, 9 | options?: Omit, 10 | ) { 11 | return render(ui, { ...options, wrapper: Root }); 12 | } 13 | 14 | export function renderWithForm( 15 | ui: React.ReactElement, 16 | options?: Omit & { 17 | formProps?: Partial>; 18 | }, 19 | ) { 20 | const { formProps, ...testingLibraryOptions } = options ?? {}; 21 | 22 | let formInstance: CubeFormInstance; 23 | 24 | return { 25 | ...render(ui, { 26 | ...testingLibraryOptions, 27 | wrapper: function Wrapper({ children }) { 28 | const [form] = Form.useForm(); 29 | formInstance = form; 30 | 31 | return ( 32 | 33 |
34 | {children} 35 |
36 |
37 | ); 38 | }, 39 | }), 40 | // @ts-expect-error TS2454 41 | formInstance, 42 | } as const; 43 | } 44 | 45 | export * from '@testing-library/react'; 46 | export { default as userEvent } from '@testing-library/user-event'; 47 | -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import 'jest-styled-components'; 3 | 4 | import { configure } from '@testing-library/react'; 5 | import { AbortController } from 'node-abort-controller'; 6 | import { config } from 'react-transition-group'; 7 | 8 | // @ts-expect-error Setup AbortController for test environment 9 | global.AbortController = AbortController; 10 | config.disabled = true; 11 | 12 | configure({ testIdAttribute: 'data-qa', asyncUtilTimeout: 10000 }); 13 | -------------------------------------------------------------------------------- /src/test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './wait'; 2 | -------------------------------------------------------------------------------- /src/test/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export function wait(timeout = 100) { 2 | return new Promise((res) => setTimeout(res, timeout)); 3 | } 4 | -------------------------------------------------------------------------------- /src/type-checks.tsx: -------------------------------------------------------------------------------- 1 | import { RadioButton } from './components/fields/RadioGroup/Radio'; 2 | import { tasty } from './tasty'; 3 | 4 | tasty(RadioButton, { 5 | // value is required 6 | inputStyles: { 7 | fill: '#clear', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/utils/modules.ts: -------------------------------------------------------------------------------- 1 | export function extractModule(promise) { 2 | return promise.then((module) => module.default || module); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/promise.ts: -------------------------------------------------------------------------------- 1 | export function timeout(ms = 30) { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, ms); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/random.ts: -------------------------------------------------------------------------------- 1 | export function random(min: number, max: number) { 2 | const realMin = min < max ? min : max; 3 | const realMax = min < max ? max : min; 4 | 5 | return realMin + Math.random() * (realMax - realMin); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/range.ts: -------------------------------------------------------------------------------- 1 | export function range(count: Length): Array { 2 | return Array.from({ length: count }, (_, i) => i); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/react/chain.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calls all functions in the order they were chained with the same arguments. 3 | */ 4 | export function chain(...callbacks) { 5 | return (...args) => { 6 | for (let callback of callbacks) { 7 | if (typeof callback === 'function') { 8 | callback(...args); 9 | } 10 | } 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/react/index.ts: -------------------------------------------------------------------------------- 1 | export { chain } from './chain'; 2 | export { isTextOnly } from './isTextOnly'; 3 | export { mergeProps } from './mergeProps'; 4 | export { modAttrs } from '../../tasty'; 5 | export { useSlotProps, SlotProvider, ClearSlots } from './Slots'; 6 | export { useLayoutEffect } from './useLayoutEffect'; 7 | export { useCombinedRefs } from './useCombinedRefs'; 8 | export { wrapNodeIfPlain } from './wrapNodeIfPlain'; 9 | export { useViewportSize } from './useViewportSize'; 10 | export { useQaProps } from './useQaProps'; 11 | -------------------------------------------------------------------------------- /src/utils/react/interactions.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useFocus as reactAriaUseFocus, useFocusVisible } from 'react-aria'; 3 | 4 | export function useFocus( 5 | { isDisabled }: { isDisabled?: boolean }, 6 | onlyVisible = false, 7 | ) { 8 | useEffect(() => { 9 | setIsFocused(false); 10 | }, [isDisabled]); 11 | 12 | let [isFocused, setIsFocused] = useState(false); 13 | let { isFocusVisible } = useFocusVisible({}); 14 | let { focusProps } = reactAriaUseFocus({ 15 | isDisabled, 16 | // @ts-ignore 17 | onFocusChange: setIsFocused, 18 | }); 19 | 20 | return { 21 | focusProps, 22 | isFocused: isFocused && (onlyVisible ? isFocusVisible : true), 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/react/isTextOnly.ts: -------------------------------------------------------------------------------- 1 | import { Children, isValidElement } from 'react'; 2 | 3 | export function isTextOnly(children) { 4 | return Children.toArray(children).every((c) => !isValidElement(c)); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/react/mapProps.ts: -------------------------------------------------------------------------------- 1 | import { AriaButtonProps } from 'react-aria'; 2 | 3 | import { CubeButtonProps } from '../../components/actions'; 4 | 5 | /** Converts AriaButtonProps to CubeButtonProps */ 6 | export function ariaToCubeButtonProps( 7 | props: AriaButtonProps<'button'>, 8 | ): CubeButtonProps { 9 | const { type, ...filteredProps } = props; 10 | 11 | return { 12 | ...filteredProps, 13 | htmlType: type, 14 | }; 15 | } 16 | 17 | /** Converts CubeButtonProps to AriaButtonProps */ 18 | export function cubeToAriaButtonProps( 19 | props: CubeButtonProps, 20 | ): AriaButtonProps<'button'> { 21 | const { htmlType, ...filteredProps } = props; 22 | 23 | return { 24 | ...filteredProps, 25 | type: htmlType, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/react/useCombinedRefs.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef } from 'react'; 2 | 3 | export function useCombinedRefs(...refs: any[]): RefObject { 4 | const targetRef = useRef(); 5 | 6 | useEffect(() => { 7 | refs.forEach((ref) => { 8 | if (!ref) return; 9 | 10 | if (typeof ref === 'function') { 11 | ref(targetRef.current); 12 | } else { 13 | ref.current = targetRef.current; 14 | } 15 | }); 16 | }, [refs]); 17 | 18 | return targetRef; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/react/useLayoutEffect.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // During SSR, React emits a warning when calling useLayoutEffect. 4 | // Since neither useLayoutEffect nor useEffect run on the server, 5 | // we can suppress this by replacing it with a noop on the server. 6 | export const useLayoutEffect = 7 | typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; 8 | -------------------------------------------------------------------------------- /src/utils/react/wrapNodeIfPlain.ts: -------------------------------------------------------------------------------- 1 | import { isValidElement, ReactNode } from 'react'; 2 | import { isFragment } from 'react-is'; 3 | 4 | export function wrapNodeIfPlain(children: ReactNode, render: () => ReactNode) { 5 | const childrenIsFragment = 6 | children && (Array.isArray(children) || isFragment(children)); 7 | 8 | if (!children || (!childrenIsFragment && isValidElement(children))) { 9 | return children; 10 | } 11 | 12 | return render(); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/tree.ts: -------------------------------------------------------------------------------- 1 | export function toFlatTree(tree, includingFolders) { 2 | return tree.reduce((result, node) => { 3 | if (!node.isLeaf) { 4 | result = [ 5 | ...result, 6 | ...(includingFolders ? [node] : []), 7 | ...toFlatTree(node.children || [], includingFolders), 8 | ]; 9 | } else { 10 | result.push(node); 11 | } 12 | return result; 13 | }, []); 14 | } 15 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | CubeUIKit: { 3 | version: string; 4 | }; 5 | } 6 | 7 | if (typeof window !== 'undefined') { 8 | const version = '__UIKIT_VERSION__'; 9 | 10 | // Ensure CubeUIKit is defined on window in a way bundlers recognize 11 | window.CubeUIKit = window.CubeUIKit || { version }; 12 | 13 | // Check for multiple versions 14 | if (window.CubeUIKit.version && window.CubeUIKit.version !== version) { 15 | console.error('More than one version of CubeUIKit is loaded', { 16 | loadedVersions: [window.CubeUIKit.version, version], 17 | }); 18 | } else { 19 | // Set version if not already set 20 | window.CubeUIKit.version = version; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declarationDir": "dist/types", 5 | "outDir": "./dist/es", 6 | "noEmit": false 7 | }, 8 | "exclude": [ 9 | "src/**/__mocks__/**/*", 10 | "src/**/*.stories.tsx", 11 | "src/**/*.stories.ts", 12 | "src/test/**/*", 13 | "src/**/*.test.tsx", 14 | "src/**/*.test.ts", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "es2022", 5 | "lib": ["dom", "es2023"], 6 | "declaration": true, 7 | "noEmitOnError": false, 8 | "allowJs": true, 9 | "checkJs": true, 10 | "jsx": "react-jsx", 11 | "allowSyntheticDefaultImports": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "preserveSymlinks": true, 22 | "noImplicitAny": false, 23 | "target": "es2022", 24 | "noEmit": true, 25 | "types": ["jest", "react", "react-dom"], 26 | }, 27 | "include": ["src/**/*"] 28 | } 29 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | react({ 9 | jsxRuntime: 'automatic', 10 | }), 11 | ], 12 | }); 13 | --------------------------------------------------------------------------------