├── .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: `Logo` 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 <LargeTitle weight="1" caps {...restProps} />; 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 | <Badge 16 | type="number" 17 | className={classNames(styles.wrapper, className)} 18 | {...restProps} 19 | /> 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<typeof AvatarStack>; 14 | 15 | export default meta; 16 | 17 | export const Playground: StoryObj<AvatarProps> = { 18 | args: { 19 | children: ( 20 | <> 21 | <Avatar size={48} src={AVATAR_URL} /> 22 | <Avatar size={48} src={AVATAR_URL} /> 23 | <Avatar size={48} src={AVATAR_URL} /> 24 | <Avatar size={48} src={AVATAR_URL} /> 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<HTMLDivElement> { 9 | /** An array of `Avatar` components to be rendered within the stack. */ 10 | children: ReactElement<AvatarProps>[]; 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 | <div 24 | {...restProps} 25 | className={classNames(styles.wrapper)} 26 | > 27 | {children} 28 | </div> 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<typeof Badge>; 9 | 10 | export default meta; 11 | type Story = StoryObj<typeof meta>; 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<TypographyProps, 'level'> { 8 | } 9 | 10 | export const BannerDescriptionTypography = (props: BannerDescriptionTypographyProps) => { 11 | const platform = usePlatform(); 12 | 13 | if (platform === 'ios') { 14 | return <Caption level="1" {...props} />; 15 | } 16 | 17 | return <Subheadline level="2" {...props} />; 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<typeof Blockquote>; 17 | 18 | export default meta; 19 | type Story = StoryObj<typeof meta>; 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<TypographyProps, 'size'> { 6 | size: 's' | 'm' | 'l'; 7 | } 8 | 9 | export const ButtonTypography = ({ size, ...restProps }: ButtonTypographyProps) => { 10 | if (size === 'l') { 11 | return <Text weight="2" {...restProps} />; 12 | } 13 | 14 | return <Subheadline level="2" weight="2" {...restProps} />; 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<typeof Card>; 11 | 12 | export default meta; 13 | type Story = StoryObj<typeof meta>; 14 | 15 | export const Playground: Story = { 16 | args: { 17 | children: ( 18 | <> 19 | <Card.Chip readOnly>Hot place</Card.Chip> 20 | <img 21 | alt="Dog" 22 | src="https://i.imgur.com/892vhef.jpeg" 23 | style={{ display: 'block', width: 254, height: 308, objectFit: 'cover' }} 24 | /> 25 | <Card.Cell readOnly subtitle="United states"> 26 | New York 27 | </Card.Cell> 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<CardContextInterface>({ 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 | <Cell 22 | className={classNames( 23 | styles.wrapper, 24 | cardContext.type === 'ambient' && styles['wrapper--ambient'], 25 | className, 26 | )} 27 | subtitle={hasReactNode(subtitle) && <span className={styles.subtitle}>{subtitle}</span>} 28 | {...restProps} 29 | > 30 | {hasReactNode(children) && <span className={styles.header}>{children}</span>} 31 | </Cell> 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 | <Chip className={classNames(styles.wrapper, className)} {...restProps} /> 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<typeof Cell>; 17 | 18 | export default meta; 19 | type Story = StoryObj<typeof meta>; 20 | 21 | export const Playground: Story = { 22 | args: { 23 | subhead: 'Subhead', 24 | children: 'Title', 25 | subtitle: 'Subtitle', 26 | description: 'Description', 27 | titleBadge: <Badge type="dot" />, 28 | before: <Avatar size={48} />, 29 | after: <Badge type="number">99</Badge>, 30 | }, 31 | } satisfies Story; 32 | 33 | export const CellWithInfo: Story = { 34 | args: { 35 | children: 'Noah', 36 | subtitle: 'Yesterday', 37 | before: <Avatar size={48} />, 38 | after: <Info type="text" subtitle="Received">+1000</Info>, 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<typeof ButtonCell>; 19 | 20 | export default meta; 21 | type Story = StoryObj<typeof meta>; 22 | 23 | export const Playground: Story = { 24 | args: { 25 | before: <Icon28AddCircle />, 26 | children: 'Create Ad', 27 | }, 28 | render: (props) => ( 29 | <List style={{ background: 'var(--tgui--secondary_bg_color)', padding: 10 }}> 30 | <Section> 31 | <Cell before={<Icon32ProfileColoredSquare />} subtitle="Manage ads and payment settings">My Ads</Cell> 32 | <ButtonCell {...props} /> 33 | </Section> 34 | </List> 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<typeof Info>; 16 | 17 | export default meta; 18 | type Story = StoryObj<typeof meta>; 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 | <Info type="avatarStack" avatarStack={ 33 | <AvatarStack> 34 | <Avatar size={28} /> 35 | <Avatar size={28} /> 36 | <Avatar size={28} /> 37 | </AvatarStack> 38 | }> 39 | Action 40 | </Info> 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<typeof Navigation>; 12 | 13 | export default meta; 14 | type Story = StoryObj<typeof meta>; 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<HTMLDivElement>; 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 | <div className={classNames(styles.wrapper, className)}> 26 | {hasChildren && <Text className={styles.text}>{children}</Text>} 27 | {(!hasChildren || platform === 'ios') && <Icon16Chevron className={styles.icon} />} 28 | </div> 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 <Text {...props} />; 19 | } 20 | 21 | return <Subheadline level="1" {...props} />; 22 | }, [isIOS]); 23 | 24 | const Description = useCallback((props: TypographyProps) => { 25 | if (isIOS) { 26 | return <Caption {...props} />; 27 | } 28 | 29 | return <Subheadline level="2" {...props} />; 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<typeof IconButton>; 15 | 16 | export default meta; 17 | type Story = StoryObj<typeof meta>; 18 | 19 | export const Playground: Story = { 20 | args: { 21 | size: 's', 22 | mode: 'bezeled', 23 | }, 24 | render: (args) => ( 25 | <IconButton 26 | {...args} 27 | > 28 | {args.size === 's' && <Icon20QuestionMark />} 29 | {args.size === 'm' && <Icon24QR />} 30 | {args.size === 'l' && <Icon28Stats />} 31 | </IconButton> 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<HTMLDivElement> {} 7 | 8 | export const IconContainer = ({ className, children, ...restProps }: IconContainerProps) => ( 9 | <div className={classNames(styles.wrapper, className)} {...restProps}> 10 | {children} 11 | </div> 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 | <Badge 17 | type="number" 18 | className={classNames(styles.wrapper, className)} 19 | {...restProps} 20 | /> 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<typeof InlineButtons>; 17 | 18 | export default meta; 19 | type Story = StoryObj<typeof meta>; 20 | 21 | export const Playground: Story = { 22 | args: { 23 | mode: 'plain', 24 | children: [ 25 | <InlineButtons.Item text="Chat"> 26 | <Icon24Chat /> 27 | </InlineButtons.Item>, 28 | <InlineButtons.Item text="Mute"> 29 | <Icon24Notifications /> 30 | </InlineButtons.Item>, 31 | <InlineButtons.Item text="QR"> 32 | <Icon24QR /> 33 | </InlineButtons.Item>, 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<InlineButtonsContextProps>({}); 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<typeof InlineButtonsItem>; 15 | 16 | export default meta; 17 | type Story = StoryObj<typeof meta>; 18 | 19 | export const Playground: Story = { 20 | args: { 21 | mode: 'plain', 22 | text: 'Chat', 23 | children: <Icon24Chat />, 24 | }, 25 | render: (args) => ( 26 | <div style={{ maxWidth: 160 }}> 27 | <InlineButtonsItem {...args} /> 28 | </div> 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<typeof List>; 13 | 14 | export default meta; 15 | type Story = StoryObj<typeof meta>; 16 | 17 | const PreparedSection = () => ( 18 | <Section 19 | header="Personal Information" 20 | footer="The official Telegram app is available for Android, iPhone, iPad, Windows, macOS and Linux." 21 | > 22 | <Input 23 | header="First name" 24 | placeholder="21 y.o. designer from San Francisco" 25 | /> 26 | </Section> 27 | ); 28 | 29 | export const Playground: Story = { 30 | args: {}, 31 | render: () => ( 32 | <List> 33 | <List style={{ background: 'var(--tgui--secondary_bg_color)' }}> 34 | <PreparedSection /> 35 | <PreparedSection /> 36 | <PreparedSection /> 37 | </List> 38 | </List> 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<HTMLDivElement> { 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 | <Component 28 | className={classNames( 29 | styles.wrapper, 30 | platform === 'ios' && styles['wrapper--ios'], 31 | className, 32 | )} 33 | {...restProps} 34 | > 35 | {children} 36 | </Component> 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<typeof SectionFooter>; 11 | 12 | export default meta; 13 | type Story = StoryObj<typeof meta>; 14 | 15 | export const Playground: Story = { 16 | args: { 17 | children: 'SectionFooter', 18 | }, 19 | decorators: [ 20 | (StoryComponent) => ( 21 | <div style={{ background: 'var(--tgui--secondary_bg_color)' }}> 22 | <StoryComponent /> 23 | </div> 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<typeof SectionHeader>; 11 | 12 | export default meta; 13 | type Story = StoryObj<typeof meta>; 14 | 15 | export const Playground: Story = { 16 | render: (args) => ( 17 | <> 18 | <SectionHeader {...args}>{args.children || 'Usual title'}</SectionHeader> 19 | <SectionHeader large {...args}>{args.children || 'Large title'}</SectionHeader> 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<HTMLHeadElement> { 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 | <header 23 | className={classNames( 24 | styles.wrapper, 25 | platform === 'ios' && styles['wrapper--ios'], 26 | large && styles['wrapper--large'], 27 | className, 28 | )} 29 | {...restProps} 30 | > 31 | <Component Component="h1" className={styles.title}>{children}</Component> 32 | </header> 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 <Caption caps {...restProps} />; 16 | } 17 | 18 | return <Subheadline level="2" weight="2" {...restProps} />; 19 | }; 20 | 21 | const Large = ({ ...restProps }: TypographyProps) => { 22 | if (platform === 'ios') { 23 | return <Subheadline level="1" weight="2" {...restProps} />; 24 | } 25 | 26 | return <Text weight="2" {...restProps} />; 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<typeof Steps>; 9 | 10 | export default meta; 11 | type Story = StoryObj<typeof meta>; 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<HTMLDivElement> { 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 | <div className={classNames(styles.wrapper, className)}> 19 | {Array.from({ length: count }, (_, i) => ( 20 | <div 21 | key={i} 22 | className={classNames(styles.step, { 23 | [styles['step--active']]: i < progress, 24 | })} 25 | /> 26 | ))} 27 | </div> 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<typeof Timeline>; 11 | 12 | export default meta; 13 | type Story = StoryObj<typeof meta>; 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 | <Timeline.Item key={item.key} header={item.header}> 48 | {item.children} 49 | </Timeline.Item> 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<typeof TimelineItem>; 9 | 10 | export default meta; 11 | type Story = StoryObj<typeof meta>; 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<typeof CircularProgress>; 9 | 10 | export default meta; 11 | type Story = StoryObj<typeof meta>; 12 | 13 | export const Playground: Story = { 14 | render: (args) => ( 15 | <> 16 | <CircularProgress size="small" progress={10} {...args} /> <br /> 17 | <CircularProgress size="medium" progress={50} {...args} /> <br /> 18 | <CircularProgress size="large" progress={80} {...args} /> <br /> 19 | </> 20 | ), 21 | decorators: [ 22 | (StoryComponent) => ( 23 | <div style={{ 24 | display: 'flex', 25 | gap: 20, 26 | }}> 27 | <StoryComponent /> 28 | </div> 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<typeof Progress>; 9 | 10 | export default meta; 11 | type Story = StoryObj<typeof meta>; 12 | 13 | export const Playground: Story = { 14 | render: (args) => ( 15 | <div style={{ 16 | width: '400px', 17 | border: '1px dashed #9747FF', 18 | borderRadius: '5px', 19 | padding: '20px', 20 | }}> 21 | <Progress value={20} {...args} /> <br /> 22 | <Progress value={40} {...args} /> <br /> 23 | <Progress value={60} {...args} /> <br /> 24 | <Progress value={80} {...args} /> <br /> 25 | <Progress value={100} {...args} /> 26 | </div> 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<typeof Skeleton>; 10 | 11 | export default meta; 12 | type Story = StoryObj<typeof meta>; 13 | 14 | export const Playground: Story = { 15 | render: (args) => ( 16 | <div style={{ 17 | width: '400px', 18 | border: '1px dashed #9747FF', 19 | borderRadius: '5px', 20 | padding: '20px', 21 | }}> 22 | <Skeleton {...args}> 23 | <Cell subtitle="That's live">Hello!!!!</Cell> 24 | </Skeleton> 25 | </div> 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<HTMLDivElement> { 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 | <div 25 | className={classNames( 26 | styles.wrapper, 27 | visible && styles['wrapper--visible'], 28 | withoutAnimation && styles['wrapper--noAnimation'], 29 | className, 30 | )} 31 | {...restProps} 32 | > 33 | {children} 34 | </div> 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<HTMLButtonElement> {} 9 | 10 | export const SnackbarButton = ({ className, children, ...restProps }: SnackbarButtonProps) => ( 11 | <Tappable 12 | Component="button" 13 | className={classNames(styles.wrapper, className)} 14 | {...restProps} 15 | > 16 | {children} 17 | </Tappable> 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<typeof Spinner>; 11 | 12 | export default meta; 13 | 14 | export const Playground: StoryObj<SpinnerProps> = { 15 | render: (args) => ( 16 | <div style={{ 17 | width: '400px', 18 | border: '1px dashed #9747FF', 19 | borderRadius: '5px', 20 | padding: '20px', 21 | }}> 22 | <Spinner {...args} size="s" /> <br /> 23 | <Spinner {...args} size="m" /> <br /> 24 | <Spinner {...args} size="l" /> <br /> 25 | </div> 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 | <Component> 28 | <animateTransform 29 | attributeName="transform" 30 | attributeType="XML" 31 | type="rotate" 32 | from={`0 ${rotateCenter} ${rotateCenter}`} 33 | to={`360 ${rotateCenter} ${rotateCenter}`} 34 | dur="0.7s" 35 | repeatCount="indefinite" 36 | /> 37 | </Component> 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 | <svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <use xlinkHref="#spinner_44" fill="none"> 6 | {children} 7 | </use> 8 | <symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44" id="spinner_44"> 9 | <path 10 | d="M22 4C25.1288 4 28.2036 4.81556 30.9211 6.36624C33.6386 7.91693 35.9049 10.1492 37.4967 12.8429C39.0884 15.5365 39.9505 18.5986 39.9979 21.727C40.0454 24.8555 39.2765 27.9423 37.7672 30.683C36.258 33.4237 34.0603 35.7236 31.3911 37.356C28.7219 38.9884 25.6733 39.8968 22.5459 39.9917C19.4185 40.0866 16.3204 39.3647 13.5571 37.8971C10.7939 36.4296 8.46085 34.2671 6.78817 31.6229" 11 | stroke="currentColor" strokeWidth="4" strokeLinecap="round" /> 12 | </symbol> 13 | </svg> 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 | <svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 6 | <use xlinkHref="#spinner_36" fill="none"> 7 | {children} 8 | </use> 9 | <symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" id="spinner_36"> 10 | <path 11 | d="M18 4c2.4335 0 4.825.63432 6.9386 1.84041S28.815 8.7827 30.053 10.8778c1.238 2.0951 1.9085 4.4766 1.9454 6.9099.0369 2.4332-.5611 4.8341-1.735 6.9657-1.1739 2.1317-2.8831 3.9205-4.9592 5.1902-2.0761 1.2696-4.4472 1.9762-6.8796 2.05-2.4324.0738-4.842-.4877-6.9913-1.6292-2.14918-1.1414-3.96375-2.8234-5.26472-4.8799" 12 | stroke="currentColor" strokeWidth="3" strokeLinecap="round" /> 13 | </symbol> 14 | </svg> 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 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <use xlinkHref="#spinner_24" fill="none"> 6 | {children} 7 | </use> 8 | <symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="spinner_24"> 9 | <path 10 | d="M12 3c1.5644 0 3.1018.40778 4.4605 1.18312 1.3588.77535 2.492 1.89147 3.2878 3.23831.7959 1.34683 1.2269 2.87787 1.2507 4.44207.0237 1.5642-.3607 3.1076-1.1154 4.478-.7546 1.3703-1.8534 2.5203-3.188 3.3365-1.3347.8162-2.859 1.2704-4.4227 1.3179-1.5636.0474-3.11269-.3136-4.49433-1.0473-1.38163-.7338-2.54815-1.8151-3.38448-3.1371" 11 | stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" /> 12 | </symbol> 13 | </svg> 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 <IconLarge />; 14 | case 'm': 15 | return <IconMedium />; 16 | default: 17 | return <IconSmall />; 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<typeof Spoiler>; 10 | 11 | export default meta; 12 | type Story = StoryObj<typeof meta>; 13 | 14 | export const Playground: Story = { 15 | render: (args) => ( 16 | <div> 17 | <Spoiler {...args}> 18 | <div style={{ width: 200, height: 200, background: 'yellowgreen' }} /> 19 | </Spoiler> 20 | <br /> 21 | <Spoiler {...args}> 22 | <Cell description="Pass Component='label' to Cell to make it clickable."> 23 | First radio 24 | </Cell> 25 | </Spoiler> 26 | </div> 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<typeof Checkbox>; 10 | 11 | export default meta; 12 | type Story = StoryObj<typeof meta>; 13 | 14 | export const Playground: Story = { 15 | args: { 16 | defaultChecked: true, 17 | }, 18 | render: (args) => ( 19 | <Placeholder description="This component wraps input with type=checkbox, see usage example on the With Cell page"> 20 | <Checkbox {...args} /> 21 | </Placeholder> 22 | ), 23 | } satisfies Story; 24 | 25 | export const WithCells: Story = { 26 | render: (args) => ( 27 | <> 28 | <Cell 29 | Component="label" 30 | before={<Checkbox name="checkbox" value="1" {...args} />} 31 | description="Pass Component='label' to Cell to make it clickable." 32 | multiline 33 | > 34 | Apples 35 | </Cell> 36 | <Cell 37 | Component="label" 38 | before={<Checkbox name="checkbox" value="2" {...args} />} 39 | description="Pass Component='label' to Cell to make it clickable." 40 | multiline 41 | > 42 | Milk 43 | </Cell> 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 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path 6 | d="M6.4 1h7.2c1.14 0 1.93 0 2.55.05.6.05.95.14 1.21.28a3 3 0 0 1 1.31 1.3c.14.27.23.62.28 1.22.05.62.05 1.41.05 2.55v7.2c0 1.14 0 1.93-.05 2.55-.05.6-.14.95-.28 1.21a3 3 0 0 1-1.3 1.31c-.27.14-.62.23-1.22.28-.62.05-1.41.05-2.55.05H6.4c-1.14 0-1.93 0-2.55-.05-.6-.05-.95-.14-1.21-.28a3 3 0 0 1-1.31-1.3 3.2 3.2 0 0 1-.28-1.22A34.7 34.7 0 0 1 1 13.6V6.4c0-1.14 0-1.93.05-2.55.05-.6.14-.95.28-1.21a3 3 0 0 1 1.3-1.31 3.2 3.2 0 0 1 1.22-.28C4.47 1 5.26 1 6.4 1Z" 7 | stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/components/Form/Checkbox/icons/checkbox_checked.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconCheckboxChecked = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M.48 2.87C0 3.88 0 5.2 0 7.8v4.4c0 2.61 0 3.92.48 4.93a5 5 0 0 0 2.4 2.4c1 .47 2.3.47 4.92.47h4.4c2.61 0 3.92 0 4.93-.48a5 5 0 0 0 2.4-2.4c.47-1 .47-2.3.47-4.92V7.8c0-2.61 0-3.92-.48-4.93a5 5 0 0 0-2.4-2.4C16.13 0 14.83 0 12.2 0H7.8C5.19 0 3.88 0 2.87.48a5 5 0 0 0-2.4 2.4ZM15.7 7.46a1 1 0 0 0-1.42-1.42L8 12.34l-2.3-2.3a1 1 0 1 0-1.4 1.42l3 3a1 1 0 0 0 1.4 0l7-7Z" 7 | fill="currentColor" /> 8 | <path fillRule="evenodd" clipRule="evenodd" 9 | d="M15.7 7.46a1 1 0 0 0-1.4-1.42L8 12.34l-2.3-2.3a1 1 0 1 0-1.4 1.42l3 3a1 1 0 0 0 1.4 0l7-7Z" fill="#fff" /> 10 | </svg> 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/Form/Checkbox/icons/checkbox_indeterminate.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconCheckboxIndeterminate = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M6.4 0h7.2c2.24 0 3.36 0 4.22.44a4 4 0 0 1 1.74 1.74c.44.86.44 1.98.44 4.22v7.2c0 2.24 0 3.36-.44 4.22a4 4 0 0 1-1.74 1.74c-.86.44-1.98.44-4.22.44H6.4c-2.24 0-3.36 0-4.22-.44a4 4 0 0 1-1.74-1.74C0 16.96 0 15.84 0 13.6V6.4c0-2.24 0-3.36.44-4.22A4 4 0 0 1 2.18.44C3.04 0 4.16 0 6.4 0ZM4 10a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1Z" 7 | fill="currentColor" /> 8 | <path d="M4 10a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1Z" fill="#fff" /> 9 | </svg> 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<typeof ColorInput>; 13 | 14 | export default meta; 15 | type Story = StoryObj<typeof meta>; 16 | 17 | export const Playground: Story = { 18 | render: () => ( 19 | <List style={{ background: 'var(--tgui--secondary_bg_color)', width: 500 }}> 20 | <ColorInput 21 | header="Color" 22 | placeholder="Select color" 23 | /> 24 | </List> 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<typeof FileInput>; 12 | 13 | export default meta; 14 | type Story = StoryObj<typeof meta>; 15 | 16 | export const Playground: Story = { 17 | render: (args) => { 18 | const [files, setFiles] = useState<FileList | null>(null); 19 | 20 | return ( 21 | <FileInput multiple onChange={(event) => setFiles(event.target.files)} {...args}> 22 | {files && Array.from(files).map((file) => ( 23 | <Cell key={file.name} subtitle={`${file.size} bytes`}>{file.name}</Cell> 24 | ))} 25 | </FileInput> 26 | ); 27 | }, 28 | decorators: [ 29 | (DecoratorStory) => ( 30 | <List> 31 | <Section 32 | header="Component includes only logic of input and label" 33 | footer="Listen to the onChange event to get the selected files. You can pass children to display the selected files." 34 | > 35 | <DecoratorStory /> 36 | </Section> 37 | </List> 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<HTMLInputElement> { 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<HTMLDivElement, FileInputProps>(({ 18 | label = 'Attach file', 19 | className, 20 | children, 21 | ...restProps 22 | }, ref) => ( 23 | <div ref={ref} className={className}> 24 | {children} 25 | <ButtonCell Component="label" before={<Icon28Attach />}> 26 | <VisuallyHidden> 27 | <input type="file" placeholder={label} {...restProps} /> 28 | </VisuallyHidden> 29 | {label} 30 | </ButtonCell> 31 | </div> 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 <Caption caps {...restProps} />; 14 | } 15 | 16 | return <Subheadline level="2" weight="2" {...restProps} />; 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 <Chip mode="mono" {...rest} />; 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 | <Cell 16 | {...props} 17 | after={props.selected ? <SelectedIcon className={styles.selectedIcon} /> : 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<FilterFn> 12 | ): ReturnType<FilterFn> { 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 = <O extends MultiselectOption>(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 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="2" /> 6 | </svg> 7 | ); 8 | -------------------------------------------------------------------------------- /src/components/Form/Multiselectable/icons/multiselectable_checked.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconMultiselectableChecked = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M10 20c5.523 0 10-4.477 10-10S15.523 0 10 0 0 4.477 0 10s4.477 10 10 10Z" fill="currentColor" /> 7 | <path fillRule="evenodd" clipRule="evenodd" 8 | d="M15.375 6.56a1 1 0 0 1-.036 1.415l-6.31 6a1 1 0 0 1-1.416-.037l-2.84-3a1 1 0 0 1 1.453-1.375l2.15 2.272 5.585-5.31a1 1 0 0 1 1.414.036Z" 9 | fill="#fff" /> 10 | </svg> 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/Form/Multiselectable/icons/multiselectable_ios.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconMultiselectableIOS = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <circle cx="12" cy="12" r="11" stroke="currentColor" strokeWidth="2" /> 6 | </svg> 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 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M12 24a12 12 0 1 0 0-24 12 12 0 0 0 0 24Zm4.78-17.1a1 1 0 0 1 .32 1.38l-5.63 9a1 1 0 0 1-1.62.1l-3.37-4.12a1 1 0 1 1 1.54-1.27l2.5 3.05 4.88-7.82a1 1 0 0 1 1.38-.32Z" 7 | fill="currentColor" /> 8 | </svg> 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<typeof PinInput>; 11 | 12 | export default meta; 13 | type Story = StoryObj<typeof meta>; 14 | 15 | export const Playground: Story = { 16 | decorators: [ 17 | (Component) => ( 18 | <div style={{ height: '600px' }}> 19 | <Component /> 20 | </div> 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<HTMLButtonElement> {} 13 | 14 | export const ButtonTypography = (props: TypographyProps) => { 15 | const platform = usePlatform(); 16 | 17 | if (platform === 'ios') { 18 | return <LargeTitle {...props} />; 19 | } 20 | 21 | return <Title {...props} />; 22 | }; 23 | 24 | export const PinInputButton = ({ 25 | children, 26 | ...restProps 27 | }: PinInputButtonProps) => { 28 | const platform = usePlatform(); 29 | 30 | return ( 31 | <Tappable 32 | Component="button" 33 | className={classNames( 34 | styles.wrapper, 35 | platform === 'ios' && styles['wrapper--ios'], 36 | )} 37 | {...restProps} 38 | > 39 | <ButtonTypography>{children}</ButtonTypography> 40 | </Tappable> 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<HTMLInputElement> { 10 | isTyped?: boolean; 11 | } 12 | 13 | export const PinInputCell = forwardRef<HTMLLabelElement, PinInputCellProps>(({ 14 | isTyped, 15 | ...restProps 16 | }, ref) => { 17 | const platform = usePlatform(); 18 | const isIOS = platform === 'ios'; 19 | 20 | return ( 21 | <label 22 | ref={ref} 23 | className={classNames( 24 | styles.wrapper, 25 | isIOS && styles['wrapper--ios'], 26 | isTyped && styles['wrapper--typed'], 27 | )} 28 | > 29 | <VisuallyHidden 30 | Component="input" 31 | type="number" 32 | maxLength={1} 33 | className={styles.input} 34 | {...restProps} 35 | /> 36 | {isTyped && !isIOS && <div className={styles.dot} />} 37 | </label> 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<typeof Radio>; 11 | 12 | export default meta; 13 | type Story = StoryObj<typeof meta>; 14 | 15 | export const Playground: Story = { 16 | args: { 17 | defaultChecked: true, 18 | }, 19 | render: (args) => ( 20 | <Placeholder description="This component wraps input with type=radio, see usage example on the With Cell page"> 21 | <Radio {...args} /> 22 | </Placeholder> 23 | ), 24 | } satisfies Story; 25 | 26 | export const WithCells: Story = { 27 | render: (args) => ( 28 | <form> 29 | <Cell 30 | Component="label" 31 | before={<Radio name="radio" value="1" {...args} />} 32 | description="Pass Component='label' to Cell to make it clickable." 33 | multiline 34 | > 35 | First radio 36 | </Cell> 37 | <Cell 38 | Component="label" 39 | before={<Radio name="radio" value="2" {...args} />} 40 | description="Pass Component='label' to Cell to make it clickable." 41 | multiline 42 | > 43 | Second radio 44 | </Cell> 45 | </form> 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<HTMLInputElement> { 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<HTMLInputElement, RadioProps>(({ 19 | style, 20 | className, 21 | disabled, 22 | ...restProps 23 | }, ref) => ( 24 | <label 25 | className={classNames( 26 | styles.wrapper, 27 | disabled && styles['wrapper--disabled'], 28 | className, 29 | )} 30 | > 31 | <VisuallyHidden 32 | {...restProps} 33 | Component="input" 34 | type="radio" 35 | className={styles.input} 36 | disabled={disabled} 37 | ref={ref} 38 | /> 39 | <IconRadio className={styles.icon} aria-hidden /> 40 | <IconRadioChecked className={styles.checkedIcon} aria-hidden /> 41 | </label> 42 | )); 43 | -------------------------------------------------------------------------------- /src/components/Form/Radio/icons/radio.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconRadio = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm0 2a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z" fill="currentColor" /> 7 | </svg> 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/Form/Radio/icons/radio_checked.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconRadioChecked = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm0 2a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z" fill="currentColor" /> 7 | <path d="M15 10a5 5 0 1 1-10 0 5 5 0 0 1 10 0Z" fill="currentColor" /> 8 | </svg> 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<typeof Rating>; 13 | 14 | export default meta; 15 | type Story = StoryObj<typeof meta>; 16 | 17 | export const Playground: Story = { 18 | render: (args) => ( 19 | <Section 20 | header="Navigate with tabs!" 21 | footer="Use the keyboard to navigate between the stars and also click on them" 22 | > 23 | <Rating {...args} /> 24 | </Section> 25 | ), 26 | } satisfies Story; 27 | 28 | export const CustomIcon: Story = { 29 | render: (args) => ( 30 | <Section header="We use custom icon here"> 31 | <Rating IconContainer={Icon28Heart} {...args} /> 32 | </Section> 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 | <svg width="40" height="40" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> 5 | <path 6 | d="M16.228 9.993c1.166-3.164 1.75-4.746 2.598-5.199a2.492 2.492 0 0 1 2.348 0c.849.453 1.432 2.035 2.598 5.199l.562 1.525c.337.914.506 1.372.796 1.715.257.303.58.54.945.694.413.173.895.194 1.86.235l1.608.07c3.338.143 5.006.215 5.694.89a2.56 2.56 0 0 1 .726 2.258c-.164.955-1.472 2.005-4.088 4.104l-1.262 1.011c-.756.607-1.134.91-1.367 1.296-.206.34-.33.725-.361 1.123-.036.45.094.92.353 1.86l.432 1.568c.896 3.253 1.345 4.88.921 5.75a2.518 2.518 0 0 1-1.9 1.395c-.949.137-2.34-.796-5.124-2.663l-1.341-.9c-.805-.54-1.207-.809-1.642-.914a2.488 2.488 0 0 0-1.168 0c-.435.105-.837.375-1.642.914l-1.341.9c-2.783 1.867-4.175 2.8-5.124 2.663a2.518 2.518 0 0 1-1.9-1.396c-.424-.87.025-2.496.921-5.749l.432-1.568c.26-.94.389-1.41.353-1.86a2.563 2.563 0 0 0-.361-1.123c-.233-.386-.611-.689-1.367-1.296l-1.262-1.011c-2.616-2.1-3.924-3.149-4.088-4.104a2.56 2.56 0 0 1 .726-2.258c.688-.675 2.356-.747 5.694-.89l1.608-.07c.965-.041 1.447-.062 1.86-.235.364-.153.688-.391.945-.694.29-.343.459-.8.796-1.715l.562-1.525Z" 7 | fill="currentColor" opacity=".8" /> 8 | </svg> 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<typeof Select>; 13 | 14 | export default meta; 15 | 16 | export const Playground: StoryObj<SelectProps> = { 17 | render: () => ( 18 | <List style={{ 19 | width: 240, 20 | background: 'var(--tgui--secondary_bg_color)', 21 | }}> 22 | <Select header="Select"> 23 | <option>Hello</option> 24 | <option>Okay</option> 25 | </Select> 26 | </List> 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 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path d="M2.5 10.82 7 15.75l10.5-11.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" 6 | strokeLinejoin="round" /> 7 | </svg> 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/Form/Selectable/icons/selectable_ios.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconSelectableIOS = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path 6 | d="M8.17 18a1.5 1.5 0 0 1-1.2-.63l-4.6-5.82a1.73 1.73 0 0 1-.29-.46A1.42 1.42 0 0 1 2 10.6a1.22 1.22 0 0 1 1.25-1.26c.41 0 .75.18 1.03.54l3.86 5.02 7.52-12.24c.16-.25.32-.42.48-.51.17-.1.38-.16.63-.16A1.2 1.2 0 0 1 18 3.23c0 .15-.02.3-.07.44-.05.15-.12.3-.22.46l-8.32 13.2c-.28.45-.69.67-1.22.67Z" 7 | fill="currentColor" /> 8 | </svg> 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 | <div 21 | key={XCoordinate} 22 | className={classNames(styles.step, { 23 | [styles['step--ios']]: platform === 'ios', 24 | [styles['step--passed']]: isPassed, 25 | })} 26 | style={{ left: `${XCoordinate}%` }} 27 | /> 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<HTMLSpanElement> { 12 | inputProps?: InputHTMLAttributes<HTMLInputElement>; 13 | withTooltip?: boolean; 14 | } 15 | 16 | export const SliderThumb = forwardRef<HTMLSpanElement, SliderThumbProps>( 17 | ({ className, inputProps, withTooltip, ...restProps }, ref) => { 18 | const platform = usePlatform(); 19 | 20 | return ( 21 | <span 22 | className={classNames( 23 | styles.wrapper, 24 | platform === 'ios' && styles['wrapper--ios'], 25 | className, 26 | )} 27 | {...restProps} 28 | > 29 | <VisuallyHidden 30 | {...inputProps} 31 | Component="input" 32 | type="range" 33 | ref={ref} 34 | className={classNames(styles.input, className)} 35 | aria-orientation="horizontal" 36 | /> 37 | </span> 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 = <T extends AriaAttributes>( 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 = <T extends (EventTarget & HTMLElement) | null>( 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<typeof Switch>; 10 | 11 | export default meta; 12 | type Story = StoryObj<typeof meta>; 13 | 14 | export const Playground: Story = { 15 | args: { 16 | defaultChecked: true, 17 | }, 18 | render: (args) => ( 19 | <Placeholder description="This component wraps input with type=checkbox, see usage example on the With Cell page"> 20 | <div style={{ display: 'flex', gap: '6px' }}> 21 | <Switch {...args} /> <br /> 22 | <Switch defaultChecked {...args} /> <br /> 23 | <Switch disabled {...args} /> <br /> 24 | <Switch disabled checked {...args} /> <br /> 25 | </div> 26 | </Placeholder> 27 | ), 28 | } satisfies Story; 29 | 30 | export const WithCell: Story = { 31 | render: (args) => ( 32 | <Cell 33 | Component="label" 34 | after={<Switch defaultChecked {...args} />} 35 | description="Pass Component='label' to Cell to make it clickable." 36 | multiline 37 | > 38 | First radio 39 | </Cell> 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<typeof Textarea>; 13 | 14 | export default meta; 15 | type Story = StoryObj<typeof meta>; 16 | 17 | export const Playground: Story = { 18 | render: () => ( 19 | <List style={{ background: 'var(--tgui--secondary_bg_color)', width: 240 }}> 20 | <Textarea 21 | header="Textarea" 22 | placeholder="I am usual textarea" 23 | /> 24 | </List> 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<typeof FixedLayout>; 13 | 14 | export default meta; 15 | type Story = StoryObj<typeof meta>; 16 | 17 | export const Playground: Story = { 18 | render: () => ( 19 | <div style={{ height: 200, width: 400 }}> 20 | <FixedLayout vertical="top" style={{ padding: 16 }}> 21 | <Button size="l" stretched> 22 | This is FixedLayout with top vertical 23 | </Button> 24 | </FixedLayout> 25 | <FixedLayout style={{ padding: 16 }}> 26 | <Button size="l" stretched> 27 | This is FixedLayout with default vertical 28 | </Button> 29 | </FixedLayout> 30 | </div> 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<typeof TabbarItem>; 12 | 13 | export default meta; 14 | 15 | export const Playground: StoryObj<TabbarItemProps> = { 16 | args: { 17 | text: 'Hello', 18 | children: <Icon28Devices />, 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<typeof Divider>; 10 | 11 | export default meta; 12 | type Story = StoryObj<typeof meta>; 13 | 14 | export const Playground: Story = { 15 | render: (args) => ( 16 | <List style={{ padding: 16, background: 'var(--tgui--secondary_bg_color)' }}> 17 | <div style={{ background: 'var(--tgui--bg_color)' }}> 18 | <Cell>Divider is under</Cell> 19 | <Divider {...args} /> 20 | <Cell>Divider is above</Cell> 21 | </div> 22 | </List> 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<HTMLHRElement> {} 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 | <hr className={classNames(styles.wrapper, className)} {...restProps} /> 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<HTMLElement> { 9 | Component?: ElementType; 10 | } 11 | 12 | export const BreadCrumbsItem = ({ 13 | Component = 'div', 14 | className, 15 | children, 16 | ...restProps 17 | }: BreadCrumbsItemProps) => ( 18 | <Component className={classNames(styles.wrapper, className)} {...restProps}> 19 | <Subheadline level="2" weight="2">{children}</Subheadline> 20 | </Component> 21 | ); 22 | -------------------------------------------------------------------------------- /src/components/Navigation/Breadcrumbs/icons/dot.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconDot = ({ ...restProps }: Icon) => ( 4 | <svg width="21" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <circle cx="10.5" cy="10" r="2" fill="currentColor" /> 6 | </svg> 7 | ); 8 | -------------------------------------------------------------------------------- /src/components/Navigation/Breadcrumbs/icons/slash.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const IconSlash = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path d="M13 5L8 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /> 6 | </svg> 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<typeof CompactPaginationItem>; 12 | 13 | export default meta; 14 | 15 | export const Playground: StoryObj<CompactPaginationItemProps> = { 16 | decorators: [ 17 | (Story) => ( 18 | <> 19 | <Subheadline> 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 | </Subheadline> 23 | <br /> 24 | <Story /> 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<HTMLButtonElement> { 10 | selected?: boolean; 11 | } 12 | 13 | export const CompactPaginationItem = ({ 14 | selected, 15 | className, 16 | children, 17 | ...restProps 18 | }: CompactPaginationItemProps) => ( 19 | <button 20 | type="button" 21 | role="tab" 22 | aria-selected={selected} 23 | className={classNames( 24 | styles.wrapper, 25 | selected && styles['wrapper--selected'], 26 | className, 27 | )} 28 | {...restProps} 29 | > 30 | {hasReactNode(children) ? <VisuallyHidden>{children}</VisuallyHidden> : undefined} 31 | </button> 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<HTMLAnchorElement> {} 7 | 8 | export const Link = ({ className, children, ...restProps }: LinkProps) => ( 9 | <a 10 | className={classNames(styles.wrapper, className)} 11 | {...restProps} 12 | > 13 | {children} 14 | </a> 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<typeof Pagination>; 9 | 10 | export default meta; 11 | 12 | export const Playground: StoryObj<PaginationProps> = {}; 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<unknown>, 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<Element, Event>) => 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<typeof SegmentedControlItem>; 9 | 10 | export default meta; 11 | 12 | export const Playground: StoryObj<SegmentedControlItemProps> = { 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<typeof TabsItem>; 9 | 10 | export default meta; 11 | 12 | export const Playground: StoryObj<TabsItemProps> = { 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<typeof ModalClose>; 10 | 11 | export default meta; 12 | 13 | export const Playground: StoryObj<ModalCloseProps> = { 14 | render: () => ( 15 | <Placeholder 16 | description="This is a modal closer component. Wrap any component in Modal.Close and it will close after the click (or fire event onOpenChange if modal is controlled)" 17 | /> 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 | <Drawer.Close asChild {...props} /> 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<typeof ModalHeader>; 12 | 13 | export default meta; 14 | 15 | export const Playground: StoryObj<ModalHeaderProps> = { 16 | args: { 17 | children: 'Only iOS header', 18 | }, 19 | render: (args) => ( 20 | <Placeholder description="This is a modal header. If you want to see the text title, change the platform to iOS"> 21 | <ModalHeader {...args} /> 22 | </Placeholder> 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<HTMLElement> { 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<HTMLElement, ModalHeaderProps>(({ 17 | before, 18 | after, 19 | className, 20 | children, 21 | ...props 22 | }, ref) => { 23 | const platform = usePlatform(); 24 | 25 | return ( 26 | <header 27 | ref={ref} 28 | className={classNames(styles.wrapper, className)} 29 | {...props} 30 | > 31 | <div className={styles.before}> 32 | {before} 33 | </div> 34 | {platform === 'ios' && <Text weight="2" className={styles.children}>{children}</Text>} 35 | <div className={styles.after}> 36 | {after} 37 | </div> 38 | </header> 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<SVGSVGElement>) => ( 11 | <svg 12 | width={DEFAULT_ARROW_WIDTH} 13 | height={ARROW_HEIGHT_WITH_WHITE_SPACE} 14 | viewBox={`0 0 ${DEFAULT_ARROW_WIDTH} ${ARROW_HEIGHT_WITH_WHITE_SPACE}`} 15 | xmlns="http://www.w3.org/2000/svg" 16 | {...props} 17 | > 18 | <path d="M10.804 0C6.387 0 6.94 6 .865 6h19.878c-6.074 0-5.521-6-9.939-6Z" fill="currentColor" /> 19 | </svg> 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<HTMLDivElement>; 9 | isRendered: boolean; 10 | } 11 | 12 | export const AppRootContext = createContext<AppRootContextInterface>({ 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<AppRootContextInterface['appearance']>) => 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<AppRootContextInterface['platform']> => { 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<AppRootContextInterface['portalContainer']> => { 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<HTMLElement> { 7 | Component?: ElementType; 8 | } 9 | 10 | export const HorizontalScroll = ({ 11 | Component = 'div', 12 | className, 13 | children, 14 | ...restProps 15 | }: HorizontalScrollProps) => ( 16 | <Component className={classNames(styles.wrapper, className)} {...restProps}> 17 | {children} 18 | </Component> 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 | <span 16 | aria-hidden 17 | className={styles.wrapper} 18 | > 19 | {clicks.map((wave) => ( 20 | <span 21 | key={wave.date} 22 | className={styles.wave} 23 | style={{ 24 | top: wave.y, 25 | left: wave.x, 26 | }} 27 | /> 28 | ))} 29 | </span> 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<T> extends AllHTMLAttributes<T> { 7 | Component?: ElementType; 8 | } 9 | 10 | export const VisuallyHidden = forwardRef<HTMLSpanElement, VisuallyHiddenProps<HTMLSpanElement>>( 11 | ({ Component = 'span', className, ...restProps }, ref) => ( 12 | <Component {...restProps} ref={ref} className={classNames(styles.wrapper, className)} /> 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<typeof Caption>; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj<typeof meta>; 15 | 16 | export const Caption1: Story = { 17 | args: { 18 | level: '1', 19 | }, 20 | render: (args) => ( 21 | <> 22 | <Caption weight="3" {...args}> 23 | Caption 1 · Regular 24 | </Caption> 25 | <br /><br /> 26 | <Caption weight="2" {...args}> 27 | Caption 1 · Semibold 28 | </Caption> 29 | <br /><br /> 30 | <Caption weight="1" {...args}> 31 | Caption 1 · Bold 32 | </Caption> 33 | </> 34 | ), 35 | }; 36 | 37 | export const Caption2: Story = { 38 | args: { 39 | level: '2', 40 | }, 41 | render: (args) => ( 42 | <> 43 | <Caption weight="3" {...args}> 44 | Caption 2 · Regular 45 | </Caption> 46 | <br /><br /> 47 | <Caption weight="2" {...args}> 48 | Caption 2 · Semibold 49 | </Caption> 50 | <br /><br /> 51 | <Caption weight="1" {...args}> 52 | Caption 2 · Bold 53 | </Caption> 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<TypographyProps, 'plain'> { 10 | /** The size level of the caption, influencing its styling and typography size. */ 11 | level?: CaptionLevel; 12 | } 13 | 14 | const captionLevelStyles: Record<CaptionLevel, string> = { 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 | <Typography 32 | {...restProps} 33 | className={classNames(styles.wrapper, captionLevelStyles[level], className)} 34 | Component={Component || 'span'} 35 | /> 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<typeof Headline>; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj<typeof meta>; 15 | 16 | export const Playground: Story = { 17 | args: { 18 | plain: false, 19 | }, 20 | render: (args) => ( 21 | <> 22 | <Headline weight="3" {...args}> 23 | Headline · Regular 24 | </Headline> 25 | <Headline weight="2" {...args}> 26 | Headline · Semibold 27 | </Headline> 28 | <Headline weight="1" {...args}> 29 | Headline · Bold 30 | </Headline> 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 `<h5>` 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 | <Typography 18 | {...restProps} 19 | className={classNames(styles.wrapper, className)} 20 | Component={Component || 'h5'} 21 | /> 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<typeof LargeTitle>; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj<typeof meta>; 15 | 16 | export const Playground: Story = { 17 | args: { 18 | plain: false, 19 | }, 20 | render: (args) => ( 21 | <> 22 | <LargeTitle weight="3" {...args}> 23 | Large Title · Regular 24 | </LargeTitle> 25 | <LargeTitle weight="2" {...args}> 26 | Large Title · Semibold 27 | </LargeTitle> 28 | <LargeTitle weight="1" {...args}> 29 | Large Title · Bold 30 | </LargeTitle> 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 `<h1>` 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 | <Typography 18 | {...restProps} 19 | Component={Component || 'h1'} 20 | className={classNames(styles.wrapper, className)} 21 | /> 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<typeof Text>; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj<typeof meta>; 15 | 16 | export const Playground: Story = { 17 | render: (args) => ( 18 | <> 19 | <Text weight="3" {...args}> 20 | Text · Regular 21 | </Text> 22 | <br /><br /> 23 | <Text weight="2" {...args}> 24 | Text · Semibold 25 | </Text> 26 | <br /><br /> 27 | <Text weight="1" {...args}> 28 | Text · Bold 29 | </Text> 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<TypographyProps, 'plain'> 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 | <Typography 24 | ref={ref} 25 | {...restProps} 26 | weight={weight} 27 | className={classNames(styles.wrapper, className)} 28 | Component={Component || 'span'} 29 | /> 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> = 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<typeof Keys>; 20 | 21 | export const getHorizontalSideByKey = ( 22 | keys: Extract<KeysValues, 'ArrowUp' | 'ArrowLeft' | 'ArrowDown' | 'ArrowRight'>, 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 = <T>(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 = <T extends Element>( 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 = <T>(element: T, ref?: Ref<T>): 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<T>).current = element; 10 | } 11 | } 12 | }; 13 | 14 | export const multipleRef = <T>(...refs: Array<Ref<T> | undefined>): RefObject<T> => { 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 <AppRoot> 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<T>( 8 | ...externRefs: Array<Ref<T> | undefined | false> 9 | ): MutableRefObject<T | null> { 10 | const stableRef = useRef<T | null>(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<ElementType> | 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 = <T>(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<AppRootContextInterface['platform']> => { 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<ReturnType<typeof setTimeout>>(); 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 | <svg width="12" height="12" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path 6 | d="M3.07 7.7c.14-.36-.14-.74-.5-.93A2 2 0 1 1 5.5 5V5c0 1.55-.27 2.67-.57 3.43a5.33 5.33 0 0 1-.67 1.22 1 1 0 0 1-1.53-1.3h.01l.07-.1c.06-.1.16-.28.26-.54ZM4.26 9.65ZM8.07 7.7c.14-.36-.14-.74-.5-.93A2 2 0 1 1 10.5 5V5c0 1.55-.27 2.67-.57 3.43a5.33 5.33 0 0 1-.67 1.22 1 1 0 0 1-1.53-1.3h.01l.07-.1c.06-.1.16-.28.26-.54ZM9.26 9.65Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/16/cancel.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon16Cancel = ({ ...restProps }: Icon) => ( 4 | <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M3.3 3.3a1 1 0 0 1 1.4 0L8 6.58l3.3-3.3a1 1 0 1 1 1.4 1.42L9.42 8l3.3 3.3a1 1 0 0 1-1.42 1.4L8 9.42l-3.3 3.3a1 1 0 0 1-1.4-1.42L6.58 8l-3.3-3.3a1 1 0 0 1 0-1.4Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/16/chevron.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon16Chevron = ({ ...restProps }: Icon) => ( 4 | <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path d="m6 3 5 5-5 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> 6 | </svg> 7 | ); 8 | -------------------------------------------------------------------------------- /src/icons/20/chevron_down.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon20ChevronDown = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M3.29289 6.29289c.39053-.39052 1.02369-.39052 1.41422 0L10 11.5858l5.2929-5.29291c.3905-.39052 1.0237-.39052 1.4142 0 .3905.39053.3905 1.02369 0 1.41422l-6 5.99999c-.3905.3905-1.02368.3905-1.41421 0l-6-5.99999c-.39052-.39053-.39052-1.02369 0-1.41422Z" 7 | fill="currentColor" /> 8 | </svg> 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/icons/20/select.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon20Select = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path 6 | d="M2.5 10.821 7 15.75l10.5-11.5" 7 | stroke="currentColor" 8 | strokeWidth="2" 9 | strokeLinecap="round" 10 | strokeLinejoin="round" 11 | /> 12 | </svg> 13 | ); 14 | -------------------------------------------------------------------------------- /src/icons/20/select_ios.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon20SelectIOS = ({ ...restProps }: Icon) => ( 4 | <svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path 6 | d="M8.174 18c-.473 0-.876-.21-1.208-.63l-4.602-5.82a1.727 1.727 0 0 1-.284-.465 1.423 1.423 0 0 1-.08-.474c0-.365.118-.666.355-.903s.536-.356.898-.356c.408 0 .752.18 1.03.539l3.856 5.017 7.525-12.242c.154-.243.313-.414.48-.51.165-.104.372-.156.621-.156.361 0 .657.116.889.347.23.23.346.526.346.884 0 .146-.024.292-.071.438a2.017 2.017 0 0 1-.222.456L9.39 17.335c-.284.443-.69.665-1.217.665Z" 7 | fill="currentColor" 8 | /> 9 | </svg> 10 | ); 11 | -------------------------------------------------------------------------------- /src/icons/24/cancel.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon24Cancel = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M4.5 4.44a.9.9 0 0 1 1.27 0L12 10.56l6.22-6.14a.9.9 0 0 1 1.27 1.28l-6.21 6.13 6.2 6.13a.9.9 0 0 1-1.26 1.28L12 13.1l-6.23 6.15a.9.9 0 1 1-1.26-1.28l6.2-6.13-6.2-6.13a.9.9 0 0 1-.01-1.27Z" 7 | fill="#A2ACB0" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/24/chat.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon24Chat = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M6.665 18.088A4.269 4.269 0 0 0 7 16.37c-1.54-1.259-2.5-3.04-2.5-5.017 0-3.815 3.582-6.908 8-6.908 4.419 0 8 3.093 8 6.908 0 3.816-3.581 6.909-8 6.909-.69 0-1.36-.076-2-.218-.423.464-1.236 1.062-2.59 1.539-.78.274-1.741.508-2.91.652.644-.635 1.288-1.27 1.665-2.148Zm4.38 1.88c.475.062.961.095 1.455.095 5.156 0 9.8-3.66 9.8-8.709 0-5.048-4.644-8.708-9.8-8.708-5.155 0-9.8 3.66-9.8 8.708 0 2.232.938 4.227 2.414 5.73-.175.65-.623 1.126-1.379 1.871a1.8 1.8 0 0 0 1.485 3.068c2.768-.341 4.648-1.165 5.824-2.056Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/24/chevron_down.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon24ChevronDown = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M4.3 7.54a1 1 0 0 1 1.4 0l6.8 6.8 6.8-6.8a1 1 0 1 1 1.4 1.42l-7.5 7.5a1 1 0 0 1-1.4 0l-7.5-7.5a1 1 0 0 1 0-1.42Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/24/chevron_left.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon24ChevronLeft = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M15.7071 3.79289c-.3905-.39052-1.0237-.39052-1.4142 0L6.79289 11.2929c-.39052.3905-.39052 1.0237 0 1.4142l7.50001 7.5c.3905.3905 1.0237.3905 1.4142 0 .3905-.3905.3905-1.0237 0-1.4142L8.91421 12l6.79289-6.79289c.3905-.39053.3905-1.02369 0-1.41422Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/24/chevron_right.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon24ChevronRight = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M7.29289 3.79289c.39053-.39052 1.02369-.39052 1.41422 0l7.49999 7.50001c.3905.3905.3905 1.0237 0 1.4142l-7.49999 7.5c-.39053.3905-1.02369.3905-1.41422 0-.39052-.3905-.39052-1.0237 0-1.4142L14.0858 12 7.29289 5.20711c-.39052-.39053-.39052-1.02369 0-1.41422Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/24/close.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon24Close = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <g opacity=".5" fill="#000" clipPath="url(#close_a)"> 6 | <path 7 | d="M12 24c6.5647 0 12-5.4471 12-12 0-6.56471-5.4471-12-12.0118-12C5.43529 0 0 5.43529 0 12c0 6.5529 5.44705 12 12 12Z" 8 | fillOpacity=".04" /> 9 | <path 10 | d="M7.86242 17.1429c-.56394 0-1.00528-.4542-1.00528-1.0187 0-.2701.09807-.5279.29422-.7121L10.5472 12 7.15136 8.60006c-.19615-.19637-.29422-.44187-.29422-.71189 0-.57689.44134-1.00648 1.00528-1.00648.28196 0 .50263.09819.69878.28231l3.4204 3.4122 3.4449-3.42448c.2084-.20866.4291-.29458.6988-.29458.5639 0 1.0176.44187 1.0176 1.00648 0 .28231-.0859.50324-.3066.72417L13.4282 12l3.3959 3.4c.2084.1841.3065.4417.3065.7242 0 .5645-.4536 1.0187-1.0298 1.0187-.282 0-.5395-.0982-.7234-.2947l-3.3958-3.4121-3.38363 3.4121c-.19613.1965-.45359.2947-.73555.2947Z" 11 | fillOpacity=".8" /> 12 | </g> 13 | <defs> 14 | <clipPath id="close_a"> 15 | <path fill="#fff" d="M0 0h24v24H0z" /> 16 | </clipPath> 17 | </defs> 18 | </svg> 19 | ); 20 | -------------------------------------------------------------------------------- /src/icons/24/person_remove.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon24PersonRemove = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M9.40001 3.89998c-1.47856 0-2.7 1.22035-2.7 2.75413s1.22144 2.75414 2.7 2.75414c1.47859 0 2.69999-1.22036 2.69999-2.75414 0-1.53378-1.2214-2.75413-2.69999-2.75413Zm-4.5 2.75413c0-2.50247 2.0021-4.55413 4.5-4.55413 2.49789 0 4.49999 2.05166 4.49999 4.55413 0 2.50247-2.0021 4.55409-4.49999 4.55409-2.4979 0-4.5-2.05162-4.5-4.55409Zm9.36629 8.79329c-.2335.4434-.3663.951-.3663 1.4924 0 1.758 1.4005 3.1602 3.1 3.1602.5337 0 1.0362-.1371 1.4755-.3801l-4.2092-4.2725Zm-.7352-2.0108c-.8855.8992-1.4311 2.1395-1.4311 3.5032 0 2.7267 2.1812 4.9602 4.9 4.9602 1.3573 0 2.5849-.5597 3.4696-1.4576.8843-.8977 1.4304-2.1375 1.4304-3.5026 0-2.7267-2.1812-4.9601-4.9-4.9601-1.3591 0-2.5838.5581-3.4689 1.4569Zm1.9938.7236 4.2086 4.2719c.2338-.4439.3665-.9515.3665-1.4923 0-1.758-1.4005-3.1601-3.1-3.1601-.5323 0-1.0353.1375-1.4751.3805ZM3.53465 13.847c.84767-.7943 1.88294-1.0553 2.66536-1.0553h4.37769c.497 0 .9.4029.9.9 0 .4971-.403.9-.9.9H6.20001c-.41759 0-.98231.145-1.43465.5688-.43226.405-.86535 1.1619-.86535 2.5914 0 .497-.40295.9-.9.9-.49706 0-.9-.403-.9-.9 0-1.8187.56691-3.0919 1.43464-3.9049Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/24/sun_low.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon24SunLow = ({ ...restProps }: Icon) => ( 4 | <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M12 15.45c1.9054 0 3.45-1.5446 3.45-3.45 0-1.9054-1.5446-3.45-3.45-3.45-1.9054 0-3.45 1.5446-3.45 3.45 0 1.9054 1.5446 3.45 3.45 3.45Zm0 1.8c2.8995 0 5.25-2.3505 5.25-5.25 0-2.89949-2.3505-5.25-5.25-5.25-2.89949 0-5.25 2.35051-5.25 5.25 0 2.8995 2.35051 5.25 5.25 5.25Z" 7 | fill="currentColor" /> 8 | <circle cx="18.5" cy="5.5" r="1" fill="currentColor" /> 9 | <circle cx="5.5" cy="5.5" r="1" fill="currentColor" /> 10 | <circle cx="20.5" cy="12" r="1" fill="currentColor" /> 11 | <circle cx="3.5" cy="12" r="1" fill="currentColor" /> 12 | <circle cx="18.5" cy="18.5" r="1" fill="currentColor" /> 13 | <path d="M13 20.5c0 .5523-.4477 1-1 1s-1-.4477-1-1 .4477-1 1-1 1 .4477 1 1Z" fill="currentColor" /> 14 | <circle cx="5.5" cy="18.5" r="1" fill="currentColor" /> 15 | <path d="M13 3.5c0 .55228-.4477 1-1 1s-1-.44772-1-1 .4477-1 1-1 1 .44772 1 1Z" fill="currentColor" /> 16 | </svg> 17 | ); 18 | -------------------------------------------------------------------------------- /src/icons/28/add_circle.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28AddCircle = ({ ...restProps }: Icon) => ( 4 | <svg width="29" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M14.5 3.9C8.92193 3.9 4.40001 8.42192 4.40001 14c0 5.5781 4.52192 10.1 10.09999 10.1 5.5781 0 10.1-4.5219 10.1-10.1 0-5.57808-4.5219-10.1-10.1-10.1ZM2.60001 14c0-6.57219 5.32781-11.9 11.89999-11.9 6.5722 0 11.9 5.32781 11.9 11.9 0 6.5722-5.3278 11.9-11.9 11.9-6.57218 0-11.89999-5.3278-11.89999-11.9ZM14.5 8.6c.4971 0 .9.40294.9.9v3.6H19c.4971 0 .9.4029.9.9 0 .4971-.4029.9-.9.9h-3.6v3.6c0 .4971-.4029.9-.9.9-.4971 0-.9-.4029-.9-.9v-3.6H10c-.49705 0-.89999-.4029-.89999-.9 0-.4971.40294-.9.89999-.9h3.6V9.5c0-.49706.4029-.9.9-.9Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/28/archive.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28Archive = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="29" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M9.72 5.35c-.59 0-1.14.25-1.54.67l-.9.98H20.7l-.9-.98c-.4-.42-.95-.67-1.53-.67H9.72ZM22.29 8.8H5.7c-.19.32-.3.7-.3 1.08V20.2c0 1.7 1.38 3.07 3.08 3.07h11.05c1.7 0 3.07-1.37 3.07-3.07V9.88c0-.39-.1-.76-.3-1.08ZM4.54 7.33c-.6.7-.94 1.61-.94 2.55V20.2c0 2.7 2.18 4.87 4.87 4.87h11.05c2.69 0 4.87-2.18 4.87-4.87V9.88c0-.98-.36-1.91-1.02-2.63l-2.24-2.44a3.88 3.88 0 0 0-2.86-1.26H9.72c-1.09 0-2.13.46-2.86 1.26L4.62 7.25a4.13 4.13 0 0 0-.08.08ZM14 11.55c.5 0 .9.4.9.9v5.36l1.83-1.75a.9.9 0 0 1 1.25 1.3l-3.36 3.2a.9.9 0 0 1-1.24 0l-3.35-3.2a.9.9 0 1 1 1.24-1.3l1.83 1.75v-5.36c0-.5.4-.9.9-.9Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/28/attach.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28Attach = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M20.286 6.445c-2.342-2.307-6.19-2.307-8.53 0l-5.353 5.272a.99.99 0 0 1-1.388-1.41l5.352-5.272c3.112-3.065 8.196-3.065 11.307 0a7.598 7.598 0 0 1 0 10.885l-7.347 7.238c-2.355 2.32-6.198 2.32-8.553 0a5.762 5.762 0 0 1 0-8.253l7.381-7.27c1.585-1.56 4.141-1.632 5.814-.167a4.06 4.06 0 0 1 .082 6.068l-6.158 5.688a.99.99 0 0 1-1.343-1.454l6.16-5.687c.93-.859.91-2.29-.044-3.127a2.315 2.315 0 0 0-3.122.088l-7.381 7.27a3.784 3.784 0 0 0 0 5.435c1.584 1.56 4.191 1.56 5.775 0l7.348-7.238a5.62 5.62 0 0 0 0-8.066Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/28/chat.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28Chat = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M7.15 20.64c.27-.6.45-1.29.45-2.13a7.55 7.55 0 0 1-2.91-5.84c0-4.44 4.17-8.04 9.3-8.04 5.15 0 9.32 3.6 9.32 8.04 0 4.44-4.17 8.04-9.31 8.04-.8 0-1.58-.1-2.33-.26a7.55 7.55 0 0 1-3.19 1.86c-.87.29-1.94.54-3.21.7a8.4 8.4 0 0 0 1.88-2.37Zm5.12 1.93c.57.08 1.14.12 1.73.12 5.95 0 11.29-4.23 11.29-10.02 0-5.8-5.34-10.02-11.3-10.02-5.94 0-11.28 4.22-11.28 10.02 0 2.58 1.1 4.9 2.82 6.63-.2.82-.75 1.4-1.65 2.3a1.98 1.98 0 0 0 1.63 3.37c3.24-.4 5.42-1.37 6.76-2.4Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/28/close.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28Close = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <g clipPath="url(#close_a)" fill="currentColor"> 6 | <path d="M14 28c7.66 0 14-6.35 14-14 0-7.66-6.35-14-14.01-14A14.1 14.1 0 0 0 0 14c0 7.65 6.35 14 14 14Z" 7 | fillOpacity=".04" /> 8 | <path opacity=".5" 9 | d="M9.17 20C8.51 20 8 19.47 8 18.81c0-.31.11-.61.34-.83L12.31 14l-3.97-3.97A1.15 1.15 0 0 1 8 9.2c0-.67.51-1.17 1.17-1.17.33 0 .59.11.82.33l3.99 3.98 4.02-4c.24-.24.5-.34.81-.34.66 0 1.19.52 1.19 1.17 0 .33-.1.6-.36.85L15.67 14l3.96 3.97c.24.21.36.51.36.84 0 .66-.53 1.19-1.2 1.19-.33 0-.64-.11-.85-.34l-3.96-3.98-3.95 3.98c-.23.23-.53.34-.86.34Z" 10 | fillOpacity=".8" /> 11 | </g> 12 | <defs> 13 | <clipPath id="close_a"> 14 | <path fill="#fff" d="M0 0h28v28H0z" /> 15 | </clipPath> 16 | </defs> 17 | </svg> 18 | ); 19 | -------------------------------------------------------------------------------- /src/icons/28/close_ambient.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28CloseAmbient = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <g clipPath="url(#close_ambient_a)"> 6 | <path d="M14 28c7.66 0 14-6.35 14-14 0-7.66-6.35-14-14.01-14A14.1 14.1 0 0 0 0 14c0 7.65 6.35 14 14 14Z" 7 | fill="#000" fillOpacity=".1" /> 8 | <path 9 | d="M9.17 20C8.51 20 8 19.47 8 18.81c0-.31.11-.61.34-.83L12.31 14l-3.97-3.97A1.15 1.15 0 0 1 8 9.2c0-.67.51-1.17 1.17-1.17.33 0 .59.11.82.33l3.99 3.98 4.02-4c.24-.24.5-.34.81-.34.66 0 1.19.52 1.19 1.17 0 .33-.1.6-.36.85L15.67 14l3.96 3.97c.24.21.36.51.36.84 0 .66-.53 1.19-1.2 1.19-.33 0-.64-.11-.85-.34l-3.96-3.98-3.95 3.98c-.23.23-.53.34-.86.34Z" 10 | fill="#fff" /> 11 | </g> 12 | <defs> 13 | <clipPath id="close_ambient_a"> 14 | <path fill="#fff" d="M0 0h28v28H0z" /> 15 | </clipPath> 16 | </defs> 17 | </svg> 18 | ); 19 | -------------------------------------------------------------------------------- /src/icons/28/devices.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28Devices = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M4.03 7.9c0-1.59 1.3-2.88 2.89-2.88h16.74a.9.9 0 0 1 0 1.8H6.92c-.6 0-1.09.48-1.09 1.08v12.9h9.63a.9.9 0 0 1 0 1.8H2a.9.9 0 1 1 0-1.8h2.03V7.9Zm16.93 3.77c-.6 0-1.08.5-1.08 1.1v6.95c0 .6.48 1.08 1.08 1.08h3.05c.6 0 1.09-.48 1.09-1.08v-6.96c0-.6-.49-1.09-1.09-1.09h-3.05Zm-2.88 1.1c0-1.6 1.29-2.9 2.88-2.9h3.05c1.6 0 2.89 1.3 2.89 2.9v6.95c0 1.6-1.3 2.88-2.89 2.88h-3.05c-1.6 0-2.88-1.29-2.88-2.88v-6.96Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/28/edit.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28Edit = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M18.92 3.4c.21-.03.43-.03.64 0 .58.06 1.04.32 1.47.66.41.32.86.77 1.4 1.29l.06.07c.55.54 1 1 1.34 1.4.35.44.62.91.7 1.5.02.21.02.43 0 .65a2.89 2.89 0 0 1-.7 1.5c-.33.4-.8.86-1.34 1.4L10.48 23.73l-.08.08a4.6 4.6 0 0 1-1.23.97 2.9 2.9 0 0 1-.26.11c-.47.17-.97.17-1.56.17H7.12c-.74 0-1.37 0-1.88-.06a2.9 2.9 0 0 1-1.5-.55c-.2-.15-.37-.32-.52-.51a2.9 2.9 0 0 1-.54-1.5c-.06-.51-.06-1.14-.06-1.88v-.16c0-.58 0-1.07.16-1.53l.14-.33c.22-.44.56-.78.97-1.19l.08-.08L16.06 5.35c.53-.52.98-.97 1.39-1.29.43-.34.9-.6 1.47-.67Zm.44 1.78h-.24c-.1.01-.26.06-.56.3-.32.25-.7.62-1.27 1.18l-.7.7 3.96 3.9.64-.64c.6-.58.98-.96 1.23-1.28.25-.3.3-.46.31-.57v-.25c-.01-.1-.06-.27-.3-.57-.26-.32-.65-.7-1.24-1.29a17.1 17.1 0 0 0-1.27-1.18c-.3-.24-.45-.29-.56-.3Zm-.1 7.34-3.95-3.9-10.07 9.94c-.53.52-.64.64-.7.77l-.06.13a17.21 17.21 0 0 0-.01 2.77c.04.38.11.52.18.6.05.08.12.14.19.2.08.07.23.14.6.18.4.05.92.05 1.73.05h.07a3.66 3.66 0 0 0 1.17-.1c.13-.07.26-.18.8-.71l10.06-9.93Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/28/heart.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28Heart = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M14 5.126c-.994-.932-2.343-1.678-3.823-1.95-1.761-.323-3.71.03-5.402 1.524-1.128.996-1.833 2.563-2.067 4.256a9.678 9.678 0 0 0 .834 5.41c.607 1.287 2.054 2.908 3.557 4.4 1.537 1.523 3.247 3.023 4.5 4.083a3.706 3.706 0 0 0 4.803 0c1.252-1.06 2.962-2.56 4.499-4.084 1.503-1.491 2.95-3.112 3.558-4.4a9.677 9.677 0 0 0 .833-5.409c-.233-1.693-.939-3.26-2.067-4.256-1.692-1.495-3.64-1.847-5.402-1.524-1.48.272-2.828 1.018-3.823 1.95Zm-4.148-.18c-1.274-.233-2.648.01-3.886 1.103-.72.635-1.283 1.758-1.475 3.153a7.878 7.878 0 0 0 .678 4.395c.455.964 1.677 2.381 3.198 3.89 1.488 1.477 3.159 2.942 4.394 3.988.72.61 1.757.61 2.478 0 1.235-1.046 2.906-2.511 4.394-3.988 1.521-1.509 2.743-2.926 3.198-3.89a7.878 7.878 0 0 0 .678-4.395c-.192-1.395-.756-2.518-1.475-3.153-1.238-1.093-2.612-1.336-3.886-1.103-1.304.24-2.502.984-3.271 1.857a1.17 1.17 0 0 1-1.754 0c-.768-.873-1.967-1.617-3.271-1.857Z" 7 | fill="currentColor" /> 8 | </svg> 9 | ); 10 | -------------------------------------------------------------------------------- /src/icons/28/stats.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from 'types/Icon'; 2 | 3 | export const Icon28Stats = ({ ...restProps }: Icon) => ( 4 | <svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg" {...restProps}> 5 | <path fillRule="evenodd" clipRule="evenodd" 6 | d="M13.1 4.4a9.51 9.51 0 0 0-8.7 9.41 9.53 9.53 0 0 0 9.6 9.46c5 0 9.1-3.76 9.55-8.56h-7.29c-.5 0-.96 0-1.33-.05a2.04 2.04 0 0 1-1.23-.55c-.36-.36-.5-.8-.55-1.22-.05-.38-.05-.84-.05-1.34V4.4Zm1.8 0v7.1a9.97 9.97 0 0 0 .08 1.34l.19.04c.24.03.58.03 1.15.03h7.23A9.53 9.53 0 0 0 14.9 4.4Zm.06 8.43h.01Zm.02.01v.01Zm-12.38.97C2.6 7.6 7.72 2.56 14 2.56S25.4 7.59 25.4 13.8c0 6.23-5.12 11.26-11.4 11.26S2.6 20.04 2.6 13.81Z" 7 | fill="currentColor" /> 8 | </svg> 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<Record<string, { 5 | control: { type: ControlTypes }, 6 | }>>((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<SVGSVGElement> { 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<SVGProps<SVGSVGElement> & { title?: string }>; 10 | 11 | const src: string; 12 | export default src; 13 | } 14 | --------------------------------------------------------------------------------