├── .eslintrc.js
├── .gitignore
├── .pretterrc
├── .storybook
├── decorators
│ ├── appearance.tsx
│ ├── root.tsx
│ └── strict.tsx
├── main.ts
├── manager-head.html
├── manager.ts
├── media
│ └── logo.png
└── preview.tsx
├── LICENSE
├── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── package.swcrc
├── src
├── Getting Started.mdx
├── Platform And Palette.mdx
├── components
│ ├── Blocks
│ │ ├── Accordion
│ │ │ ├── According.stories.tsx
│ │ │ ├── Accordion.tsx
│ │ │ ├── AccordionContext.tsx
│ │ │ ├── components
│ │ │ │ ├── AccordionContent
│ │ │ │ │ ├── AccordionContent.module.css
│ │ │ │ │ ├── AccordionContent.stories.tsx
│ │ │ │ │ ├── AccordionContent.tsx
│ │ │ │ │ └── helpers
│ │ │ │ │ │ └── calcMaxHeight.ts
│ │ │ │ └── AccordionSummary
│ │ │ │ │ ├── AccordionSummary.module.css
│ │ │ │ │ ├── AccordionSummary.stories.tsx
│ │ │ │ │ └── AccordionSummary.tsx
│ │ │ └── hooks
│ │ │ │ └── useAccordionId.ts
│ │ ├── Avatar
│ │ │ ├── Avatar.module.css
│ │ │ ├── Avatar.stories.tsx
│ │ │ ├── Avatar.tsx
│ │ │ └── components
│ │ │ │ ├── AvatarAcronym
│ │ │ │ └── AvatarAcronym.tsx
│ │ │ │ └── AvatarBadge
│ │ │ │ ├── AvatarBadge.module.css
│ │ │ │ └── AvatarBadge.tsx
│ │ ├── AvatarStack
│ │ │ ├── AvatarStack.module.css
│ │ │ ├── AvatarStack.stories.tsx
│ │ │ └── AvatarStack.tsx
│ │ ├── Badge
│ │ │ ├── Badge.module.css
│ │ │ ├── Badge.stories.tsx
│ │ │ └── Badge.tsx
│ │ ├── Banner
│ │ │ ├── Banner.module.css
│ │ │ ├── Banner.stories.tsx
│ │ │ ├── Banner.tsx
│ │ │ └── components
│ │ │ │ └── BannerDescriptionTypography
│ │ │ │ └── BannerDescriptionTypography.tsx
│ │ ├── Blockquote
│ │ │ ├── Blockquote.module.css
│ │ │ ├── Blockquote.stories.tsx
│ │ │ └── Blockquote.tsx
│ │ ├── Button
│ │ │ ├── Button.module.css
│ │ │ ├── Button.stories.tsx
│ │ │ ├── Button.tsx
│ │ │ └── components
│ │ │ │ └── ButtonTypography
│ │ │ │ └── ButtonTypography.tsx
│ │ ├── Card
│ │ │ ├── Card.module.css
│ │ │ ├── Card.stories.tsx
│ │ │ ├── Card.tsx
│ │ │ ├── CardContext.ts
│ │ │ └── components
│ │ │ │ ├── CardCell
│ │ │ │ ├── CardCell.module.css
│ │ │ │ └── CardCell.tsx
│ │ │ │ └── CardChip
│ │ │ │ ├── CardChip.module.css
│ │ │ │ └── CardChip.tsx
│ │ ├── Cell
│ │ │ ├── Cell.module.css
│ │ │ ├── Cell.stories.tsx
│ │ │ ├── Cell.tsx
│ │ │ ├── components
│ │ │ │ ├── ButtonCell
│ │ │ │ │ ├── ButtonCell.module.css
│ │ │ │ │ ├── ButtonCell.stories.tsx
│ │ │ │ │ └── ButtonCell.tsx
│ │ │ │ ├── Info
│ │ │ │ │ ├── Info.module.css
│ │ │ │ │ ├── Info.stories.tsx
│ │ │ │ │ └── Info.tsx
│ │ │ │ └── Navigation
│ │ │ │ │ ├── Navigation.module.css
│ │ │ │ │ ├── Navigation.stories.tsx
│ │ │ │ │ └── Navigation.tsx
│ │ │ └── hooks
│ │ │ │ └── useTypographyCellComponents.tsx
│ │ ├── IconButton
│ │ │ ├── IconButton.module.css
│ │ │ ├── IconButton.stories.tsx
│ │ │ └── IconButton.tsx
│ │ ├── IconContainer
│ │ │ ├── IconContainer.module.css
│ │ │ └── IconContainer.tsx
│ │ ├── Image
│ │ │ ├── Image.module.css
│ │ │ ├── Image.stories.tsx
│ │ │ ├── Image.tsx
│ │ │ ├── components
│ │ │ │ └── ImageBadge
│ │ │ │ │ ├── ImageBadge.module.css
│ │ │ │ │ └── ImageBadge.tsx
│ │ │ └── helpers
│ │ │ │ └── getBorderRadius.ts
│ │ ├── InlineButtons
│ │ │ ├── InlineButtons.module.css
│ │ │ ├── InlineButtons.stories.tsx
│ │ │ ├── InlineButtons.tsx
│ │ │ ├── InlineButtonsContext.ts
│ │ │ └── components
│ │ │ │ └── InlineButtonsItem
│ │ │ │ ├── InlineButtonsItem.module.css
│ │ │ │ ├── InlineButtonsItem.stories.tsx
│ │ │ │ └── InlineButtonsItem.tsx
│ │ ├── List
│ │ │ ├── List.module.css
│ │ │ ├── List.stories.tsx
│ │ │ └── List.tsx
│ │ ├── Placeholder
│ │ │ ├── Placeholder.module.css
│ │ │ ├── Placeholder.stories.module.css
│ │ │ ├── Placeholder.stories.tsx
│ │ │ └── Placeholder.tsx
│ │ ├── Section
│ │ │ ├── Section.module.css
│ │ │ ├── Section.stories.tsx
│ │ │ ├── Section.tsx
│ │ │ └── components
│ │ │ │ ├── SectionFooter
│ │ │ │ ├── SectionFooter.module.css
│ │ │ │ ├── SectionFooter.stories.tsx
│ │ │ │ └── SectionFooter.tsx
│ │ │ │ └── SectionHeader
│ │ │ │ ├── SectionHeader.module.css
│ │ │ │ ├── SectionHeader.stories.tsx
│ │ │ │ ├── SectionHeader.tsx
│ │ │ │ └── hooks
│ │ │ │ └── useHeaderComponents.tsx
│ │ ├── Steps
│ │ │ ├── Steps.module.css
│ │ │ ├── Steps.stories.tsx
│ │ │ └── Steps.tsx
│ │ ├── Timeline
│ │ │ ├── Timeline.module.css
│ │ │ ├── Timeline.stories.tsx
│ │ │ ├── Timeline.tsx
│ │ │ └── components
│ │ │ │ └── TimelineItem
│ │ │ │ ├── TimelineItem.module.css
│ │ │ │ ├── TimelineItem.stories.tsx
│ │ │ │ └── TimelineItem.tsx
│ │ └── index.ts
│ ├── Feedback
│ │ ├── CircularProgress
│ │ │ ├── CircularProgress.module.css
│ │ │ ├── CircularProgress.stories.tsx
│ │ │ ├── CircularProgress.tsx
│ │ │ └── helpers
│ │ │ │ └── getCircleAttributes.ts
│ │ ├── Progress
│ │ │ ├── Progress.module.css
│ │ │ ├── Progress.stories.tsx
│ │ │ └── Progress.tsx
│ │ ├── Skeleton
│ │ │ ├── Skeleton.module.css
│ │ │ ├── Skeleton.stories.tsx
│ │ │ └── Skeleton.tsx
│ │ ├── Snackbar
│ │ │ ├── Snackbar.module.css
│ │ │ ├── Snackbar.stories.tsx
│ │ │ ├── Snackbar.tsx
│ │ │ └── components
│ │ │ │ └── SnackbarButton
│ │ │ │ ├── SnackbarButton.module.css
│ │ │ │ └── SnackbarButton.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.module.css
│ │ │ ├── Spinner.stories.tsx
│ │ │ ├── Spinner.tsx
│ │ │ └── components
│ │ │ │ ├── BaseSpinner
│ │ │ │ ├── BaseSpinner.tsx
│ │ │ │ └── icons
│ │ │ │ │ ├── large.tsx
│ │ │ │ │ ├── medium.tsx
│ │ │ │ │ └── small.tsx
│ │ │ │ └── IOSSpinner
│ │ │ │ ├── IOSSpinner.tsx
│ │ │ │ └── icons
│ │ │ │ ├── large.tsx
│ │ │ │ ├── medium.tsx
│ │ │ │ └── small.tsx
│ │ ├── Spoiler
│ │ │ ├── Spoiler.module.css
│ │ │ ├── Spoiler.stories.tsx
│ │ │ ├── Spoiler.tsx
│ │ │ └── icons
│ │ │ │ └── spoiler.svg
│ │ └── index.tsx
│ ├── Form
│ │ ├── Checkbox
│ │ │ ├── Checkbox.module.css
│ │ │ ├── Checkbox.stories.tsx
│ │ │ ├── Checkbox.tsx
│ │ │ └── icons
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── checkbox_checked.tsx
│ │ │ │ └── checkbox_indeterminate.tsx
│ │ ├── Chip
│ │ │ ├── Chip.module.css
│ │ │ ├── Chip.stories.tsx
│ │ │ └── Chip.tsx
│ │ ├── ColorInput
│ │ │ ├── ColorInput.module.css
│ │ │ ├── ColorInput.stories.tsx
│ │ │ └── ColorInput.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.stories.tsx
│ │ │ └── FileInput.tsx
│ │ ├── FormInput
│ │ │ ├── FormInput.module.css
│ │ │ ├── FormInput.tsx
│ │ │ └── components
│ │ │ │ └── FormInputTitle.tsx
│ │ ├── Input
│ │ │ ├── Input.module.css
│ │ │ ├── Input.stories.tsx
│ │ │ └── Input.tsx
│ │ ├── Multiselect
│ │ │ ├── Multiselect.module.css
│ │ │ ├── Multiselect.stories.tsx
│ │ │ ├── Multiselect.tsx
│ │ │ ├── components
│ │ │ │ ├── MultiselectBase
│ │ │ │ │ ├── MultiselectBase.module.css
│ │ │ │ │ ├── MultiselectBase.tsx
│ │ │ │ │ ├── constants.tsx
│ │ │ │ │ └── helpers
│ │ │ │ │ │ └── getValueOptionByHTMLElement.ts
│ │ │ │ └── MultiselectDropdown
│ │ │ │ │ ├── MultiselectDropdown.module.css
│ │ │ │ │ ├── MultiselectDropdown.tsx
│ │ │ │ │ └── constants.tsx
│ │ │ ├── hooks
│ │ │ │ ├── constants.tsx
│ │ │ │ ├── helpers
│ │ │ │ │ ├── filter
│ │ │ │ │ │ ├── filter.test.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── getNewOptionData.ts
│ │ │ │ │ ├── isValueLikeOption.ts
│ │ │ │ │ ├── simulateReactInput.ts
│ │ │ │ │ └── transformOptions.ts
│ │ │ │ ├── useMultiselect.ts
│ │ │ │ └── useMultiselectInput.ts
│ │ │ └── types.ts
│ │ ├── Multiselectable
│ │ │ ├── Multiselectable.module.css
│ │ │ ├── Multiselectable.stories.tsx
│ │ │ ├── Multiselectable.tsx
│ │ │ └── icons
│ │ │ │ ├── multiselectable.tsx
│ │ │ │ ├── multiselectable_checked.tsx
│ │ │ │ ├── multiselectable_ios.tsx
│ │ │ │ └── multiselectable_ios_checked.tsx
│ │ ├── PinInput
│ │ │ ├── PinInput.module.css
│ │ │ ├── PinInput.stories.tsx
│ │ │ ├── PinInput.tsx
│ │ │ ├── components
│ │ │ │ ├── PinInputButton
│ │ │ │ │ ├── PinInputButton.module.css
│ │ │ │ │ └── PinInputButton.tsx
│ │ │ │ └── PinInputCell
│ │ │ │ │ ├── PinInputCell.module.css
│ │ │ │ │ └── PinInputCell.tsx
│ │ │ └── hooks
│ │ │ │ └── usePinInput.ts
│ │ ├── Radio
│ │ │ ├── Radio.module.css
│ │ │ ├── Radio.stories.tsx
│ │ │ ├── Radio.tsx
│ │ │ └── icons
│ │ │ │ ├── radio.tsx
│ │ │ │ └── radio_checked.tsx
│ │ ├── Rating
│ │ │ ├── Rating.module.css
│ │ │ ├── Rating.stories.tsx
│ │ │ ├── Rating.tsx
│ │ │ └── icons
│ │ │ │ └── star.tsx
│ │ ├── Select
│ │ │ ├── Select.module.css
│ │ │ ├── Select.stories.tsx
│ │ │ └── Select.tsx
│ │ ├── Selectable
│ │ │ ├── Selectable.module.css
│ │ │ ├── Selectable.stories.tsx
│ │ │ ├── Selectable.tsx
│ │ │ └── icons
│ │ │ │ ├── selectable_base.tsx
│ │ │ │ └── selectable_ios.tsx
│ │ ├── Slider
│ │ │ ├── Slider.module.css
│ │ │ ├── Slider.stories.tsx
│ │ │ ├── Slider.tsx
│ │ │ ├── components
│ │ │ │ ├── SliderSteps
│ │ │ │ │ ├── SliderSteps.module.css
│ │ │ │ │ └── SliderSteps.tsx
│ │ │ │ └── SliderThumb
│ │ │ │ │ ├── SliderThumb.module.css
│ │ │ │ │ └── SliderThumb.tsx
│ │ │ └── hooks
│ │ │ │ ├── helpers
│ │ │ │ ├── html.ts
│ │ │ │ ├── math
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── math.test.ts
│ │ │ │ └── state.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── useSlider.ts
│ │ ├── Switch
│ │ │ ├── Switch.module.css
│ │ │ ├── Switch.stories.tsx
│ │ │ └── Switch.tsx
│ │ ├── Textarea
│ │ │ ├── Textarea.module.css
│ │ │ ├── Textarea.stories.tsx
│ │ │ └── Textarea.tsx
│ │ └── index.tsx
│ ├── Layout
│ │ ├── FixedLayout
│ │ │ ├── FixedLayout.module.css
│ │ │ ├── FixedLayout.stories.tsx
│ │ │ └── FixedLayout.tsx
│ │ ├── Tabbar
│ │ │ ├── Tabbar.module.css
│ │ │ ├── Tabbar.stories.tsx
│ │ │ ├── Tabbar.tsx
│ │ │ └── components
│ │ │ │ └── TabbarItem
│ │ │ │ ├── TabbarItem.module.css
│ │ │ │ ├── TabbarItem.stories.tsx
│ │ │ │ └── TabbarItem.tsx
│ │ └── index.ts
│ ├── Misc
│ │ ├── Divider
│ │ │ ├── Divider.module.css
│ │ │ ├── Divider.stories.tsx
│ │ │ └── Divider.tsx
│ │ └── index.tsx
│ ├── Navigation
│ │ ├── Breadcrumbs
│ │ │ ├── Breadcrumbs.module.css
│ │ │ ├── Breadcrumbs.stories.tsx
│ │ │ ├── Breadcrumbs.tsx
│ │ │ ├── components
│ │ │ │ └── BreadCrumbsItem
│ │ │ │ │ ├── BreadCrumbsItem.module.css
│ │ │ │ │ └── BreadCrumbsItem.tsx
│ │ │ └── icons
│ │ │ │ ├── dot.tsx
│ │ │ │ └── slash.tsx
│ │ ├── CompactPagination
│ │ │ ├── CompactPagination.module.css
│ │ │ ├── CompactPagination.stories.tsx
│ │ │ ├── CompactPagination.tsx
│ │ │ └── components
│ │ │ │ └── CompactPaginationItem
│ │ │ │ ├── CompactPaginationItem.module.css
│ │ │ │ ├── CompactPaginationItem.stories.tsx
│ │ │ │ └── CompactPaginationItem.tsx
│ │ ├── Link
│ │ │ ├── Link.module.css
│ │ │ └── Link.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.module.css
│ │ │ ├── Pagination.stories.tsx
│ │ │ ├── Pagination.tsx
│ │ │ └── hooks
│ │ │ │ ├── enum.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── usePagination.ts
│ │ ├── SegmentedControl
│ │ │ ├── SegmentedControl.module.css
│ │ │ ├── SegmentedControl.stories.tsx
│ │ │ ├── SegmentedControl.tsx
│ │ │ └── components
│ │ │ │ └── SegmentedControlItem
│ │ │ │ ├── SegmentedControlItem.module.css
│ │ │ │ ├── SegmentedControlItem.stories.tsx
│ │ │ │ └── SegmentedControlItem.tsx
│ │ ├── TabsList
│ │ │ ├── TabsList.module.css
│ │ │ ├── TabsList.stories.tsx
│ │ │ ├── TabsList.tsx
│ │ │ └── components
│ │ │ │ └── TabsItem
│ │ │ │ ├── TabsItem.module.css
│ │ │ │ ├── TabsItem.stories.tsx
│ │ │ │ └── TabsItem.tsx
│ │ └── index.tsx
│ ├── Overlays
│ │ ├── Modal
│ │ │ ├── Modal.module.css
│ │ │ ├── Modal.stories.tsx
│ │ │ ├── Modal.tsx
│ │ │ └── components
│ │ │ │ ├── ModalClose
│ │ │ │ ├── ModalClose.stories.tsx
│ │ │ │ └── ModalClose.tsx
│ │ │ │ ├── ModalHeader
│ │ │ │ ├── ModalHeader.module.css
│ │ │ │ ├── ModalHeader.stories.tsx
│ │ │ │ └── ModalHeader.tsx
│ │ │ │ └── ModalOverlay
│ │ │ │ ├── ModalOverlay.module.css
│ │ │ │ └── ModalOverlay.tsx
│ │ ├── Popper
│ │ │ ├── Popper.module.css
│ │ │ ├── Popper.stories.tsx
│ │ │ ├── Popper.tsx
│ │ │ ├── components
│ │ │ │ └── FloatingArrow
│ │ │ │ │ ├── FloatingArrow.module.css
│ │ │ │ │ ├── FloatingArrow.tsx
│ │ │ │ │ ├── helpers
│ │ │ │ │ └── getArrowPositionData.ts
│ │ │ │ │ └── icons
│ │ │ │ │ └── arrow.tsx
│ │ │ ├── helpers
│ │ │ │ └── autoUpdateFloatingElement.ts
│ │ │ └── hooks
│ │ │ │ ├── helpers
│ │ │ │ └── alignment.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── useFloatingMiddlewares.ts
│ │ ├── Tooltip
│ │ │ ├── Tooltip.module.css
│ │ │ ├── Tooltip.stories.tsx
│ │ │ └── Tooltip.tsx
│ │ └── index.ts
│ ├── Service
│ │ ├── AppRoot
│ │ │ ├── AppRoot.module.css
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppRootContext.ts
│ │ │ └── hooks
│ │ │ │ ├── helpers
│ │ │ │ ├── getBrowserAppearanceSubscriber.ts
│ │ │ │ ├── getInitialAppearance.ts
│ │ │ │ └── getInitialPlatform.ts
│ │ │ │ ├── useAppearance.ts
│ │ │ │ ├── usePlatform.ts
│ │ │ │ └── usePortalContainer.ts
│ │ ├── HorizontalScroll
│ │ │ ├── HorizontalScroll.module.css
│ │ │ └── HorizontalScroll.tsx
│ │ ├── RootRenderer
│ │ │ └── RootRenderer.tsx
│ │ ├── Tappable
│ │ │ ├── Tappable.module.css
│ │ │ ├── Tappable.tsx
│ │ │ └── components
│ │ │ │ └── Ripple
│ │ │ │ ├── Ripple.module.css
│ │ │ │ ├── Ripple.tsx
│ │ │ │ ├── hooks
│ │ │ │ └── useRipple.ts
│ │ │ │ └── types
│ │ │ │ └── Wave.ts
│ │ ├── Touch
│ │ │ ├── Touch.tsx
│ │ │ └── helpers
│ │ │ │ ├── touch.ts
│ │ │ │ └── types.ts
│ │ ├── VisuallyHidden
│ │ │ ├── VisuallyHidden.module.css
│ │ │ └── VisuallyHidden.tsx
│ │ └── index.tsx
│ ├── Typography
│ │ ├── Caption
│ │ │ ├── Caption.module.css
│ │ │ ├── Caption.stories.tsx
│ │ │ └── Caption.tsx
│ │ ├── Headline
│ │ │ ├── Headline.module.css
│ │ │ ├── Headline.stories.tsx
│ │ │ └── Headline.tsx
│ │ ├── LargeTitle
│ │ │ ├── LargeTitle.module.css
│ │ │ ├── LargeTitle.stories.tsx
│ │ │ └── LargeTitle.tsx
│ │ ├── Subheadline
│ │ │ ├── Subheadline.module.css
│ │ │ ├── Subheadline.stories.tsx
│ │ │ └── Subheadline.tsx
│ │ ├── Text
│ │ │ ├── Text.module.css
│ │ │ ├── Text.stories.tsx
│ │ │ └── Text.tsx
│ │ ├── Title
│ │ │ ├── Title.module.css
│ │ │ ├── Title.stories.tsx
│ │ │ └── Title.tsx
│ │ ├── Typography.module.css
│ │ ├── Typography.tsx
│ │ └── index.ts
│ └── index.ts
├── helpers
│ ├── accessibility.ts
│ ├── array.ts
│ ├── chunk.ts
│ ├── classNames.ts
│ ├── color.ts
│ ├── dom.ts
│ ├── equal.ts
│ ├── fuctions.ts
│ ├── function.ts
│ ├── math.ts
│ ├── object.ts
│ ├── react
│ │ ├── children.ts
│ │ ├── node.ts
│ │ └── refs.ts
│ └── telegram.ts
├── hooks
│ ├── useAppRootContext.ts
│ ├── useEnhancedEffect.ts
│ ├── useEnsureControl.ts
│ ├── useEventListener.ts
│ ├── useExternalRefs.ts
│ ├── useGlobalClicks.ts
│ ├── useObjectMemo.ts
│ ├── usePlatform.ts
│ └── useTimeout.ts
├── icons
│ ├── 12
│ │ └── quote.tsx
│ ├── 16
│ │ ├── cancel.tsx
│ │ └── chevron.tsx
│ ├── 20
│ │ ├── chevron_down.tsx
│ │ ├── copy.tsx
│ │ ├── question_mark.tsx
│ │ ├── select.tsx
│ │ └── select_ios.tsx
│ ├── 24
│ │ ├── cancel.tsx
│ │ ├── channel.tsx
│ │ ├── chat.tsx
│ │ ├── chevron_down.tsx
│ │ ├── chevron_left.tsx
│ │ ├── chevron_right.tsx
│ │ ├── close.tsx
│ │ ├── notifications.tsx
│ │ ├── person_remove.tsx
│ │ ├── qr.tsx
│ │ └── sun_low.tsx
│ ├── 28
│ │ ├── add_circle.tsx
│ │ ├── archive.tsx
│ │ ├── attach.tsx
│ │ ├── chat.tsx
│ │ ├── close.tsx
│ │ ├── close_ambient.tsx
│ │ ├── devices.tsx
│ │ ├── edit.tsx
│ │ ├── heart.tsx
│ │ └── stats.tsx
│ ├── 32
│ │ └── profile_colored_square.tsx
│ └── 36
│ │ └── backspace.tsx
├── index.ts
├── storybook
│ └── controls.ts
└── types
│ └── Icon.ts
├── tsconfig.dist.json
├── tsconfig.json
├── types
└── declarations.d.ts
└── webpack.styles.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Builds
2 | dist/
3 | storybook-static/
4 |
5 | # Dependency directory
6 | node_modules/
7 |
8 | # Logs
9 | npm-debug.log
10 | yarn-error.log
11 |
12 | # Caches
13 | .cache
14 | tmp/
15 |
16 | # CI
17 | .env
18 | .lfs-assets-id
19 |
20 | # IDE
21 | .idea/
22 | .vscode/
23 |
24 | # OS
25 | .DS_Store
26 |
27 | # SWC cache
28 | .swc
29 |
--------------------------------------------------------------------------------
/.pretterrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "es5",
8 | "bracketSpacing": true,
9 | "jsxSingleQuote": false,
10 | "arrowParens": "always",
11 | "proseWrap": "never",
12 | "htmlWhitespaceSensitivity": "strict",
13 | "endOfLine": "lf"
14 | }
15 |
--------------------------------------------------------------------------------
/.storybook/decorators/appearance.tsx:
--------------------------------------------------------------------------------
1 | import { Decorator } from '@storybook/react';
2 |
3 | export const AppearanceDecorator: Decorator = (Story, context) => {
4 | const styles = `
5 | .AppearanceDecorator::before {
6 | content: '';
7 | position: absolute;
8 | inset: 0;
9 | z-index: -1;
10 | background-color: ${context.globals.theme === 'dark' ? '#212121' : '#FFF'}
11 | }
12 | `;
13 |
14 | return (
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/.storybook/decorators/root.tsx:
--------------------------------------------------------------------------------
1 | import { AppRoot } from 'components';
2 | import { Decorator } from '@storybook/react';
3 |
4 | export const AppRootDecorator: Decorator = (Story, context) => (
5 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/.storybook/decorators/strict.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react';
2 | import { Decorator } from '@storybook/react';
3 |
4 | export const StrictDecorator: Decorator = (Story) => (
5 |
6 |
7 |
8 | );
9 |
10 |
--------------------------------------------------------------------------------
/.storybook/manager-head.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.storybook/manager.ts:
--------------------------------------------------------------------------------
1 | import { create } from '@storybook/theming';
2 | import logo from './media/logo.png';
3 |
4 | import { addons } from '@storybook/manager-api';
5 |
6 | const favicon = document.querySelector('link[rel="icon"]');
7 | if (favicon) {
8 | favicon.type = 'image/png';
9 | favicon.href = logo;
10 | }
11 |
12 | addons.setConfig({
13 | theme: create({
14 | base: 'light',
15 | brandTitle: ` `
16 | }),
17 | toolbar: {
18 | zoom: { hidden: true },
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/.storybook/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Telegram-Mini-Apps/TelegramUI/78231341ea121f574123abbceceba0f9679981f0/.storybook/media/logo.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 XeleneStudio
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 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | moduleDirectories: ['node_modules', 'src'],
5 | };
6 |
--------------------------------------------------------------------------------
/package.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/swcrc",
3 | "exclude": ["\\.(stories|test)\\.[jt]sx?$"],
4 | "module": {
5 | "type": "es6"
6 | },
7 | "jsc": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "components/*": ["./src/components/*"],
11 | "helpers/*": ["./src/helpers/*"],
12 | "hooks/*": ["./src/hooks/*"],
13 | "icons/*": ["./src/icons/*"],
14 | },
15 | "externalHelpers": true,
16 | "parser": {
17 | "syntax": "typescript",
18 | "tsx": true
19 | },
20 | "transform": {
21 | "react": {
22 | "runtime": "automatic"
23 | }
24 | },
25 | "target": "es2015",
26 | "experimental": {
27 | "cacheRoot": "node_modules/.cache/swc",
28 | "plugins": [
29 | [
30 | "swc-plugin-css-modules",
31 | {
32 | "generate_scoped_name": "tgui.[hash]",
33 | }
34 | ],
35 | [
36 | "swc-plugin-transform-remove-imports",
37 | {
38 | "test": "\\.css$"
39 | }
40 | ],
41 | ]
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/Blocks/Accordion/AccordionContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export interface AccordionContextProps {
4 | labelId: string;
5 | contentId: string;
6 | expanded: boolean;
7 | onChange: (expanded: boolean) => void;
8 | }
9 |
10 | export const AccordionContext = createContext({
11 | labelId: '',
12 | contentId: '',
13 | expanded: false,
14 | onChange: () => {},
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Blocks/Accordion/components/AccordionContent/AccordionContent.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow: hidden;
3 | background: var(--tgui--bg_color);
4 | }
5 |
6 | .body {
7 | max-block-size: 0;
8 | transition: max-height 100ms ease-in-out;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Blocks/Accordion/components/AccordionContent/AccordionContent.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Section } from 'components/Blocks/Section/Section';
4 | import { Subheadline } from 'components/Typography/Subheadline/Subheadline';
5 | import { Accordion } from '../../Accordion';
6 | import { AccordionContent, AccordionContentProps } from './AccordionContent';
7 |
8 | const meta = {
9 | title: 'Blocks/Accordion/Accordion.Content',
10 | component: AccordionContent,
11 | } satisfies Meta;
12 |
13 | export default meta;
14 |
15 | export const Playground: StoryObj = {
16 | args: {
17 | children: 'Accordion content',
18 | },
19 | render: (args) => (
20 |
21 |
22 | This is Accordion.Content component, it is just body of Accordion.
23 |
24 |
25 | ),
26 | decorators: [
27 | (Story) => (
28 |
29 |
30 | Accordion summary
31 |
32 |
33 |
34 | ),
35 | ],
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/Blocks/Accordion/components/AccordionContent/helpers/calcMaxHeight.ts:
--------------------------------------------------------------------------------
1 | export const calcMaxHeight = (expanded: boolean, bodyElement: HTMLElement | null): string => {
2 | if (!expanded) {
3 | return '0px';
4 | }
5 |
6 | // We don't know the height of the element in the first render
7 | if (bodyElement === null) {
8 | return 'inherit';
9 | }
10 |
11 | return `${bodyElement.scrollHeight}px`;
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Blocks/Accordion/components/AccordionSummary/AccordionSummary.module.css:
--------------------------------------------------------------------------------
1 | .chevron {
2 | transition: transform .15s ease-out;
3 | color: var(--tgui--link_color);
4 | }
5 |
6 | .chevron--expanded {
7 | transform: rotate(180deg);
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Blocks/Accordion/hooks/useAccordionId.ts:
--------------------------------------------------------------------------------
1 | import { useId } from 'react';
2 |
3 | export const useAccordionId = (id?: string) => {
4 | const randomId = useId();
5 |
6 | const labelId = id ?? `Accordion${randomId}`;
7 | const contentId = `AccordionContent${id ?? randomId}`;
8 |
9 | return { labelId, contentId };
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/Blocks/Avatar/Avatar.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | border-radius: 50%;
3 | }
4 |
5 | .wrapper--withAcronym.wrapper--withAcronym {
6 | background-color: var(--tgui--secondary_fill);
7 | color: var(--tgui--link_color);
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Blocks/Avatar/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import styles from './Avatar.module.css';
2 |
3 | import { classNames } from 'helpers/classNames';
4 |
5 | import { Image, ImageProps } from 'components/Blocks/Image/Image';
6 | import { AvatarAcronym } from './components/AvatarAcronym/AvatarAcronym';
7 | import { AvatarBadge } from './components/AvatarBadge/AvatarBadge';
8 |
9 | export interface AvatarProps extends ImageProps {
10 | /** One or two letters to be shown as a placeholder. `fallbackIcon` will not be used if `acronym` is provided. */
11 | acronym?: string;
12 | }
13 |
14 | /**
15 | * Renders an image with specific styles for an avatar presentation, including optional acronym display and badge support.
16 | * Utilizes the `Image` component for core functionality, enhancing it with avatar-specific features like acronyms and badges.
17 | */
18 | export const Avatar = ({
19 | className,
20 | style,
21 | acronym,
22 | fallbackIcon,
23 | size,
24 | ...restProps
25 | }: AvatarProps) => (
26 | {acronym} : fallbackIcon}
34 | size={size}
35 | {...restProps}
36 | />
37 | );
38 |
39 | Avatar.Badge = AvatarBadge;
40 |
--------------------------------------------------------------------------------
/src/components/Blocks/Avatar/components/AvatarAcronym/AvatarAcronym.tsx:
--------------------------------------------------------------------------------
1 | import { ImageProps } from 'components/Blocks/Image/Image';
2 | import { Caption } from 'components/Typography/Caption/Caption';
3 | import { Headline } from 'components/Typography/Headline/Headline';
4 | import { LargeTitle } from 'components/Typography/LargeTitle/LargeTitle';
5 | import { Title } from 'components/Typography/Title/Title';
6 | import { TypographyProps } from 'components/Typography/Typography';
7 |
8 | export interface AvatarAcronymProps extends TypographyProps {
9 | size: ImageProps['size'];
10 | }
11 |
12 | export const AvatarAcronym = ({ size, ...restProps }: AvatarAcronymProps) => {
13 | if (!size) {
14 | return null;
15 | }
16 |
17 | if (size <= 28) {
18 | return ;
19 | }
20 |
21 | if (size === 40) {
22 | return ;
23 | }
24 |
25 | if (size === 48) {
26 | return ;
27 | }
28 |
29 | return ;
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/Blocks/Avatar/components/AvatarBadge/AvatarBadge.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: absolute;
3 | right: -12px;
4 | top: 0;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Blocks/Avatar/components/AvatarBadge/AvatarBadge.tsx:
--------------------------------------------------------------------------------
1 | import styles from './AvatarBadge.module.css';
2 |
3 | import { classNames } from 'helpers/classNames';
4 |
5 | import { Badge, BadgeProps } from 'components/Blocks/Badge/Badge';
6 |
7 | export interface AvatarBadgeProps extends BadgeProps {}
8 |
9 | export const AvatarBadge = ({ type, className, ...restProps }: AvatarBadgeProps) => {
10 | if (type !== 'number') {
11 | throw new Error('[ImageBadge]: Component supports only type="number"');
12 | }
13 |
14 | return (
15 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/Blocks/AvatarStack/AvatarStack.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | }
4 |
5 | .wrapper > :not(:first-child) {
6 | margin-left: -12px;
7 | }
8 |
9 | .wrapper > * {
10 | box-shadow: 0 0 0 3px var(--tgui--bg_color);
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Blocks/AvatarStack/AvatarStack.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Avatar, AvatarProps } from 'components/Blocks/Avatar/Avatar';
5 | import { AvatarStack } from './AvatarStack';
6 |
7 | const AVATAR_URL = 'https://avatars.githubusercontent.com/u/84640980?v=4';
8 |
9 | const meta = {
10 | title: 'Blocks/AvatarStack',
11 | component: AvatarStack,
12 | argTypes: hideControls('children'),
13 | } satisfies Meta;
14 |
15 | export default meta;
16 |
17 | export const Playground: StoryObj = {
18 | args: {
19 | children: (
20 | <>
21 |
22 |
23 |
24 |
25 | >
26 | ),
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/Blocks/AvatarStack/AvatarStack.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes, ReactElement } from 'react';
2 | import styles from './AvatarStack.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | import { AvatarProps } from 'components/Blocks/Avatar/Avatar';
7 |
8 | export interface AvatarStackProps extends HTMLAttributes {
9 | /** An array of `Avatar` components to be rendered within the stack. */
10 | children: ReactElement[];
11 | }
12 |
13 | /**
14 | * Renders a container for displaying avatars in a stacked layout. This component
15 | * allows for the creation of visually grouped avatar representations, often used
16 | * to indicate multiple users or participants.
17 | */
18 | export const AvatarStack = ({
19 | children,
20 | ...restProps
21 | }: AvatarStackProps) => {
22 | return (
23 |
27 | {children}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/Blocks/Badge/Badge.module.css:
--------------------------------------------------------------------------------
1 | .wrapper--number {
2 | display: inline-flex;
3 | align-items: center;
4 | justify-content: center;
5 |
6 | height: 20px;
7 | min-width: 20px;
8 |
9 | margin: 0 6px;
10 | padding: 0 5px;
11 |
12 | box-sizing: border-box;
13 | border-radius: 20px;
14 | }
15 |
16 | .wrapper--large {
17 | height: 24px;
18 | padding: 0 6px;
19 | }
20 |
21 | .wrapper--dot {
22 | display: inline-block;
23 |
24 | width: 6px;
25 | height: 6px;
26 |
27 | margin: 7px;
28 | border-radius: 50%;
29 | }
30 |
31 | .wrapper--primary {
32 | color: var(--tgui--button_text_color);
33 | background: var(--tgui--button_color);
34 | }
35 |
36 | .wrapper--critical {
37 | color: var(--tgui--button_text_color);
38 | background: var(--tgui--destructive_text_color);
39 | }
40 |
41 | .wrapper--secondary {
42 | color: var(--tgui--link_color);
43 | background: var(--tgui--secondary_fill);
44 | }
45 |
46 | .wrapper--gray {
47 | color: var(--tgui--plain_foreground);
48 | background: var(--tgui--plain_background);
49 | }
50 |
51 | .wrapper--white {
52 | color: var(--tgui--link_color);
53 | background: var(--tgui--white);
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/src/components/Blocks/Badge/Badge.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Badge } from './Badge';
4 |
5 | const meta = {
6 | title: 'Blocks/Badge',
7 | component: Badge,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Dot: Story = {
14 | args: {
15 | type: 'dot',
16 | mode: 'primary',
17 | },
18 | } satisfies Story;
19 |
20 | export const Number: Story = {
21 | args: {
22 | mode: 'primary',
23 | type: 'number',
24 | children: 50,
25 | },
26 | } satisfies Story;
27 |
--------------------------------------------------------------------------------
/src/components/Blocks/Banner/components/BannerDescriptionTypography/BannerDescriptionTypography.tsx:
--------------------------------------------------------------------------------
1 | import { usePlatform } from 'hooks/usePlatform';
2 |
3 | import { Caption } from 'components/Typography/Caption/Caption';
4 | import { Subheadline } from 'components/Typography/Subheadline/Subheadline';
5 | import { TypographyProps } from 'components/Typography/Typography';
6 |
7 | export interface BannerDescriptionTypographyProps extends Omit {
8 | }
9 |
10 | export const BannerDescriptionTypography = (props: BannerDescriptionTypographyProps) => {
11 | const platform = usePlatform();
12 |
13 | if (platform === 'ios') {
14 | return ;
15 | }
16 |
17 | return ;
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/Blocks/Blockquote/Blockquote.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | padding: 6px 28px 8px 12px;
4 |
5 | border-left: 3px solid var(--tgui--link_color);
6 | border-radius: 4px;
7 | background: var(--tgui--secondary_fill);
8 | }
9 |
10 | .text {
11 | color: var(--tgui--text_color)
12 | }
13 |
14 | .topRightIcon {
15 | position: absolute;
16 | top: 4px;
17 | right: 6px;
18 |
19 | display: block;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Blocks/Blockquote/Blockquote.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls, setControlsTypes } from 'storybook/controls';
3 |
4 | import { Blockquote } from './Blockquote';
5 |
6 | const meta = {
7 | title: 'Blocks/Blockquote',
8 | component: Blockquote,
9 | parameters: {
10 | layout: 'centered',
11 | },
12 | argTypes: {
13 | ...hideControls('topRightIcon'),
14 | ...setControlsTypes(['children'], 'text'),
15 | },
16 | } satisfies Meta;
17 |
18 | export default meta;
19 | type Story = StoryObj;
20 |
21 | export const Playground: Story = {
22 | args: {
23 | type: 'text',
24 | // eslint-disable-next-line max-len
25 | children: 'There is grandeur in this view of life, with its several powers, having been originally breathed by the Creator into a few forms or into one; and that, whilst this planet has gone circling on according to the fixed law of gravity, from so simple a beginning endless forms most beautiful and most wonderful have been, and are being evolved.',
26 | },
27 | } satisfies Story;
28 |
--------------------------------------------------------------------------------
/src/components/Blocks/Button/components/ButtonTypography/ButtonTypography.tsx:
--------------------------------------------------------------------------------
1 | import { Subheadline } from 'components/Typography/Subheadline/Subheadline';
2 | import { Text } from 'components/Typography/Text/Text';
3 | import { TypographyProps } from 'components/Typography/Typography';
4 |
5 | export interface ButtonTypographyProps extends Omit {
6 | size: 's' | 'm' | 'l';
7 | }
8 |
9 | export const ButtonTypography = ({ size, ...restProps }: ButtonTypographyProps) => {
10 | if (size === 'l') {
11 | return ;
12 | }
13 |
14 | return ;
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/Blocks/Card/Card.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | display: inline-block;
4 | overflow: hidden;
5 | border-radius: 20px;
6 | box-shadow:
7 | 0 32px 64px 0 rgba(0, 0, 0, .04),
8 | 0 0 2px 1px rgba(0, 0, 0, .02);
9 | background: var(--tgui--tertiary_bg_color);
10 | }
11 |
12 | .wrapper--ambient {
13 | background: var(--tgui--plain_foreground);
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Blocks/Card/Card.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Card } from './Card';
5 |
6 | const meta = {
7 | title: 'Blocks/Card',
8 | component: Card,
9 | argTypes: hideControls('children'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 | type Story = StoryObj;
14 |
15 | export const Playground: Story = {
16 | args: {
17 | children: (
18 | <>
19 | Hot place
20 |
25 |
26 | New York
27 |
28 | >
29 | ),
30 | },
31 | } satisfies Story;
32 |
--------------------------------------------------------------------------------
/src/components/Blocks/Card/CardContext.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { createContext } from 'react';
4 |
5 | export interface CardContextInterface {
6 | type: 'plain' | 'ambient';
7 | }
8 |
9 | export const CardContext = createContext({
10 | type: 'plain',
11 | });
12 |
--------------------------------------------------------------------------------
/src/components/Blocks/Card/components/CardCell/CardCell.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | --tgui--cell--middle--padding: 16px 0;
3 | padding: 0 20px;
4 | background: var(--tgui--card_bg_color);
5 | }
6 |
7 | .wrapper--ambient {
8 | --tgui--text_color: var(--tgui--white);
9 | --tgui--hint_color: rgba(255, 255, 255, .75);
10 |
11 | position: absolute;
12 | bottom: 0;
13 | left: 0;
14 | right: 0;
15 | padding-top: 48px;
16 | background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, var(--tgui--black) 100%);
17 | }
18 |
19 | .subtitle {
20 | display: -webkit-box;
21 | -webkit-line-clamp: 2;
22 | -webkit-box-orient: vertical;
23 | white-space: break-spaces;
24 | }
25 |
26 | .header {
27 | font-weight: var(--tgui--font_weight--accent2);
28 | }
29 |
30 | .wrapper--ambient .header {
31 | color: var(--tgui--white);
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/Blocks/Card/components/CardCell/CardCell.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import styles from './CardCell.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 | import { hasReactNode } from 'helpers/react/node';
6 |
7 | import { Cell, CellProps } from 'components/Blocks/Cell/Cell';
8 | import { CardContext } from '../../CardContext';
9 |
10 | interface CardCellProps extends CellProps {}
11 |
12 | export const CardCell = ({
13 | children,
14 | subtitle,
15 | className,
16 | ...restProps
17 | }: CardCellProps) => {
18 | const cardContext = useContext(CardContext);
19 |
20 | return (
21 | {subtitle}}
28 | {...restProps}
29 | >
30 | {hasReactNode(children) && {children} }
31 | |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/Blocks/Card/components/CardChip/CardChip.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: absolute;
3 | top: 16px;
4 | right: 16px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Blocks/Card/components/CardChip/CardChip.tsx:
--------------------------------------------------------------------------------
1 | import styles from './CardChip.module.css';
2 |
3 | import { classNames } from 'helpers/classNames';
4 |
5 | import { Chip, ChipProps } from 'components/Form/Chip/Chip';
6 |
7 | export interface CardChipProps extends ChipProps {}
8 |
9 | export const CardChip = ({ className, ...restProps }: CardChipProps) => (
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/Cell.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls, setControlsTypes } from 'storybook/controls';
3 |
4 | import { Avatar } from 'components/Blocks/Avatar/Avatar';
5 | import { Badge } from 'components/Blocks/Badge/Badge';
6 | import { Info } from 'components/Blocks/Cell/components/Info/Info';
7 | import { Cell } from './Cell';
8 |
9 | const meta = {
10 | title: 'Blocks/Cell',
11 | component: Cell,
12 | argTypes: {
13 | ...hideControls('before', 'after', 'titleBadge'),
14 | ...setControlsTypes(['Component', 'subhead', 'subtitle', 'children', 'hint', 'description'], 'text'),
15 | },
16 | } satisfies Meta;
17 |
18 | export default meta;
19 | type Story = StoryObj;
20 |
21 | export const Playground: Story = {
22 | args: {
23 | subhead: 'Subhead',
24 | children: 'Title',
25 | subtitle: 'Subtitle',
26 | description: 'Description',
27 | titleBadge: ,
28 | before: ,
29 | after: 99 ,
30 | },
31 | } satisfies Story;
32 |
33 | export const CellWithInfo: Story = {
34 | args: {
35 | children: 'Noah',
36 | subtitle: 'Yesterday',
37 | before: ,
38 | after: +1000 ,
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/components/ButtonCell/ButtonCell.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | width: 100%;
3 | display: flex;
4 | align-items: center;
5 |
6 | gap: 24px;
7 | height: 48px;
8 | padding: 10px 24px;
9 | box-sizing: border-box;
10 |
11 | color: var(--tgui--link_color);
12 | border: none;
13 | background: transparent;
14 | }
15 |
16 | .wrapper--destructive {
17 | color: var(--tgui--destructive_text_color);
18 | }
19 |
20 | .wrapper--ios {
21 | gap: 18px;
22 | height: 44px;
23 | padding: 8px 18px;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/components/ButtonCell/ButtonCell.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Icon28AddCircle } from 'icons/28/add_circle';
3 | import { Icon32ProfileColoredSquare } from 'icons/32/profile_colored_square';
4 | import { hideControls } from 'storybook/controls';
5 |
6 | import { Cell } from 'components/Blocks/Cell/Cell';
7 | import { List } from 'components/Blocks/List/List';
8 | import { Section } from 'components/Blocks/Section/Section';
9 | import { ButtonCell } from './ButtonCell';
10 |
11 | const meta = {
12 | title: 'Blocks/Cell/ButtonCell',
13 | component: ButtonCell,
14 | parameters: {
15 | layout: 'centered',
16 | },
17 | argTypes: hideControls('before', 'after', 'Component'),
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | export const Playground: Story = {
24 | args: {
25 | before: ,
26 | children: 'Create Ad',
27 | },
28 | render: (props) => (
29 |
30 |
31 | | } subtitle="Manage ads and payment settings">My Ads
32 |
33 |
34 |
35 | ),
36 | } satisfies Story;
37 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/components/Info/Info.module.css:
--------------------------------------------------------------------------------
1 | .wrapper--text {
2 | text-align: right;
3 | }
4 |
5 | .wrapper--avatarStack {
6 | display: flex;
7 | align-items: center;
8 | gap: 12px;
9 |
10 | color: var(--tgui--secondary_hint_color);
11 | }
12 |
13 | .subtitle {
14 | margin: 2px 0 0;
15 | color: var(--tgui--hint_color);
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/components/Info/Info.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Avatar } from 'components/Blocks/Avatar/Avatar';
5 | import { AvatarStack } from 'components/Blocks/AvatarStack/AvatarStack';
6 | import { Info } from './Info';
7 |
8 | const meta = {
9 | title: 'Blocks/Cell/Info',
10 | component: Info,
11 | parameters: {
12 | layout: 'centered',
13 | },
14 | argTypes: hideControls('avatarStack'),
15 | } satisfies Meta;
16 |
17 | export default meta;
18 | type Story = StoryObj;
19 |
20 | export const Playground: Story = {
21 | args: {
22 | type: 'text',
23 | children: 'Action',
24 | subtitle: 'Subtitle',
25 | },
26 | } satisfies Story;
27 |
28 | export const _AvatarStack: Story = {
29 | args: {
30 | type: 'avatarStack',
31 | avatarStack: (
32 |
34 |
35 |
36 |
37 |
38 | }>
39 | Action
40 |
41 | ),
42 | },
43 | } satisfies Story;
44 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/components/Navigation/Navigation.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | align-items: center;
4 | gap: 8px;
5 |
6 | color: var(--tgui--hint_color);
7 | }
8 |
9 | .text {
10 | flex-grow: 1;
11 | overflow-wrap: anywhere;
12 | }
13 |
14 | .icon {
15 | flex-shrink: 0;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/components/Navigation/Navigation.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Navigation } from './Navigation';
4 |
5 | const meta = {
6 | title: 'Blocks/Cell/Navigation',
7 | component: Navigation,
8 | parameters: {
9 | layout: 'centered',
10 | },
11 | } satisfies Meta;
12 |
13 | export default meta;
14 | type Story = StoryObj;
15 |
16 | export const Playground: Story = {
17 | args: {
18 | children: 'Action',
19 | },
20 | } satisfies Story;
21 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/components/Navigation/Navigation.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { HTMLAttributes } from 'react';
4 | import styles from './Navigation.module.css';
5 |
6 | import { classNames } from 'helpers/classNames';
7 | import { hasReactNode } from 'helpers/react/node';
8 | import { usePlatform } from 'hooks/usePlatform';
9 |
10 | import { Icon16Chevron } from 'icons/16/chevron';
11 |
12 | import { Text } from 'components/Typography/Text/Text';
13 |
14 | export type NavigationProps = HTMLAttributes;
15 |
16 | /**
17 | * Renders a navigation element with optional text content and an icon. The presence of the icon is
18 | * dependent on the content and the platform, providing flexibility for different UI scenarios.
19 | */
20 | export const Navigation = ({ className, children }: NavigationProps) => {
21 | const platform = usePlatform();
22 | const hasChildren = hasReactNode(children);
23 |
24 | return (
25 |
26 | {hasChildren && {children} }
27 | {(!hasChildren || platform === 'ios') && }
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/Blocks/Cell/hooks/useTypographyCellComponents.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useCallback } from 'react';
4 |
5 | import { usePlatform } from 'hooks/usePlatform';
6 |
7 | import { Caption } from 'components/Typography/Caption/Caption';
8 | import { Subheadline } from 'components/Typography/Subheadline/Subheadline';
9 | import { Text } from 'components/Typography/Text/Text';
10 | import { TypographyProps } from 'components/Typography/Typography';
11 |
12 | export const useTypographyCellComponents = () => {
13 | const platform = usePlatform();
14 | const isIOS = platform === 'ios';
15 |
16 | const Title = useCallback((props: TypographyProps) => {
17 | if (isIOS) {
18 | return ;
19 | }
20 |
21 | return ;
22 | }, [isIOS]);
23 |
24 | const Description = useCallback((props: TypographyProps) => {
25 | if (isIOS) {
26 | return ;
27 | }
28 |
29 | return ;
30 | }, [isIOS]);
31 |
32 | return {
33 | Title,
34 | Description,
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/Blocks/IconButton/IconButton.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: inline-flex;
3 | border-radius: 12px;
4 | border: none;
5 | padding: 8px;
6 | }
7 |
8 | .wrapper--s {
9 | padding: 6px;
10 | border-radius: 50%;
11 | }
12 |
13 | .wrapper--bezeled {
14 | color: var(--tgui--link_color);
15 | background: var(--tgui--secondary_fill);
16 | }
17 |
18 | .wrapper--plain {
19 | color: var(--tgui--link_color);
20 | background: transparent;
21 | }
22 |
23 | .wrapper--gray {
24 | color: var(--tgui--plain_foreground);
25 | background: var(--tgui--plain_background);
26 | }
27 |
28 | .wrapper--outline {
29 | color: var(--tgui--plain_foreground);
30 | background: inherit;
31 | box-shadow: 0 0 0 1px var(--tgui--outline);
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/Blocks/IconButton/IconButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Icon20QuestionMark } from 'icons/20/question_mark';
3 | import { Icon24QR } from 'icons/24/qr';
4 | import { Icon28Stats } from 'icons/28/stats';
5 |
6 | import { IconButton } from './IconButton';
7 |
8 | const meta = {
9 | title: 'Blocks/IconButton',
10 | component: IconButton,
11 | parameters: {
12 | layout: 'centered',
13 | },
14 | } satisfies Meta;
15 |
16 | export default meta;
17 | type Story = StoryObj;
18 |
19 | export const Playground: Story = {
20 | args: {
21 | size: 's',
22 | mode: 'bezeled',
23 | },
24 | render: (args) => (
25 |
28 | {args.size === 's' && }
29 | {args.size === 'm' && }
30 | {args.size === 'l' && }
31 |
32 | ),
33 | } satisfies Story;
34 |
--------------------------------------------------------------------------------
/src/components/Blocks/IconContainer/IconContainer.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | color: var(--tgui--link_color);
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/src/components/Blocks/IconContainer/IconContainer.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from 'react';
2 | import styles from './IconContainer.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | export interface IconContainerProps extends HTMLAttributes {}
7 |
8 | export const IconContainer = ({ className, children, ...restProps }: IconContainerProps) => (
9 |
10 | {children}
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/components/Blocks/Image/Image.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | background-color: var(--tgui--tertiary_bg_color);
4 |
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 |
9 | box-shadow: 0 0 0 1px var(--tgui--outline);
10 | }
11 |
12 | .image {
13 | position: absolute;
14 | display: block;
15 | width: 100%;
16 | height: 100%;
17 | object-fit: cover;
18 | visibility: hidden;
19 | border-radius: inherit;
20 | }
21 |
22 | .wrapper--loaded .image {
23 | visibility: visible;
24 | }
25 |
26 | .fallback {
27 | position: absolute;
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Blocks/Image/components/ImageBadge/ImageBadge.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: absolute;
3 | right: -12px;
4 | top: -12px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Blocks/Image/components/ImageBadge/ImageBadge.tsx:
--------------------------------------------------------------------------------
1 | import styles from './ImageBadge.module.css';
2 |
3 | import { classNames } from 'helpers/classNames';
4 |
5 | import { Badge, BadgeProps } from 'components/Blocks/Badge/Badge';
6 |
7 | export interface ImageBadgeProps extends BadgeProps {}
8 |
9 | export const ImageBadge = ({ type, className, ...restProps }: ImageBadgeProps) => {
10 | if (type !== 'number') {
11 | console.error('[ImageBadge]: Component supports only type="number"');
12 | return null;
13 | }
14 |
15 | return (
16 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/Blocks/Image/helpers/getBorderRadius.ts:
--------------------------------------------------------------------------------
1 | export const getBorderRadius = (size: number) => {
2 | if (size < 40) {
3 | return 4;
4 | }
5 |
6 | if (size < 96) {
7 | return 8;
8 | }
9 |
10 | return 12;
11 | };
12 |
--------------------------------------------------------------------------------
/src/components/Blocks/InlineButtons/InlineButtons.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | gap: 12px;
4 | }
5 |
6 | .wrapper--ios {
7 | gap: 8px;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Blocks/InlineButtons/InlineButtons.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Icon24Chat } from 'icons/24/chat';
3 | import { Icon24Notifications } from 'icons/24/notifications';
4 | import { Icon24QR } from 'icons/24/qr';
5 | import { hideControls } from 'storybook/controls';
6 |
7 | import { InlineButtons } from './InlineButtons';
8 |
9 | const meta = {
10 | title: 'Blocks/InlineButtons',
11 | component: InlineButtons,
12 | parameters: {
13 | layout: 'centered',
14 | },
15 | argTypes: hideControls('children'),
16 | } satisfies Meta;
17 |
18 | export default meta;
19 | type Story = StoryObj;
20 |
21 | export const Playground: Story = {
22 | args: {
23 | mode: 'plain',
24 | children: [
25 |
26 |
27 | ,
28 |
29 |
30 | ,
31 |
32 |
33 | ,
34 | ],
35 | },
36 | } satisfies Story;
37 |
--------------------------------------------------------------------------------
/src/components/Blocks/InlineButtons/InlineButtonsContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export interface InlineButtonsContextProps {
4 | mode?: 'plain' | 'bezeled' | 'gray';
5 | }
6 |
7 | export const InlineButtonsContext = createContext({});
8 |
--------------------------------------------------------------------------------
/src/components/Blocks/InlineButtons/components/InlineButtonsItem/InlineButtonsItem.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | flex: 1 0 0;
3 |
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 | gap: 3px;
9 |
10 | min-height: 60px;
11 | min-width: 64px;
12 | padding: 0 12px;
13 | max-inline-size: 100%;
14 |
15 | border: none;
16 | border-radius: 12px;
17 | color: var(--tgui--link_color);
18 | background: transparent;
19 | box-sizing: border-box;
20 | }
21 |
22 | .wrapper--ios {
23 | min-height: 64px;
24 | min-width: 72px;
25 | gap: 4px;
26 | }
27 |
28 | .wrapper--bezeled {
29 | background: var(--tgui--secondary_fill);
30 | }
31 |
32 | .wrapper--gray {
33 | color: var(--tgui--plain_foreground);
34 | background: var(--tgui--plain_background);
35 | }
36 |
37 | .text {
38 | overflow: hidden;
39 | white-space: nowrap;
40 | text-overflow: ellipsis;
41 | max-inline-size: inherit;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Blocks/InlineButtons/components/InlineButtonsItem/InlineButtonsItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Icon24Chat } from 'icons/24/chat';
3 | import { hideControls } from 'storybook/controls';
4 |
5 | import { InlineButtonsItem } from './InlineButtonsItem';
6 |
7 | const meta = {
8 | title: 'Blocks/InlineButtons/InlineButtons.Item',
9 | component: InlineButtonsItem,
10 | parameters: {
11 | layout: 'centered',
12 | },
13 | argTypes: hideControls('children'),
14 | } satisfies Meta;
15 |
16 | export default meta;
17 | type Story = StoryObj;
18 |
19 | export const Playground: Story = {
20 | args: {
21 | mode: 'plain',
22 | text: 'Chat',
23 | children: ,
24 | },
25 | render: (args) => (
26 |
27 |
28 |
29 | ),
30 | } satisfies Story;
31 |
--------------------------------------------------------------------------------
/src/components/Blocks/List/List.module.css:
--------------------------------------------------------------------------------
1 | .wrapper > :not(:last-child) {
2 | margin-bottom: 12px;
3 | }
4 |
5 | .wrapper--ios {
6 | padding: 10px 18px;
7 | box-sizing: border-box;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Blocks/List/List.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Section } from 'components';
5 | import { Input } from 'components/Form';
6 | import { List } from './List';
7 |
8 | const meta = {
9 | title: 'Blocks/List',
10 | component: List,
11 | argTypes: hideControls('children'),
12 | } satisfies Meta;
13 |
14 | export default meta;
15 | type Story = StoryObj;
16 |
17 | const PreparedSection = () => (
18 |
27 | );
28 |
29 | export const Playground: Story = {
30 | args: {},
31 | render: () => (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ),
40 | } satisfies Story;
41 |
--------------------------------------------------------------------------------
/src/components/Blocks/List/List.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ElementType, HTMLAttributes } from 'react';
4 | import styles from './List.module.css';
5 |
6 | import { classNames } from 'helpers/classNames';
7 | import { usePlatform } from 'hooks/usePlatform';
8 |
9 | export interface ListProps extends HTMLAttributes {
10 | /** Specifies the HTML tag or React component used to render the list, defaulting to `div`. */
11 | Component?: ElementType;
12 | }
13 |
14 | /**
15 | * Renders a container for list items, applying platform-specific styles for consistency across different operating systems.
16 | * This component serves as a foundational element for creating lists in a user interface.
17 | */
18 | export const List = ({
19 | className,
20 | children,
21 | Component = 'div',
22 | ...restProps
23 | }: ListProps) => {
24 | const platform = usePlatform();
25 |
26 | return (
27 |
35 | {children}
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/Blocks/Placeholder/Placeholder.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow: hidden;
3 |
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 |
9 | gap: 24px;
10 | padding: 32px;
11 | }
12 |
13 | .fields {
14 | overflow-wrap: anywhere;
15 | text-align: center;
16 | margin: 0;
17 | }
18 |
19 | .description {
20 | color: var(--tgui--hint_color);
21 | }
22 |
23 | .description:not(:first-child) {
24 | margin-top: 8px;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Blocks/Placeholder/Placeholder.stories.module.css:
--------------------------------------------------------------------------------
1 | .placeholderWrapper {
2 | max-width: 400px;
3 | }
4 |
5 | .sticker {
6 | display: block;
7 | width: 144px;
8 | height: 144px;
9 | }
10 |
11 | @keyframes logoShift {
12 | 0% {
13 | background-position: 0 0;
14 | }
15 | 100% {
16 | background-position: 100% 0;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Blocks/Section/Section.module.css:
--------------------------------------------------------------------------------
1 | .wrapper--base .bodyWithHeader {
2 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
3 | background: var(--tgui--section_bg_color);
4 | }
5 |
6 | .wrapper--ios .body {
7 | border-radius: 12px;
8 | background: var(--tgui--section_bg_color);
9 | }
10 |
11 | .wrapper--ios .body > :first-child {
12 | border-radius: 12px 12px 0 0;
13 | }
14 |
15 | .wrapper--ios .body > :last-child {
16 | border-radius: 0 0 12px 12px;
17 | }
18 |
19 | .wrapper--ios .body > :only-child {
20 | border-radius: 12px;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Blocks/Section/components/SectionFooter/SectionFooter.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 12px 24px;
3 | }
4 |
5 | .wrapper--ios {
6 | padding: 8px 16px 0;
7 | }
8 |
9 | .wrapper--centered {
10 | padding: 16px 24px 20px;
11 | text-align: center;
12 | }
13 |
14 | .wrapper--ios.wrapper--centered {
15 | padding: 16px 16px 0;
16 | }
17 |
18 | .text {
19 | color: var(--tgui--section_header_text_color);
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Blocks/Section/components/SectionFooter/SectionFooter.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { setControlsTypes } from 'storybook/controls';
3 |
4 | import { SectionFooter } from './SectionFooter';
5 |
6 | const meta = {
7 | title: 'Blocks/Section/Section.Footer',
8 | component: SectionFooter,
9 | argTypes: setControlsTypes(['children'], 'text'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 | type Story = StoryObj;
14 |
15 | export const Playground: Story = {
16 | args: {
17 | children: 'SectionFooter',
18 | },
19 | decorators: [
20 | (StoryComponent) => (
21 |
22 |
23 |
24 | ),
25 | ],
26 | } satisfies Story;
27 |
--------------------------------------------------------------------------------
/src/components/Blocks/Section/components/SectionHeader/SectionHeader.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 20px 24px 4px 22px;
3 | color: var(--tgui--link_color);
4 | }
5 |
6 | .wrapper--large {
7 | padding-left: 24px;
8 | color: var(--tgui--text_color);
9 | }
10 |
11 | .wrapper--ios {
12 | padding: 16px 16px 8px 16px;
13 | color: var(--tgui--section_header_text_color);
14 | }
15 |
16 | .wrapper--ios.wrapper--large {
17 | padding: 0 0 12px;
18 | color: var(--tgui--text_color);
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Blocks/Section/components/SectionHeader/SectionHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { setControlsTypes } from 'storybook/controls';
3 |
4 | import { SectionHeader } from './SectionHeader';
5 |
6 | const meta = {
7 | title: 'Blocks/Section/Section.Header',
8 | component: SectionHeader,
9 | argTypes: setControlsTypes(['children'], 'text'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 | type Story = StoryObj;
14 |
15 | export const Playground: Story = {
16 | render: (args) => (
17 | <>
18 | {args.children || 'Usual title'}
19 | {args.children || 'Large title'}
20 | >
21 | ),
22 | } satisfies Story;
23 |
--------------------------------------------------------------------------------
/src/components/Blocks/Section/components/SectionHeader/SectionHeader.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { HTMLAttributes } from 'react';
4 | import styles from './SectionHeader.module.css';
5 |
6 | import { classNames } from 'helpers/classNames';
7 | import { usePlatform } from 'hooks/usePlatform';
8 |
9 | import { useHeaderComponents } from './hooks/useHeaderComponents';
10 |
11 | export interface SectionHeaderProps extends HTMLAttributes {
12 | /** Large title, changes font size, padding and color */
13 | large?: boolean;
14 | }
15 |
16 | export const SectionHeader = ({ large, className, children, ...restProps }: SectionHeaderProps) => {
17 | const platform = usePlatform();
18 | const { Default, Large } = useHeaderComponents();
19 |
20 | const Component = large ? Large : Default;
21 | return (
22 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/Blocks/Section/components/SectionHeader/hooks/useHeaderComponents.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { usePlatform } from 'hooks/usePlatform';
4 |
5 | import { Caption } from 'components/Typography/Caption/Caption';
6 | import { Subheadline } from 'components/Typography/Subheadline/Subheadline';
7 | import { Text } from 'components/Typography/Text/Text';
8 | import { TypographyProps } from 'components/Typography/Typography';
9 |
10 | export const useHeaderComponents = () => {
11 | const platform = usePlatform();
12 |
13 | const Default = ({ ...restProps }: TypographyProps) => {
14 | if (platform === 'ios') {
15 | return ;
16 | }
17 |
18 | return ;
19 | };
20 |
21 | const Large = ({ ...restProps }: TypographyProps) => {
22 | if (platform === 'ios') {
23 | return ;
24 | }
25 |
26 | return ;
27 | };
28 |
29 | return {
30 | Default,
31 | Large,
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/Blocks/Steps/Steps.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | gap: 9px;
4 | padding: 12px;
5 | }
6 |
7 | .step {
8 | min-width: 3px;
9 | height: 3px;
10 | width: 100%;
11 |
12 | border-radius: 2px;
13 | background: var(--tgui--tertiary_bg_color);
14 | }
15 |
16 | .step--active {
17 | background: var(--tgui--link_color);
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Blocks/Steps/Steps.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Steps } from './Steps';
4 |
5 | const meta = {
6 | title: 'Blocks/Steps',
7 | component: Steps,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Playground: Story = {
14 | args: {
15 | count: 10,
16 | progress: 5,
17 | },
18 | } satisfies Story;
19 |
--------------------------------------------------------------------------------
/src/components/Blocks/Steps/Steps.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from 'react';
2 | import styles from './Steps.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | export interface StepsProps extends HTMLAttributes {
7 | /** Total number of steps. */
8 | count: number;
9 | /** Current progress, indicating how many steps have been completed. Progress is 0-indexed and goes up to `count`. */
10 | progress: number;
11 | }
12 |
13 | /**
14 | * Renders a visual indicator of steps or progress in a process, such as a tutorial or a multi-step form.
15 | * It visually represents total steps and current progress.
16 | */
17 | export const Steps = ({ className, count, progress }: StepsProps) => (
18 |
19 | {Array.from({ length: count }, (_, i) => (
20 |
26 | ))}
27 |
28 | );
29 |
--------------------------------------------------------------------------------
/src/components/Blocks/Timeline/Timeline.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 24px;
5 | padding: 32px 44px;
6 | margin: 0;
7 | }
8 |
9 | .wrapper--horizontal {
10 | flex-direction: row;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Blocks/Timeline/Timeline.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Timeline } from './Timeline';
5 |
6 | const meta = {
7 | title: 'Blocks/Timeline',
8 | component: Timeline,
9 | argTypes: hideControls('children'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 | type Story = StoryObj;
14 |
15 | const TimelineItems = [
16 | {
17 | key: '1',
18 | header: 'Arrived',
19 | children: 'Yesterday',
20 | },
21 | {
22 | key: '2',
23 | header: 'Departed',
24 | children: 'Today',
25 | },
26 | {
27 | key: '3',
28 | header: 'In transit',
29 | children: 'Tomorrow',
30 | },
31 | {
32 | key: '4',
33 | header: 'Processed to delivery center',
34 | children: 'Next week',
35 | },
36 | {
37 | key: '5',
38 | header: 'Shipped',
39 | children: 'Someday',
40 | },
41 | ];
42 |
43 | export const Playground: Story = {
44 | args: {
45 | active: 2,
46 | children: TimelineItems.map((item) => (
47 |
48 | {item.children}
49 |
50 | )),
51 | },
52 | } satisfies Story;
53 |
54 | export const Horizontal: Story = {
55 | args: {
56 | horizontal: true,
57 | ...Playground.args,
58 | },
59 | } satisfies Story;
60 |
--------------------------------------------------------------------------------
/src/components/Blocks/Timeline/components/TimelineItem/TimelineItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { TimelineItem } from './TimelineItem';
4 |
5 | const meta = {
6 | title: 'Blocks/Timeline/Timeline.Item',
7 | component: TimelineItem,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Playground: Story = {
14 | args: {
15 | header: 'It\'s my header = header prop',
16 | children: 'It\'s my description = children prop',
17 | },
18 | } satisfies Story;
19 |
--------------------------------------------------------------------------------
/src/components/Feedback/CircularProgress/CircularProgress.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | stroke: var(--tgui--link_color);
3 | transform: rotate(-90deg);
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Feedback/CircularProgress/CircularProgress.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { CircularProgress } from './CircularProgress';
4 |
5 | const meta = {
6 | title: 'Feedback/CircularProgress',
7 | component: CircularProgress,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Playground: Story = {
14 | render: (args) => (
15 | <>
16 |
17 |
18 |
19 | >
20 | ),
21 | decorators: [
22 | (StoryComponent) => (
23 |
27 |
28 |
29 | ),
30 | ],
31 | } satisfies Story;
32 |
--------------------------------------------------------------------------------
/src/components/Feedback/CircularProgress/helpers/getCircleAttributes.ts:
--------------------------------------------------------------------------------
1 | export const getCircleAttributes = (size: 'small' | 'medium' | 'large') => {
2 | switch (size) {
3 | case 'large':
4 | return {
5 | size: 56,
6 | strokeWidth: 4,
7 | radius: 18,
8 | };
9 |
10 | case 'medium':
11 | return {
12 | size: 36,
13 | strokeWidth: 3,
14 | radius: 14,
15 | };
16 |
17 | case 'small':
18 | return {
19 | size: 28,
20 | strokeWidth: 3,
21 | radius: 10,
22 | };
23 |
24 | default:
25 | return undefined;
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/Feedback/Progress/Progress.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow: hidden;
3 | position: relative;
4 | height: 4px;
5 | border-radius: 2px;
6 | }
7 |
8 | .wrapper--base::after {
9 | content: '';
10 | position: absolute;
11 | inset: 0;
12 | opacity: .4;
13 | background: var(--tgui--link_color);
14 | }
15 |
16 | .progress {
17 | position: absolute;
18 | block-size: 100%;
19 | border-radius: inherit;
20 | transition: width 0.2s ease;
21 | background: var(--tgui--link_color);
22 | z-index: var(--tgui--z-index--simple);
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Feedback/Progress/Progress.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Progress } from './Progress';
4 |
5 | const meta = {
6 | title: 'Feedback/Progress',
7 | component: Progress,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 | type Story = StoryObj;
12 |
13 | export const Playground: Story = {
14 | render: (args) => (
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ),
28 | } satisfies Story;
29 |
--------------------------------------------------------------------------------
/src/components/Feedback/Skeleton/Skeleton.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | }
4 |
5 | .wrapper--visible::before,
6 | .wrapper--visible::after {
7 | content: '';
8 | position: absolute;
9 | inset: 0;
10 | z-index: var(--tgui--z-index--simple);
11 | }
12 |
13 | .wrapper::before {
14 | background: var(--tgui--secondary_bg_color);
15 | }
16 |
17 | .wrapper:not(.wrapper--noAnimation)::after {
18 | z-index: var(--tgui--z-index--skeleton);
19 | background-color: var(--tgui--bg_color);
20 | animation: fade 1.8s linear infinite;
21 | }
22 |
23 | @keyframes fade {
24 | 0%, 100% {
25 | opacity: .4;
26 | }
27 |
28 | 50% {
29 | opacity: .7;
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/components/Feedback/Skeleton/Skeleton.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Cell } from 'components/Blocks/Cell/Cell';
4 | import { Skeleton } from './Skeleton';
5 |
6 | const meta = {
7 | title: 'Feedback/Skeleton',
8 | component: Skeleton,
9 | } satisfies Meta;
10 |
11 | export default meta;
12 | type Story = StoryObj;
13 |
14 | export const Playground: Story = {
15 | render: (args) => (
16 |
22 |
23 | Hello!!!! |
24 |
25 |
26 | ),
27 | } satisfies Story;
28 |
--------------------------------------------------------------------------------
/src/components/Feedback/Skeleton/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from 'react';
2 | import styles from './Skeleton.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | export interface SkeletonProps extends HTMLAttributes {
7 | /** If true, disables the shimmering animation of the skeleton. */
8 | withoutAnimation?: boolean;
9 | /** If true, the skeleton overlay is shown above the content. When false, the skeleton is hidden, showing any underlying content. */
10 | visible?: boolean;
11 | }
12 |
13 | /**
14 | * Used as a placeholder during the loading state of a component or page. It can visually mimic
15 | * the layout that will be replaced by the actual content once loaded, improving user experience by reducing perceived loading times.
16 | */
17 | export const Skeleton = ({
18 | withoutAnimation,
19 | visible,
20 | className,
21 | children,
22 | ...restProps
23 | }: SkeletonProps) => (
24 |
33 | {children}
34 |
35 | );
36 |
--------------------------------------------------------------------------------
/src/components/Feedback/Snackbar/components/SnackbarButton/SnackbarButton.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | border: none;
3 | padding: 0;
4 | color: var(--tgui--toast_accent_color);
5 | background: transparent;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Feedback/Snackbar/components/SnackbarButton/SnackbarButton.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes } from 'react';
2 | import styles from './SnackbarButton.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | import { Tappable } from 'components/Service/Tappable/Tappable';
7 |
8 | export interface SnackbarButtonProps extends ButtonHTMLAttributes {}
9 |
10 | export const SnackbarButton = ({ className, children, ...restProps }: SnackbarButtonProps) => (
11 |
16 | {children}
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spinner/Spinner.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | color: var(--tgui--link_color);
3 | }
4 |
5 | .wrapper--ios {
6 | color: var(--tgui--hint_color);
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spinner/Spinner.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Spinner, SpinnerProps } from './Spinner';
5 |
6 | const meta = {
7 | title: 'Feedback/Spinner',
8 | component: Spinner,
9 | argTypes: hideControls('size'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 |
14 | export const Playground: StoryObj = {
15 | render: (args) => (
16 |
22 |
23 |
24 |
25 |
26 | ),
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spinner/components/BaseSpinner/BaseSpinner.tsx:
--------------------------------------------------------------------------------
1 | import { SpinnerProps } from 'components/Feedback/Spinner/Spinner';
2 | import { IconLarge } from './icons/large';
3 | import { IconMedium } from './icons/medium';
4 | import { IconSmall } from './icons/small';
5 |
6 | interface BaseSpinnerProps {
7 | size: SpinnerProps['size'];
8 | }
9 |
10 | const componentBySize = {
11 | s: IconSmall,
12 | m: IconMedium,
13 | l: IconLarge,
14 | };
15 |
16 | const rotateCenterBySize = {
17 | s: 12,
18 | m: 18,
19 | l: 22,
20 | };
21 |
22 | export const BaseSpinner = ({ size }: BaseSpinnerProps) => {
23 | const Component = componentBySize[size];
24 | const rotateCenter = rotateCenterBySize[size];
25 |
26 | return (
27 |
28 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spinner/components/BaseSpinner/icons/large.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconLarge = ({ children, ...restProps }: Icon) => (
4 |
5 |
6 | {children}
7 |
8 |
9 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spinner/components/BaseSpinner/icons/medium.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | /** Each path wrapped in symbol for correct animation inside children in Safari */
4 | export const IconMedium = ({ children, ...restProps }: Icon) => (
5 |
6 |
7 | {children}
8 |
9 |
10 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spinner/components/BaseSpinner/icons/small.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconSmall = ({ children, ...restProps }: Icon) => (
4 |
5 |
6 | {children}
7 |
8 |
9 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spinner/components/IOSSpinner/IOSSpinner.tsx:
--------------------------------------------------------------------------------
1 | import { SpinnerProps } from 'components/Feedback/Spinner/Spinner';
2 | import { IconLarge } from './icons/large';
3 | import { IconMedium } from './icons/medium';
4 | import { IconSmall } from './icons/small';
5 |
6 | interface IOSSpinnerProps {
7 | size: SpinnerProps['size'];
8 | }
9 |
10 | export const IOSSpinner = ({ size }: IOSSpinnerProps) => {
11 | switch (size) {
12 | case 'l':
13 | return ;
14 | case 'm':
15 | return ;
16 | default:
17 | return ;
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spoiler/Spoiler.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | display: table
4 | }
5 |
6 | .wrapper::before {
7 | position: absolute;
8 | content: '';
9 | inset: 0;
10 | background-color: var(--tgui--bg_color);
11 | background-image: url('./icons/spoiler.svg');
12 | z-index: var(--tgui--z-index--simple);
13 | transition: .4s ease;
14 | }
15 |
16 | .wrapper--visible::before {
17 | opacity: 0;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Feedback/Spoiler/Spoiler.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Cell } from 'components/Blocks/Cell/Cell';
4 | import { Spoiler } from './Spoiler';
5 |
6 | const meta = {
7 | title: 'Feedback/Spoiler',
8 | component: Spoiler,
9 | } satisfies Meta;
10 |
11 | export default meta;
12 | type Story = StoryObj;
13 |
14 | export const Playground: Story = {
15 | render: (args) => (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | First radio
24 | |
25 |
26 |
27 | ),
28 | } satisfies Story;
29 |
--------------------------------------------------------------------------------
/src/components/Feedback/index.tsx:
--------------------------------------------------------------------------------
1 | export type { CircularProgressProps } from './CircularProgress/CircularProgress';
2 | export { CircularProgress } from './CircularProgress/CircularProgress';
3 | export type { ProgressProps } from './Progress/Progress';
4 | export { Progress } from './Progress/Progress';
5 | export type { SkeletonProps } from './Skeleton/Skeleton';
6 | export { Skeleton } from './Skeleton/Skeleton';
7 | export type { SnackbarProps } from './Snackbar/Snackbar';
8 | export { Snackbar } from './Snackbar/Snackbar';
9 | export type { SpinnerProps } from './Spinner/Spinner';
10 | export { Spinner } from './Spinner/Spinner';
11 | export type { SpoilerProps } from './Spoiler/Spoiler';
12 | export { Spoiler } from './Spoiler/Spoiler';
13 |
--------------------------------------------------------------------------------
/src/components/Form/Checkbox/Checkbox.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | cursor: pointer;
3 | position: relative;
4 | }
5 |
6 | .wrapper--disabled {
7 | cursor: default;
8 | opacity: .3;
9 | }
10 |
11 | .icon {
12 | display: block;
13 | color: var(--tgui--outline);
14 | }
15 |
16 | .checkedIcon {
17 | position: absolute;
18 | top: 0;
19 | opacity: 0;
20 | color: var(--tgui--link_color);
21 | transition: opacity .15s ease-out;
22 | }
23 |
24 | .input:checked ~ .checkedIcon {
25 | opacity: 1;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Form/Checkbox/Checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Cell, Placeholder } from 'components';
4 | import { Checkbox } from './Checkbox';
5 |
6 | const meta = {
7 | title: 'Form/Checkbox',
8 | component: Checkbox,
9 | } satisfies Meta;
10 |
11 | export default meta;
12 | type Story = StoryObj;
13 |
14 | export const Playground: Story = {
15 | args: {
16 | defaultChecked: true,
17 | },
18 | render: (args) => (
19 |
20 |
21 |
22 | ),
23 | } satisfies Story;
24 |
25 | export const WithCells: Story = {
26 | render: (args) => (
27 | <>
28 | | }
31 | description="Pass Component='label' to Cell to make it clickable."
32 | multiline
33 | >
34 | Apples
35 |
36 | | }
39 | description="Pass Component='label' to Cell to make it clickable."
40 | multiline
41 | >
42 | Milk
43 |
44 | >
45 | ),
46 | } satisfies Story;
47 |
--------------------------------------------------------------------------------
/src/components/Form/Checkbox/icons/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconCheckbox = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Form/Checkbox/icons/checkbox_checked.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconCheckboxChecked = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/Form/Checkbox/icons/checkbox_indeterminate.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconCheckboxIndeterminate = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/components/Form/Chip/Chip.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | user-select: none;
3 |
4 | display: inline-flex;
5 | align-items: center;
6 | justify-content: center;
7 | gap: 8px;
8 | box-sizing: border-box;
9 |
10 | padding: 8px 12px;
11 | border-radius: 10px;
12 | }
13 |
14 | .wrapper--elevated {
15 | background: var(--tgui--surface_primary);
16 | box-shadow: 0 12px 24px 0 rgba(0, 0, 0, .05);
17 | }
18 |
19 | .wrapper--mono {
20 | background: var(--tgui--plain_background);
21 | }
22 |
23 | .wrapper--outline {
24 | border-radius: 10px;
25 | box-shadow: 0 0 0 1px var(--tgui--outline);
26 | }
27 |
28 | .text {
29 | overflow: hidden;
30 | text-overflow: ellipsis;
31 |
32 | flex: 1 1 0;
33 | color: var(--tgui--plain_foreground);
34 | }
35 |
36 | .before {
37 | margin-right: 2px;
38 | }
39 |
40 | .after {
41 | display: flex;
42 | align-items: center;
43 |
44 | /* Visually centering icons, because of line-height */
45 | margin-top: 1.5px;
46 | color: var(--tgui--secondary_hint_color);
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Form/ColorInput/ColorInput.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | gap: 10px;
4 |
5 | min-height: 48px;
6 | padding: 10px 12px 10px 16px;
7 | }
8 |
9 | .wrapper--ios {
10 | padding: 10px 16px;
11 | min-height: 50px;
12 | }
13 |
14 | .circle {
15 | position: relative;
16 |
17 | display: flex;
18 | align-items: center;
19 | justify-content: center;
20 |
21 | width: 24px;
22 | height: 24px;
23 |
24 | border-radius: 50%;
25 | background: conic-gradient(
26 | from 0deg at 50% 50%,
27 | #0C28FF 0deg,
28 | #03FFFF 60deg,
29 | #24D627 120deg,
30 | #FDFF22 180deg,
31 | #FF2227 240deg,
32 | #FE2DF6 300deg,
33 | #7122FF 360deg
34 | );
35 | }
36 |
37 | .circleColor {
38 | width: 16px;
39 | height: 16px;
40 | border-radius: 50%;
41 | }
42 |
43 | .circleColor::before,
44 | .circleColor::after {
45 | content: '';
46 | position: absolute;
47 | border-radius: inherit;
48 | }
49 |
50 | .circleColor::before {
51 | inset: 2px;
52 | background: var(--tgui--bg_color);
53 | }
54 |
55 | .circleColor::after {
56 | inset: 4px;
57 | background: inherit;
58 | }
59 |
60 | .text {
61 | flex: 1 1 0;
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/Form/ColorInput/ColorInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { List } from 'components/Blocks/List/List';
4 | import { ColorInput } from './ColorInput';
5 |
6 | const meta = {
7 | title: 'Form/ColorInput',
8 | component: ColorInput,
9 | parameters: {
10 | layout: 'centered',
11 | },
12 | } satisfies Meta;
13 |
14 | export default meta;
15 | type Story = StoryObj;
16 |
17 | export const Playground: Story = {
18 | render: () => (
19 |
20 |
24 |
25 | ),
26 | } satisfies Story;
27 |
28 |
--------------------------------------------------------------------------------
/src/components/Form/FileInput/FileInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import type { Meta, StoryObj } from '@storybook/react';
4 |
5 | import { Cell, List, Section } from 'components';
6 | import { FileInput } from './FileInput';
7 |
8 | const meta = {
9 | title: 'Form/FileInput',
10 | component: FileInput,
11 | } satisfies Meta;
12 |
13 | export default meta;
14 | type Story = StoryObj;
15 |
16 | export const Playground: Story = {
17 | render: (args) => {
18 | const [files, setFiles] = useState(null);
19 |
20 | return (
21 | setFiles(event.target.files)} {...args}>
22 | {files && Array.from(files).map((file) => (
23 | {file.name} |
24 | ))}
25 |
26 | );
27 | },
28 | decorators: [
29 | (DecoratorStory) => (
30 |
31 |
37 |
38 | ),
39 | ],
40 | } satisfies Story;
41 |
--------------------------------------------------------------------------------
/src/components/Form/FileInput/FileInput.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, InputHTMLAttributes } from 'react';
2 |
3 | import { Icon28Attach } from 'icons/28/attach';
4 |
5 | import { ButtonCell } from 'components/Blocks/Cell/components/ButtonCell/ButtonCell';
6 | import { VisuallyHidden } from 'components/Service/VisuallyHidden/VisuallyHidden';
7 |
8 | export interface FileInputProps extends InputHTMLAttributes {
9 | /** Text label for the file input, used as the button label. */
10 | label?: string;
11 | }
12 |
13 | /**
14 | * Renders a file input disguised as a button, enhancing the user interface and improving usability.
15 | * It leverages the `ButtonCell` component for consistent styling across the application.
16 | */
17 | export const FileInput = forwardRef(({
18 | label = 'Attach file',
19 | className,
20 | children,
21 | ...restProps
22 | }, ref) => (
23 |
24 | {children}
25 | }>
26 |
27 |
28 |
29 | {label}
30 |
31 |
32 | ));
33 |
--------------------------------------------------------------------------------
/src/components/Form/FormInput/components/FormInputTitle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { usePlatform } from 'hooks/usePlatform';
4 |
5 | import { Caption } from 'components/Typography/Caption/Caption';
6 | import { Subheadline } from 'components/Typography/Subheadline/Subheadline';
7 | import { TypographyProps } from 'components/Typography/Typography';
8 |
9 | export const FormInputTitle = ({ ...restProps }: TypographyProps) => {
10 | const platform = usePlatform();
11 |
12 | if (platform === 'ios') {
13 | return ;
14 | }
15 |
16 | return ;
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Form/Input/Input.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 12px 16px;
3 | gap: 12px;
4 | }
5 |
6 | .wrapper--ios {
7 | min-height: 48px;
8 | }
9 |
10 | .input {
11 | display: block;
12 | width: 100%;
13 | margin: 0;
14 | border: 0;
15 | outline: 0;
16 | padding: 0;
17 | resize: none;
18 | background: transparent;
19 | box-sizing: border-box;
20 | text-overflow: ellipsis;
21 | color: var(--tgui--text_color);
22 | }
23 |
24 | .input::-webkit-outer-spin-button,
25 | .input::-webkit-inner-spin-button {
26 | -webkit-appearance: none;
27 | }
28 |
29 | .input::placeholder {
30 | color: var(--tgui--secondary_hint_color);
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/Multiselect.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | --tgui--multiselect--min-height: 52px;
3 |
4 | position: relative;
5 | width: 100%;
6 | min-height: var(--tgui--multiselect--min-height);
7 | }
8 |
9 | .base.base {
10 | min-height: var(--tgui--multiselect--min-height);
11 | padding-right: 48px;
12 | }
13 |
14 | .chevron {
15 | position: absolute;
16 | right: 16px;
17 | color: var(--tgui--secondary_hint_color);
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/components/MultiselectBase/MultiselectBase.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | --tgui--multiselect--gap: 8px;
3 |
4 | overflow: hidden;
5 | position: relative;
6 | display: flex;
7 | flex-grow: 1;
8 | flex-shrink: 1;
9 | flex-wrap: wrap;
10 | gap: var(--tgui--multiselect--gap);
11 | max-inline-size: 100%;
12 | padding: 8px;
13 | box-sizing: border-box;
14 |
15 | margin: 0;
16 | }
17 |
18 | .chip {
19 | max-inline-size: calc(100% - var(--tgui--multiselect--gap));
20 | padding: 6px 12px;
21 | }
22 |
23 | .input {
24 | display: flex;
25 | justify-content: center;
26 | flex-direction: column;
27 | flex: 1;
28 | padding: 0 8px;
29 | position: relative;
30 | inline-size: 100%;
31 | color: var(--tgui--text_color);
32 | background: transparent;
33 | border: 0;
34 | box-shadow: none;
35 | appearance: none;
36 | outline: none;
37 | }
38 |
39 | .input::placeholder {
40 | color: var(--tgui--secondary_hint_color);
41 | }
42 |
43 | .input[readonly] {
44 | cursor: default;
45 | }
46 |
47 | .wrapper--withPlaceholder .input {
48 | white-space: nowrap;
49 | text-overflow: ellipsis;
50 | }
51 |
52 | .closeIcon {
53 | display: flex;
54 | color: var(--tgui--hint_color);
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/components/MultiselectBase/constants.tsx:
--------------------------------------------------------------------------------
1 | import { Chip, ChipProps } from 'components/Form/Chip/Chip';
2 |
3 | export const renderChipDefault = (props: ChipProps) => {
4 | const { ...rest } = props;
5 | return ;
6 | };
7 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/components/MultiselectBase/helpers/getValueOptionByHTMLElement.ts:
--------------------------------------------------------------------------------
1 | import { MultiselectOption } from 'components/Form/Multiselect/types';
2 |
3 | export const getValueOptionByHTMLElement = (options: MultiselectOption[], el: HTMLElement) => {
4 | const value = el.getAttribute('value');
5 | return options.find((v) => v.value === value);
6 | };
7 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/components/MultiselectDropdown/MultiselectDropdown.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow-y: scroll;
3 | border-radius: 12px;
4 | background: var(--tgui--bg_color);
5 |
6 | margin-top: 8px;
7 | box-sizing: border-box;
8 | inline-size: 100%;
9 | max-height: 168px;
10 |
11 | box-shadow:
12 | 0 32px 64px 0 rgba(0, 0, 0, .04),
13 | 0 0 2px 1px rgba(0, 0, 0, .02);
14 | }
15 |
16 | .empty {
17 | color: var(--tgui--hint_color);
18 | }
19 |
20 | .option {
21 | height: 48px;
22 | padding: 0 16px;
23 | }
24 |
25 | .selectedIcon {
26 | color: var(--tgui--link_color);
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/components/MultiselectDropdown/constants.tsx:
--------------------------------------------------------------------------------
1 | import styles from './MultiselectDropdown.module.css';
2 |
3 | import { usePlatform } from 'hooks/usePlatform';
4 |
5 | import { Icon20Select } from 'icons/20/select';
6 | import { Icon20SelectIOS } from 'icons/20/select_ios';
7 |
8 | import { Cell, CellProps } from 'components/Blocks/Cell/Cell';
9 |
10 | export const renderOptionDefault = (props: CellProps) => {
11 | const platform = usePlatform();
12 |
13 | const SelectedIcon = platform === 'ios' ? Icon20SelectIOS : Icon20Select;
14 | return (
15 | | : undefined}
18 | />
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/hooks/constants.tsx:
--------------------------------------------------------------------------------
1 | import { MultiselectOption } from 'components/Form/Multiselect/types';
2 |
3 | export type FocusActionType = 'next' | 'prev';
4 |
5 | export const DEFAULT_SELECTED_BEHAVIOR = 'highlight';
6 |
7 | export const DEFAULT_EMPTY_TEXT = 'Nothing found';
8 |
9 | export const FOCUS_ACTION_NEXT: FocusActionType = 'next';
10 |
11 | export const FOCUS_ACTION_PREV: FocusActionType = 'prev';
12 |
13 | export const isCreateNewOptionPreset = (option: MultiselectOption) => {
14 | return option && 'actionText' in option;
15 | };
16 |
17 | export const isEmptyOptionPreset = (option: MultiselectOption) => {
18 | return option && 'placeholder' in option;
19 | };
20 |
21 | export const isServicePreset = (option: MultiselectOption) =>
22 | isCreateNewOptionPreset(option) || isEmptyOptionPreset(option);
23 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/hooks/helpers/filter/index.ts:
--------------------------------------------------------------------------------
1 | import { getTextFromChildren } from 'helpers/react/children';
2 |
3 | import { MultiselectOption } from 'components/Form/Multiselect/types';
4 |
5 | export type FilterFn = (
6 | inputValue: string,
7 | option: MultiselectOption,
8 | ) => boolean;
9 |
10 | export function defaultFilterFn(
11 | ...args: Parameters
12 | ): ReturnType {
13 | const [rawSearchQuery = '', option] = args;
14 |
15 | if (option?.label === undefined) {
16 | return false;
17 | }
18 |
19 | const searchQuery = rawSearchQuery.trim().toLocaleLowerCase();
20 | const label = getTextFromChildren(option.label).toLocaleLowerCase();
21 |
22 | if (label.startsWith(searchQuery)) {
23 | return true;
24 | }
25 |
26 | const findAllIncludes = (target = '', search = '') => {
27 | const includes = [];
28 | let i = target.indexOf(search);
29 | while (i !== -1) {
30 | includes.push(i);
31 | i = target.indexOf(search, i + 1);
32 | }
33 | return includes;
34 | };
35 |
36 | const includes = findAllIncludes(label, searchQuery);
37 | if (includes.length === 0) {
38 | return false;
39 | }
40 |
41 | return includes.some(index => index === 0 || !/\p{L}/u.test(label[index - 1]));
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/hooks/helpers/getNewOptionData.ts:
--------------------------------------------------------------------------------
1 | import { MultiselectOption, MultiselectOptionLabel, MultiselectOptionValue } from 'components/Form/Multiselect/types';
2 |
3 | export const getNewOptionData = (
4 | value: MultiselectOptionValue,
5 | label: MultiselectOptionLabel,
6 | ): MultiselectOption => ({
7 | value,
8 | label,
9 | });
10 |
11 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/hooks/helpers/isValueLikeOption.ts:
--------------------------------------------------------------------------------
1 | import { MultiselectOption, MultiselectOptionValue } from 'components/Form/Multiselect/types';
2 |
3 | export const isValueLikeOption = (value: O | MultiselectOptionValue): value is O =>
4 | typeof value === 'object' && 'value' in value;
5 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/hooks/helpers/simulateReactInput.ts:
--------------------------------------------------------------------------------
1 | interface SimulateReactInputTargetState {
2 | _valueTracker?: {
3 | getValue(): string;
4 | setValue(value: string): void;
5 | stopTracking(): void;
6 | };
7 | }
8 |
9 | /** @see https://github.com/facebook/react/issues/11488#issuecomment-347775628 */
10 | export const simulateReactInput = (
11 | target: HTMLInputElement & SimulateReactInputTargetState,
12 | nextValue = '',
13 | ) => {
14 | try {
15 | const simulateTarget = target;
16 | const prevValue = simulateTarget.value;
17 | simulateTarget.value = nextValue;
18 |
19 | // eslint-disable-next-line no-underscore-dangle
20 | const tracker = simulateTarget._valueTracker;
21 | tracker?.setValue(prevValue);
22 |
23 | const event = new Event('input', { bubbles: true });
24 | target.dispatchEvent(event);
25 | } catch (error) {
26 | if (process.env.NODE_ENV === 'development') {
27 | throw error;
28 | }
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselect/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 |
3 | export type MultiselectOptionValue = string | number;
4 | export type MultiselectOptionLabel = ReactElement | string | number;
5 |
6 | export type MultiselectOption = {
7 | value: MultiselectOptionValue;
8 | label: MultiselectOptionLabel;
9 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
10 | [index: string]: any;
11 | };
12 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselectable/Multiselectable.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | cursor: pointer;
4 | }
5 |
6 | .wrapper--disabled {
7 | cursor: default;
8 | opacity: .25;
9 | }
10 |
11 | .icon {
12 | display: block;
13 | color: var(--tgui--outline);
14 | }
15 |
16 | .checkedIcon {
17 | position: absolute;
18 | top: 0;
19 | opacity: 0;
20 | color: var(--tgui--link_color);
21 | }
22 |
23 | .icon,
24 | .checkedIcon {
25 | transition: opacity .15s ease-out;
26 | }
27 |
28 | .input:checked ~ .icon {
29 | opacity: 0;
30 | }
31 |
32 | .input:checked ~ .checkedIcon {
33 | opacity: 1;
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselectable/icons/multiselectable.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconMultiselectable = ({ ...restProps }: Icon) => (
4 |
5 |
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselectable/icons/multiselectable_checked.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconMultiselectableChecked = ({ ...restProps }: Icon) => (
4 |
5 |
7 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselectable/icons/multiselectable_ios.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconMultiselectableIOS = ({ ...restProps }: Icon) => (
4 |
5 |
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/src/components/Form/Multiselectable/icons/multiselectable_ios_checked.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconMultiselectableIOSChecked = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Form/PinInput/PinInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { PinInput } from './PinInput';
5 |
6 | const meta = {
7 | title: 'Form/PinInput',
8 | component: PinInput,
9 | argTypes: hideControls('value'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 | type Story = StoryObj;
14 |
15 | export const Playground: Story = {
16 | decorators: [
17 | (Component) => (
18 |
19 |
20 |
21 | ),
22 | ],
23 | } satisfies Story;
24 |
--------------------------------------------------------------------------------
/src/components/Form/PinInput/components/PinInputButton/PinInputButton.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | width: var(--tgui--pin_input--button-width);
3 | height: 56px;
4 |
5 | padding: 0;
6 |
7 | border: none;
8 | border-radius: 16px;
9 | color: var(--tgui--text_color);
10 | background: var(--tgui--tertiary_bg_color);
11 | }
12 |
13 | .wrapper--ios {
14 | width: 76px;
15 | height: 76px;
16 |
17 | border-radius: 50%;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Form/PinInput/components/PinInputButton/PinInputButton.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes } from 'react';
2 | import styles from './PinInputButton.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 | import { usePlatform } from 'hooks/usePlatform';
6 |
7 | import { Tappable } from 'components/Service/Tappable/Tappable';
8 | import { LargeTitle } from 'components/Typography/LargeTitle/LargeTitle';
9 | import { Title } from 'components/Typography/Title/Title';
10 | import { TypographyProps } from 'components/Typography/Typography';
11 |
12 | export interface PinInputButtonProps extends ButtonHTMLAttributes {}
13 |
14 | export const ButtonTypography = (props: TypographyProps) => {
15 | const platform = usePlatform();
16 |
17 | if (platform === 'ios') {
18 | return ;
19 | }
20 |
21 | return ;
22 | };
23 |
24 | export const PinInputButton = ({
25 | children,
26 | ...restProps
27 | }: PinInputButtonProps) => {
28 | const platform = usePlatform();
29 |
30 | return (
31 |
39 | {children}
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/Form/PinInput/components/PinInputCell/PinInputCell.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 |
6 | width: 40px;
7 | height: 44px;
8 |
9 | border-radius: 12px;
10 | border: 2.5px solid var(--tgui--divider);
11 | background: var(--tgui--bg_color);
12 |
13 | transition: border-color .15s ease-out;
14 | padding: 0;
15 | }
16 |
17 | .wrapper--ios {
18 | width: 13px;
19 | height: 13px;
20 |
21 | opacity: .1;
22 | border: none;
23 | border-radius: 50%;
24 | background: var(--tgui--link_color);
25 | }
26 |
27 | .wrapper--ios.wrapper--typed {
28 | opacity: 1;
29 | }
30 |
31 | .wrapper:focus-within {
32 | border-color: var(--tgui--link_color);
33 | }
34 |
35 | .dot {
36 | width: 8px;
37 | height: 8px;
38 |
39 | border-radius: 50%;
40 | background: var(--tgui--text_color);
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Form/PinInput/components/PinInputCell/PinInputCell.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, InputHTMLAttributes } from 'react';
2 | import styles from './PinInputCell.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 | import { usePlatform } from 'hooks/usePlatform';
6 |
7 | import { VisuallyHidden } from 'components/Service/VisuallyHidden/VisuallyHidden';
8 |
9 | export interface PinInputCellProps extends InputHTMLAttributes {
10 | isTyped?: boolean;
11 | }
12 |
13 | export const PinInputCell = forwardRef(({
14 | isTyped,
15 | ...restProps
16 | }, ref) => {
17 | const platform = usePlatform();
18 | const isIOS = platform === 'ios';
19 |
20 | return (
21 |
29 |
36 | {isTyped && !isIOS &&
}
37 |
38 | );
39 | });
40 |
--------------------------------------------------------------------------------
/src/components/Form/Radio/Radio.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | cursor: pointer;
4 | display: block;
5 | }
6 |
7 | .wrapper--disabled {
8 | cursor: default;
9 | opacity: .25;
10 | }
11 |
12 | .icon {
13 | display: block;
14 | color: var(--tgui--outline);
15 | }
16 |
17 | .checkedIcon {
18 | position: absolute;
19 | top: 0;
20 |
21 | opacity: 0;
22 | color: var(--tgui--link_color);
23 | }
24 |
25 | .icon,
26 | .checkedIcon {
27 | transition: opacity .15s ease-out;
28 | }
29 |
30 | .input:checked ~ .icon {
31 | opacity: 0;
32 | }
33 |
34 | .input:checked ~ .checkedIcon {
35 | opacity: 1;
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/src/components/Form/Radio/Radio.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Placeholder } from 'components';
4 | import { Cell } from 'components/Blocks/Cell/Cell';
5 | import { Radio } from './Radio';
6 |
7 | const meta = {
8 | title: 'Form/Radio',
9 | component: Radio,
10 | } satisfies Meta;
11 |
12 | export default meta;
13 | type Story = StoryObj;
14 |
15 | export const Playground: Story = {
16 | args: {
17 | defaultChecked: true,
18 | },
19 | render: (args) => (
20 |
21 |
22 |
23 | ),
24 | } satisfies Story;
25 |
26 | export const WithCells: Story = {
27 | render: (args) => (
28 |
46 | ),
47 | } satisfies Story;
48 |
--------------------------------------------------------------------------------
/src/components/Form/Radio/Radio.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, InputHTMLAttributes } from 'react';
2 | import styles from './Radio.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | import { VisuallyHidden } from 'components/Service/VisuallyHidden/VisuallyHidden';
7 | import { IconRadio } from './icons/radio';
8 | import { IconRadioChecked } from './icons/radio_checked';
9 |
10 | export interface RadioProps
11 | extends InputHTMLAttributes {
12 | }
13 |
14 | /**
15 | * Renders a custom radio button, visually hiding the actual input while displaying custom icons for unchecked and checked states.
16 | * It supports all standard properties and events of an HTML input element of type "radio".
17 | */
18 | export const Radio = forwardRef(({
19 | style,
20 | className,
21 | disabled,
22 | ...restProps
23 | }, ref) => (
24 |
31 |
39 |
40 |
41 |
42 | ));
43 |
--------------------------------------------------------------------------------
/src/components/Form/Radio/icons/radio.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconRadio = ({ ...restProps }: Icon) => (
4 |
5 |
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/src/components/Form/Radio/icons/radio_checked.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconRadioChecked = ({ ...restProps }: Icon) => (
4 |
5 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Form/Rating/Rating.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | display: flex;
4 | gap: 4px;
5 | padding: 12px;
6 | }
7 |
8 | .element {
9 | position: relative;
10 | color: var(--tgui--tertiary_bg_color);
11 | }
12 |
13 | .element:focus-visible {
14 | outline: 2px solid var(--tgui--link_color);
15 | }
16 |
17 | .element--picked {
18 | position: absolute;
19 | color: var(--tgui--link_color);
20 | }
21 |
22 | .input {
23 | position: absolute;
24 | top: 0;
25 | bottom: 0;
26 | margin: 0;
27 | opacity: 0;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Form/Rating/Rating.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Icon28Heart } from 'icons/28/heart';
3 | import { hideControls } from 'storybook/controls';
4 |
5 | import { Section } from 'components';
6 | import { Rating } from './Rating';
7 |
8 | const meta = {
9 | title: 'Form/Rating',
10 | component: Rating,
11 | argTypes: hideControls('IconContainer'),
12 | } satisfies Meta;
13 |
14 | export default meta;
15 | type Story = StoryObj;
16 |
17 | export const Playground: Story = {
18 | render: (args) => (
19 |
25 | ),
26 | } satisfies Story;
27 |
28 | export const CustomIcon: Story = {
29 | render: (args) => (
30 |
33 | ),
34 | } satisfies Story;
35 |
36 |
--------------------------------------------------------------------------------
/src/components/Form/Rating/icons/star.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconStar = (props: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Form/Select/Select.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | }
4 |
5 | .select {
6 | appearance: none;
7 |
8 | padding: 12px 58px 12px 16px;
9 |
10 | width: 100%;
11 | border: none;
12 |
13 | color: var(--tgui--text_color);
14 | outline: none;
15 | border-radius: inherit;
16 | background: inherit;
17 | }
18 |
19 | .chevron {
20 | position: absolute;
21 | top: 50%;
22 | right: 16px;
23 | transform: translateY(-50%);
24 | color: var(--tgui--secondary_hint_color);
25 | pointer-events: none;
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/src/components/Form/Select/Select.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { List } from 'components/Blocks/List/List';
4 | import { Select, SelectProps } from './Select';
5 |
6 | const meta = {
7 | title: 'Form/Select',
8 | component: Select,
9 | parameters: {
10 | layout: 'centered',
11 | },
12 | } satisfies Meta;
13 |
14 | export default meta;
15 |
16 | export const Playground: StoryObj = {
17 | render: () => (
18 |
22 |
23 | Hello
24 | Okay
25 |
26 |
27 | ),
28 | };
29 |
30 |
--------------------------------------------------------------------------------
/src/components/Form/Selectable/Selectable.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | cursor: pointer;
4 | }
5 |
6 | .wrapper--disabled {
7 | cursor: default;
8 | opacity: .25;
9 | }
10 |
11 | .icon {
12 | display: block;
13 | opacity: 0;
14 | color: var(--tgui--link_color);
15 | transition: opacity .15s ease-out;
16 | }
17 |
18 | .input:checked ~ .icon {
19 | opacity: 1;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Form/Selectable/icons/selectable_base.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconSelectableBase = ({ ...restProps }: Icon) => (
4 |
5 |
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/src/components/Form/Selectable/icons/selectable_ios.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconSelectableIOS = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Form/Slider/components/SliderSteps/SliderSteps.module.css:
--------------------------------------------------------------------------------
1 | .step {
2 | position: absolute;
3 |
4 | width: 2px;
5 | height: 2px;
6 |
7 | border-radius: 50%;
8 | background: var(--tgui--secondary_hint_color);
9 | }
10 |
11 | .step--passed {
12 | opacity: .35;
13 | background: var(--tgui--secondary_hint_color);
14 | }
15 |
16 | .step--ios {
17 | width: 4px;
18 | height: 20px;
19 |
20 | border-radius: 3px;
21 | background: var(--tgui--tertiary_bg_color);
22 | }
23 |
24 | .step--ios.step--passed {
25 | opacity: 1;
26 | background: var(--tgui--button_color);
27 | }
28 |
29 | .step:not(.step--ios):first-child {
30 | transform: translateX(50%);
31 | }
32 |
33 | .step:last-child {
34 | transform: translateX(-150%);
35 | }
36 |
37 | .step--ios:last-child {
38 | transform: translateX(-100%);
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/Form/Slider/components/SliderSteps/SliderSteps.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import styles from './SliderSteps.module.css';
4 |
5 | import { classNames } from 'helpers/classNames';
6 | import { usePlatform } from 'hooks/usePlatform';
7 |
8 | import { Step } from '../../hooks/types';
9 |
10 | export interface SliderStepsProps {
11 | steps: Step[];
12 | }
13 |
14 | export const SliderSteps = ({ steps }: SliderStepsProps) => {
15 | const platform = usePlatform();
16 |
17 | return (
18 | <>
19 | {steps.map(({ isPassed, XCoordinate }) => (
20 |
28 | ))}
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/Form/Slider/components/SliderThumb/SliderThumb.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: absolute;
3 | width: var(--tgui--slider--thumb--size);
4 | height: var(--tgui--slider--thumb--size);
5 | border-radius: 50%;
6 | background: var(--tgui--button_color);
7 | user-select: none;
8 | inset-block-start: 50%;
9 | transform: translate(-50%, -50%);
10 | z-index: var(--tgui--z-index--simple);
11 | }
12 |
13 | .wrapper--ios {
14 | background: var(--tgui--white);
15 | box-shadow: 0 6px 6.5px rgba(0, 0, 0, .12), 0 .5px 2px rgba(0, 0, 0, .12);
16 | }
17 |
18 | .input {
19 | inline-size: 100%;
20 | block-size: 100%;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Form/Slider/components/SliderThumb/SliderThumb.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { forwardRef, HTMLAttributes, InputHTMLAttributes } from 'react';
4 | import styles from './SliderThumb.module.css';
5 |
6 | import { classNames } from 'helpers/classNames';
7 | import { usePlatform } from 'hooks/usePlatform';
8 |
9 | import { VisuallyHidden } from 'components/Service/VisuallyHidden/VisuallyHidden';
10 |
11 | export interface SliderThumbProps extends HTMLAttributes {
12 | inputProps?: InputHTMLAttributes;
13 | withTooltip?: boolean;
14 | }
15 |
16 | export const SliderThumb = forwardRef(
17 | ({ className, inputProps, withTooltip, ...restProps }, ref) => {
18 | const platform = usePlatform();
19 |
20 | return (
21 |
29 |
37 |
38 | );
39 | },
40 | );
41 |
--------------------------------------------------------------------------------
/src/components/Form/Slider/hooks/helpers/html.ts:
--------------------------------------------------------------------------------
1 | import { AriaAttributes } from 'react';
2 |
3 | import type { InternalDraggingType } from '../types';
4 |
5 | export const extractSliderAriaAttributes = (
6 | restProps: T,
7 | ) => {
8 | const {
9 | 'aria-label': ariaLabel,
10 | 'aria-valuetext': ariaValueText,
11 | 'aria-labelledby': ariaLabelledBy,
12 | ...restPropsWithoutAria
13 | } = restProps;
14 |
15 | return {
16 | aria: {
17 | ariaLabel,
18 | ariaValueText,
19 | ariaLabelledBy,
20 | },
21 | ...restPropsWithoutAria,
22 | };
23 | };
24 |
25 | export const getDraggingTypeByTargetDataset = (
26 | target: T,
27 | ) => {
28 | if (!target) {
29 | return null;
30 | }
31 |
32 | if (['start', 'end'].includes(target.dataset.type || '')) {
33 | return target.dataset.type as InternalDraggingType;
34 | }
35 |
36 | return null;
37 | };
38 |
39 |
--------------------------------------------------------------------------------
/src/components/Form/Slider/hooks/types.ts:
--------------------------------------------------------------------------------
1 | export type InternalValueState = [number, number | null];
2 |
3 | export type InternalDraggingType = 'start' | 'end';
4 |
5 | export interface InternalGestureRef {
6 | dragging: InternalDraggingType | null;
7 | startX: number;
8 | containerWidth: number;
9 | }
10 |
11 | export type Step = {
12 | isPassed: boolean;
13 | XCoordinate: number;
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/Form/Switch/Switch.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Cell, Placeholder } from 'components';
4 | import { Switch } from './Switch';
5 |
6 | const meta = {
7 | title: 'Form/Switch',
8 | component: Switch,
9 | } satisfies Meta;
10 |
11 | export default meta;
12 | type Story = StoryObj;
13 |
14 | export const Playground: Story = {
15 | args: {
16 | defaultChecked: true,
17 | },
18 | render: (args) => (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ),
28 | } satisfies Story;
29 |
30 | export const WithCell: Story = {
31 | render: (args) => (
32 | | }
35 | description="Pass Component='label' to Cell to make it clickable."
36 | multiline
37 | >
38 | First radio
39 |
40 | ),
41 | } satisfies Story;
42 |
--------------------------------------------------------------------------------
/src/components/Form/Textarea/Textarea.module.css:
--------------------------------------------------------------------------------
1 | .textarea {
2 | padding: 12px 16px;
3 | min-height: 80px;
4 |
5 | width: 100%;
6 | resize: none;
7 |
8 | color: var(--tgui--text_color);
9 | outline: none;
10 | border: none;
11 | background: inherit;
12 | border-radius: inherit;
13 | }
14 |
15 | .textarea::placeholder {
16 | color: var(--tgui--secondary_hint_color);
17 | }
18 |
19 | .wrapper--ios .textarea {
20 | padding: 16px;
21 | min-height: 84px;
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src/components/Form/Textarea/Textarea.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { List } from 'components/Blocks/List/List';
4 | import { Textarea } from './Textarea';
5 |
6 | const meta = {
7 | title: 'Form/Textarea',
8 | component: Textarea,
9 | parameters: {
10 | layout: 'centered',
11 | },
12 | } satisfies Meta;
13 |
14 | export default meta;
15 | type Story = StoryObj;
16 |
17 | export const Playground: Story = {
18 | render: () => (
19 |
20 |
24 |
25 | ),
26 | } satisfies Story;
27 |
28 |
--------------------------------------------------------------------------------
/src/components/Layout/FixedLayout/FixedLayout.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: fixed;
3 | left: 0;
4 | right: 0;
5 | }
6 |
7 | .wrapper--top {
8 | top: 0;
9 | }
10 |
11 | .wrapper--bottom {
12 | padding-bottom: var(--tgui--safe_area_inset_bottom);
13 | bottom: 0;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Layout/FixedLayout/FixedLayout.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Button } from 'components';
4 | import { FixedLayout } from './FixedLayout';
5 |
6 | const meta = {
7 | title: 'Layout/FixedLayout',
8 | component: FixedLayout,
9 | parameters: {
10 | layout: 'centered',
11 | },
12 | } satisfies Meta;
13 |
14 | export default meta;
15 | type Story = StoryObj;
16 |
17 | export const Playground: Story = {
18 | render: () => (
19 |
20 |
21 |
22 | This is FixedLayout with top vertical
23 |
24 |
25 |
26 |
27 | This is FixedLayout with default vertical
28 |
29 |
30 |
31 | ),
32 | } satisfies Story;
33 |
34 |
--------------------------------------------------------------------------------
/src/components/Layout/Tabbar/Tabbar.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | justify-items: stretch;
4 |
5 | box-shadow: 0 -1px 0 var(--tgui--divider);
6 | background: var(--tgui--surface_primary);
7 | padding: 0 16px;
8 | }
9 |
10 | .wrapper--ios {
11 | padding: 0;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Layout/Tabbar/components/TabbarItem/TabbarItem.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | flex-direction: column;
6 | gap: 6px;
7 |
8 | flex: 1 0 0;
9 | max-inline-size: 100%;
10 | min-inline-size: 0;
11 |
12 | padding: 12px 16px 16px;
13 | margin: 0;
14 | border: none;
15 | background: transparent;
16 |
17 | transition: .15s ease-out;
18 | color: var(--tgui--secondary_hint_color);
19 | }
20 |
21 | .wrapper--ios {
22 | padding: 8px 12px 4px;
23 | gap: 4px;
24 | }
25 |
26 | .wrapper--selected {
27 | color: var(--tgui--link_color);
28 | }
29 |
30 | .wrapper--selected:not(.wrapper--ios) .icon {
31 | background: var(--tgui--secondary_fill);
32 | }
33 |
34 | .icon {
35 | display: flex;
36 | justify-content: center;
37 | min-width: 64px;
38 | padding: 2px 10px;
39 | border-radius: 35px;
40 | }
41 |
42 | .wrapper--ios .icon {
43 | padding: 0;
44 | }
45 |
46 | .text {
47 | white-space: nowrap;
48 | max-inline-size: 100%;
49 | overflow: hidden;
50 | text-overflow: ellipsis;
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Layout/Tabbar/components/TabbarItem/TabbarItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Icon28Devices } from 'icons/28/devices';
3 | import { hideControls } from 'storybook/controls';
4 |
5 | import { TabbarItem, TabbarItemProps } from './TabbarItem';
6 |
7 | const meta = {
8 | title: 'Layout/Tabbar/Tabbar.Item',
9 | component: TabbarItem,
10 | argTypes: hideControls('children'),
11 | } satisfies Meta;
12 |
13 | export default meta;
14 |
15 | export const Playground: StoryObj = {
16 | args: {
17 | text: 'Hello',
18 | children: ,
19 | },
20 | };
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Layout/index.ts:
--------------------------------------------------------------------------------
1 | export type { FixedLayoutProps } from './FixedLayout/FixedLayout';
2 | export { FixedLayout } from './FixedLayout/FixedLayout';
3 | export type { TabbarProps } from './Tabbar/Tabbar';
4 | export { Tabbar } from './Tabbar/Tabbar';
5 |
--------------------------------------------------------------------------------
/src/components/Misc/Divider/Divider.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | margin: 0;
3 | border-top: none;
4 | border-width: var(--tgui--border--width);
5 | border-color: var(--tgui--outline);
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Misc/Divider/Divider.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Cell, List, Section } from 'components';
4 | import { Divider } from './Divider';
5 |
6 | const meta = {
7 | title: 'Misc/Divider',
8 | component: Divider,
9 | } satisfies Meta;
10 |
11 | export default meta;
12 | type Story = StoryObj;
13 |
14 | export const Playground: Story = {
15 | render: (args) => (
16 |
17 |
18 |
Divider is under |
19 |
20 |
Divider is above |
21 |
22 |
23 | ),
24 | } satisfies Story;
25 |
--------------------------------------------------------------------------------
/src/components/Misc/Divider/Divider.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from 'react';
2 | import styles from './Divider.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | export interface DividerProps extends HTMLAttributes {}
7 |
8 | /**
9 | * Represents a horizontal line used to separate content within a layout or component.
10 | * The component allows for customization through additional HTML attributes and custom CSS classes.
11 | */
12 | export const Divider = ({ className, ...restProps }: DividerProps) => (
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/Misc/index.tsx:
--------------------------------------------------------------------------------
1 | export type { DividerProps } from './Divider/Divider';
2 | export { Divider } from './Divider/Divider';
3 |
--------------------------------------------------------------------------------
/src/components/Navigation/Breadcrumbs/Breadcrumbs.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | align-items: center;
4 | }
5 |
6 | .divider {
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 |
11 | width: 24px;
12 | height: 20px;
13 |
14 | margin: 0 -6px;
15 | color: var(--tgui--divider);
16 | }
17 |
18 | .chevron {
19 | color: var(--tgui--link_color);
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Navigation/Breadcrumbs/components/BreadCrumbsItem/BreadCrumbsItem.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | cursor: pointer;
3 | padding: 8px 10px;
4 | border-radius: 8px;
5 |
6 | text-decoration: none;
7 | transition: opacity .15s ease-out;
8 | color: var(--tgui--hint_color);
9 | }
10 |
11 | .wrapper:active {
12 | opacity: .5;
13 | }
14 |
15 | @media (hover: hover) and (pointer: fine) {
16 | .wrapper:hover {
17 | background: var(--tgui--tertiary_bg_color);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Navigation/Breadcrumbs/components/BreadCrumbsItem/BreadCrumbsItem.tsx:
--------------------------------------------------------------------------------
1 | import { AllHTMLAttributes, ElementType } from 'react';
2 | import styles from './BreadCrumbsItem.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | import { Subheadline } from 'components/Typography/Subheadline/Subheadline';
7 |
8 | export interface BreadCrumbsItemProps extends AllHTMLAttributes {
9 | Component?: ElementType;
10 | }
11 |
12 | export const BreadCrumbsItem = ({
13 | Component = 'div',
14 | className,
15 | children,
16 | ...restProps
17 | }: BreadCrumbsItemProps) => (
18 |
19 | {children}
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/src/components/Navigation/Breadcrumbs/icons/dot.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconDot = ({ ...restProps }: Icon) => (
4 |
5 |
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/src/components/Navigation/Breadcrumbs/icons/slash.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const IconSlash = ({ ...restProps }: Icon) => (
4 |
5 |
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/src/components/Navigation/CompactPagination/CompactPagination.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | --tgui--compact_pagination--dot--opacity--selected: .1;
3 | --tgui--compact_pagination--dot--background--selected: var(--tgui--link_color);
4 |
5 | display: inline-flex;
6 | gap: 8px;
7 | padding: 4px;
8 | }
9 |
10 | .wrapper--ambient {
11 | --tgui--compact_pagination--dot--opacity--selected: .25;
12 | --tgui--compact_pagination--dot--background--selected: var(--tgui--white);
13 |
14 | padding: 8px 9px;
15 | gap: 6px;
16 | border-radius: 28px;
17 | background: rgba(0, 0, 0, .25);
18 | backdrop-filter: blur(22px);
19 | -webkit-backdrop-filter: blur(22px);
20 | }
21 |
22 | .wrapper--white {
23 | --tgui--compact_pagination--dot--opacity--selected: .25;
24 | --tgui--compact_pagination--dot--background--selected: var(--tgui--white);
25 |
26 | gap: 6px;
27 | padding: 0;
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/src/components/Navigation/CompactPagination/components/CompactPaginationItem/CompactPaginationItem.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | cursor: pointer;
3 | display: block;
4 |
5 | width: 8px;
6 | height: 8px;
7 |
8 | padding: 0;
9 | border: none;
10 | border-radius: 50%;
11 |
12 | transition: opacity .15s ease-in-out;
13 | opacity: var(--tgui--compact_pagination--dot--opacity--selected, .25);
14 | background: var(--tgui--compact_pagination--dot--background--selected, var(--tgui--link_color));
15 | }
16 |
17 | .wrapper--selected {
18 | opacity: 1;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Navigation/CompactPagination/components/CompactPaginationItem/CompactPaginationItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Subheadline } from 'components';
5 | import { CompactPaginationItem, CompactPaginationItemProps } from './CompactPaginationItem';
6 |
7 | const meta = {
8 | title: 'Navigation/CompactPagination/CompactPagination.Item',
9 | component: CompactPaginationItem,
10 | argTypes: hideControls('children'),
11 | } satisfies Meta;
12 |
13 | export default meta;
14 |
15 | export const Playground: StoryObj = {
16 | decorators: [
17 | (Story) => (
18 | <>
19 |
20 | CompactPagination.Item is just a child for CompactPagination component, it exists separately for passing area
21 | labels (It is really just a dot)
22 |
23 |
24 |
25 | >
26 | ),
27 | ],
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/Navigation/CompactPagination/components/CompactPaginationItem/CompactPaginationItem.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes } from 'react';
2 | import styles from './CompactPaginationItem.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 | import { hasReactNode } from 'helpers/react/node';
6 |
7 | import { VisuallyHidden } from 'components/Service/VisuallyHidden/VisuallyHidden';
8 |
9 | export interface CompactPaginationItemProps extends ButtonHTMLAttributes {
10 | selected?: boolean;
11 | }
12 |
13 | export const CompactPaginationItem = ({
14 | selected,
15 | className,
16 | children,
17 | ...restProps
18 | }: CompactPaginationItemProps) => (
19 |
30 | {hasReactNode(children) ? {children} : undefined}
31 |
32 | );
33 |
--------------------------------------------------------------------------------
/src/components/Navigation/Link/Link.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | text-decoration: none;
3 | color: var(--tgui--link_color);
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Navigation/Link/Link.tsx:
--------------------------------------------------------------------------------
1 | import { AnchorHTMLAttributes } from 'react';
2 | import styles from './Link.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | export interface LinkProps extends AnchorHTMLAttributes {}
7 |
8 | export const Link = ({ className, children, ...restProps }: LinkProps) => (
9 |
13 | {children}
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/components/Navigation/Pagination/Pagination.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | gap: 8px;
4 | padding: 16px;
5 | }
6 |
7 | .wrapper--disabled {
8 | opacity: .35;
9 | }
10 |
11 | .button {
12 | cursor: pointer;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | box-sizing: border-box;
17 |
18 | min-width: 44px;
19 | height: 44px;
20 |
21 | color: var(--tgui--hint_color);
22 | padding: 0 10px;
23 | border: none;
24 | border-radius: 12px;
25 | background: transparent;
26 | }
27 |
28 | .button--selected {
29 | color: var(--textColor);
30 | background: var(--tgui--tertiary_bg_color);
31 | }
32 |
33 | .button--disabled {
34 | cursor: default;
35 | opacity: .35;
36 | }
37 |
38 | .button--ellipsis {
39 | cursor: default;
40 | opacity: 1;
41 | }
42 |
43 | .icon {
44 | color: var(--tgui--link_color);
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Navigation/Pagination/Pagination.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Pagination, PaginationProps } from './Pagination';
4 |
5 | const meta = {
6 | title: 'Navigation/Pagination',
7 | component: Pagination,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 |
12 | export const Playground: StoryObj = {};
13 |
14 |
--------------------------------------------------------------------------------
/src/components/Navigation/Pagination/hooks/enum.ts:
--------------------------------------------------------------------------------
1 | export enum PaginationType {
2 | Page = 'page',
3 | Next = 'next',
4 | Previous = 'previous',
5 | StartEllipsis = 'start-ellipsis',
6 | EndEllipsis = 'end-ellipsis',
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Navigation/Pagination/hooks/types.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, SyntheticEvent } from 'react';
2 |
3 | import { PaginationType } from './enum';
4 |
5 | export interface UsePaginationProps {
6 | /** Number of always visible pages at the beginning and end. */
7 | boundaryCount?: number;
8 | /** The total number of pages. */
9 | count?: number;
10 | /** The page selected by default when the component is uncontrolled */
11 | defaultPage?: number;
12 | /** If `true`, hide the next-page button. */
13 | hideNextButton?: boolean;
14 | /** If `true`, hide the previous-page button. */
15 | hidePrevButton?: boolean;
16 | /** Callback fired when the page is changed. */
17 | onChange?: (event: ChangeEvent, page: number) => void;
18 | /** The current page. */
19 | page?: number;
20 | /** Number of always visible pages before and after the current page. */
21 | siblingCount?: number;
22 | }
23 |
24 | export interface UsePaginationItem {
25 | onClick: (event: SyntheticEvent) => void;
26 | type: PaginationType;
27 | page: number | null;
28 | selected: boolean;
29 | disabled: boolean;
30 | 'aria-current'?: 'true' | 'false';
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Navigation/SegmentedControl/SegmentedControl.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow: hidden;
3 |
4 | width: 100%;
5 | height: 100%;
6 |
7 | padding: 2px;
8 | box-sizing: border-box;
9 |
10 | border-radius: 44px;
11 | background: var(--tgui--tertiary_bg_color);
12 | }
13 |
14 | .body {
15 | position: relative;
16 | display: flex;
17 | align-items: center;
18 | align-content: stretch;
19 |
20 | width: 100%;
21 | height: 100%;
22 |
23 | box-sizing: border-box;
24 | border-radius: inherit;
25 | }
26 |
27 | .slider {
28 | position: absolute;
29 | inset: 0;
30 | transition: transform 150ms;
31 | border-radius: 40px;
32 | z-index: var(--tgui--z-index--simple);
33 | box-sizing: border-box;
34 | background: var(--tgui--segmented_control_active_bg);
35 | }
36 |
37 | .wrapper--ios {
38 | border-radius: 9px;
39 | background: var(--tgui--tertiary_bg_color);
40 | }
41 |
42 | .wrapper--ios .slider {
43 | border: var(--tgui--border--width) solid rgba(0, 0, 0, .04);
44 | border-radius: inherit;
45 | box-shadow:
46 | 0 3px 1px 0 rgba(0, 0, 0, .04),
47 | 0 3px 8px 0 rgba(0, 0, 0, .12);
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/Navigation/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow: hidden;
3 | white-space: nowrap;
4 | text-overflow: ellipsis;
5 | flex: 1 1 0;
6 | max-inline-size: 100%;
7 | padding: 10px 24px;
8 | border: none;
9 | border-radius: inherit;
10 | background: transparent;
11 | z-index: var(--tgui--z-index--simple);
12 | color: var(--tgui--text_color);
13 | }
14 |
15 | .wrapper--ios {
16 | padding: 6px 24px;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Navigation/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { SegmentedControlItem, SegmentedControlItemProps } from './SegmentedControlItem';
4 |
5 | const meta = {
6 | title: 'Navigation/SegmentedControl/SegmentedControl.Item',
7 | component: SegmentedControlItem,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 |
12 | export const Playground: StoryObj = {
13 | args: {
14 | selected: true,
15 | children: 'This is a SegmentedControl.Item',
16 | },
17 | };
18 |
19 |
--------------------------------------------------------------------------------
/src/components/Navigation/TabsList/TabsList.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow: hidden;
3 | position: relative;
4 | display: flex;
5 | align-items: center;
6 | align-content: stretch;
7 | gap: 12px;
8 | width: 100%;
9 | height: 100%;
10 | }
11 |
12 | .slider {
13 | position: absolute;
14 | left: 0;
15 | bottom: 0;
16 | right: 0;
17 | height: 3px;
18 | transition: transform 125ms;
19 | border-radius: 4px 4px 1px 1px;
20 | background: var(--tgui--button_color);
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Navigation/TabsList/components/TabsItem/TabsItem.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow: hidden;
3 | white-space: nowrap;
4 | text-overflow: ellipsis;
5 | flex: 1 0 0;
6 | max-inline-size: 100%;
7 | height: 44px;
8 | border: none;
9 | border-radius: inherit;
10 | background: transparent;
11 | transition: color 125ms;
12 | color: var(--tgui--secondary_hint_color);
13 | }
14 |
15 | .wrapper--selected {
16 | color: var(--tgui--link_color);
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Navigation/TabsList/components/TabsItem/TabsItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { TabsItem, TabsItemProps } from './TabsItem';
4 |
5 | const meta = {
6 | title: 'Navigation/TabsList/TabsList.Item',
7 | component: TabsItem,
8 | } satisfies Meta;
9 |
10 | export default meta;
11 |
12 | export const Playground: StoryObj = {
13 | args: {
14 | selected: false,
15 | children: 'This is a TabsList.Item',
16 | },
17 | };
18 |
19 |
--------------------------------------------------------------------------------
/src/components/Navigation/index.tsx:
--------------------------------------------------------------------------------
1 | export type { BreadcrumbsProps } from './Breadcrumbs/Breadcrumbs';
2 | export { Breadcrumbs } from './Breadcrumbs/Breadcrumbs';
3 | export type { CompactPaginationProps } from './CompactPagination/CompactPagination';
4 | export { CompactPagination } from './CompactPagination/CompactPagination';
5 | export type { LinkProps } from './Link/Link';
6 | export { Link } from './Link/Link';
7 | export type { PaginationProps } from './Pagination/Pagination';
8 | export { Pagination } from './Pagination/Pagination';
9 | export type { SegmentedControlProps } from './SegmentedControl/SegmentedControl';
10 | export { SegmentedControl } from './SegmentedControl/SegmentedControl';
11 | export type { TabsListProps } from './TabsList/TabsList';
12 | export { TabsList } from './TabsList/TabsList';
13 |
--------------------------------------------------------------------------------
/src/components/Overlays/Modal/Modal.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: fixed;
3 | bottom: 0;
4 | left: 0;
5 | right: 0;
6 | max-height: 96%;
7 |
8 | border-top-left-radius: 16px;
9 | border-top-right-radius: 16px;
10 |
11 | display: flex;
12 | flex-direction: column;
13 |
14 | outline: none;
15 | background-color: var(--tgui--bg_color);
16 | z-index: var(--tgui--z-index--overlay);
17 | }
18 |
19 | .header {
20 | display: flex;
21 | justify-content: space-between;
22 | align-items: center;
23 |
24 | padding: 16px;
25 | border-bottom: 1px solid var(--tgui--divider);
26 | }
27 |
28 | .body {
29 | overflow-y: auto;
30 | padding-bottom: var(--tgui--safe_area_inset_bottom);
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Overlays/Modal/components/ModalClose/ModalClose.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import { Placeholder } from 'components';
4 | import { ModalClose, ModalCloseProps } from './ModalClose';
5 |
6 | const meta = {
7 | title: 'Overlays/Modal/Modal.Close',
8 | component: ModalClose,
9 | } satisfies Meta;
10 |
11 | export default meta;
12 |
13 | export const Playground: StoryObj = {
14 | render: () => (
15 |
18 | ),
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/Overlays/Modal/components/ModalClose/ModalClose.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { Drawer } from '@xelene/vaul-with-scroll-fix';
4 |
5 | export interface ModalCloseProps {
6 | children?: ReactNode;
7 | }
8 |
9 | export const ModalClose = (props: ModalCloseProps) => (
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/Overlays/Modal/components/ModalHeader/ModalHeader.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 |
4 | display: flex;
5 | align-items: center;
6 | justify-content: space-between;
7 |
8 | gap: 12px;
9 | box-sizing: border-box;
10 |
11 | padding: 16px;
12 | }
13 |
14 | .wrapper::before {
15 | position: absolute;
16 | top: 8px;
17 | left: 50%;
18 | transform: translateX(-50%);
19 |
20 | content: '';
21 | width: 36px;
22 | height: 4px;
23 | border-radius: 4px;
24 | background: var(--tgui--divider);
25 | }
26 |
27 | .before,
28 | .after {
29 | display: flex;
30 | align-items: center;
31 | flex: 1 0 0;
32 | }
33 |
34 | .before {
35 | justify-content: flex-start;
36 | }
37 |
38 | .after {
39 | justify-content: flex-end;
40 | }
41 |
42 | .children {
43 | --tgui--text--line_height: 28px;
44 |
45 | display: block;
46 | overflow: hidden;
47 | text-overflow: ellipsis;
48 | text-align: center;
49 | white-space: nowrap;
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Overlays/Modal/components/ModalHeader/ModalHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Placeholder } from 'components';
5 | import { ModalHeader, ModalHeaderProps } from './ModalHeader';
6 |
7 | const meta = {
8 | title: 'Overlays/Modal/Modal.Header',
9 | component: ModalHeader,
10 | argTypes: hideControls('children', 'before', 'after'),
11 | } satisfies Meta;
12 |
13 | export default meta;
14 |
15 | export const Playground: StoryObj = {
16 | args: {
17 | children: 'Only iOS header',
18 | },
19 | render: (args) => (
20 |
21 |
22 |
23 | ),
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Overlays/Modal/components/ModalHeader/ModalHeader.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef, HTMLAttributes, ReactNode } from 'react';
2 | import styles from './ModalHeader.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 | import { usePlatform } from 'hooks/usePlatform';
6 |
7 | import { Text } from 'components/Typography/Text/Text';
8 |
9 | export interface ModalHeaderProps extends HTMLAttributes {
10 | /** Inserts a component before the header text, e.g. Icon */
11 | before?: ReactNode;
12 | /** Inserts a component after the header text, e.g. Icon */
13 | after?: ReactNode;
14 | }
15 |
16 | export const ModalHeader = forwardRef(({
17 | before,
18 | after,
19 | className,
20 | children,
21 | ...props
22 | }, ref) => {
23 | const platform = usePlatform();
24 |
25 | return (
26 |
31 |
32 | {before}
33 |
34 | {platform === 'ios' && {children} }
35 |
36 | {after}
37 |
38 |
39 | );
40 | });
41 |
--------------------------------------------------------------------------------
/src/components/Overlays/Modal/components/ModalOverlay/ModalOverlay.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: fixed;
3 | inset: 0;
4 | z-index: var(--tgui--z-index--overlay);
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Overlays/Popper/Popper.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | z-index: var(--tgui--z-index--simple);
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/Overlays/Popper/components/FloatingArrow/FloatingArrow.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: absolute;
3 | }
4 |
5 | .icon {
6 | content: '';
7 | display: block;
8 | transform: translateY(1px);
9 | }
10 |
11 | .wrapper--placement-right {
12 | transform: rotate(90deg) translate(50%, -50%);
13 | transform-origin: right;
14 | }
15 |
16 | .wrapper--placement-bottom {
17 | transform: rotate(180deg);
18 | }
19 |
20 | .wrapper--placement-left {
21 | transform: rotate(-90deg) translate(-50%, -50%);
22 | transform-origin: left;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Overlays/Popper/components/FloatingArrow/icons/arrow.tsx:
--------------------------------------------------------------------------------
1 | import { SVGAttributes } from 'react';
2 |
3 | export const DEFAULT_ARROW_WIDTH = 22;
4 | export const DEFAULT_ARROW_HEIGHT = 6;
5 | export const DEFAULT_ARROW_PADDING = 12;
6 |
7 | const PLATFORM_HEIGHT = 1;
8 | const ARROW_HEIGHT_WITH_WHITE_SPACE = DEFAULT_ARROW_HEIGHT + PLATFORM_HEIGHT;
9 |
10 | export const DefaultIcon = (props: SVGAttributes) => (
11 |
18 |
19 |
20 | );
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Overlays/Popper/hooks/helpers/alignment.ts:
--------------------------------------------------------------------------------
1 | import { Placement } from '@floating-ui/react-dom';
2 |
3 | import { AutoPlacementType, PlacementWithAuto } from '../types';
4 |
5 | export const isNotAutoPlacement = (placement: PlacementWithAuto): placement is Placement => {
6 | return !placement.startsWith('auto');
7 | };
8 |
9 | export const getAutoPlacementAlignment = (placement: AutoPlacementType): 'start' | 'end' | null => {
10 | const align = placement.replace(/auto-|auto/, '');
11 | return align === 'start' || align === 'end' ? align : null;
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/Overlays/Popper/hooks/types.ts:
--------------------------------------------------------------------------------
1 | import type { Placement } from '@floating-ui/react-dom';
2 |
3 | export type AutoPlacementType = 'auto' | 'auto-start' | 'auto-end';
4 |
5 | export type PlacementWithAuto = AutoPlacementType | Placement;
6 |
--------------------------------------------------------------------------------
/src/components/Overlays/Tooltip/Tooltip.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 10px;
3 | border-radius: 12px;
4 | color: var(--tgui--black);
5 | background: var(--tgui--white);
6 | box-shadow: 0 8px 24px 0 rgba(0, 0, 0, .10);
7 | }
8 |
9 | .wrapper--dark {
10 | box-shadow: none;
11 | color: var(--tgui--white);
12 | background: var(--tooltip_background_dark);
13 | }
14 |
15 | .wrapper .arrow {
16 | color: var(--tgui--white);
17 | }
18 |
19 | .wrapper--dark .arrow {
20 | color: var(--tooltip_background_dark);
21 | }
22 |
23 | .wrapper--ios .wrapper--dark {
24 | backdrop-filter: blur(50px);
25 | -webkit-backdrop-filter: blur(50px);
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Overlays/index.ts:
--------------------------------------------------------------------------------
1 | export type { ModalProps } from './Modal/Modal';
2 | export { Modal } from './Modal/Modal';
3 | export type { PopperProps } from './Popper/Popper';
4 | export { Popper } from './Popper/Popper';
5 | export type { TooltipProps } from './Tooltip/Tooltip';
6 | export { Tooltip } from './Tooltip/Tooltip';
7 |
--------------------------------------------------------------------------------
/src/components/Service/AppRoot/AppRootContext.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { createContext, RefObject } from 'react';
4 |
5 | export interface AppRootContextInterface {
6 | platform?: 'base' | 'ios';
7 | appearance?: 'light' | 'dark';
8 | portalContainer?: RefObject;
9 | isRendered: boolean;
10 | }
11 |
12 | export const AppRootContext = createContext({
13 | isRendered: false,
14 | });
15 |
--------------------------------------------------------------------------------
/src/components/Service/AppRoot/hooks/helpers/getBrowserAppearanceSubscriber.ts:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'helpers/dom';
2 |
3 | import { AppRootContextInterface } from 'components/Service/AppRoot/AppRootContext';
4 |
5 | export const getBrowserAppearanceSubscriber = (
6 | setAppearance: (appearance: NonNullable) => void,
7 | ): () => void => {
8 | if (!canUseDOM || !window.matchMedia) {
9 | return () => {};
10 | }
11 |
12 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
13 | const listener = () => {
14 | setAppearance(mediaQuery.matches ? 'dark' : 'light');
15 | };
16 |
17 | mediaQuery.addEventListener('change', listener);
18 | return () => mediaQuery.removeEventListener('change', listener);
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/Service/AppRoot/hooks/helpers/getInitialAppearance.ts:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'helpers/dom';
2 |
3 | export const getInitialAppearance = () => {
4 | if (canUseDOM && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
5 | return 'dark';
6 | }
7 |
8 | return 'light';
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/Service/AppRoot/hooks/helpers/getInitialPlatform.ts:
--------------------------------------------------------------------------------
1 | import { getTelegramData } from 'helpers/telegram';
2 |
3 | export const getInitialPlatform = () => {
4 | const telegramData = getTelegramData();
5 | if (!telegramData) {
6 | return 'base';
7 | }
8 |
9 | if (['ios', 'macos'].includes(telegramData.platform)) {
10 | return 'ios';
11 | }
12 |
13 | return 'base';
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/Service/AppRoot/hooks/usePlatform.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { AppRootContext, AppRootContextInterface } from '../AppRootContext';
4 | import { getInitialPlatform } from './helpers/getInitialPlatform';
5 |
6 | export const usePlatform = (platform?: AppRootContextInterface['platform']): NonNullable => {
7 | if (platform !== undefined) {
8 | return platform;
9 | }
10 |
11 | const appContext = useContext(AppRootContext);
12 | if (appContext.isRendered && appContext.platform !== undefined) {
13 | return appContext.platform;
14 | }
15 |
16 | return getInitialPlatform();
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Service/AppRoot/hooks/usePortalContainer.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useContext, useRef } from 'react';
4 |
5 | import { AppRootContext, AppRootContextInterface } from '../AppRootContext';
6 |
7 | export const usePortalContainer = (portalContainer?: AppRootContextInterface['portalContainer']): NonNullable => {
8 | if (portalContainer !== undefined) {
9 | return portalContainer;
10 | }
11 |
12 | const appContext = useContext(AppRootContext);
13 | if (appContext.isRendered && appContext.portalContainer !== undefined) {
14 | return appContext.portalContainer;
15 | }
16 |
17 | return useRef(null);
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/Service/HorizontalScroll/HorizontalScroll.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | overflow-x: scroll;
4 |
5 | -webkit-overflow-scrolling: touch;
6 | scrollbar-width: none;
7 | }
8 |
9 | .HorizontalScroll__in::-webkit-scrollbar {
10 | display: none;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Service/HorizontalScroll/HorizontalScroll.tsx:
--------------------------------------------------------------------------------
1 | import { AllHTMLAttributes, ElementType } from 'react';
2 | import styles from './HorizontalScroll.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | export interface HorizontalScrollProps extends AllHTMLAttributes {
7 | Component?: ElementType;
8 | }
9 |
10 | export const HorizontalScroll = ({
11 | Component = 'div',
12 | className,
13 | children,
14 | ...restProps
15 | }: HorizontalScrollProps) => (
16 |
17 | {children}
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/components/Service/RootRenderer/RootRenderer.tsx:
--------------------------------------------------------------------------------
1 | import { isValidElement, ReactNode } from 'react';
2 | import { createPortal } from 'react-dom';
3 |
4 | import { useAppRootContext } from 'hooks/useAppRootContext';
5 |
6 | export interface RootRendererProps {
7 | children?: ReactNode;
8 | }
9 |
10 | export const RootRenderer = ({ children }: RootRendererProps) => {
11 | const { portalContainer } = useAppRootContext();
12 |
13 | if (!portalContainer?.current) {
14 | return isValidElement(children) ? children : null;
15 | }
16 |
17 | return createPortal(
18 | children,
19 | portalContainer.current,
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/Service/Tappable/Tappable.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | isolation: isolate;
4 | cursor: pointer;
5 | transition: opacity .15s ease-out;
6 | }
7 |
8 | .wrapper[readonly] {
9 | cursor: default;
10 | pointer-events: visible;
11 | }
12 |
13 | .wrapper[disabled] {
14 | cursor: default;
15 | opacity: .35;
16 | }
17 |
18 | .wrapper--opacity:active,
19 | .wrapper--ios:active {
20 | opacity: .65;
21 | }
22 |
23 | @media (hover: hover) and (pointer: fine) {
24 | .wrapper--opacity:hover,
25 | .wrapper--ios:hover {
26 | opacity: .85;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Service/Tappable/components/Ripple/Ripple.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | overflow: hidden;
3 | position: absolute;
4 | inset: 0;
5 | border-radius: inherit;
6 | transition: background-color .15s ease-out;
7 | }
8 |
9 | .wave {
10 | content: '';
11 | position: absolute;
12 |
13 | height: 24px;
14 | width: 24px;
15 | margin: -12px 0;
16 | border-radius: 50%;
17 |
18 | background: var(--tgui--outline);
19 | animation: waveRise .3s cubic-bezier(.3, .3, .5, 1);
20 | opacity: 0;
21 | }
22 |
23 | @keyframes waveRise {
24 | 0% {
25 | transform: scale(1);
26 | opacity: 1;
27 | }
28 |
29 | 30% {
30 | opacity: 1;
31 | }
32 |
33 | 100% {
34 | transform: scale(8);
35 | opacity: 0;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Service/Tappable/components/Ripple/Ripple.tsx:
--------------------------------------------------------------------------------
1 | import styles from './Ripple.module.css';
2 |
3 | interface Wave {
4 | x: number;
5 | y: number;
6 | date: number;
7 | pointerId: number;
8 | }
9 |
10 | export interface RippleProps {
11 | clicks: Wave[];
12 | }
13 |
14 | export const Ripple = ({ clicks }: RippleProps) => (
15 |
19 | {clicks.map((wave) => (
20 |
28 | ))}
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/src/components/Service/Tappable/components/Ripple/types/Wave.ts:
--------------------------------------------------------------------------------
1 | export interface Wave {
2 | x: number;
3 | y: number;
4 | date: number;
5 | pointerId: number;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Service/Touch/helpers/types.ts:
--------------------------------------------------------------------------------
1 | export interface CustomTouchEvent extends MouseEvent, TouchEvent {}
2 |
3 | export interface Gesture {
4 | startX: number;
5 | startY: number;
6 | startT: Date;
7 | duration: number;
8 | isPressed: boolean;
9 | isY: boolean;
10 | isX: boolean;
11 | isSlideX: boolean;
12 | isSlideY: boolean;
13 | isSlide: boolean;
14 | clientX: number;
15 | clientY: number;
16 | shiftX: number;
17 | shiftY: number;
18 | shiftXAbs: number;
19 | shiftYAbs: number;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/Service/VisuallyHidden/VisuallyHidden.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: absolute;
3 | block-size: 1px;
4 | inline-size: 1px;
5 | padding: 0;
6 | margin: -1px;
7 | white-space: nowrap;
8 | clip: rect(0, 0, 0, 0);
9 | clip-path: inset(50%);
10 | overflow: hidden;
11 | border: 0;
12 | opacity: 0;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Service/VisuallyHidden/VisuallyHidden.tsx:
--------------------------------------------------------------------------------
1 | import React, { AllHTMLAttributes, ElementType,forwardRef } from 'react';
2 | import styles from './VisuallyHidden.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | export interface VisuallyHiddenProps extends AllHTMLAttributes {
7 | Component?: ElementType;
8 | }
9 |
10 | export const VisuallyHidden = forwardRef>(
11 | ({ Component = 'span', className, ...restProps }, ref) => (
12 |
13 | ),
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/Service/index.tsx:
--------------------------------------------------------------------------------
1 | export type { AppRootProps } from './AppRoot/AppRoot';
2 | export { AppRoot } from './AppRoot/AppRoot';
3 | export type { RootRendererProps } from './RootRenderer/RootRenderer';
4 | export { RootRenderer } from './RootRenderer/RootRenderer';
5 | export type { TappableProps } from './Tappable/Tappable';
6 | export { Tappable } from './Tappable/Tappable';
7 | export type { VisuallyHiddenProps } from './VisuallyHidden/VisuallyHidden';
8 | export { VisuallyHidden } from './VisuallyHidden/VisuallyHidden';
9 |
--------------------------------------------------------------------------------
/src/components/Typography/Caption/Caption.module.css:
--------------------------------------------------------------------------------
1 | .wrapper--1 {
2 | font-size: var(--tgui--caption1--font_size);
3 | line-height: var(--tgui--caption1--line_height);
4 | }
5 |
6 | .wrapper--2 {
7 | font-size: var(--tgui--caption2--font_size);
8 | line-height: var(--tgui--caption2--line_height);
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Typography/Caption/Caption.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Caption } from './Caption';
5 |
6 | const meta = {
7 | title: 'Typography/Caption',
8 | component: Caption,
9 | argTypes: hideControls('Component'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 |
14 | type Story = StoryObj;
15 |
16 | export const Caption1: Story = {
17 | args: {
18 | level: '1',
19 | },
20 | render: (args) => (
21 | <>
22 |
23 | Caption 1 · Regular
24 |
25 |
26 |
27 | Caption 1 · Semibold
28 |
29 |
30 |
31 | Caption 1 · Bold
32 |
33 | >
34 | ),
35 | };
36 |
37 | export const Caption2: Story = {
38 | args: {
39 | level: '2',
40 | },
41 | render: (args) => (
42 | <>
43 |
44 | Caption 2 · Regular
45 |
46 |
47 |
48 | Caption 2 · Semibold
49 |
50 |
51 |
52 | Caption 2 · Bold
53 |
54 | >
55 | ),
56 | };
57 |
--------------------------------------------------------------------------------
/src/components/Typography/Caption/Caption.tsx:
--------------------------------------------------------------------------------
1 | import styles from './Caption.module.css';
2 |
3 | import { classNames } from 'helpers/classNames';
4 |
5 | import { Typography, TypographyProps } from '../Typography';
6 |
7 | type CaptionLevel = '1' | '2';
8 |
9 | export interface CaptionProps extends Omit {
10 | /** The size level of the caption, influencing its styling and typography size. */
11 | level?: CaptionLevel;
12 | }
13 |
14 | const captionLevelStyles: Record = {
15 | '1': styles['wrapper--1'],
16 | '2': styles['wrapper--2'],
17 | };
18 |
19 | /**
20 | * The Caption component is a text wrapper that applies specific typographic styles,
21 | * based on the provided `level` prop. It's built on top of the Typography component,
22 | * ensuring consistent text styling across the application. It primarily serves for text
23 | * that acts as a small, descriptive label or annotation.
24 | */
25 | export const Caption = ({
26 | level = '1',
27 | className,
28 | Component,
29 | ...restProps
30 | }: CaptionProps) => (
31 |
36 | );
37 |
38 |
--------------------------------------------------------------------------------
/src/components/Typography/Headline/Headline.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | font-size: var(--tgui--headline--font_size);
3 | line-height: var(--tgui--headline--line_height);
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Typography/Headline/Headline.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Headline } from './Headline';
5 |
6 | const meta = {
7 | title: 'Typography/Headline',
8 | component: Headline,
9 | argTypes: hideControls('Component'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 |
14 | type Story = StoryObj;
15 |
16 | export const Playground: Story = {
17 | args: {
18 | plain: false,
19 | },
20 | render: (args) => (
21 | <>
22 |
23 | Headline · Regular
24 |
25 |
26 | Headline · Semibold
27 |
28 |
29 | Headline · Bold
30 |
31 | >
32 | ),
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/Typography/Headline/Headline.tsx:
--------------------------------------------------------------------------------
1 | import styles from './Headline.module.css';
2 |
3 | import { classNames } from 'helpers/classNames';
4 |
5 | import { Typography, TypographyProps } from '../Typography';
6 |
7 | export type HeadlineProps = TypographyProps;
8 |
9 | /**
10 | * The Headline component serves as a wrapper for text that is intended to be displayed prominently,
11 | * typically used for section headings or important titles within the application. It leverages the Typography
12 | * component for consistent typographic styling, offering a range of customization options through its props.
13 | * The component defaults to an `` HTML tag, providing semantic meaning and ensuring good SEO practices,
14 | * but can be customized as needed.
15 | */
16 | export const Headline = ({ className, Component, ...restProps }: HeadlineProps) => (
17 |
22 | );
23 |
24 |
--------------------------------------------------------------------------------
/src/components/Typography/LargeTitle/LargeTitle.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | font-size: var(--tgui--large_title--font_size);
3 | line-height: var(--tgui--large_title--line_height);
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Typography/LargeTitle/LargeTitle.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { LargeTitle } from './LargeTitle';
5 |
6 | const meta = {
7 | title: 'Typography/LargeTitle',
8 | component: LargeTitle,
9 | argTypes: hideControls('Component'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 |
14 | type Story = StoryObj;
15 |
16 | export const Playground: Story = {
17 | args: {
18 | plain: false,
19 | },
20 | render: (args) => (
21 | <>
22 |
23 | Large Title · Regular
24 |
25 |
26 | Large Title · Semibold
27 |
28 |
29 | Large Title · Bold
30 |
31 | >
32 | ),
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/Typography/LargeTitle/LargeTitle.tsx:
--------------------------------------------------------------------------------
1 | import styles from './LargeTitle.module.css';
2 |
3 | import { classNames } from 'helpers/classNames';
4 |
5 | import { Typography, TypographyProps } from '../Typography';
6 |
7 | export type LargeTitleProps = TypographyProps;
8 |
9 | /**
10 | * The LargeTitle component is designed for prominent display text, typically used for major headings
11 | * or titles within an application. It encapsulates the Typography component's features, offering
12 | * extensive styling and semantic customization options while defaulting to an `` HTML element.
13 | * This choice of default component underscores the importance and hierarchy of the text it encapsulates,
14 | * making it suitable for primary page titles or significant headings.
15 | */
16 | export const LargeTitle = ({ className, Component, ...restProps }: LargeTitleProps) => (
17 |
22 | );
23 |
24 |
--------------------------------------------------------------------------------
/src/components/Typography/Subheadline/Subheadline.module.css:
--------------------------------------------------------------------------------
1 | .wrapper--1 {
2 | font-size: var(--tgui--subheadline1--font_size);
3 | line-height: var(--tgui--subheadline1--line_height);
4 | }
5 |
6 | .wrapper--2 {
7 | font-size: var(--tgui--subheadline2--font_size);
8 | line-height: var(--tgui--subheadline2--line_height);
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Typography/Text/Text.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | font-size: var(--tgui--text--font_size);
3 | line-height: var(--tgui--text--line_height);
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Typography/Text/Text.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { hideControls } from 'storybook/controls';
3 |
4 | import { Text } from './Text';
5 |
6 | const meta = {
7 | title: 'Typography/Text',
8 | component: Text,
9 | argTypes: hideControls('Component'),
10 | } satisfies Meta;
11 |
12 | export default meta;
13 |
14 | type Story = StoryObj;
15 |
16 | export const Playground: Story = {
17 | render: (args) => (
18 | <>
19 |
20 | Text · Regular
21 |
22 |
23 |
24 | Text · Semibold
25 |
26 |
27 |
28 | Text · Bold
29 |
30 | >
31 | ),
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/Typography/Text/Text.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react';
2 | import styles from './Text.module.css';
3 |
4 | import { classNames } from 'helpers/classNames';
5 |
6 | import { Typography, TypographyProps } from '../Typography';
7 |
8 | export type TextProps = Omit
9 |
10 | /**
11 | * Text component is designed for general-purpose text rendering,
12 | * offering a wide range of typographic options. It extends the Typography
13 | * component, inheriting its flexibility and styling capabilities.
14 | * This component is ideal for paragraphs, labels, or any textual content, providing
15 | * consistent styling across the application.
16 | */
17 | export const Text = forwardRef(({
18 | weight,
19 | className,
20 | Component,
21 | ...restProps
22 | }: TextProps, ref) => (
23 |
30 | ));
31 |
32 |
--------------------------------------------------------------------------------
/src/components/Typography/Title/Title.module.css:
--------------------------------------------------------------------------------
1 | .wrapper--1 {
2 | font-size: var(--tgui--title1--font_size);
3 | line-height: var(--tgui--title1--line_height);
4 | }
5 |
6 | .wrapper--2 {
7 | font-size: var(--tgui--title2--font_size);
8 | line-height: var(--tgui--title2--line_height);
9 | }
10 |
11 | .wrapper--3 {
12 | font-size: var(--tgui--title3--font_size);
13 | line-height: var(--tgui--title3--line_height);
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Typography/Typography.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | font-family: var(--tgui--font-family);
3 | }
4 |
5 | .wrapper--plain {
6 | margin: 0;
7 | }
8 |
9 | .wrapper--weight-1 {
10 | font-weight: var(--tgui--font_weight--accent1);
11 | }
12 |
13 | .wrapper--weight-2 {
14 | font-weight: var(--tgui--font_weight--accent2);
15 | }
16 |
17 | .wrapper--weight-3 {
18 | font-weight: var(--tgui--font_weight--accent3);
19 | }
20 |
21 | .wrapper--caps {
22 | text-transform: uppercase;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Typography/index.ts:
--------------------------------------------------------------------------------
1 | export type { CaptionProps } from './Caption/Caption';
2 | export { Caption } from './Caption/Caption';
3 | export type { HeadlineProps } from './Headline/Headline';
4 | export { Headline } from './Headline/Headline';
5 | export type { LargeTitleProps } from './LargeTitle/LargeTitle';
6 | export { LargeTitle } from './LargeTitle/LargeTitle';
7 | export type { SubheadlineProps } from './Subheadline/Subheadline';
8 | export { Subheadline } from './Subheadline/Subheadline';
9 | export type { TextProps } from './Text/Text';
10 | export { Text } from './Text/Text';
11 | export type { TitleProps } from './Title/Title';
12 | export { Title } from './Title/Title';
13 | export type { TypographyProps } from './Typography';
14 | export { Typography } from './Typography';
15 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Blocks';
2 | export * from './Feedback';
3 | export * from './Form';
4 | export * from './Layout';
5 | export * from './Misc';
6 | export * from './Navigation';
7 | export * from './Overlays';
8 | export * from './Service';
9 | export * from './Typography';
10 |
--------------------------------------------------------------------------------
/src/helpers/accessibility.ts:
--------------------------------------------------------------------------------
1 | export type ValuesOfObject = T[keyof T];
2 |
3 | export const Keys = {
4 | ENTER: 'Enter',
5 | SPACE: 'Space',
6 | TAB: 'Tab',
7 | ESCAPE: 'Escape',
8 | HOME: 'Home',
9 | END: 'End',
10 | BACKSPACE: 'Backspace',
11 | ARROW_LEFT: 'ArrowLeft',
12 | ARROW_RIGHT: 'ArrowRight',
13 | ARROW_UP: 'ArrowUp',
14 | ARROW_DOWN: 'ArrowDown',
15 | PAGE_UP: 'PageUp',
16 | PAGE_DOWN: 'PageDown',
17 | } as const;
18 |
19 | export type KeysValues = ValuesOfObject;
20 |
21 | export const getHorizontalSideByKey = (
22 | keys: Extract,
23 | ) => {
24 | switch (keys) {
25 | case Keys.ARROW_UP:
26 | case Keys.ARROW_LEFT:
27 | return 'left';
28 | case Keys.ARROW_DOWN:
29 | case Keys.ARROW_RIGHT:
30 | return 'right';
31 |
32 | default:
33 | return undefined;
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/src/helpers/array.ts:
--------------------------------------------------------------------------------
1 | export const range = (startPosition: number, endPosition: number) => {
2 | const length = endPosition - startPosition + 1;
3 | return Array.from({ length }, (_, i) => startPosition + i);
4 | };
5 |
--------------------------------------------------------------------------------
/src/helpers/chunk.ts:
--------------------------------------------------------------------------------
1 | export const createChunks = (array: T[], chunkSize: number): T[][] => {
2 | const chunks = [];
3 |
4 | for (let i = 0; i < array.length; i += chunkSize) {
5 | chunks.push(array.slice(i, i + chunkSize));
6 | }
7 |
8 | return chunks;
9 | };
10 |
--------------------------------------------------------------------------------
/src/helpers/classNames.ts:
--------------------------------------------------------------------------------
1 | interface ClassNamesDictionary {
2 | [index: string]: boolean | undefined | null;
3 | }
4 |
5 | export type ClassName = string | number | ClassNamesDictionary | boolean | undefined | null;
6 |
7 | export function classNames(...args: ClassName[]): string {
8 | const result: string[] = [];
9 |
10 | args.forEach((item): void => {
11 | if (!item) {
12 | return;
13 | }
14 |
15 | switch (typeof item) {
16 | case 'string':
17 | result.push(item);
18 | break;
19 |
20 | case 'object':
21 | Object.keys(item).forEach((key: string) => {
22 | if (item[key]) {
23 | result.push(key);
24 | }
25 | });
26 | break;
27 |
28 | default:
29 | result.push(`${item}`);
30 | }
31 | });
32 |
33 | return result.join(' ');
34 | }
35 |
--------------------------------------------------------------------------------
/src/helpers/color.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-bitwise */
2 | export const hexToRGB = (hex: string): [number, number, number] => {
3 | let fullHex = hex;
4 |
5 | // If hex is short, make it double
6 | if (hex.length === 4) {
7 | fullHex = hex.replace(/([^#])/g, '$1$1');
8 | }
9 |
10 | const bigint = parseInt(fullHex.replace('#', ''), 16);
11 | const channelR = bigint >> 16;
12 | const channelG = bigint >> 8;
13 |
14 | return [
15 | channelR & 255,
16 | channelG & 255,
17 | bigint & 255,
18 | ];
19 | };
20 | /* eslint-enable no-bitwise */
21 |
--------------------------------------------------------------------------------
/src/helpers/dom.ts:
--------------------------------------------------------------------------------
1 | import { isHTMLElement } from '@floating-ui/utils/dom';
2 |
3 | export const canUseDOM = (() =>
4 | !!(typeof window !== 'undefined' && window.document && window.document.createElement))();
5 |
6 | export const getHTMLElementByChildren = (children: HTMLCollection, index: number) => {
7 | const foundEl = children[index];
8 | return isHTMLElement(foundEl) ? foundEl : null;
9 | };
10 |
11 | export const getHTMLElementSiblingByDirection = (
12 | el: T,
13 | direction: 'left' | 'right',
14 | ) => {
15 | let siblingEl: Element | null = null;
16 | switch (direction) {
17 | case 'left':
18 | siblingEl = el.previousElementSibling;
19 | break;
20 | case 'right':
21 | siblingEl = el.nextElementSibling;
22 | break;
23 |
24 | default:
25 | return null;
26 | }
27 |
28 | return isHTMLElement(siblingEl) ? siblingEl : null;
29 | };
30 |
31 |
--------------------------------------------------------------------------------
/src/helpers/equal.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | import { isObjectLike } from './object';
4 |
5 | export const isEqual = (value: any, other: any): boolean => {
6 | if (value === other) {
7 | return true;
8 | }
9 |
10 | if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
11 | return false;
12 | }
13 |
14 | if (isObjectLike(value) && isObjectLike(other)) {
15 | if (Object.keys(value).length !== Object.keys(other).length) {
16 | return false;
17 | }
18 |
19 | // eslint-disable-next-line no-restricted-syntax
20 | for (const prop in value) {
21 | if (Object.prototype.hasOwnProperty.call(value, prop) && Object.prototype.hasOwnProperty.call(other, prop)) {
22 | if (!isEqual(value[prop], other[prop])) {
23 | return false;
24 | }
25 | } else {
26 | return false;
27 | }
28 | }
29 |
30 | return true;
31 | }
32 |
33 | return false;
34 | };
35 |
36 |
--------------------------------------------------------------------------------
/src/helpers/function.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | export const callMultiple =
4 | (...fns: any) =>
5 | (...args: any) =>
6 | fns.filter((f: any) => typeof f === 'function').forEach((f: any) => f(...args));
7 |
--------------------------------------------------------------------------------
/src/helpers/math.ts:
--------------------------------------------------------------------------------
1 | export const clamp = (value: number, min: number, max: number) => {
2 | return Math.max(min, Math.min(value, max));
3 | };
4 |
--------------------------------------------------------------------------------
/src/helpers/object.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | export const isObjectLike = (object: any): boolean => {
4 | return typeof object === 'object' && object !== null;
5 | };
6 |
--------------------------------------------------------------------------------
/src/helpers/react/children.ts:
--------------------------------------------------------------------------------
1 | // Code from library from react-children-utilities
2 | // @see https://github.com/fernandopasik/react-children-utilities/tree/main
3 |
4 | import { Children, isValidElement, ReactNode } from 'react';
5 |
6 | export const childToString = (child?: ReactNode): string => {
7 | if (typeof child === 'undefined' || child === null || typeof child === 'boolean') {
8 | return '';
9 | }
10 |
11 | if (JSON.stringify(child) === '{}') {
12 | return '';
13 | }
14 |
15 | return (child as number | string).toString();
16 | };
17 |
18 | export const getTextFromChildren = (children: ReactNode | ReactNode[]): string => {
19 | if (!(children instanceof Array) && !isValidElement(children)) {
20 | return childToString(children);
21 | }
22 |
23 | return Children.toArray(children).reduce((text: string, child: ReactNode): string => {
24 | let newText = '';
25 | const isValidElementResult = isValidElement<{ children?: ReactNode | ReactNode[] }>(child);
26 | const hasChildren = isValidElementResult && 'children' in child.props;
27 |
28 | if (isValidElementResult && hasChildren) {
29 | newText = getTextFromChildren(child.props.children);
30 | } else if (isValidElementResult && !hasChildren) {
31 | newText = '';
32 | } else {
33 | newText = childToString(child);
34 | }
35 |
36 | return text.concat(newText);
37 | }, '');
38 | };
39 |
--------------------------------------------------------------------------------
/src/helpers/react/node.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | export const hasReactNode = (value: ReactNode): boolean => {
4 | return value !== undefined && value !== false && value !== null && value !== '';
5 | };
6 |
7 | export function isPrimitiveReactNode(node: ReactNode): boolean {
8 | return typeof node === 'string' || typeof node === 'number';
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/react/refs.ts:
--------------------------------------------------------------------------------
1 | import { MutableRefObject, Ref, RefObject } from 'react';
2 |
3 | export const setRef = (element: T, ref?: Ref): void => {
4 | if (ref) {
5 | if (typeof ref === 'function') {
6 | ref(element);
7 | } else {
8 | // eslint-disable-next-line no-param-reassign
9 | (ref as MutableRefObject).current = element;
10 | }
11 | }
12 | };
13 |
14 | export const multipleRef = (...refs: Array[ | undefined>): RefObject] => {
15 | let current: T | null = null;
16 | return {
17 | get current() {
18 | return current;
19 | },
20 | set current(element) {
21 | current = element;
22 | refs.forEach((ref) => ref && setRef(element, ref));
23 | },
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/src/helpers/telegram.ts:
--------------------------------------------------------------------------------
1 | import { canUseDOM } from 'helpers/dom';
2 |
3 | import { Telegram } from '@twa-dev/types';
4 |
5 | declare global {
6 | interface Window {
7 | Telegram?: Telegram;
8 | }
9 | }
10 |
11 | export const getTelegramData = () => {
12 | if (!canUseDOM) {
13 | return undefined;
14 | }
15 |
16 | return window.Telegram?.WebApp;
17 | };
18 |
--------------------------------------------------------------------------------
/src/hooks/useAppRootContext.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useContext } from 'react';
4 |
5 | import { AppRootContext } from 'components/Service/AppRoot/AppRootContext';
6 |
7 | export const useAppRootContext = () => {
8 | const appRootContext = useContext(AppRootContext);
9 |
10 | if (!appRootContext.isRendered) {
11 | throw new Error('[TGUI] Wrap your app with component');
12 | }
13 |
14 | return appRootContext;
15 | };
16 |
--------------------------------------------------------------------------------
/src/hooks/useEnhancedEffect.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useLayoutEffect } from 'react';
2 |
3 | import { canUseDOM } from 'helpers/dom';
4 |
5 | /**
6 | * A version of `useLayoutEffect` that does not show a warning when server-side rendering.
7 | * This is useful for effects that are only needed for client-side rendering but not for SSR.
8 | */
9 | export const useEnhancedEffect = canUseDOM ? useLayoutEffect : useEffect;
10 |
--------------------------------------------------------------------------------
/src/hooks/useExternalRefs.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { MutableRefObject, Ref, useMemo, useRef } from 'react';
4 |
5 | import { setRef } from 'helpers/react/refs';
6 |
7 | export function useExternRef(
8 | ...externRefs: Array[ | undefined | false>
9 | ): MutableRefObject] {
10 | const stableRef = useRef(null);
11 |
12 | return useMemo(
13 | () => ({
14 | get current() {
15 | return stableRef.current;
16 | },
17 | set current(el) {
18 | stableRef.current = el;
19 | externRefs.forEach((ref) => {
20 | if (ref) {
21 | setRef(el, ref);
22 | }
23 | });
24 | },
25 | }),
26 | externRefs,
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/hooks/useGlobalClicks.ts:
--------------------------------------------------------------------------------
1 | import { RefObject } from 'react';
2 |
3 | import { useEnhancedEffect } from 'hooks/useEnhancedEffect';
4 |
5 | import { isElement } from '@floating-ui/utils/dom';
6 |
7 | /**
8 | * Function helps to handle global clicks outside the given refs
9 | * If the click is outside the given refs, the callback will be called
10 | */
11 | export const useGlobalClicks = <
12 | T extends RefObject | undefined | null,
13 | ElementType extends Element = Element,
14 | >(
15 | callback: (event: MouseEvent) => void,
16 | ...refs: T[]
17 | ) => {
18 | useEnhancedEffect(() => {
19 | const hasNotNullRefs = refs.some((ref) => ref && ref.current !== null);
20 | if (!document || !hasNotNullRefs) {
21 | return () => {};
22 | }
23 |
24 | const handleClick = (event: MouseEvent) => {
25 | const targetEl = event.target;
26 | const isClickInsideGivenRefs =
27 | isElement(targetEl) &&
28 | refs.some((ref) => ref && ref.current && ref.current.contains(targetEl));
29 |
30 | !isClickInsideGivenRefs && callback(event);
31 | };
32 |
33 | document.addEventListener('click', handleClick, {
34 | passive: true,
35 | capture: true,
36 | });
37 |
38 | return () => document.removeEventListener('click', handleClick, true);
39 | }, [document, callback, ...refs]);
40 | };
41 |
--------------------------------------------------------------------------------
/src/hooks/useObjectMemo.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRef } from 'react';
4 |
5 | import { isEqual } from 'helpers/equal';
6 |
7 | export const useObjectMemo = (object: T): T => {
8 | const cache = useRef(object);
9 |
10 | if (!isEqual(cache.current, object)) {
11 | cache.current = object;
12 | }
13 |
14 | return cache.current;
15 | };
16 |
17 |
--------------------------------------------------------------------------------
/src/hooks/usePlatform.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useAppRootContext } from 'hooks/useAppRootContext';
4 |
5 | import { AppRootContextInterface } from 'components/Service/AppRoot/AppRootContext';
6 |
7 | export const usePlatform = (): NonNullable => {
8 | const context = useAppRootContext();
9 | return context.platform || 'base';
10 | };
11 |
--------------------------------------------------------------------------------
/src/hooks/useTimeout.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useCallback, useRef } from 'react';
4 |
5 | import { useEnhancedEffect } from 'hooks/useEnhancedEffect';
6 |
7 | export const useTimeout = (callbackFunction: () => void, duration: number) => {
8 | const options = useRef({ callbackFunction, duration });
9 |
10 | useEnhancedEffect(() => {
11 | options.current.callbackFunction = callbackFunction;
12 | options.current.duration = duration;
13 | }, [callbackFunction, duration]);
14 |
15 | const timeout = useRef>();
16 |
17 | const clear = useCallback(() => clearTimeout(timeout?.current), []);
18 |
19 | const set = useCallback(() => {
20 | clear();
21 | timeout.current = setTimeout(options.current.callbackFunction, options.current.duration);
22 | }, [clear]);
23 |
24 | return { set, clear };
25 | };
26 |
--------------------------------------------------------------------------------
/src/icons/12/quote.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon12Quote = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/16/cancel.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon16Cancel = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/16/chevron.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon16Chevron = ({ ...restProps }: Icon) => (
4 |
5 |
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/src/icons/20/chevron_down.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon20ChevronDown = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/icons/20/select.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon20Select = ({ ...restProps }: Icon) => (
4 |
5 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/icons/20/select_ios.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon20SelectIOS = ({ ...restProps }: Icon) => (
4 |
5 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/icons/24/cancel.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon24Cancel = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/24/chat.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon24Chat = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/24/chevron_down.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon24ChevronDown = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/24/chevron_left.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon24ChevronLeft = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/24/chevron_right.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon24ChevronRight = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/24/close.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon24Close = ({ ...restProps }: Icon) => (
4 |
5 |
6 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/icons/24/person_remove.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon24PersonRemove = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/24/sun_low.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon24SunLow = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/icons/28/add_circle.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28AddCircle = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/28/archive.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28Archive = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/28/attach.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28Attach = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/28/chat.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28Chat = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/28/close.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28Close = ({ ...restProps }: Icon) => (
4 |
5 |
6 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/icons/28/close_ambient.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28CloseAmbient = ({ ...restProps }: Icon) => (
4 |
5 |
6 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/icons/28/devices.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28Devices = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/28/edit.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28Edit = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/28/heart.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28Heart = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/icons/28/stats.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from 'types/Icon';
2 |
3 | export const Icon28Stats = ({ ...restProps }: Icon) => (
4 |
5 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components';
2 |
--------------------------------------------------------------------------------
/src/storybook/controls.ts:
--------------------------------------------------------------------------------
1 | type ControlTypes = 'text' | null;
2 |
3 | export const setControlsTypes = (controls: string[], type: ControlTypes) => {
4 | return controls.reduce>((acc, control) => {
7 | acc[control] = {
8 | control: { type },
9 | };
10 |
11 | return acc;
12 | }, {});
13 | };
14 |
15 | export const hideControls = (...controls: string[]) => {
16 | return setControlsTypes(controls, null);
17 | };
18 |
19 | export const textControl = {
20 | type: 'text',
21 | };
22 |
23 | export const hiddenControl = {
24 | type: null,
25 | };
26 |
--------------------------------------------------------------------------------
/src/types/Icon.ts:
--------------------------------------------------------------------------------
1 | import { SVGProps } from 'react';
2 |
3 | export interface Icon extends SVGProps {
4 | title?: string
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "./src",
5 | "outDir": "dist",
6 | "declarationMap": true
7 | },
8 | "include": ["src/**/*.ts*"],
9 | "exclude": [
10 | "node_modules",
11 | "src/**/*.stories.ts*",
12 | "src/**/*.test.ts*",
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "lib": ["dom", "es2015"],
5 | "module": "commonjs",
6 | "baseUrl": ".",
7 | "paths": {
8 | "*": ["./src/*"]
9 | },
10 | "outDir": "./dist",
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "strict": true,
14 | "skipLibCheck": true,
15 | },
16 | "include": [
17 | "**/*.tsx",
18 | "**/*.ts",
19 | "**/*.js",
20 | ".eslintrc.js"
21 | ],
22 | "exclude": [
23 | "node_modules",
24 | "dist"
25 | ],
26 | "files": [
27 | "./types/declarations.d.ts"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/types/declarations.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.module.css' {
2 | const classes: { readonly [key: string]: string };
3 | export default classes;
4 | }
5 |
6 | declare module '*.svg' {
7 | import { FunctionComponent, SVGProps } from 'react';
8 |
9 | export const ReactComponent: FunctionComponent & { title?: string }>;
10 |
11 | const src: string;
12 | export default src;
13 | }
14 |
--------------------------------------------------------------------------------